游戏物理与碰撞:刚体、碰撞检测与响应

深入游戏物理引擎核心:刚体动力学(力、速度、积分)、碰撞检测的宽相位与窄相位(包围体层级、分离轴定理 SAT、GJK)、碰撞响应与约束求解(脉冲法、位置修正)、物理引擎对比(Box2D/PhysX/Bullet),含 Unity/Godot 物理系统实战。

在固定步长帧循环里,物理系统是"世界会不会穿模、子弹会不会命中"的判定者。很多游戏开发者把物理引擎当黑盒用——拖个 Rigidbody、加个 Collider 完事,直到遇到高速穿透、角色抖进墙里、布娃娃爆炸这些问题时才意识到需要理解引擎内部。本文从刚体动力学出发,拆解碰撞检测的宽/窄相位、碰撞响应的脉冲与约束求解,最后横向对比 Box2D、PhysX、Bullet 三大引擎的定位与选型。

前置知识:固定步长帧循环见 游戏引擎架构:ECS 与资源管理;物理相关的网络确定性要求见 游戏网络同步:延迟补偿与回滚。

1. 刚体动力学:世界如何动起来

1.1 运动学基础

刚体(Rigid Body)在物理引擎里是一个"有质量、有速度、可受力"的对象。引擎用积分逐帧推进状态:

每个固定步长 dt:
  v += (a) * dt                  // 速度 = 速度 + 加速度×dt
  x += v * dt                    // 位置 = 位置 + 速度×dt

其中加速度来自外力(重力、推力、碰撞产生的冲量)。这是显式欧拉积分,简单但误差累积。质量越大的物体越难被推动,体现在冲量分配上:Δv = J / m,质量 m 越大,同样冲量 J 造成的速度变化越小。

1.2 力的种类与接口

物理引擎对外暴露的典型力接口:

// 伪代码:物理引擎力接口
body.AddForce(Vector3 force, ForceMode mode);
body.AddTorque(Vector3 torque, ForceMode mode);
body.AddImpulse(Vector3 impulse);          // 瞬时冲量,如子弹命中
body.SetVelocity(Vector3 v);               // 直接设速度(可能破坏物理一致性)
ForceMode含义适用
Force持续力(F,加速度 = F/m)推力、风
Impulse瞬时冲量(J,Δv = J/m)射击、爆炸
VelocityChange无视质量直接改速度玩家控制角色
Acceleration加速度(无视质量)重力、恒定力

法则:能不用 SetVelocity 就别用,直接改速度会跳过引擎的约束求解,容易导致"物体穿透 + 抖动"。

1.3 刚体类型对比

类型动力学参与碰撞响应典型用途
Static(静态)不参与,质量无限大只做碰撞面地面、墙
Kinematic(运动学)手动控制速度只推动态物体电梯、平台
Dynamic(动态)完全模拟全参与玩家、可推动物

Godot 的 RigidBody3D/CharacterBody3D/StaticBody3D、Unity 的 Rigidbody(IsKinematic)都遵循这套三分法。玩家角色建议用 Kinematic 自己控制 + 手动检测碰撞,而不是 Dynamic——Dynamic 角色会被物理引擎推得满地打滑。

2. 碰撞检测:宽相位与窄相位

碰撞检测是物理引擎最贵的部分,因此被拆成两阶段:

宽相位(Broad Phase):找出"可能相交"的候选对(快速排除)
    ↓
窄相位(Narrow Phase):对候选对做精确的几何相交测试
    ↓
碰撞响应(Response):计算冲量/约束并更新状态

2.1 宽相位:包围体与空间索引

宽相位用廉价的包围体快速排除明显不相交的物体。常用结构:

结构原理复杂度适用
暴力两两每对测试O(n²)n < 100
Sweep and Prune (SAP)按轴排序扫描,只测重叠区间O(n log n)2D 物理
均匀网格空间分桶,桶内测试O(n) 摊还大量均匀分布物体
BVH / 四叉树 / 八叉树包围体层级树O(log n) 查询3D 大世界
// Sweep and Prune 的思想:把 AABB 投影到轴上,按 min/max 排序
// 只有 min_A < max_B 且 min_B < max_A 的重叠区间才进入窄相位
struct AABB { float minX, maxX, minY, maxY; };

bool Overlap(AABB a, AABB b) {
    return a.minX < b.maxX && b.minX < a.maxX &&
           a.minY < b.maxY && b.minY < a.maxY;
}

关键优化:运动预测(Swept)。物体高速移动时,单纯做瞬时相交测试会漏掉"上一帧没穿、这一帧已经穿透"的情况。宽相位常用扫描体(Swept AABB)——把这一帧的运动轨迹包起来再测。

2.2 窄相位:精确相交测试

窄相位对候选对做精确测试。最经典的三种:

分离轴定理(SAT):两个凸多边形不相交,当且仅当存在一条分离轴,沿该轴投影的区间不重叠。测试所有候选法线轴(每边法线 + 每对顶点连线法线)。

// SAT 核心:对每个轴做投影区间重叠测试
bool TestOverlapOnAxis(Polygon a, Polygon b, Vector2 axis) {
    float aMin = Project(a, axis).min, aMax = Project(a, axis).max;
    float bMin = Project(b, axis).min, bMax = Project(b, axis).max;
    return aMin <= bMax && bMin <= aMax;
}

bool SATIntersect(Polygon a, Polygon b) {
    foreach (var axis in GetCandidateAxes(a, b))
        if (!TestOverlapOnAxis(a, b, axis))
            return false;   // 找到分离轴即不相交
    return true;            // 所有轴都重叠才相交
}

GJK(Gilbert-Johnson-Keerthi):利用明可夫斯基差(Minkowski Difference)——两物体相交当且仅当原点在两者之差内。用单纯形(simplex)逐步逼近,收敛快,是 3D 物理引擎(PhysX、Bullet)的默认方案。

连续碰撞检测(CCD):处理高速穿透的终极手段。常用 swept volume / conservative advancement,把运动轨迹卷成扫掠体再测试。代价高,只对高速小物体(子弹、箭矢)开启。

2.3 碰撞形状的选择

形状内存精度典型用途
AABB(轴对齐盒)极低低宽相位、静态环境
OBB(有向盒)低中中等物体
Sphere / Capsule极低低玩家角色、子弹
Convex Hull中高复杂但凸的物体
三角网格(Mesh Collider)高最高地形、静态场景

铁律:非凸物体的碰撞形状必须用多个凸形状组合(Compound Collider),物理引擎的窄相位只支持凸体相交测试。用 Mesh Collider 做动态物体碰撞是性能灾难。

3. 碰撞响应:脉冲与约束

3.1 脉冲法(Impulse)

找到碰撞点后,引擎计算一个沿法线方向的冲量 J,让两物体在碰撞点分离。核心公式(简化版):

J = -(1 + e) * (v_rel · n) / (1/m1 + 1/m2 + ...)

其中 e 是恢复系数(restitution,0 完全非弹性、1 完全弹性),v_rel 是相对速度,n 是碰撞法线。

// 伪代码:一次脉冲碰撞响应
void ResolveCollision(RigidBody a, RigidBody b, Vector2 normal) {
    float relV = Vector2.Dot(b.velocity - a.velocity, normal);
    if (relV > 0) return;               // 正在分离,无需处理

    float e = Math.Min(a.restitution, b.restitution);
    float invMassSum = a.invMass + b.invMass;
    float j = -(1 + e) * relV / invMassSum;   // 冲量大小

    a.velocity -= (j * normal) * a.invMass;
    b.velocity += (j * normal) * b.invMass;
}

3.2 约束求解与位置修正

单纯脉冲法在堆叠(一摞箱子)和多接触点场景下会不稳定——每个接触点单独解会产生抖动。现代引擎用约束求解器(Constraint Solver):把所有接触点(和关节)组织成约束方程组,迭代求解(Sequential Impulse,顺序冲量,常见迭代 4-10 次)。

迭代求解(每次迭代逐步改进):
  for iter in 1..8:
    for each contact:
      J = solve_contact(contact)   // 顺序处理每个接触点
      apply_impulse(contact, J)

位置修正(Baumgarte Stabilization / Split Impulse):避免物体陷入地面时靠速度反弹"滑进去"——求解后额外按穿透深度给一个小位移修正。注意修正系数过大物体会抖。

3.3 关节与约束类型

关节约束自由度用途
距离关节(Distance)保持两点距离绳索、锁链
铰链关节(Hinge)绕一轴旋转门、摆锤
滑动关节(Prismatic)沿一轴平移活塞、滑轨
弹簧关节(Spring)带弹性距离车辆悬挂、布娃娃

3.4 触发器(Trigger)与物理查询

不是所有碰撞都需要"弹开"。**触发器(Trigger/传感器)**只报告重叠事件、不做物理响应,用于检测区、伤害区域、捡拾判定:

// Unity:IsTrigger 的碰撞体只触发事件不产生冲量
void OnTriggerEnter(Collider other) {
    if (other.CompareTag("Player")) ApplyDamage(other);
}

物理引擎还提供查询接口用于主动检测,这是玩法代码的常用工具:

查询用途代价
Raycast(射线检测)射击命中、视线便宜,可每帧大量调用
SphereCast / BoxCast移动碰撞扫描中
OverlapSphere / OverlapBox范围敌人检测中
凸包扫描(Sweep)连续移动检测贵,谨慎使用
// Unity:每帧一次的射线查询
if (Physics.Raycast(ray, out var hit, range, layerMask)) {
    hit.collider.GetComponent<IDamageable>()?.TakeDamage(damage);
}

查询优化:射线/范围查询也要用 LayerMask 过滤(只测相关层),否则引擎会对无关物体做相交测试。大世界尽量用**空间索引(宽相位结构)**做粗筛选,再做精确查询。

4. 物理引擎对比与选型

维度Box2DPhysXBullet
维度2D3D3D
语言C++(被移植无数)C++(NVIDIA 维护)C++
求解器顺序冲量PGS/迭代顺序冲量 + 各类改进
特色简单稳定、教学首选工业级、NVIDIA 生态开源、游戏+机器人
依赖无闭源 SDK(有开源版)纯开源
集成Godot 默认 2D、Cocos2dUnity 默认物理Blender、机器人仿真

选型指南:

  1. 2D 游戏:直接用 Godot 内置(基于自研+Box2D 思想)或 Lua + Box2D 生态。
  2. Unity 项目:PhysX 是默认且深度集成(PhysX 4),一般不需要换。
  3. 自研引擎 / 仿真:Bullet 开源、文档全、跨平台成熟。
  4. 需要确定性(帧同步回滚,见 游戏网络同步:延迟补偿与回滚):Box2D 与 Bullet 都提供确定性模式,但浮点一致性是硬伤——同一引擎在不同平台结果可能不同,需要做定点数替换。

4.1 物理引擎的三大性能热点

热点成本优化
宽相位O(n log n)减少碰撞体数量、合理尺寸(别用超大 Mesh)
窄相位精确几何测试优先用球/胶囊,少用 Mesh Collider
约束求解每接触点迭代限制迭代次数、减少关节数量

4.2 物理子步(Sub-stepping)与休眠(Sleeping)

子步:当物体速度过快或碰撞密集时,单步 dt=1/60 可能不够精确。引擎可在一个固定步长内跑多次子步(如 2-4 次 1/240),提升稳定性,但成本线性上升。只在需要的地方(高速场景)开子步,全局开启是浪费。

休眠(Sleeping):物理引擎的自动优化——静止的刚体自动进入休眠,停止参与模拟,直到受到外力才唤醒:

刚体静止 → 速度/角速度 < 阈值 连续 N 帧 → 进入休眠(不再模拟)
被碰撞/受力 → 唤醒 → 重新模拟

休眠是移动端物理性能的隐形功臣:一摞箱子堆完静止后,物理成本几乎归零。坑:玩家推了一下静止物体却发现推不动——那是它仍在休眠,需要 WakeUp() 或调低休眠阈值。

4.3 常见物理故障排查表

故障现象大概率原因排查方向
高速穿透未开 CCD / 子步不足开 CCD、加子步、缩小碰撞体
堆叠抖动约束迭代不足 / 位置修正过大提高迭代次数、调 Baumgarte 系数
角色滑墙用 Dynamic 做角色换 Kinematic/CharacterController
布娃娃爆炸关节强度不足 / 大穿透调关节阻尼、加位置修正
物理突然卡顿大量刚体同时唤醒查休眠阈值、减少碰撞体数

5. 物理层最佳实践与总结

物理系统设计检查单:

  1. 固定步长驱动物理:物理必须在固定步长(Unity FixedUpdate / Godot _PhysicsProcess)内推进,否则结果不确定(回滚/帧同步大忌)。
  2. 高速物体开 CCD:子弹、箭、移动平台,否则必然穿透。
  3. 角色用 Kinematic / CharacterController:动态角色手感差、容易卡墙。
  4. Compound Collider 而非 Mesh Collider:动态物体碰撞必须凸分解。
  5. 分层(Layer + Mask):只让需要碰撞的层互相碰撞,减少无效候选对(如玩家子弹不跟玩家碰撞)。
  6. 物理与逻辑分离:物理表现(布娃娃、碎屑)与游戏判定(伤害计算)解耦,物理失败不应影响判定。

调试技巧:

  • Unity:Physics Debugger(可视化碰撞体、接触点、velocity)。
  • Godot:Rendering > Visible Collision Shapes。
  • 通用:把 dt 调小看是否稳定(排除积分误差);把穿透深度打印出来看是否超阈值。

物理引擎不是魔法:理解宽/窄相位就能定位"为什么碰撞检测慢",理解冲量与约束就能解决"为什么堆叠会抖",理解确定性限制就能避免回滚网络游戏里"各平台结果不一致"的灾难。

相关阅读:游戏网络同步:延迟补偿与回滚 讲解物理确定性与回滚的结合;游戏渲染管线基础 讲解物理结果如何被渲染消费;游戏引擎架构:ECS 与资源管理 讲解物理在帧循环中的位置。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「game」更多文章

  1. 游戏性能剖析与优化:从 Profiler 到平台适配
  2. 游戏网络同步:延迟补偿、客户端预测与回滚
  3. 游戏 AI:行为树、寻路与群集行为