GameDevMind 物理系统实战:用纯 Python 实现 Verlet 积分 + AABB 碰撞模拟
2026/9/17 18:09:05 网站建设 项目流程

GameDevMind 物理系统实战:用纯 Python 实现 Verlet 积分 + AABB 碰撞模拟

【免费下载链接】GameDevMind最全面的游戏开发技术图谱(Game Development Map)。帮助游戏开发者们在已知问题上节省时间,省出更多的精力投入到更有创造性的工作中去。项目地址: https://gitcode.com/GitHub_Trending/ga/GameDevMind

本篇文章基于 GameDevMind 仓库中「二-02 物理系统」配套示例,通过 physics_demo.py 这份纯标准库、零第三方依赖的 Python 实现,完整讲解游戏物理引擎的两个核心基石——Verlet 位置积分AABB 轴对齐包围盒碰撞检测,并延伸到 MTV 最小分离向量、质量比碰撞响应、静态碰撞体与边界约束。读完本文,你将能够从零搭建一个可运行、可调参的 2D 粒子物理沙盒,并理解它与 Box2D、PhysX 等真实物理引擎之间的能力差距,为后续深入学习 物理图谱文档 打好代码基础。

一、示例概览:一个自包含的物理沙盒

1.1 运行方式

示例位于 code/artile-sample-code/02-technical/02-physics/,运行只需一条命令:

python physics_demo.py

纯标准库,无需安装任何依赖。程序不依赖 pygame、numpy 等任何第三方库,仅使用mathrandomtimesys四个标准库模块(见源码文件头部导入区),配合一个自实现的AsciiRenderer在终端里以字符画形式渲染每一帧物理状态。这意味着它可以在任何装有 Python 3 的机器上直接运行,非常适合作为理解物理引擎原理的入门载体。

1.2 功能清单

从文档与源码中可以确认,这个 demo 实现了以下物理引擎核心能力:

  • Verlet 积分器:位置驱动,无需显式存储速度(速度由pos - prev_pos隐式表达),天然稳定;
  • AABB 碰撞检测:轴对齐包围盒快速重叠测试,只需 4 个一维区间比较;
  • MTV 分离:最小分离向量(Minimum Translation Vector)驱动碰撞响应,按质量比分配位移;
  • 静态碰撞体:平台、地面等不可动物体,模拟关卡地形;
  • 边界约束:弹性边界反弹,防止粒子飞出世界;
  • 子步骤(Substepping):每帧 4 次子步,提高积分精度与碰撞稳定性。

二、核心概念拆解:从公式到代码

文档给出了五个核心概念的实现要点,下面逐一对照源码展开。

2.1 Verlet 积分:位置驱动,速度隐含

文档中的核心公式:

概念实现
Verlet 积分pos_new = pos + (pos - prev_pos) * damping + gravity * dt²

与欧拉积分(存储velocity,每帧pos += vel * dt; vel += acc * dt)不同,Verlet 积分只维护两个量:当前位置pos与上一帧位置prev_pos。速度被隐式地表示为两帧位置的差值。

源码中 Particle 的定义印证了这一点:

class Particle: """Verlet 积分粒子 — 位置 + 上一帧位置 (速度隐含)""" __slots__ = ("pos", "prev_pos", "radius", "mass", "color_char", "id") def __init__(self, position, velocity=Vec2(), radius=1.0, mass=1.0, color_char="●"): self.pos = position # Verlet: prev_pos = pos - velocity * dt (假设 dt=1) self.prev_pos = position - velocity ... def velocity(self): """从 Verlet 积分还原速度""" return self.pos - self.prev_pos

注意两处关键细节:

  1. 构造时的速度注入prev_pos = position - velocity,即「上一帧位置 = 当前位置 - 速度」,把初始速度翻译成位置差,从而进入 Verlet 的隐式速度框架(这里假设dt = 1);
  2. 速度还原velocity()只是简单的pos - prev_pos,在碰撞响应与动能统计时随时可还原出速度向量。

积分步在 PhysicsWorld.step() 中完成:

for p in self.particles: velocity = p.velocity() * self.damping p.prev_pos = Vec2(p.pos.x, p.pos.y) # 保存当前位置 p.pos = p.pos + velocity + self.gravity * (sub_dt * sub_dt)

这里严格对应文档公式pos_new = pos + (pos - prev_pos) * damping + gravity * dt²:先对速度施加阻尼系数damping,再叠加重力项gravity * dt²。因为采用位置驱动,修改prev_pos就等于修改速度——这是后面实现碰撞反弹与边界反弹的核心技巧。

2.2 AABB 重叠检测:4 个一维区间测试

文档给出的定义:

概念实现
AABB 重叠4 个一维区间测试

AABB(Axis-Aligned Bounding Box,轴对齐包围盒)用min(左下角)与max(右上角)两个点描述一个轴对齐矩形。两个 AABB 是否重叠,只需在 X、Y 两个轴向上分别做区间重叠判断。

源码中 AABB.overlap():

@staticmethod def overlap(a, b): """检测两个 AABB 是否重叠""" return ( a.min.x < b.max.x and a.max.x > b.min.x and a.min.y < b.max.y and a.max.y > b.min.y )

之所以叫「4 个一维区间测试」,是因为 X 轴两个条件、Y 轴两个条件,共 4 个比较。这份实现在数学上等价于 SAT(分离轴定理)在轴对齐情况下的特例——仓库中另一处 game_math.py 的 3D AABB 用 6 个条件实现了同样的逻辑,且其 docstring 明确注明「SAT 分离轴定理的特例」,可以互相印证。

对于圆形粒子,这里用radius生成方形包围盒(get_aabb()返回AABB(pos, Vec2(radius, radius))),即用 AABB 粗粒度近似圆形——这正是真实引擎中「粗检测阶段」的典型做法。

2.3 MTV 最小分离向量:取穿透最浅的轴

文档定义:

概念实现
MTV (最小分离向量)X/Y 轴穿透深度取最小

两个 AABB 重叠后,需要找到推动它们分开的最小位移。做法是分别计算 X、Y 两轴的穿透深度,取较小者作为分离轴,方向指向让两者分开的一侧。

源码 AABB.penetration():

@staticmethod def penetration(a, b): """计算最小分离向量 (MTV)""" overlap_x = min(a.max.x - b.min.x, b.max.x - a.min.x) overlap_y = min(a.max.y - b.min.y, b.max.y - a.min.y) if overlap_x < overlap_y: # X 轴分离 direction = 1.0 if a.max.x - b.min.x < b.max.x - a.min.x else -1.0 return Vec2(overlap_x * direction, 0.0) else: direction = 1.0 if a.max.y - b.min.y < b.max.y - a.min.y else -1.0 return Vec2(0.0, overlap_y * direction)

逻辑拆解:

  1. 对每个轴计算两个方向的穿透量,取较小值作为该轴穿透深度;
  2. 比较 X/Y 轴穿透深度,取穿透更浅的轴作为分离轴(这是「最小分离」的含义);
  3. 根据 AABB 的相对位置确定分离方向(+1-1);
  4. 返回一个只含单轴分量的向量,即 MTV。

MTV 同时服务于碰撞响应静态碰撞修正两处调用(见下文)。

2.4 碰撞响应:位置修正 + 速度方向反弹

文档定义:

概念实现
碰撞响应位置修正 + 速度方向反弹

粒子与粒子的碰撞(_resolve_particle_collision):

mtv = AABB.penetration(box_a, box_b) total_mass = a.mass + b.mass # 按质量比分配位移 ratio_a = b.mass / total_mass if total_mass > 0 else 0.5 ratio_b = a.mass / total_mass if total_mass > 0 else 0.5 a.pos = a.pos + mtv * ratio_a b.pos = b.pos - mtv * ratio_b

粒子与静态体的碰撞(_resolve_static_collision):

mtv = AABB.penetration(box_p, body) p.pos = p.pos + mtv # 静态体质量无限大,位移全部由粒子承担 # 速度响应:沿 MTV 方向反弹 (简单系数) vel = p.velocity() mtv_dir = mtv.normalized() proj = vel.dot(mtv_dir) if proj < 0: # 仅当速度朝向碰撞面时反弹 # 修改 prev_pos 实现反弹 (restitution ≈ 0.3) p.prev_pos = p.prev_pos + mtv_dir * (proj * 1.3)

反弹技巧值得专门说明:proj是速度在 MTV 方向上的投影,proj < 0表示粒子正朝碰撞面运动。此时通过修改prev_pos间接修改速度——prev_pos沿法线方向向后多推proj * 1.3,等价于给速度施加了约 0.3 的恢复系数(restitution),模拟了弹性碰撞的能量损失。这正体现了 Verlet 积分「改位置即改速度」的便利性。

2.5 质量比:位移按质量反比分配

文档定义:

概念实现
质量比碰撞位移按mass_other / total_mass分配

两个粒子碰撞时,质量小的物体应被推开更多,质量大的被推开更少。源码中的分配公式为:

ratio_a = b.mass / total_mass # a 拿到的位移比例 = 对方质量 / 总质量 ratio_b = a.mass / total_mass # b 拿到的位移比例 = 对方质量 / 总质量 a.pos = a.pos + mtv * ratio_a b.pos = b.pos - mtv * ratio_b

验证极端情况:若a.mass极大、b.mass极小,则ratio_a ≈ 0(重物几乎不动),ratio_b ≈ 1(轻物被完全推开),符合物理直觉。main()中粒子质量设为random.uniform(0.5, 2.0),正是为了让观众直观看到轻重粒子碰撞时的差异。

2.6 边界约束与子步骤

边界约束_constrain_to_bounds):将粒子位置 clamp 在世界边界AABB(Vec2(0, 0), Vec2(50, 25))内,并通过反向调整prev_pos实现弹性反弹(反弹系数 0.3)。四条边(左右下上)对称处理,属于「弹性边界反弹」。

子步骤substeps = 4):step()内部把一帧的dt拆成 4 个sub_dt,每帧依次执行「Verlet 积分 → 粒子间碰撞 → 粒子与静态体碰撞 → 边界约束」共 4 轮。子步数越多,积分与碰撞的精度越高,但计算量也线性增长——这正是真实引擎「固定时间步长 + 多子步」思路的微缩版,对应图谱文档中「不稳定 → 调整时间步长、固定步长」的优化方向。

三、源码结构:四个核心类逐个拆解

整个 demo 由四个类构成,结构清晰、职责单一:

文件位置职责
Vec2physics_demo.py#L25-L58二维向量:加减乘除、点积、长度、归一化
AABBphysics_demo.py#L66-L96轴对齐包围盒:重叠检测 + MTV 计算
Particlephysics_demo.py#L104-L127Verlet 粒子:位置、上一帧位置、半径、质量
PhysicsWorldphysics_demo.py#L134-L230物理世界:重力、阻尼、静态体、积分与碰撞求解

3.1 每帧的求解管线

PhysicsWorld.step(dt=1.0)内部是典型的求解器管线(每个子步执行一遍):

① Verlet 积分(所有粒子施加重力 + 阻尼) ↓ ② 粒子间 AABB 碰撞(O(n²) 两两检测 + MTV 位置修正) ↓ ③ 粒子 vs 静态体碰撞(位置修正 + 法向反弹) ↓ ④ 边界约束(clamp 到世界边界 + 弹性反弹)

这与真实引擎的「积分 → 碰撞检测(broadphase → narrowphase)→ 求解 → 约束」管线在概念上完全同构,只是去掉了空间加速结构与迭代求解器。

3.2 ASCII 可视化与程序输出

AsciiRenderer(physics_demo.py#L238-L283)将世界坐标映射到 80×30 的字符网格:静态平台用填充、粒子用●○◆◇★☆▲△等字符绘制、边界用┌┐└┘│─绘制。main()(physics_demo.py#L291-L355)模拟 6 个显示帧(每显示帧内部跑 5 个物理步),并在每帧末尾打印总动能平均高度两项统计:

total_ke = sum(p.velocity().length() ** 2 * p.mass for p in world.particles) * 0.5 avg_y = sum(p.pos.y for p in world.particles) / len(world.particles)

总动能½mv²可以直观观察系统能量的衰减趋势(阻尼与反弹损耗),平均高度则反映粒子整体沉降过程——这两项指标也是后续调参时判断「物理是否稳定」的直观依据。

四、可调参数:把沙盒调出不同物理手感

文档明确指出所有参数集中在PhysicsWorld.__init__()main()两处。对照源码整理如下:

参数默认值位置说明
gravity0.5(向下)PhysicsWorld.__init__重力向量Vec2(0.0, 0.5),单位/帧²
damping0.99(构造)/0.995(main 中)同上 /main()每帧速度阻尼系数,越大衰减越慢
substeps4PhysicsWorld.__init__每帧子步数,越大越稳定、越慢
粒子数量15main()循环粒子总数
初始位置random.uniform(2, 48)/(18, 24)main()粒子出生区域
初始速度(-3..3, -5..-1)main()初始速度范围,向上抛撒
粒子半径0.5main()碰撞体积
粒子质量random.uniform(0.5, 2.0)main()参与质量比碰撞响应
平台位置3 个静态体main()中间、右侧、左侧平台

main()中的场景配置(physics_demo.py#L300-L321):

# 中间平台 world.add_static_body(AABB(Vec2(15, 15), Vec2(10, 1))) # 右侧平台 world.add_static_body(AABB(Vec2(35, 10), Vec2(8, 1))) # 左侧小平台 world.add_static_body(AABB(Vec2(5, 20), Vec2(5, 1)))

AABB(center, half_size)的构造约定是「中心点 + 半宽高」,因此Vec2(15, 15)表示平台中心、Vec2(10, 1)表示半宽 10、半高 1——即一个 20×2 的扁平平台。

调参建议(基于源码行为推断):

  • 想让粒子更"飘":调小gravity(如 0.2);
  • 想让粒子更"粘稠"(水中感):调小damping(如 0.95);
  • 解决高速穿透/抖动:增大substeps(如 8),这是图谱文档「不稳定 → 固定步长」建议的最直接落地方式;
  • 想观察质量差异:扩大mass的随机范围,如random.uniform(0.2, 5.0)
  • 修改场景关卡:增删add_static_body调用即可,如添加一个斜坡(旋转 AABB 无法表达,这正是 AABB 的局限——真实引擎用 OBB/凸包表达斜面)。

注意:由于程序以 ASCII 渲染,世界范围建议保持与AsciiRenderer默认的AABB(Vec2(0,0), Vec2(50,25))一致(main()中已通过renderer.world_bounds = world.bounds同步),大幅改动边界需同步调整渲染器。

五、局限与真实物理引擎对比

文档明确指出:本 demo 是教学简化版,真实引擎(Box2D、PhysX)在此之上增加了四项关键能力:

能力说明本 demo 的对应缺失
旋转刚体 + 惯性张量刚体的旋转自由度、角速度、转动惯量只有平移,无旋转
连续碰撞检测 (CCD)高速物体防穿透(Discrete 检测可能"穿墙")只有离散检测,高速可能穿透
约束求解器(关节、铰链)Hinge/Ball/Fixed Joint、绳索、链条无关节系统
空间划分(BVH、网格)将碰撞从 O(n²) 降到 O(n log n)粒子间两两检测 O(n²)

对照 物理图谱文档 中「碰撞检测模式」章节:本 demo 使用的是Discrete(离散)模式,性能好但快速物体可能穿透,这与真实引擎中「快速物体用Continuous、慢速用Discrete」的取舍完全对应。

关于空间划分,仓库中的 四叉树示例 给出了直接量化证据:500 个对象暴力检测需执行C(500,2) = 124,750次 AABB 判断(耗时约 15420 µs),而四叉树粗检测后仅需约 412 µs,加速约 37 倍。本 demo 的粒子间碰撞正是暴力 O(n²) 实现——读者若把粒子数量调大(如 500+),可以亲身体验性能瓶颈,然后对照四叉树方案理解优化方向。

物理中间件选型上,图谱文档归纳了三大主流引擎:PhysX(NVIDIA,硬件加速、PC/主机)、Bullet(开源免费、跨平台、PC/移动)、Havok(商业、大型游戏、支持好)。本 demo 的意义在于:无论最终选用哪个引擎,其内部的积分器、包围体、MTV 求解思路都与本文代码同源。

六、延伸阅读与下一步

  • 物理系统图谱文档:刚体、碰撞体选型(Box/Sphere/Capsule/Mesh/Compound)、触发器、物理材质、摩擦力与弹性系数、约束与软体的完整知识框架;
  • 游戏数学 AABB 实现:3D 版 AABB 的intersects/contains_point实现,可对照学习二维到三维的推广;
  • 四叉树空间分割:把本 demo 的 O(n²) 碰撞升级为 O(n log n) 的完整方案与性能实测;
  • 04-math 配套代码:向量、点积、叉积、Lerp/Slerp 等物理模拟所需的前置数学工具。

动手建议:先原样运行python physics_demo.py观察 6 帧 ASCII 动画与动能衰减曲线;随后依次尝试——调大substeps消除抖动、调小damping观察能量快速损耗、增加粒子数量感受 O(n²) 瓶颈、添加/移动平台构建自己的关卡。这套「先跑通、再调参、后改造」的路径,正是从理解物理引擎原理走向真实引擎实战的最短路线。

【免费下载链接】GameDevMind最全面的游戏开发技术图谱(Game Development Map)。帮助游戏开发者们在已知问题上节省时间,省出更多的精力投入到更有创造性的工作中去。项目地址: https://gitcode.com/GitHub_Trending/ga/GameDevMind

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询