ARTICLE DETAIL

资讯详情

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

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

GameDevMind 物理系统实战:用纯 Python 实现 Verlet 积分 + AABB 碰撞模拟 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 等任何第三方库仅使用math、random、time、sys四个标准库模块见源码文件头部导入区配合一个自实现的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, velocityVec2(), radius1.0, mass1.0, color_char●): self.pos position # Verlet: prev_pos pos - velocity * dt (假设 dt1) self.prev_pos position - velocity ... def velocity(self): 从 Verlet 积分还原速度 return self.pos - self.prev_pos注意两处关键细节构造时的速度注入prev_pos position - velocity即「上一帧位置 当前位置 - 速度」把初始速度翻译成位置差从而进入 Verlet 的隐式速度框架这里假设dt 1速度还原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 个一维区间测试AABBAxis-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)逻辑拆解对每个轴计算两个方向的穿透量取较小值作为该轴穿透深度比较 X/Y 轴穿透深度取穿透更浅的轴作为分离轴这是「最小分离」的含义根据 AABB 的相对位置确定分离方向1或-1返回一个只含单轴分量的向量即 MTV。MTV 同时服务于碰撞响应与静态碰撞修正两处调用见下文。2.4 碰撞响应位置修正 速度方向反弹文档定义概念实现碰撞响应位置修正 速度方向反弹粒子与粒子的碰撞_resolve_particle_collisionmtv 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_collisionmtv 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 4step()内部把一帧的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(dt1.0)内部是典型的求解器管线每个子步执行一遍① Verlet 积分所有粒子施加重力 阻尼 ↓ ② 粒子间 AABB 碰撞O(n²) 两两检测 MTV 位置修正 ↓ ③ 粒子 vs 静态体碰撞位置修正 法向反弹 ↓ ④ 边界约束clamp 到世界边界 弹性反弹这与真实引擎的「积分 → 碰撞检测broadphase → narrowphase→ 求解 → 约束」管线在概念上完全同构只是去掉了空间加速结构与迭代求解器。3.2 ASCII 可视化与程序输出AsciiRendererphysics_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.995main 中同上 /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可以亲身体验性能瓶颈然后对照四叉树方案理解优化方向。物理中间件选型上图谱文档归纳了三大主流引擎PhysXNVIDIA硬件加速、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),仅供参考
返回列表