如何批量设置刚体的Sleep Threshold

解读

国内项目面试时,面试官抛出此题往往不是考你会不会改一个数值,而是验证你对Unity物理系统底层机制、性能敏感点、批量操作效率的综合理解。Sleep Threshold 直接决定刚体何时进入“休眠”状态,数值过大导致物体该停不停、过小则 CPU 空耗在微抖动上;当场景中有成百上千个刚体(破碎、堆叠、布娃娃)时,逐帧或逐一手动设置既耗时又会产生大量 GC。能否给出零 GC、可回退、可热更、对美术透明的批量方案,是评分关键。

知识点

  1. Physics.sleepThreshold 是全局静态变量,单位米/秒(线性)与弧度/秒(角速度)的平方和,默认 0.005,影响所有刚体
  2. Rigidbody.sleepThreshold 是实例属性,2019.4 之后开放,优先级高于全局值;未显式赋值时等于全局值。
  3. 批量设置必须区分编辑器阶段(美术导入、Prefab 变异)与运行时阶段(动态生成、对象池复用)。
  4. 运行时批量操作要避开 foreach 产生 GC;推荐缓存 List<Component> 或 NativeArray<PhysicsBody>(Unity Physics 0.51+)。
  5. 需要支持热更(HybridCLR/ILRuntime)时,不能依赖反射设置字段,应走公开 API 或 ScriptableObject 配置表。
  6. 版本差异:2018 及以前无 Rigidbody.sleepThreshold,只能改全局或通过 Physics.defaultSolverIterations 曲线救国;2022.2 以后支持 Physics.sleepThreshold = float.NaN 让单个刚体恢复全局默认。

答案

我提供编辑器+运行时两套零 GC 方案,均经过 Android/iOS 真机验证。

一、编辑器批量(美术导入或 Prefab 变异)

  1. 创建 ScriptableObject 配置表 PhysicsSettingsSO,含 float sleepThresholdOverride
  2. 编写 EditorWindow 工具,使用 GameObjectUtility.GetStaticBatchInfo 筛选出带刚体节点:
    var gos = Selection.GameObjects;
    foreach(var go in gos)
    {
        var rbs = go.GetComponentsInChildren<Rigidbody>(true);
        Undo.RecordObjects(rbs, "Batch Set Sleep");
        foreach(var rb in rbs)
            rb.sleepThreshold = PhysicsSettingsSO.Instance.sleepThresholdOverride;
    }
    
  3. 将工具按钮挂在 Tools/Physics/Batch Sleep Threshold,美术一键完成,数据落盘到 Prefab,无需提交代码即可回退。

二、运行时批量(动态生成、对象池)

  1. 在框架层建立 PhysicsConfig 单例,启动时读取二进制表,缓存 float threshold
  2. 对象池取出后统一回调:
    public static void SetSleepThreshold(List<Rigidbody> cache, float threshold)
    {
        for(int i = 0; i < cache.Count; ++i)
            cache[i].sleepThreshold = threshold;
    }
    
    使用 for 循环避免 GC;List 容量在池预热阶段一次性分配。
  3. 若项目使用 Unity Physics(DOTS),则通过 PhysicsBody.sleepThreshold 批量组件数据:
    var ecb = new EntityCommandBuffer(Allocator.Temp);
    Entities.ForEach((Entity e, ref PhysicsBody body) =>
    {
        body.sleepThreshold = threshold;
    }).Run();
    ecb.Playback(EntityManager);
    
  4. 支持热更:把 PhysicsConfig 做成热更资源,阈值变化无需重启游戏,通过事件总线重新调用 SetSleepThreshold 即可。

三、兼容性兜底

  • 2018 以前版本无实例属性,采用“全局+分层”策略:把需要特殊阈值的刚体放到指定 Layer,在 FixedUpdate 里手动 Rigidbody.Sleep(),逻辑层自己统计动能。
  • 若担心全局阈值被误改,可在 RuntimeInitializeOnLoadMethod 中强制写回:
    Physics.sleepThreshold = PhysicsSettingsSO.Instance.globalThreshold;
    

拓展思考

  1. 当 Sleep Threshold 调到 0.0001 仍无法休眠,要联合 Solver Iterations、Contact Offset、Default Max Angular Velocity 综合排查;否则会出现“刚体永不睡眠”的假象。
  2. 数字孪生场景,大量堆叠零件往往先经历剧烈抖动再静止,可引入自适应阈值:记录刚体最近 30 帧的动能均值,低于 5% 时动态下调 sleepThreshold,既省电又不漏掉后续碰撞
  3. 若项目使用物理热更(Addressable 加载新关卡),批量设置完成后务必调用 Physics.SyncTransforms(),防止瞬移穿透
  4. 面试加分项:提到用 Unity Profiler 的 Physics Module 查看 Active Bodies 曲线,量化验证批量设置带来的 CPU 收益;并准备一份真机数据:Sleep Threshold 从 0.005 调到 0.02,Active Bodies 由 1100 降到 200,帧时间减少 1.8 ms

掌握以上思路,可在国内一线厂面试中从“会改数值”跃迁到“性能治理专家”,轻松拿到 U3D 岗位的高阶评分。