如何获取碰撞时的接触点冲量

解读

面试官问“如何获取接触点冲量”,并不是想听你背一句“用 collision.impulse”,而是想看你是否真正理解 Unity 物理引擎的事件顺序、数据生命周期、单位换算以及性能陷阱。国内项目普遍要在 Android 低端机 + 热更脚本(Lua/ILRuntime) 环境下跑,主线程卡顿 1 ms 都会被测试提单,所以你必须给出 零 GC、可热更、可回放调试 的完整方案。回答时先讲 事件阶段 → 数据入口 → 坐标系转换 → 缓存策略 → 实战踩坑,才能把“冲量”说透。

知识点

  1. 事件阶段:Unity 物理在 FixedUpdate 后统一解算,碰撞事件回调顺序为 OnCollisionEnter → Stay → Exit冲量值只在 Enter 和 Stay 的 Collision 实例里有效;Exit 阶段对象已被移除,此时读 impulse 会得到 0。
  2. 数据入口Collision.impulse(Unity 2020.2+ 才暴露),类型 Vector3,单位 牛顿·秒(N·s)与质量无关,是碰撞器在这一次求解迭代中受到的“瞬时冲量”。
  3. 接触点关联Collision.GetContacts() 返回 ContactPoint[],每个 ContactPoint 只有 separation、normal、point没有冲量;想拿到“每个接触点的冲量”必须 自己按法线方向拆分,公式:pointImpulse = Vector3.Project(impulse, contact.normal)
  4. 坐标系转换impulse 在 world space,如果后续要驱动特效或 Lua 层,需要 转成本地坐标系缓存为 float3,避免在热更层频繁 new Vector3。
  5. 性能与 GCGetContacts(List<ContactPoint>) 的重载可复用 List,避免每次 new ContactPoint[]impulse 本身不分配内存,但如果你在回调里做 string 拼接或 Debug.LogFixedUpdate 里会瞬间产生上百 KB GC
  6. 旧版本兼容:2019 LTS 及以下没有 impulse 字段,只能 用 relativeVelocity 反推近似冲量J ≈ Δv * m,误差 20% 左右,面试必须主动提到“版本差异”,体现真实项目经验。
  7. 网络同步与回放:冲量值要在 FixedUpdate 序号 下采样,不能依赖 Time.time用 uint tick 做 key,才能与后端物理服对齐,防止客户端插值抖动

答案

分三步给出 零 GC、可热更、可回放 的正式代码骨架,直接说给面试官听

  1. 缓存容器
static readonly List<ContactPoint> s_Contacts = new List<ContactPoint>(16);
  1. 事件回调
void OnCollisionEnter(Collision c)
{
    // 1. 取总冲量
    Vector3 totalImpulse = c.impulse;          // 2020.2+ 有效
    // 2. 取所有接触点
    c.GetContacts(s_Contacts);
    for (int i = 0; i < s_Contacts.Count; ++i)
    {
        var cp = s_Contacts[i];
        // 3. 按法线拆分
        Vector3 pointImpulse = Vector3.Project(totalImpulse, cp.normal);
        // 4. 立即转本地坐标并写回热更层
        Vector3 localImp = transform.InverseTransformVector(pointImpulse);
        GameFacade.Lua.Fire("OnContactImpulse", selfID, cp.point, localImp);
    }
}
  1. 版本降级
#if !UNITY_2020_2_OR_NEWER
    Vector3 totalImpulse = c.relativeVelocity * c.rigidbody.mass;
#endif

重点强调

  • impulse 只在 Enter/Stay 里有效,Exit 里读是 0;
  • GetContacts(List) 保证 0 GC;
  • 拆分到每个接触点 才能做 局部断裂、掉漆、音效 等表现;
  • 旧版本用 relativeVelocity * mass 是估算误差可接受但要在文档里标注

拓展思考

  1. 高速小物体穿模:Unity 默认 Default Max Depenetration Velocity=∞高速子弹会出现 impulse 正常但穿透 的假象;国内项目会把 Continuous Speculative + Max Depenetration Velocity=2 写进 ProjectSettings.asset 的代码化流水线,面试可提“用 Jenkins 打包时自动写 yaml” 来体现工程化思维。
  2. 冲量可视化:在 Editor 下用 Gizmos.DrawLine(point, point + impulse * 0.01f)颜色按大小映射到 HSV策划一眼就能看出物理参数是否合理记得包一层 #if UNITY_EDITOR避免打包后残留
  3. 网络帧同步:物理服与客户端 tick 对齐 后,把 impulse 量化成 int16(单位 0.01 N·s),单个接触点 6 byte100 个接触点 600 byteUDP 一个包即可带走面试提到“用 BitStream.WriteSignedDelta” 能直接击中 后端主程 的爽点。
  4. Lua/ILRuntime 热更陷阱Vector3 到 Lua 表 会产生 3 次 box解决方案是提前申请 float[3] 缓冲用 memcpy 直写可把 GC 降到 0
  5. 面试反问:你可以主动问面试官 “咱们项目物理帧率是多少?需要网络回滚吗?”——国内大厂普遍 50 Hz 物理 + 20 Hz 网络如果你能接上“用 impulse 做预测回滚”直接加分

把上面五点任意挑两条展开,就能从“回答正确”变成“回答惊艳”稳稳拿到 U3D 客户端 Offer