ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pygame足球小游戏开发:从碰撞检测到物理模拟的完整实战

Pygame足球小游戏开发:从碰撞检测到物理模拟的完整实战 用 Pygame 写足球游戏是挺经典的个人练手项目。标题里的几个关键词——Pygame、碰撞检测、物理模拟、用户输入处理——其实正好对应了几乎任何 2D 小游戏都要面对的三个核心问题怎么收玩家的操作、怎么判断两个东西碰上了、怎么让物体的运动看起来合理。做一个小足球游戏这三个问题吃透了后面做什么打砖块、坦克大战、平台跳跃套路都是相通的。这篇文章我会从零开始带你用 Python 和 Pygame 做一个能跑起来的小游戏一个用方向键/WASD 控制的球员一个圆一个会滚动的足球另一个圆加上墙壁反弹、球员带球/撞球、摩擦力让球逐渐停下这些基础物理效果。整个工程不依赖任何其他第三方库代码结构也足够简单适合刚入门 Pygame 的读者跟着敲一遍也适合已经写过几个小项目的人看看我踩过的坑和补全细节的思路。1. 项目目标与整体设计思路1.1 最小可玩版本包含什么标题描述的场景很明确一个球员、一个足球、玩家能控制球员移动、带碰撞检测、带简单物理模拟、带基本用户输入处理。我理解里面所谓“最小可玩版本”就是指你打开窗口之后能实际玩起来而不是只有一张静态画面。换句话说至少要满足下面这几点屏幕上有一个可以被键盘控制的“球员”按住方向键它会持续移动松开就停。屏幕里有一个“足球”它不会自己乱跑但如果被球员碰到会被撞出去。足球撞到窗口四周的边界会被弹回来方向改变而且速度会逐渐衰减。球员不能飞出窗口边界球也不会卡在边框里。这四点看起来不难但每一环都有需要处理的细节。比如“持续移动”靠的是键盘状态检测而不是单次按键事件“碰到就被撞出去”需要确认碰撞发生的方向否则球可能被推向左上角或右下角“速度逐渐衰减”在数学上就是每一帧把一个小于 1 的系数乘到速度上。我在设计这个项目时故意没把功能堆得很满。球门、得分、双人模式、AI 对手都是后续可以扩展的但第一版最重要的是把核心手感做对。手感这个东西很微妙——球员移动太快容易过冲太慢又显得拖沓足球摩擦系数太大一下就停太小就滚个没完。所以实现的时候所有参数都应该提出来做成常量方便随时调。1.2 为什么选 Pygame 做这种小游戏选 Pygame 的理由对我个人来说很简单它足够低层能让你真正理解游戏循环是怎么转的又不至于低层到连图片都得自己写像素。很多人一上来就上 Unity 或 Godot结果发现大部分时间都在拖节点、调 Inspector对底层事件循环和碰撞检测的理解反而被引擎封装掉了。Pygame 则相反窗口、事件、绘制、碰撞全都要你亲自写这恰好是学编程最需要的东西。另外 Pygame 非常轻量。pip install pygame装完就能用不需要额外编辑器不需要配复杂的构建链。做这种只有一个球员、一个球的 demo写出来的代码量大概在两三百行以内非常适合拿来培养“把想法拆成代码”的能力。等以后真的要做复杂游戏你也能清楚地知道哪些是引擎帮你做掉的哪些是你自己该设计的。当然Pygame 也有不适合的场景3D、大规模地图、复杂物理模拟它干不了。对这类需求应该去用专门的引擎或物理库。但这个足球游戏里需要的“简单物理模拟”Pygame 的手写实现反而更合适因为逻辑简单到不值得引入一个重型依赖。1.3 三个核心模块的拆解抛开窗口初始化、画圆这些体力活整个项目的核心逻辑其实只有三块用户输入处理负责“读键盘”这件事。Pygame 提供了两种方式一种是通过pygame.event.get()拿到事件比如按键按下那一瞬间另一种是通过pygame.key.get_pressed()拿到当前所有按键的实时状态。持续移动必须用后者因为它代表“现在这一刻哪些键被按着”而事件只代表“刚刚发生了什么”。这个区别刚入门的人很容易踩坑。碰撞检测负责判断“球员和足球是否碰到一起”。在这个项目里两个物体都是圆形所以最合适的是圆形碰撞检测算两圆心之间的距离如果距离小于两圆半径之和就判定发生了碰撞。这个原理我在第 3.2 节会展开讲。物理模拟负责让足球的运动“看起来像真的”。其实不需要引入多少物理公式只需要两件事给球一个速度向量vx和vy每一帧把速度加到坐标上然后对速度做衰减乘以一个小于 1 的摩擦系数并在碰撞或撞墙时改变速度方向。这三大块各自独立又通过主循环串在一起读输入 → 更新物理 → 检测碰撞 → 绘制画面 → 等下一帧。理解了这个循环游戏开发的大量思路都能串起来。2. 环境准备与工程骨架2.1 安装 Pygame 的正确姿势理论上安装 Pygame 就是一行命令的事pip install pygame但实际运行中我见过很多人卡在这一步尤其是报failed to build pygame when getting requirements to build wheel这种错。这个报错的意思是pip 没找到可以直接安装的 Pygame 预编译包转而尝试从源码编译结果编译过程中缺少依赖构建失败。遇到这个问题先做两件事升级 pip 到最新版python -m pip install --upgrade pip。尽量用官方渠道安装 Python不要用 Microsoft Store 或某些集成环境里自带的那种阉割版。官方版 Python 对 Pygame 的预编译 wheel 支持最稳妥。在 Windows 上升级完 pip 再执行pip install pygame绝大多数情况都能直接成功。如果还不行可以明确指定版本比如pip install pygame2.6.1因为某些环境下最新版可能没有对应 wheel或者有兼容性问题。在 Linux 上如果编译失败通常要装 SDL 相关依赖sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev装完依赖再重新pip install pygame基本就能过了。如果你用的是 VSCode别忘了检查当前终端激活的是不是你的目标 Python 环境。很多“我明明 pip 装过了为什么还是 ModuleNotFoundError”的情况都是因为终端用的 Python 解释器和 VSCode 里选的不是同一个。设置里搜 “Python: Select Interpreter”选对虚拟环境再重开一个终端问题就消失。2.2 游戏主循环的骨架Pygame 游戏本质上就是一个无限循环这个循环每转一圈就是一帧。先记住这个骨架后面的所有代码都是往这个骨架里填肉import pygame pygame.init() screen pygame.display.set_mode((800, 500)) clock pygame.time.Clock() FPS 60 running True while running: # 1. 处理事件窗口关闭、按键按下等 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 2. 根据按键状态更新对象位置 # 3. 碰撞检测 # 4. 绘制画面 # 5. 控制帧率 clock.tick(FPS) pygame.display.flip() pygame.quit()逼着你写clock.tick(FPS)是 Pygame 的一点“苦心”。要是没有这一行游戏会以几百上千帧的速度运行物理计算全乱套球会因为单帧移动距离过大而穿过墙壁方向键一按人物就像瞬移。把帧率锁在 60 FPS一方面保证物理模拟稳定另一方面也避免 CPU 空转发热。pygame.display.flip()负责把已经画好的内容真正显示到屏幕上。Pygame 默认是双缓冲机制你先画后翻页这样不会出现画面闪烁。2.3 坐标、颜色、帧率的基本约定写代码之前先把坐标系弄明白。Pygame 的窗口坐标系以左上角为原点x 轴向右为正y 轴向下为正。这意味着“向上移动”实际上是 y 坐标减小“向下移动”是 y 坐标增大。刚开始写足球游戏时我总习惯把“向下”想成正方向结果方向键按下一阵子角色跑反后来稍微改一下逻辑就顺了。颜色方面我会定义几个常量WHITE (255, 255, 255) GREEN (34, 139, 34) BLUE (30, 144, 255) BLACK (0, 0, 0)用常量的好处是改颜色只改一处不用满代码翻着替换。这个项目里我用绿色背景代表草坪蓝色圆代表球员白色圆代表足球。定义窗口大小为 800x500作为所有边界判断的基础。这些值都是可调参数放在文件开头后续调手感方便很多。3. 核心细节解析输入、碰撞与物理3.1 用户输入处理事件与按键状态我在前面提到 Pygame 处理键盘输入有两种方式这里详细说说区别。pygame.event.get()返回的是“自上一帧以来发生的事件列表”里面包含按键按下KEYDOWN、按键抬起KEYUP、鼠标点击、窗口关闭等。它适合做“一次性触发”的逻辑比如按一次空格跳跃。但如果你在 KEYDOWN 里写“让球员左移”会发现按住方向键时球员只移动一格因为 KEYDOWN 事件在你按住期间只会触发有限几次或者只有按下瞬间触发一次。持续移动的正确做法是使用pygame.key.get_pressed()。它返回一个布尔数组表示此刻每个键是否被按住。你每一帧都去检查这个数组判断当前要往哪个方向移动keys pygame.key.get_pressed() dx dy 0 if keys[pygame.K_LEFT] or keys[pygame.K_a]: dx - 1 if keys[pygame.K_RIGHT] or keys[pygame.K_d]: dx 1 if keys[pygame.K_UP] or keys[pygame.K_w]: dy - 1 if keys[pygame.K_DOWN] or keys[pygame.K_s]: dy 1这里我给每个方向都支持两种按键方向键和 WASD。方向键符合直觉WASD 是很多键鼠玩家的肌肉记忆两个都支持能照顾到不同习惯。计算得到一个 dx 和 dy它们的值是 -1、0 或 1表示方向向量。然后乘上速度常量再加到球员坐标上player_x dx * player_speed player_y dy * player_speed如果同时按左和右dx 先减 1 再加 1变成 0保证球员不会因为对撞按键而原地抖动。这是很实用的小细节。至于窗口关闭事件还是要靠pygame.event.get()里的 QUIT 事件来处理。这两种输入方式并行使用并不冲突一个是“瞬时反馈”一个是“持续状态”。3.2 圆形碰撞检测的原理与实现碰撞检测是游戏开发里的经典问题。这个项目里的球员和足球都是圆所以最自然的碰撞检测方式就是圆形碰撞检测。原理非常简单计算两个圆心之间的直线距离如果这个距离小于两个圆的半径之和就说明它们发生碰撞了。用数学公式表示就是dist r1 r2实际代码里不会真的开平方根来算距离而是用勾股定理先算距离再比较。因为开平方的开销对游戏循环来说微乎其微这里直接开方也没关系。Python 里可以用math.hypot(dx, dy)dx ball_x - player_x dy ball_y - player_y dist math.hypot(dx, dy) if dist player_radius ball_radius: # 发生碰撞 pass判断出碰撞之后核心问题变成“接下来怎么处理”。比较简单的做法是把足球“推”到球员边缘处同时给足球一个沿着两圆心连线方向的速度。方向向量叫法向量计算方式是nx dx / dist ny dy / dist这个(nx, ny)是一个单位向量它的方向是从球员指向足球。然后把球的位置纠正为刚好贴在球员边缘ball_x player_x nx * (player_radius ball_radius) ball_y player_y ny * (player_radius ball_radius)这一步看起来很基础但特别重要。如果不做位置纠正足球会陷入球员内部然后下一帧继续重复碰撞看起来就像球被“粘”在球员身上还疯狂抖动。位置纠正等于把碰撞后的状态拉到合法位置直接解决“粘球”问题。除了圆和圆碰撞游戏的后续扩展不可避免会用到矩形碰撞。Pygame 的Rect类自带colliderect方法如果以后要给球加球门矩形区域判断球是否进入球门可以这样ball_rect pygame.Rect(ball_x - ball_radius, ball_y - ball_radius, ball_radius * 2, ball_radius * 2) if ball_rect.colliderect(goal_rect): # 进球了 pass用矩形的近似检测圆形在某些边界情况下不太精确但对进球判断来说足够用了而且效率很高。后面如果要做得更严谨可以继续细化但第一版用它扩功能完全没问题。3.3 简单的物理模拟摩擦、反弹与冲量所谓“简单的物理模拟”在这个项目里说白了就是三件事速度与位置的更新、摩擦力的模拟、墙壁反弹。第一件事速度与位置更新。把足球的坐标变化拆成水平分量vx和竖直分量vy每一帧都执行ball_x ball_vx ball_y ball_vy这就是最基础的运动学方程相当于每帧根据当前速度移动一个固定的位移。如果帧率稳定这样的运动看起来就是匀速直线运动。第二件事摩擦力。现实里的足球在草地上滚动会因为摩擦逐渐减速最后停下来。模拟这个效果最简单的方式是给速度乘上一个小于 1 的系数ball_vx * friction ball_vy * friction摩擦系数我通常取 0.98。这意味着每帧速度变为原来的 98%60 FPS 下大约每秒变成原来的 0.98 的 60 次方大约是 30%。也就是说足球滚动起来后会在两秒左右明显减速并停下来这个观感很接近真实草坪。如果取 0.99球会滚很久取 0.95球蹭一下就停了。还要加一个速度阈值判断不然球永远以极小的速度滑行看起来像“幽灵球”。这个阈值可以设成 0.05if abs(ball_vx) 0.05: ball_vx 0 if abs(ball_vy) 0.05: ball_vy 0绝对值小于 0.05 的速度人眼已经分辨不出来直接归零球就彻底停了。第三件事墙壁反弹。窗口有四个边界足球撞上去要弹回来。判断的依据是圆心坐标是否跨过了边界方向是反转速度分量if ball_x - ball_radius 0: ball_x ball_radius ball_vx abs(ball_vx) * 0.8 if ball_x ball_radius WIDTH: ball_x WIDTH - ball_radius ball_vx -abs(ball_vx) * 0.8这里乘以 0.8 表示撞墙后损失 20% 的速度模拟非完全弹性碰撞。如果没有这个衰减球会在边界之间无限反弹永远不停看起来也很假。球员与足球碰撞时我给足球一个“冲量”也就是瞬间改变球的速度。为了让球的弹出方向和球员撞来的方向相关我把冲量方向设置成上面的法向量方向impulse player_speed * 0.6 ball_vx nx * impulse ball_vy ny * impulse这样球员从左边撞球球就往右边弹从上面撞球就往下面弹。虽然这不是严格意义上的动量守恒但对一个休闲小游戏来说手感已经足够自然而且代码很简单、容易调。4. 实操过程与核心代码实现4.1 初始化对象与主循环接下来进入正题我把完整代码拆开逐段讲。首先是初始化部分import pygame import math pygame.init() WIDTH, HEIGHT 800, 500 FPS 60 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(Pygame 足球小游戏) clock pygame.time.Clock() WHITE (255, 255, 255) GREEN (34, 139, 34) BLUE (30, 144, 255) player_radius 20 ball_radius 12 player_speed 5 friction 0.98 energy_loss 0.8 player_x, player_y WIDTH // 2, HEIGHT // 2 ball_x, ball_y WIDTH // 2 - 80, HEIGHT // 2 ball_vx, ball_vy 0.0, 0.0 running True初始位置需要注意球员放在窗口中央足球放在球员左侧偏 80 像素的位置。这样玩家一进游戏就能看到球不会因为球落在某个角落而找不到。初始速度设为 0等待玩家去踢它。4.2 球员移动和边界处理主循环内部处理事件之后先读键盘状态并更新球员位置keys pygame.key.get_pressed() dx dy 0 if keys[pygame.K_LEFT] or keys[pygame.K_a]: dx - 1 if keys[pygame.K_RIGHT] or keys[pygame.K_d]: dx 1 if keys[pygame.K_UP] or keys[pygame.K_w]: dy - 1 if keys[pygame.K_DOWN] or keys[pygame.K_s]: dy 1 player_x dx * player_speed player_y dy * player_speed player_x max(player_radius, min(WIDTH - player_radius, player_x)) player_y max(player_radius, min(HEIGHT - player_radius, player_y))球员移动逻辑不算难但有个细节值得注意我们用的是“方向向量乘以速度”而不是在四个方向各自的 if 里直接加减。这样当玩家同时按方向和上方向键时dx 和 dy 都为 1那么球员会同时向右和向下移动对角线方向速度是 sqrt(2) 倍。严格来说对角线方向应该做归一化处理让速度保持恒定。不过在这个项目里5 像素的帧位移量对角线速度略快的误差几乎感知不出来。如果你对细节有追求可以这样修正if dx ! 0 and dy ! 0: dx dx * 0.7071 dy dy * 0.70710.7071 是 1/sqrt(2) 的近似值目的是让对角线方向的速度和直线方向一致。边界处理的写法用了 Python 的max和min比写四个 if 分支优雅得多先限制上限再限制下限球员圆就不会超出窗口边缘。如果不做这个限制球员会带着半个身子跑到窗外后续碰撞检测也可能出错。4.3 足球的物理更新足球的物理更新放在球员更新之后。每一帧先对速度做衰减再把这个速度应用到位置上ball_vx * friction ball_vy * friction if abs(ball_vx) 0.05: ball_vx 0 if abs(ball_vy) 0.05: ball_vy 0 ball_x ball_vx ball_y ball_vy顺序上要小心先乘以摩擦再移动位置。如果先移动位置再乘摩擦相当于这一帧移动的距离是“上一帧速度乘摩擦之后的结果”逻辑上会差半帧在快速运动时可能出现速度值和位置不匹配的感觉。固定顺序能减少这种误差。然后处理墙壁反弹。注意墙壁判断需要同时考虑球半径。如果只判断圆心坐标越过窗口边界那么球的一半会跑到墙壁外面去视觉上像是穿过墙。加上半径偏移后判断的是“球的边缘”是否到达边界if ball_x - ball_radius 0: ball_x ball_radius ball_vx abs(ball_vx) * energy_loss if ball_x ball_radius WIDTH: ball_x WIDTH - ball_radius ball_vx -abs(ball_vx) * energy_loss if ball_y - ball_radius 0: ball_y ball_radius ball_vy abs(ball_vy) * energy_loss if ball_y ball_radius HEIGHT: ball_y HEIGHT - ball_radius ball_vy -abs(ball_vy) * energy_lossabs在这里很关键。当球撞到左边界时vx 应该是正数代表向右弹回所以用abs(ball_vx)后再乘以衰减系数。同理撞到右边界时 vx 应该是负数所以取负的绝对值。这样无论球的当前速度方向如何反弹方向都正确不会出现球“粘”在墙上的情况。4.4 球员与足球的碰撞反馈最后是重头戏——球员和足球的碰撞。我把代码贴在下面delta_x ball_x - player_x delta_y ball_y - player_y dist math.hypot(delta_x, delta_y) if dist player_radius ball_radius: if dist ! 0: nx delta_x / dist ny delta_y / dist else: nx, ny 1, 0 ball_x player_x nx * (player_radius ball_radius) ball_y player_y ny * (player_radius ball_radius) impulse player_speed * 0.6 ball_vx nx * impulse ball_vy ny * impulse这段代码里我做了几件比较关键的事。第一计算法向量前检查了dist ! 0。理论上球员和足球的圆心不可能完全重合但一旦巧合发生除以零会让程序直接报错。防御性写法虽然多两个分支但能避免奇怪的 bug。第二位置纠正先于赋速度。如果先给速度再纠正位置球可能在同一帧里因为位置还未更新导致下一帧继续和球员碰撞。这个顺序一开始我也没注意后来调试时发现球会卡在球员身上疯狂抖动改了顺序之后立刻正常。第三冲量大小取了player_speed * 0.6。为什么是 0.6 而不是别的数这是我反复试出来的。取 1 的话球被撞得太狠一撞就飞出几屏取 0.3 又显得软绵绵球都踢不远。0.6 乘以 5 的球员速度得到 3 像素/帧的初速度配合 0.98 的摩擦力球会滚出大概两三秒的位移手感接近“轻推”而不是“重炮”。当然参数没有绝对标准不同窗口尺寸、不同玩家偏好调整范围很大把它提成常量改起来才方便。如果你想更接近真实弹性碰撞可以用下面的公式把球员和足球的相对速度也考虑进去relative_vx ball_vx - dx * player_speed # 简化把 dx 当作球员移动向量 relative_vy ball_vy - dy * player_speed vdotn relative_vx * nx relative_vy * ny ball_vx - 2 * vdotn * nx ball_vy - 2 * vdotn * ny这在物理上更正确但调试也更麻烦。我第一版就先用固定冲量模型把手感调顺后再考虑升级。到这里完整可运行的代码已经齐了。把所有片段拼起来用screen.fill(GREEN)画背景用pygame.draw.circle画两个圆最后pygame.display.flip()刷新画面就是一个能玩的足球游戏 demo。5. 常见问题与避坑指南5.1 安装失败failed to build pygame 怎么办我在第 2.1 节提过这个报错这里再展开复盘一下。failed to build pygame when getting requirements to build wheel是 Pygame 安装中最常见的报错之一。这句话的意思是pip 在安装 Pygame 时找不到预编译的 wheel 包于是退而求其次尝试从源码构建。构建时又因为缺少编译工具链或 SDL 相关依赖最终失败。解决思路依次如下升级 pip。很多情况下这是由 pip 版本过旧导致无法正确匹配可用 wheel 引起的python -m pip install --upgrade pip指定 Pygame 版本。有时最新版刚发布wheel 还没上传全或者和你的 Python 版本不兼容pip install pygame2.6.1检查 Python 位数。在 Windows 上尽量用 64 位 Python32 位 Python 对很多现代 wheel 支持不全面。安装系统依赖。如果你在 Linux 或某些精简版 Windows 环境需要安装 SDL 相关开发包装完再重试。还有一个可能是你在 Conda 环境里使用了一个非常旧的 Python 版本。比如 Python 3.7 或更早版本Pygame 新版可能已经停止支持这时换成 Python 3.9-3.12 之间的版本通常能解决问题。5.2 运行时常见问题速查表我把开发过程中容易踩的几个运行时问题整理成一张表方便以后遇到快速定位现象可能原因解决方案窗口一闪而过主循环没写、循环条件立即为 False检查是否写了while running确认 QUIT 事件没有被误触发按方向键没反应用的KEYDOWN事件而非key.get_pressed()持续移动改用pygame.key.get_pressed()球员方向反了坐标系 y 轴向下按“上”时把 y 加 1按“上”应让dy - 1球卡在球员身上抖动碰撞后没有做位置纠正碰撞后把球推出到球员边缘外球穿过墙壁帧率过高或每帧位移太大用clock.tick(FPS)锁帧限制最高速度CPU 占用过高没有锁帧率每帧调用clock.tick(FPS)球一直停不下来摩擦系数设成了 1 或接近 1用 0.95-0.99 之间的值并加速度阈值判断这些问题的根源其实都很基础但每个我都亲手踩过。尤其是“窗口一闪而过”新手时期我常常忘了写pygame.quit()和主循环或者把while running写成了while not running结果窗口刚弹出来就关了排查了半天才发现是逻辑写反了。结合报错信息逐个核对比盲猜快得多。5.3 碰撞与物理的几个细节技巧最后分享几个在调试碰撞和物理时特别实用的技巧。第一个技巧把碰撞检测和位置更新分成两轮。不要在一个对象的位置更新代码里又更新位置又判断碰撞也没有直接写入主循环。先把所有对象的位置更新完再统一检测碰撞并修正位置。这样做能避免“A 更新时 B 还没更新导致碰撞漏判”这类问题。项目简单时影响不大但养成这个习惯对后续做复杂游戏非常重要。第二个技巧碰撞后的“推送”距离要大于碰撞发生时穿透的距离。我把球推出到“刚好接触”的位置但在高速运动时这个位置在下一帧可能又会立即进入球员内部。一个更稳妥的做法是推出到两者间距比理论接触距离多 1-2 像素ball_x player_x nx * (player_radius ball_radius 1) ball_y player_y ny * (player_radius ball_radius 1)这个 1 像素的多余间隔就是安全余量可以明显减少高速运动下“粘球”的概率。第三个技巧调参数时一次只改一个。FPS、速度、摩擦系数、冲量系数这四个参数互相影响如果同时改两个以上你会分不清手感变化来自哪个参数。我的做法是先固定 FPS60然后只调玩家速度调到移动顺手再固定玩家速度调摩擦系数调到一个球能滚出大概 3 秒最后再调冲量系数找到“轻推”和“重炮”之间的感觉。每个参数都单独验证别偷懒。第四个技巧给球的速度做上限截断。玩家疯狂撞球时球速度可能叠加到每帧十几像素一帧就穿过整个球员碰撞检测直接失效。加个最大速度限制能让物理表现稳定很多max_speed 10 speed_now math.hypot(ball_vx, ball_vy) if speed_now max_speed: ball_vx ball_vx / speed_now * max_speed ball_vy ball_vy / speed_now * max_speed这段代码本质上是把速度向量缩放到最大速度范围内属于很常见的 clamp 操作。加到物理更新后面球的运动就不会因为异常碰撞而失控。这个足球项目做完后你会发现它已经搭好了一个非常通用的框架把两个圆替换成角色和子弹就是简单的射击游戏把摩擦去掉、加上重力就能做平台跳跃把球员改成 AI 控制就变成了自动演示程序。核心的输入、碰撞、物理三块逻辑全都在这里了。我个人做下来的最大感受是游戏开发里最容易翻车的往往不是“高端物理”而是那些看着最基础的东西比如按键判断错了、碰撞后没做位置纠正、帧率没锁导致物理紊乱。把这篇里提到的细节都处理干净再往这个框架里加功能你会省下大量查 bug 的时间。
返回列表