2.1 事件函数排班表:从 Awake 到 OnDestroy


文档摘要

2.1 事件函数排班表:从 Awake 到 OnDestroy 本节摘要:Awake、OnEnable、Start、FixedUpdate、Update、LateUpdate、OnDisable、OnDestroy 构成 MonoBehaviour 的完整排班表。本节给出精确的调用时序、多脚本之间的顺序规则,以及"什么逻辑放哪个班次"的放置原则,并用一个空引用事故完整复盘。 从一个空引用报错说起 场景里有两个脚本:GameManager 负责在 Awake 里读取配置并生成一条规则表,Enemy 在 Awake 里去取这份规则表。运行后报 NullReferenceException,行号指向 Enemy 里取表的那一行。

2.1 事件函数排班表:从 Awake 到 OnDestroy

本节摘要:Awake、OnEnable、Start、FixedUpdate、Update、LateUpdate、OnDisable、OnDestroy 构成 MonoBehaviour 的完整排班表。本节给出精确的调用时序、多脚本之间的顺序规则,以及"什么逻辑放哪个班次"的放置原则,并用一个空引用事故完整复盘。

从一个空引用报错说起

场景里有两个脚本:GameManager 负责在 Awake 里读取配置并生成一条规则表,Enemy 在 Awake 里去取这份规则表。运行后报 NullReferenceException,行号指向 Enemy 里取表的那一行。逻辑看着没毛病,问题出在班次:引擎虽然保证"所有 Awake 先于所有 Start"执行,但不同对象之间的 Awake 调用顺序是不确定的——GameManager 的 Awake 可能排在 Enemy 之后。值班表只锁定了大班次,没锁定班内点名顺序。

修复思路有两个:把 Enemy 的取值推迟到 Start(此时全场 Awake 已跑完),或者让 GameManager 自己保证顺序(比如 Enemy 不主动取,等 GameManager 广播"配置就绪"再取,这就是 2.4 节事件模式的伏笔)。理解顺序规则后,你会发现一半的初始化 bug 都是时序 bug。

完整的排班表

一个对象从出生到销毁,事件函数按以下顺序被调用:

对象生命周期 出生:Awake ——> OnEnable ——> Start 存活期循环(每一帧): 物理班:FixedUpdate ——> 物理模拟 ——> 碰撞回调 OnTrigger 与 OnCollison 系列触发 主班:Update ——> 动画更新 ——> LateUpdate 渲染:交给渲染管线(脚本基本不参与) 禁用与恢复:OnDisable(下岗时)——> OnEnable(重新上岗时,Start 不再重跑) 销毁:OnDisable ——> OnDestroy

几条容易被问倒的细节:

  • Awake 只在对象首次创建时调用一次;对象被 SetActive(false) 再激活,走 OnDisable 加 OnEnable,Start 不会重跑;
  • FixedUpdate 与帧率解耦,默认固定步长下每秒约五十次,一帧内可能执行零次(帧间隔小于步长)或多次(掉帧追赶);
  • 协程的 yield return null 恰好在 Update 之后、LateUpdate 之前恢复,时序敏感的协程要记住这一点;
  • 场景卸载时,所有对象先收 OnDisable 再收 OnDestroy,想在被销毁前抢救引用,OnDisable 比 OnDestroy 稳。

生命周期时间轴

生命周期时间轴

动手:亲手量一次时序

背景:空口背排班表不如实测一遍。目标是亲眼确认 Awake、Start、Update、LateUpdate、FixedUpdate 的实际触发顺序与频率。

操作:在场景建一个空物体挂上如下脚本,按 Play 运行五秒后停止:

using UnityEngine; public class OrderProbe : MonoBehaviour { int fixCount, updCount, lateCount; void Awake() { Debug.Log("1 Awake,时间戳 " + Time.time); } void OnEnable() { Debug.Log("2 OnEnable,时间戳 " + Time.time); } void Start() { Debug.Log("3 Start,时间戳 " + Time.time); } void FixedUpdate() { fixCount++; if (fixCount <= 2) Debug.Log("F FixedUpdate 第 " + fixCount + " 次"); } void Update() { updCount++; if (updCount <= 2) Debug.Log("U Update 第 " + updCount + " 次"); } void LateUpdate() { lateCount++; if (lateCount <= 2) Debug.Log("L LateUpdate 第 " + lateCount + " 次"); } void OnDisable() { Debug.Log("下岗统计:物理 " + fixCount + " 帧 逻辑 " + updCount); } }

结果:Console 依次打出 Awake、OnEnable、Start(时间戳都是 0,说明发生在第一帧渲染之前);随后 FixedUpdate 与 Update 交错出现,且物理次数明显多于帧数;停止时 OnDisable 汇总的物理次数约为逻辑次数的两倍多(默认步长 0.02 秒,60 帧显示器上一帧通常跑一两次物理)。

解读:实测揭示了两件事。其一,三个"一次类"函数都发生在 Time.time 为 0 的开局阶段,所以开局逻辑别依赖 Time.time 做区分。其二,FixedUpdate 与帧率脱钩:掉帧时引擎用更多物理步追赶,帧率过高时可能一帧不跑——在 FixedUpdate 里写依赖"每帧恰好一次"假设的逻辑(比如每步加 1 点血)就会随帧率产生不同结果,凡是这种需求都应该挂到 Update。

变式:把脚本再复制一份挂到另一个空物体上,观察两份日志的 Awake 交错情况——顺序在两次运行间可能不同,这正是开头事故的实证。再把第二个物体的组件禁用后激活,确认 Start 不会二次打印。

放置原则小结

初始化分两级:只碰自己的字段放 Awake,要读别人的状态放 Start。逐帧逻辑里,输入与意图放 Update,摄像机与收尾放 LateUpdate,一切与刚体、碰撞相关的读写放 FixedUpdate。下岗清理优先放 OnDisable——它同时覆盖"被禁用"与"被销毁"两种情况,而 OnDestroy 只管销毁。最后补一条易漏项:OnValidate 只在编辑器里响应 Inspector 改动,适合做参数合法性提示,运行时它不值班,别把游戏逻辑写进去。记住这张表,第 3 章物理岗的所有代码都会严格照它落位。

本节要点回顾

  • 出生三连:Awake 加 OnEnable 加 Start,前两者创建即调,Start 推迟到首帧前;
  • 同级无序:不同对象的同班次调用顺序不保证,依赖他人就推迟或用事件;
  • 物理脱帧:FixedUpdate 按固定步长跑,一帧零到多次,读写刚体只在此班次;
  • 激活不重跑 Start:禁用再启用只走 OnDisable 加 OnEnable;
  • 清理认准 OnDisable:比 OnDestroy 覆盖面更广,解绑事件、归还资源都在这里做。

排班表背后的这些"值班员"到底是什么类?下一节拆开 C# 面向对象这层底座。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U