跳到正文
0:00 / 0:00
已成形

Unity 生命周期、协程与时间系统

4 分钟

AI 摘要

梳理 Unity 生命周期中 activeSelf 与 activeInHierarchy 的区别、DontDestroyOnLoad 渲染陷阱、Time 缩放与固定步长逻辑,以及 Animator 更新时序,帮助开发者避开常见状态判断与渲染失效错误。

生命周期里的每个回调都有它的「能做什么、不能做什么」,协程、时间缩放和 Animator 又各有自己的时序。这篇把这几块容易出错的点集中记录。

一、activeSelf 不等于「正在显示」

  • gameObject.activeSelf == true 只表示自己的激活标记为 true,并不能说明它在层级里真的处于显示状态。
  • 要判断「在层级中是否显示」,应该用 activeInHierarchy —— 它同时考虑了自己与所有父节点的激活状态。
  • 协程与 MonoBehaviour 属于同一层级:当挂载脚本的 GameObject 被设为非激活时,协程会停止运行(重新激活后不会自动续跑,除非重新启动)。

二、DontDestroyOnLoad 的正确用法与陷阱

DontDestroyOnLoad 只作用于根节点及其组件。对 GameObject 使用时,务必确保它没有父节点,否则调用会失败。

// 先脱离父节点,再 DontDestroyOnLoad
transform.SetParent(null);
DontDestroyOnLoad(gameObject);

陷阱:DontDestroyOnLoad 的物体会进入「隐藏场景」,可能不被渲染

被 DontDestroyOnLoad 的物体(比如粒子、界面)会进入一个特殊的隐藏场景(DontDestroyOnLoad Scene)。这个场景常常不在任何相机的渲染层级中,但如果物体的 Layer 包含在相机的 Culling Mask 里,它依然会被渲染。

原因在于:Unity 的相机并不关心 GameObject 属于哪个 Scene,它只看三件事:

  1. 相机的 Culling Mask 是否包含它所在的 Layer;
  2. 材质是否正常、Shader 队列是否正确;
  3. Renderer 是否被禁用。

两种让 DontDestroyOnLoad 物体继续可见的做法

做法一:把它移回当前场景

SceneManager.MoveGameObjectToScene(myParticle, SceneManager.GetActiveScene());

做法二:留在 DontDestroyOnLoad,但补上 Canvas 与渲染配置

如果不移动,就需要给它设置 Canvas(若没有),并根据需要设置 Layer 和相机:

  • RenderMode.ScreenSpaceOverlay:不需要设 Layer,也不需要指定相机,UI 会渲染在最上层。
  • 如果界面里还有非 UI 元素(比如粒子),则要改成 RenderMode.ScreenSpaceCamera 或 RenderMode.WorldSpace,设置 Layer,并指定一个相机来渲染(该相机的 Culling Mask 需包含该 Layer)。相机可以是 DontDestroyOnLoad 里的,但更多情况下是指定当前场景中的相机。

实战代码:

private void EnterBattleScene()
{
    // 界面脱离当前场景
    transform.SetParent(null);
    // 给界面增加 Canvas
    var canvas = transform.AddComponent<Canvas>();
    // 将界面置入 DontDestroyOnLoad
    DontDestroyOnLoad(transform.gameObject);
    // 修改渲染模式
    canvas.renderMode = RenderMode.ScreenSpaceCamera;
    // 指定渲染相机为当前场景主相机
    canvas.worldCamera = Camera.main;
    // 设置 Sorting Layer 与优先级
    canvas.sortingLayerName = "UI";
    canvas.sortingOrder = 100;
    // 之后同步加载战斗场景,达到沉浸式加载效果
}

三、Time 家族:先分清 scaled / unscaled 与 frame / fixed

字段 含义
Time.timeScale 时间缩放系数,影响 scaled 时间与物理步长
Time.time 从游戏开始到现在的缩放后时间
Time.deltaTime 上一帧到本帧的缩放后时间(用于 Update 里的帧率无关运动)
Time.fixedTime 固定时间步长累加的时间
Time.fixedDeltaTime 固定步长(默认 0.02s,即 50Hz)
Time.fixedUnscaledTime 不受 timeScale 影响的固定时间
Time.fixedUnscaledDeltaTime 不受 timeScale 影响的固定步长

要点:协程要和各类 time 搭配使用 —— 想让暂停(timeScale = 0)生效就用 scaled 版本,想让 UI 动效不受暂停影响就用 unscaled 版本。

四、物理为什么跑在 FixedUpdate

Unity 的物理引擎(PhysX)需要在固定时间步长下更新,否则会出现模拟不稳定的现象。所以物理相关的逻辑(受力、速度积分、射线)应放在 FixedUpdate 里,配合 Time.fixedDeltaTime。

五、生命周期里的顺序与坑

  • UI 的缩放等操作最好放在 Start。 放在 Awake 里不好 —— 此时 UI 可能还没被放入层级树,可能拿不到 Canvas(CommonBgAdapter 就踩过这个坑)。
  • Animator 的刷新发生在脚本之后。
  • 如果不在动画完成后失活 Animator,被它控制的 UI 参数会一直保持在动画的最后一帧状态。
  • 用 Animator 改 Text 的颜色做显隐时,就不要再在代码里动态控制文本颜色 —— 否则颜色不同的部分不会有动画效果。

六、AnimationState 的清理边界

  • ClearTracks() 只会清除动画轨道,不会清除事件委托。
  • Initialize() 会清理并重新创建内部状态,包括 AnimationState。

用之前要清楚自己清的是「轨道」还是「状态 / 委托」,避免残留的委托在重新播放时重复触发。

小结

  • 判断「是否显示」用 activeInHierarchy;GameObject 失活会停协程。
  • DontDestroyOnLoad 只认根节点,且物体进入隐藏场景后要处理 Layer / Canvas / 相机,否则可能「存在但看不见」。
  • Time 的 scaled / unscaled 要搭配协程选择;物理固定跑在 FixedUpdate。
  • UI 初始化放 Start;Animator 在脚本之后刷新,控制完记得失活。

相关:Unity 编辑器扩展、工程协作与项目优化实践、Unity asmdef 与编辑器脚本:Player 构建失败的经典原因

这是一条笔记,不是成稿文章。它可能不完整、可能有错,也可能过段时间就被我推翻。 想看成体系的文章,去 Blog。

linked from2 处引用了这篇