Unity 生命周期、协程与时间系统
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,它只看三件事:
- 相机的 Culling Mask 是否包含它所在的 Layer;
- 材质是否正常、Shader 队列是否正确;
- 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 处引用了这篇