【Pygame】第11章 游戏物理基础与运动系统

张开发
2026/4/16 12:45:49 15 分钟阅读

分享文章

【Pygame】第11章 游戏物理基础与运动系统
摘要在游戏中物理系统的作用不仅仅是“让物体动起来”更重要的是让运动看起来合理、自然并且在不同帧率下保持稳定。一个好的物理系统能够让角色跳跃更真实、物体碰撞更可信、弹簧和摆锤更有生命感。对于 2D 游戏来说物理并不一定意味着复杂的刚体引擎。很多时候我们真正需要的只是几个基础概念位置如何更新速度如何变化加速度怎样影响运动重力怎样持续作用碰撞后如何反弹摩擦如何逐渐消耗速度。只要把这些基本规律组织好游戏中的运动表现就会明显提升。本章将从运动学开始逐步理解速度、加速度和力的关系再讲到重力、碰撞、弹簧等常见物理现象。最后我们会使用一个完整的综合示例搭建一个简单但结构清晰的 2D 物理运动系统。11.1 运动学基础运动学研究的是物体“怎么动”而不是“为什么动”。在游戏里我们通常并不需要真的去模拟每一个真实世界中的力学细节而是需要一个足够稳定、足够容易控制的运动模型。这个模型最基本的组成部分就是位置、速度和加速度。位置表示物体当前在哪里速度表示它朝哪个方向、以多快的速度移动加速度则表示速度如何变化。当我们把这些量联系起来就能让物体在屏幕上完成连续、平滑的运动。11.1.1 位置、速度和加速度之间的关系在离散时间的游戏系统中运动通常按帧来更新。每一帧我们都会根据当前速度修改位置再根据加速度修改速度。这是一种非常常见的数值积分思想虽然它并不追求极高精度但对于 2D 游戏来说通常已经足够。最常见的更新思路可以理解为速度会受到加速度的影响位置会受到速度的影响也就是说速度先变位置再变。如果把“力”也加入进去那么通常会先由力得到加速度再由加速度更新速度最后由速度更新位置。这种结构的好处是非常直观。玩家按下方向键时我们给物体一个方向上的加速度或外力物体在接下来的几帧里逐渐加速当按键松开后加速度消失速度可能继续保留一小段时间从而形成惯性效果。这也是为什么很多游戏角色的移动看起来不会“瞬移”而是有一点自然的滑动感。11.1.2 帧率无关运动的重要性如果你直接写成“每帧移动 5 像素”那么程序在 30 FPS 和 60 FPS 下的运动速度就会完全不同。这就是所谓的“帧率依赖问题”。帧率高时物体移动得更快帧率低时物体移动得更慢。对于玩家来说这种差异会破坏手感也会影响游戏平衡。所以在实际开发中我们通常使用dt也就是每一帧经过的时间。如果速度的单位是“像素每秒”那么位置变化就应该写成速度 × dt。这样一来不管帧率高低单位时间内的总移动距离都比较稳定。这就是帧率无关运动的核心思想。它不是为了让代码变复杂而是为了让游戏在不同设备上都能表现一致。11.1.3 欧拉积分的基本思想欧拉积分是游戏开发中最常见、最容易实现的一种数值更新方法。它的思想很简单用当前时刻的速度和加速度去近似下一时刻的状态。虽然欧拉积分并不是最精确的数值方法但它速度快、实现简单、适合实时游戏因此非常常见。在大多数 2D 游戏里只要时间步长不要太大欧拉积分通常已经足够使用。需要注意的是欧拉积分对时间步长比较敏感。如果dt太大可能会出现穿透、抖动或者不稳定的问题。所以在一些更复杂的物理场景里开发者会进一步使用固定时间步长、子步进或更稳定的积分方式。11.2 重力、摩擦与碰撞响应如果说运动学解决的是“怎么动”那么重力、摩擦和碰撞就解决了“为什么会这样动”。11.2.1 重力模拟在游戏里重力通常表示为一个持续向下的加速度。只要物体处于受重力影响的环境中它的竖直速度就会不断增加直到碰到地面或者受到其他力的干预。这就是为什么跳跃角色会先上升再下降也为什么落体运动看起来有自然的加速过程。如果没有重力角色跳起来会像漂浮如果重力太大动作又会显得很生硬。所以重力值本身也属于“手感调节参数”不一定要严格等于现实世界的数值。11.2.2 摩擦力的作用摩擦力在游戏中常常被简化处理但它的作用非常重要。如果一个物体在地面上移动后永远不会减速那么它看起来就会非常“滑”缺少真实感。所以通常会在接触地面时让水平速度逐渐衰减模拟摩擦或阻力的效果。摩擦力不一定要做得很复杂。很多时候直接让速度乘以一个小于 1 的系数就已经能产生比较自然的减速效果。例如角色落地后滑行一段距离再停下这就是摩擦在起作用。11.2.3 简单碰撞响应碰撞响应是物理系统中很基础、也很常用的一部分。当物体碰到地面或墙壁时我们需要决定它接下来怎么走。最简单的做法是如果物体穿过了边界就把它拉回边界内部并反转对应方向的速度。如果给速度乘上一个弹性系数那么碰撞后就不会完全保留原速度而会损失一部分能量。这样物体就会越弹越小最终停下来。这比“纯反弹”更符合游戏里的常见表现也更容易控制。需要理解的是游戏中的碰撞响应通常不是完整物理模拟而是“足够合理的近似”。我们更关心的是玩家看起来是否舒服角色是否能稳定站在地面上弹跳是否自然是否会发生明显穿透。11.3 弹簧系统与弹性运动弹簧系统是理解物理模拟非常好的例子。它的行为简单但能清楚体现“恢复力”“振荡”和“阻尼”的概念。11.3.1 弹簧为什么会来回摆动弹簧有一个自然长度。当它被拉长或压缩时会产生一个把物体拉回平衡位置的力。这个力越大物体就越想回到原来的位置但由于惯性物体不会一下子停住而是会越过平衡点继续来回摆动。这种“来回振荡”正是弹簧系统的典型行为。在游戏中它常用于实现吊绳、摇摆物、弹性 UI、受击反馈、角色摆动等效果。11.3.2 阻尼让运动逐渐稳定如果只有弹簧力而没有阻尼系统会一直振荡下去。但现实中空气阻力、内部摩擦和能量损耗都会让振荡逐渐减弱。所以在游戏里通常会引入阻尼系数让速度每次更新时略微衰减。阻尼的存在非常重要因为它可以防止系统无限震荡。如果你做的是 UI 动画、摇摆装饰物或者道具悬挂效果阻尼往往决定了它最终会不会“抖个不停”。11.4 数值稳定性与物理感的平衡物理系统并不是越复杂越好。很多初学者一开始会想把现实世界的所有公式都搬进游戏但实际上游戏开发更看重的是“稳定”和“可控”。如果你的物理系统太依赖大时间步长就容易出现抖动、穿透和爆炸性速度增长。如果你的系统过于真实又可能导致玩家难以控制影响操作手感。因此在游戏中物理通常是“物理感”而不是“完全真实”。一个好的游戏物理系统通常要兼顾三件事运动是否稳定结果是否合理操作是否顺手这也是为什么很多游戏都会对重力、摩擦、弹力等参数进行大量调试而不是照搬真实世界的数值。11.5 中文字体安全加载方案你之前的教材示例里已经出现过字体兼容问题所以本章继续沿用更稳定的做法不使用pygame.font.SysFont统一使用字体文件路径加载。这样可以避免系统字体枚举导致的异常也能让示例在不同环境里更稳定地运行。11.6 综合实战2D 物理运动系统下面给出本章的完整综合示例。这个示例会把运动学、重力、碰撞、摩擦、弹簧和帧率无关更新放在一起形成一个可运行的简单物理演示程序。功能说明方向键控制物体受力移动空格生成新的小球小球受重力影响下落小球碰到地面和墙壁会反弹弹簧物体会围绕锚点振荡使用安全字体加载显示信息importpygameimportsysimportosimportmath pygame.init()screenpygame.display.set_mode((800,600))pygame.display.set_caption(游戏物理基础与运动系统演示)clockpygame.time.Clock()defget_font(size):font_paths[rC:\Windows\Fonts\simhei.ttf,rC:\Windows\Fonts\msyh.ttc,rC:\Windows\Fonts\simsun.ttc,]forpathinfont_paths:ifos.path.exists(path):try:returnpygame.font.Font(path,size)except:passreturnpygame.font.Font(None,size)fontget_font(24)GRAVITY980classKinematicObject:def__init__(self,x,y,radius20,color(0,255,0)):self.positionpygame.math.Vector2(x,y)self.velocitypygame.math.Vector2(0,0)self.accelerationpygame.math.Vector2(0,0)self.radiusradius self.colorcolor self.restitution0.8self.friction0.99defapply_force(self,force):self.accelerationforcedefupdate(self,dt):self.velocityself.acceleration*dt self.positionself.velocity*dt self.accelerationpygame.math.Vector2(0,0)ifself.position.x-self.radius0:self.position.xself.radius self.velocity.x*-self.restitutionelifself.position.xself.radius800:self.position.x800-self.radius self.velocity.x*-self.restitutionifself.position.y-self.radius0:self.position.yself.radius self.velocity.y*-self.restitutionelifself.position.yself.radius600:self.position.y600-self.radius self.velocity.y*-self.restitution self.velocity.x*self.frictiondefdraw(self,surface):pygame.draw.circle(surface,self.color,(int(self.position.x),int(self.position.y)),self.radius)classBall(KinematicObject):def__init__(self,x,y):super().__init__(x,y,radius18,color(80,220,120))defupdate(self,dt):self.apply_force(pygame.math.Vector2(0,GRAVITY))super().update(dt)classSpringObject:def__init__(self,anchor_x,anchor_y,rest_length180,k6):self.anchorpygame.math.Vector2(anchor_x,anchor_y)self.positionpygame.math.Vector2(anchor_x,anchor_yrest_length)self.velocitypygame.math.Vector2(0,0)self.rest_lengthrest_length self.kk self.damping0.96defupdate(self,dt):displacementself.position-self.anchor lengthdisplacement.length()iflength0:directiondisplacement.normalize()stretchlength-self.rest_length force-self.k*stretch*direction self.velocityforce*dt self.velocity*self.damping self.positionself.velocity*dtdefdraw(self,surface):steps20foriinrange(steps):t1i/steps t2(i1)/steps p1self.anchor.lerp(self.position,t1)p2self.anchor.lerp(self.position,t2)offset10*math.sin(i*math.pi)ifi%20:offset-offset pygame.draw.line(surface,(200,200,200),(p1.xoffset,p1.y),(p2.x-offset,p2.y),2)pygame.draw.circle(surface,(255,80,80),(int(self.anchor.x),int(self.anchor.y)),10)pygame.draw.circle(surface,(80,180,255),(int(self.position.x),int(self.position.y)),20)classSmoothMover:def__init__(self,x,y):self.positionpygame.math.Vector2(x,y)self.targetpygame.math.Vector2(x,y)self.speed300defset_target(self,x,y):self.targetpygame.math.Vector2(x,y)defupdate(self,dt):directionself.target-self.position distancedirection.length()ifdistance0:directiondirection.normalize()stepmin(self.speed*dt,distance)self.positiondirection*stepdefdraw(self,surface):pygame.draw.circle(surface,(255,0,0),(int(self.target.x),int(self.target.y)),5)pygame.draw.circle(surface,(255,255,0),(int(self.position.x),int(self.position.y)),12)playerKinematicObject(150,150,radius20,color(0,255,0))balls[Ball(400,100)]springSpringObject(600,120,rest_length180,k6)moverSmoothMover(100,500)runningTruewhilerunning:dtclock.tick(60)/1000.0foreventinpygame.event.get():ifevent.typepygame.QUIT:runningFalseelifevent.typepygame.MOUSEBUTTONDOWN:ifevent.button1:balls.append(Ball(event.pos[0],event.pos[1]))spring.positionpygame.math.Vector2(event.pos)spring.velocitypygame.math.Vector2(0,0)elifevent.button3:mover.set_target(event.pos[0],event.pos[1])keyspygame.key.get_pressed()forcepygame.math.Vector2(0,0)force_amount600ifkeys[pygame.K_LEFT]:force.x-force_amountifkeys[pygame.K_RIGHT]:force.xforce_amountifkeys[pygame.K_UP]:force.y-force_amountifkeys[pygame.K_DOWN]:force.yforce_amount player.apply_force(force)player.update(dt)forballinballs:ball.update(dt)spring.update(dt)mover.update(dt)screen.fill((40,40,40))pygame.draw.line(screen,(255,255,255),(0,580),(800,580),2)player.draw(screen)forballinballs:ball.draw(screen)spring.draw(screen)mover.draw(screen)info1font.render(方向键控制绿色物体受力移动,True,(255,255,255))info2font.render(左键生成小球并重置弹簧 右键移动黄色物体目标点,True,(255,255,255))info3font.render(小球受重力影响 碰到边界会反弹,True,(255,255,255))screen.blit(info1,(10,10))screen.blit(info2,(10,40))screen.blit(info3,(10,70))pygame.display.flip()pygame.quit()sys.exit()11.7 本章总结本章从运动学出发介绍了位置、速度、加速度之间的关系并进一步讲解了帧率无关运动、重力、摩擦、碰撞和弹簧系统。这些内容虽然看起来都很基础但它们实际上构成了游戏物理的底层骨架。只要把这些基础处理好角色移动、跳跃、反弹和摆动都会自然很多。需要记住的是游戏物理的目标不是完全复刻现实而是构建“看起来合理、用起来顺手”的运动效果。这也是游戏物理和真实物理之间最重要的区别。本章知识点回顾知识点主要内容运动学位置、速度、加速度帧率无关使用 dt 统一运动速度重力持续向下的加速度摩擦减缓水平速度碰撞边界反弹、弹性系数弹簧恢复力、振荡、阻尼课后练习实现一个角色跳跃系统支持二段跳。为小球增加不同质量和不同弹性系数。实现一个摆锤运动模拟器。尝试让弹簧连接两个物体。使用不同重力值观察运动变化。下章预告在下一章中我们将学习粒子系统这是创建火花、烟雾、爆炸和魔法特效的重要技术。

更多文章