
游戏编程入门:用 Python 实现角色移动与碰撞检测
角色移动和碰撞检测是许多 2D 游戏的基础:玩家按键改变角色速度,重力让角色下落,碰到地面或平台后停止。本教程使用 Python 和 Pygame 实现一个可运行的平台游戏示例,并解释逐轴处理碰撞的思路,以及它适用的范围和容易忽略的边界。
这里的目标不是构建完整的物理引擎,而是给一个矩形角色加入左右移动、跳跃、重力、地面与平台碰撞。代码适合学习、原型开发和简单的 2D 游戏。如果游戏需要斜坡、旋转物体、复杂刚体或精确的高速碰撞,单靠矩形重叠检测就不够了。
一、准备环境
示例使用 Pygame。建议使用 Python 3,并在终端安装依赖:
python -m pip install pygame
将后文完整代码保存为 main.py,然后运行:
python main.py
窗口中使用左右方向键或 A、D 键移动,按空格跳跃,按 Esc 退出。安装命令会从当前配置的软件包源下载 Pygame;如果所在环境无法访问该软件包源,需要按所在系统或开发环境的配置调整安装方式。代码本身不需要联网,也不会读写游戏之外的文件。
二、先明确坐标和运动模型
Pygame 的屏幕坐标原点位于左上角:x 向右增加,y 向下增加。因此,角色向下下落时,垂直速度是正数;角色向上跳时,垂直速度是负数。程序以像素为长度单位、以秒为时间单位,速度的单位是像素/秒,加速度的单位是像素/秒²。
若只按“每帧移动固定像素”编写逻辑,游戏运行帧率变化时,角色每秒移动的距离也会变化。下面的代码使用两次画面更新之间的时间差 dt,按“速度 × 时间”计算位移。这样移动速度较少依赖帧率。示例还将单帧时间限制在 0.05 秒以内,避免窗口卡顿或切回前台后一次性模拟过长时间。
角色用 pygame.Rect 表示碰撞范围。矩形的 x、y 是左上角坐标,right、bottom 是右边和下边的位置。为了避免每帧的小数位移被整数矩形过早截断,代码另外保存浮点位置 pos_x、pos_y,只在碰撞检测前同步到矩形。
三、完整可运行代码
import sys
import pygame
# 窗口尺寸,单位为像素
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
# 运动参数
MOVE_SPEED = 280.0 # 水平速度,像素/秒
GRAVITY = 1500.0 # 重力加速度,像素/秒^2
JUMP_SPEED = 600.0 # 起跳速度的大小,像素/秒
MAX_FRAME_TIME = 0.05 # 单帧最多模拟 0.05 秒
def main():
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("角色移动与碰撞检测")
clock = pygame.time.Clock()
# 玩家碰撞矩形和浮点位置。玩家出生点应避开平台。
player = pygame.Rect(100, 480, 40, 50)
pos_x = float(player.x)
pos_y = float(player.y)
velocity_x = 0.0
velocity_y = 0.0
grounded = False
# 所有平台都是不可穿过的静态矩形,最后一个是地面。
platforms = [
pygame.Rect(200, 450, 180, 20),
pygame.Rect(440, 360, 180, 20),
pygame.Rect(80, 280, 160, 20),
pygame.Rect(0, 560, SCREEN_WIDTH, 40),
]
running = True
while running:
# tick 返回两帧之间的毫秒数;换算为秒并限制最大值。
dt = min(clock.tick(60) / 1000.0, MAX_FRAME_TIME)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_SPACE and grounded:
velocity_y = -JUMP_SPEED
grounded = False
keys = pygame.key.get_pressed()
move_left = keys[pygame.K_LEFT] or keys[pygame.K_a]
move_right = keys[pygame.K_RIGHT] or keys[pygame.K_d]
# 同时按左、右时抵消;否则按输入设置水平速度。
if move_left == move_right:
velocity_x = 0.0
elif move_left:
velocity_x = -MOVE_SPEED
else:
velocity_x = MOVE_SPEED
# 先处理水平方向。水平碰撞时把玩家贴到平台边缘。
pos_x += velocity_x * dt
player.x = round(pos_x)
for platform in platforms:
if player.colliderect(platform):
if velocity_x > 0:
player.right = platform.left
elif velocity_x < 0:
player.left = platform.right
pos_x = float(player.x)
# 再处理垂直方向。重力向下,因此向下速度为正。
velocity_y += GRAVITY * dt
pos_y += velocity_y * dt
player.y = round(pos_y)
grounded = False
for platform in platforms:
if player.colliderect(platform):
if velocity_y > 0:
# 下落时落在平台顶面。
player.bottom = platform.top
grounded = True
elif velocity_y < 0:
# 上升时撞到平台底面。
player.top = platform.bottom
velocity_y = 0.0
pos_y = float(player.y)
# 绘制背景、平台和玩家。
screen.fill((30, 34, 45))
for platform in platforms:
pygame.draw.rect(screen, (100, 180, 120), platform)
pygame.draw.rect(screen, (240, 190, 70), player)
pygame.display.flip()
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
四、碰撞检测是怎样工作的
1. 检测矩形是否相交
player.colliderect(platform) 检查两个矩形是否有面积重叠,返回真或假。它是“轴对齐矩形”检测:矩形的边与屏幕横纵轴平行。角色画得像圆形或复杂精灵也没关系,但用矩形作为碰撞范围,结果只是近似。碰撞边缘与矩形一致,并不等于像素级轮廓检测。
两个矩形只在边缘接触时,通常不算重叠。因此角色落地后,玩家底边可以恰好等于平台顶边而不再触发重叠。代码在检测到下落中的重叠时,把 player.bottom 直接设为 platform.top,从而消除穿入平台的部分。
2. 为什么先横向、再纵向
如果把角色 x、y 同时更新,再检查与平台的重叠,程序有时难以判断应该把角色从平台左侧推开,还是从平台上方推开。示例将移动分成两个步骤:
- 先只更新 x。发生水平重叠时,根据水平速度把玩家的左边或右边贴到平台边缘。
- 再只更新 y。下落时把玩家放到平台顶面;上升时把玩家放到平台底面。
这种逐轴处理常用于简单的平台游戏。它能让角色沿着墙面滑动,也能减少“同时撞上角落时方向不明确”的情况。它不是普适的刚体求解方法:遇到斜坡、旋转碰撞体或复杂物理交互时,需要更合适的碰撞形状与求解方式。
3. 重力、落地和跳跃
每帧用 velocity_y += GRAVITY * dt 增加向下速度,再用 pos_y += velocity_y * dt 更新位置。落到平台上时,程序校正玩家底边,并把垂直速度清零。只有在本帧垂直碰撞是向下发生时,grounded 才会被设为真。
空格键通过 KEYDOWN 事件触发,而不是在按住空格的每一帧重复起跳。只有 grounded 为真时才允许跳跃,起跳后马上将它改为假。这样可以避免玩家在空中无限连跳。由于落地状态是在更新过程中检测出来的,按键处理使用的是上一帧记录的状态;普通的键盘操作没有明显影响。如果需要精确处理同一帧内的落地与跳跃,可以重新设计输入缓冲或更新顺序。
五、参数怎么调
MOVE_SPEED控制左右移动速度。调大后移动更快;当前实现是按下方向键立即达到该速度,没有加速或惯性。GRAVITY控制下落加速度。数值越大,下落速度增长越快。JUMP_SPEED控制起跳初速度的大小。由于屏幕 y 轴向下,起跳时实际赋值为负数。MAX_FRAME_TIME限制单次更新使用的最大时间差。限制过小会在严重卡顿时丢弃更多模拟时间;限制较大则可能让单次位移增加,碰撞更容易不稳定。clock.tick(60)请求限制循环频率在约每秒 60 次。它不是对每台设备都能精确保证 60 帧,也不会替代基于时间差的运动计算。
这些数值是游戏手感参数,不是现实物理单位,也没有一组适用于所有项目的固定“正确值”。调整时建议逐项修改,并同时检查起跳高度、到达平台所需时间、横向可控性和碰撞稳定性。跳得更高不一定只靠增加起跳速度;重力也会影响跳跃高度和滞空时间。
六、适用场景与边界
这套做法适合初学者理解游戏循环、速度、重力、矩形碰撞和平台落地,也适用于关卡固定、障碍物是轴对齐矩形、角色速度适中的小型 2D 原型。它可作为碰撞与移动的起点,但不能代替项目级的完整物理系统。
示例没有实现屏幕左右边界阻挡、摄像机、动画、移动平台、斜坡、单向平台、角色受伤判定或碰撞层。角色离开屏幕后不会自动回到场景内。若要限制角色位置,可在移动和碰撞之后增加边界校正;但要明确角色超出边界时应被阻挡、传送还是允许离场。不同规则应该由游戏设计决定。
代码假设玩家出生时不与平台重叠,并且平台是静止的。如果出生点在平台内部,逐轴逻辑可能无法判断正确的脱离方向;如果平台移动,还需要计算平台位移并处理角色站在平台上时的跟随关系。碰撞检测也不检查像素透明区域,因此视觉上看似没碰到,矩形仍可能发生碰撞,反过来也一样。
高速物体是另一个重要边界。当前代码按每帧末尾的位置检查重叠;当物体在一帧内移动很远时,它可能从薄平台一侧直接跳到另一侧而没有重叠,这叫“穿透”或“隧穿”。限制单帧时间有帮助,但不保证彻底解决。可按具体需求减小碰撞体每步位移、将一帧细分成多个较小子步,或实现连续碰撞检测;简单地增加帧率也不能保证在所有速度和设备上都安全。
七、常见错误排查
角色移动速度随帧率变化
常见原因是每帧直接执行固定像素的位移,例如每次循环都让 x 增加 5。改用 速度 × dt 计算位移。注意 dt 应以秒为单位;Clock.tick 返回的是毫秒,必须除以 1000。
角色落地后轻微抖动或慢慢穿入地面
检查是否在碰撞后把位置重新同步到浮点坐标,以及是否清零垂直速度。示例在校正矩形后更新 pos_y,并将 velocity_y 设为 0。如果只改了矩形、没改浮点位置,下一帧可能又从旧位置算出位移。
碰到平台时被弹到奇怪的方向
先确认代码按轴分别移动和检测,并且平台与玩家出生点没有初始重叠。逐轴方法根据当前轴上的速度选择校正方向;角色一开始就在障碍物内部时,这个判断未必符合预期。必要时调整出生位置,或另行实现可靠的初始重叠处理。
空格无法跳,或可以连续跳跃
检查 grounded 是否只在下落碰撞时设为真,并在跳跃后立即清零。若按键事件没有被处理,也检查事件循环是否在每帧运行。跳跃发生在落地检测之前还是之后,会影响极少数帧边界上的输入手感;要实现“提前按下也能在落地时起跳”等机制,需要增加跳跃缓冲,而不是只重复发送跳跃事件。
在不同电脑上感觉速度不一致
确认位置更新使用的是秒单位时间差,而不是固定帧数;也检查是否把帧时间限制得过低,导致卡顿时模拟时间被丢弃。帧率限制和帧时间截断影响的是更新频率与卡顿恢复策略,不是对所有设备提供完全一致的物理模拟保证。
八、参数与安全说明
本例中的移动速度、重力、跳跃速度和时间差上限都是代码内设定的普通数值,没有读取用户输入,也没有涉及金额、密码、网络访问或系统权限,因此不存在需要套用外部服务限制的特殊参数上限。若把参数改为读取配置文件、命令行或网络数据,应根据游戏设计校验类型、范围和异常值,避免负数、非有限浮点数或极大值造成意外行为。这里的参数范围属于项目设计约束,不应误认为 Pygame 对这些数值规定的通用安全范围。
碰撞检测也不是安全边界。它用于游戏玩法判断,不能用于账户认证、反作弊可信判定或其他安全用途。若游戏涉及联网竞技,客户端的位置和碰撞结果都可能被修改;关键规则应由可信服务器验证,并根据实际威胁模型设计同步与校验。本文代码是本地入门示例,不提供反作弊或网络安全能力。
结语
角色移动与碰撞的关键,不只是调用 colliderect,还包括用时间差更新运动、保留适当精度、明确坐标方向,并在碰撞后校正位置和速度。逐轴处理能覆盖许多简单平台游戏需求,但它有明确边界。理解这些假设后,再按项目需要增加边界限制、动画、跳跃缓冲或更复杂的碰撞算法,会比直接堆叠条件判断更容易维护。





