2.1 事件函数排班表:从 Awake 到 OnDestroy 本节摘要:Awake、OnEnable、Start、FixedUpdate、Update、LateUpdate、OnDisable、OnDestroy 构成 MonoBehaviour 的完整排班表。本节给出精确的调用时序、多脚本之间的顺序规则,以及"什么逻辑放哪个班次"的放置原则,并用一个空引用事故完整复盘。 从一个空引用报错说起 场景里有两个脚本:GameManager 负责在 Awake 里读取配置并生成一条规则表,Enemy 在 Awake 里去取这份规则表。运行后报 NullReferenceException,行号指向 Enemy 里取表的那一行。
本节摘要: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、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 章物理岗的所有代码都会严格照它落位。
排班表背后的这些"值班员"到底是什么类?下一节拆开 C# 面向对象这层底座。