T.TAO
返回博客
/8 min read/Game Engine

Unity Engine #5 物理

#Unity#GameEngine#CSharp

这篇笔记主要写一下关于碰撞检测和刚体(Rigidbody)组件的一些特性。

物理更新发生在什么时候

Unity 的物理不是在 Update 里跑的,而是在 FixedUpdate 里。两者的区别是:

  • Update 每渲染一帧调用一次,间隔随帧率浮动。
  • FixedUpdate 按固定时间步调用,默认 0.02 秒(50 Hz),与帧率无关。如果一帧耗时较长,同一帧内 FixedUpdate 可能被调用多次;如果一帧很快,也可能一次都不调用。

由此得出一条基本规则:所有对刚体施加的力和速度修改都应该写在 FixedUpdate。写在 Update 里,施力的次数会随帧率变化,游戏在不同机器上手感就不一样。

C#void FixedUpdate()
{
    rb.AddForce(direction * force, ForceMode.Force);
}

反过来,输入采集必须写在 UpdateInput.GetKeyDown 只在一帧内为真,放进 FixedUpdate 会漏掉。常见写法是在 Update 里记录意图,在 FixedUpdate 里消费:

C#private bool jumpQueued;

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space)) jumpQueued = true;
}

void FixedUpdate()
{
    if (jumpQueued)
    {
        rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        jumpQueued = false;
    }
}

Time.fixedDeltaTime 可以调小来提高物理精度,但代价是线性的 CPU 开销。移动平台上通常反而会调大到 0.033(30 Hz)。

Rigidbody

挂上 Rigidbody,这个物体就交给物理引擎接管了。几个关键属性:

Mass(质量)。只影响碰撞中的动量交换,不影响下落速度——真空中羽毛和铁球一样快,Unity 也是这样。用质量去调"下落更快"是无效的,应该改 Gravity Scale(2D)或自己额外加力。

Drag / Angular Drag(阻尼)。每个物理步按 velocity *= (1 - drag * fixedDeltaTime) 衰减速度。它不是空气阻力的物理模型,只是一个手感旋钮。

Is Kinematic(运动学)。打开后物体不再受力和碰撞影响,但仍然会推动别的物体、仍然会触发碰撞回调。移动平台、被动画驱动的角色通常用这个。注意:运动学刚体应该用 rb.MovePosition() 移动,而不是直接改 transform.position——前者会在物理系统内插值,碰撞检测正确;后者是"瞬移",很容易穿墙。

Interpolate(插值)。物理以 50 Hz 更新,渲染可能是 120 Hz,中间的帧刚体位置不变,看起来会顿。开启 Interpolate 让渲染在两个物理步之间插值,代价是画面落后一个物理步。玩家控制的角色一般开 Interpolate,其它物体关掉。

碰撞检测

离散与连续

默认的 Discrete 模式每个物理步只检查一次当前位置是否重叠。子弹这类高速物体在一步之内可能整个穿过墙,检查时两边都不重叠——这就是穿墙(tunneling)

解决办法是把 Collision Detection 改成:

  • Continuous:对静态碰撞体做扫掠检测(sweep test),能挡住穿墙。
  • Continuous Dynamic:对其它同样是 Continuous 的动态刚体也做扫掠。
  • Continuous Speculative:基于推测接触,开销比前两者低,也支持旋转,是近年推荐的默认选项。

连续检测很贵,只给真正需要的少数物体开。另一条思路是干脆不用刚体,自己每帧做一次 Physics.Raycast 从上一位置射到当前位置——子弹通常这么做。

Collider 与 Trigger

碰撞回调触发的前提是:两个物体都有 Collider,且至少一个有非运动学 Rigidbody。两个都只有 Collider 而没有 Rigidbody,什么都不会发生——这是最常见的"回调不触发"原因。

勾选 Is Trigger 后,碰撞体不再产生物理响应,只报告重叠:

实体碰撞触发器
OnCollisionEnterOnTriggerEnter
OnCollisionStayOnTriggerStay
OnCollisionExitOnTriggerExit

OnCollisionEnter(Collision other) 的参数带有接触点、法线和冲量信息;OnTriggerEnter(Collider other) 只给你对方的碰撞体。需要知道"撞得多重"来播不同音效时,用前者的 collision.impulse

碰撞体的形状

按开销从低到高:Sphere < Capsule < Box < 凸包 Mesh < 凹 Mesh(只能静态)。

Mesh Collider 默认是凹的,不能参与动态碰撞;勾上 Convex 才能挂刚体,但顶点数会被限制在 255 以内。实践中的原则是:永远优先用基本体拼出近似形状,Mesh Collider 只留给静态场景。一个角色用一个 Capsule 就够了,不需要每根手指都有碰撞体。

Layer Collision Matrix

Project Settings → Physics 里的这张矩阵决定了哪些层之间需要检测。它是最便宜的物理优化手段:把子弹层与拾取物层之间的勾去掉,物理引擎在宽阶段(broad phase)就不会再考虑这对组合。

在一个层数较多的项目里,默认的全勾状态意味着 O(n²) 的组合都要检查。花十分钟整理这张表,往往比优化任何一段脚本都有效。

一些实践经验

  1. 不要在 OnCollisionEnter 里做重活。 回调运行在物理步内,一次大量的实例化或 Destroy 会直接拉长这一步。把事件记下来,在 Update 或下一帧处理。
  2. Physics.autoSimulation 可以关掉。 回合制或确定性玩法里,手动调 Physics.Simulate(step) 能让物理完全可复现。
  3. Rigidbody.Sleep。静止的刚体会自动进入休眠,不再参与求解。如果你发现一个物体"推不动",通常是它睡着了,需要 WakeUp() 或施加足够大的力。
  4. 缩放是陷阱。 非均匀缩放(scale 不是 x=y=z)会让 Sphere / Capsule Collider 的行为变得不可预期,Unity 会取某个分量近似。角色身上有非均匀缩放时,物理表现出问题优先查这里。

物理系统的大部分"玄学问题",追到最后都是这四件事之一:更新时机错了(Update vs FixedUpdate)、检测模式不够(离散 vs 连续)、回调前提没满足(缺 Rigidbody),或者缩放不干净。

本系列文章

Unity 引擎
  1. 01Unity Engine #0 C#
  2. 02Unity Engine #1 内存
  3. 03Unity Engine #2 系统与网络
  4. 04Unity Engine #3 设计模式
  5. 05Unity Engine #4 MonoBehaviour
  6. 06Unity Engine #5 物理
  7. 07Unity Inspector #1 基本操作