游戏编程入门:用 Python 实现角色移动与碰撞检测

百科问答6小时前发布 jdkjadmin
0 00
游戏编程入门:用 Python 实现角色移动与碰撞检测

游戏编程入门:用 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,还包括用时间差更新运动、保留适当精度、明确坐标方向,并在碰撞后校正位置和速度。逐轴处理能覆盖许多简单平台游戏需求,但它有明确边界。理解这些假设后,再按项目需要增加边界限制、动画、跳跃缓冲或更复杂的碰撞算法,会比直接堆叠条件判断更容易维护。

© 版权声明

相关文章