Unity基础:MonoBehaviour的生命周期------Awake、OnEnable、Start、Update执行顺序

大家好,欢迎回到Unity教程系列。前面我们多次用到了Start和Update,也简单提到过Awake和OnEnable。今天这篇文章,我们要把Unity脚本的完整生命周期一次性讲清楚。
理解生命周期是写出正确Unity代码的基础。很多Bug的根源都是"在错误的时间做了错误的事"------比如在Awake里访问其他物体的组件(它可能还没初始化),或者在OnDestroy之后还试图访问已经被销毁的物体。掌握了生命周期的执行顺序,你就能避免这类问题。
一、生命周期的全景图
1.1 完整执行顺序
💡 以下是MonoBehaviour完整生命周期的执行顺序图:
脚本实例加载
↓
① Awake() ------ 脚本实例创建时调用一次
↓
② OnEnable() ------ 脚本启用时调用(每次激活都调用)
↓
③ Start() ------ 首次Update前调用一次
↓
④ FixedUpdate() ------ 固定时间间隔调用(默认0.02秒,物理更新)
↓
⑤ Update() ------ 每帧调用
↓
⑥ LateUpdate() ------ 每帧Update之后调用
↓
⑦ OnDisable() ------ 脚本禁用时调用
↓
⑧ OnDestroy() ------ 脚本销毁时调用
📝 除此之外还有几个重要的方法:OnApplicationQuit(应用退出前)、OnApplicationPause(应用暂停/切后台)、OnApplicationFocus(应用获得/失去焦点)、OnValidate(仅在编辑器中,脚本被加载或值被修改时调用)。
1.2 理解"调用一次"与"反复调用"
生命周期方法分为两类:
只调用一次的方法(初始化/清理):
- Awake
- Start
- OnDestroy
可反复调用的方法(每帧/多次):
- OnEnable / OnDisable
- FixedUpdate / Update / LateUpdate
二、初始化阶段------Awake、OnEnable、Start
2.1 Awake------最早调用的初始化
💡 Awake是脚本实例被创建后最早调用的方法。它的特点:
- 在脚本实例加载时立即调用
- 只调用一次
- 即使脚本组件被禁用(enabled=false),Awake也会被调用
- GameObject处于禁用状态时,Awake不会被调用(Unity 2019+的行为)
- 调用时机早于Start
Awake的典型用途:
csharp
void Awake()
{
// ① 获取自身组件引用(最安全的阶段)
rb = GetComponent<Rigidbody>();
animator = GetComponent<Animator>();
collider = GetComponent<Collider>();
// ② 初始化自身状态
initialPosition = transform.position;
// ③ 设置引用(但不依赖其他物体的状态)
playerCamera = GetComponentInChildren<Camera>();
}
⚠️ 在Awake中不应做的事情:
- 访问其他GameObject上的组件(其他物体的Awake可能还没执行)
- 调用GameObject.Find等全局查找(其他物体可能还不存在)
2.2 OnEnable------每次激活都调用
💡 OnEnable在脚本被启用时调用。与Awake不同的是:Awake只调用一次,而OnEnable每次激活都调用。
调用时机:
- 脚本首次加载(Awake之后、Start之前)
- 脚本从禁用状态恢复(每次SetActive(true)或enabled=true都触发)
- GameObject在Hierarchy中被重新激活
OnEnable的典型用途:
csharp
void OnEnable()
{
// ① 订阅事件
GameManager.OnGamePaused += HandlePause;
GameManager.OnGameResumed += HandleResume;
// ② 重新激活时的状态重置
healthBar.gameObject.SetActive(true);
// ③ 注册到全局管理器
EnemyManager.Instance.RegisterEnemy(this);
}
void OnDisable()
{
// 与OnEnable配对------取消订阅和注册
GameManager.OnGamePaused -= HandlePause;
GameManager.OnGameResumed -= HandleResume;
EnemyManager.Instance?.UnregisterEnemy(this);
}
💡 OnEnable和OnDisable必须成对使用:OnEnable中订阅的事件必须在OnDisable中取消订阅,否则会出现内存泄漏和空引用异常。
2.3 Start------关联初始化的最佳时机
💡 Start在脚本首次启用后、第一次Update之前调用,且只调用一次。它的关键优势在于:所有物体的Awake都已执行完毕。
Start的典型用途:
csharp
void Start()
{
// ① 查找和引用其他物体(此时所有物体的Awake已完成)
player = GameObject.FindWithTag("Player");
gameManager = FindObjectOfType<GameManager>();
// ② 建立物体间的初始关系
if (player != null)
{
SetTarget(player.transform);
}
// ③ 基于其他物体的初始化计算
initialDistanceToPlayer = Vector3.Distance(transform.position, player.transform.position);
// ④ 生成初始内容
SpawnInitialEnemies();
}
2.4 Awake vs Start 的决策指南
| 场景 | 使用哪个 |
|---|---|
| 获取自己身上的组件(GetComponent) | Awake |
| 初始化不依赖其他物体的变量 | Awake |
| 查找其他物体(Find、FindWithTag) | Start |
| 访问其他物体的组件 | Start |
| 实例化物体 | Start |
| 订阅事件 | OnEnable |
| 初始化依赖于Inspector赋值的引用 | Awake 或 Start 都可以 |
💡 简单原则:Awake管自己,Start管关联。
三、更新阶段------FixedUpdate、Update、LateUpdate
3.1 FixedUpdate------物理更新
💡 FixedUpdate以固定时间间隔调用(默认0.02秒一次,即50次/秒),与帧率无关。
为什么需要FixedUpdate?
物理模拟需要稳定的时间步长才能准确。如果物理更新频率随帧率波动,30FPS和120FPS下的物理行为就会不同------这就是为什么物理相关代码要放在FixedUpdate中。
csharp
void FixedUpdate()
{
// ① 物理计算
rb.AddForce(gravity);
// ② 物理驱动的移动
rb.velocity = new Vector3(input.x * speed, rb.velocity.y, input.z * speed);
// ③ 物理检测
grounded = Physics.CheckSphere(groundCheck.position, groundCheckRadius, groundLayer);
}
📝 FixedUpdate的时间步长可以在 Edit > Project Settings > Time > Fixed Timestep 中修改。减小这个值(如0.01秒)会让物理更新更频繁、更精确,但也更消耗性能。
3.2 Update------游戏逻辑的主循环
csharp
void Update()
{
// ① 输入检测
if (Input.GetKeyDown(KeyCode.Space)) Jump();
if (Input.GetMouseButtonDown(0)) Shoot();
// ② 非物理的移动和旋转
transform.Translate(movement * speed * Time.deltaTime);
// ③ 计时器和冷却
cooldownTimer -= Time.deltaTime;
// ④ 动画参数更新
animator.SetFloat("Speed", currentSpeed);
// ⑤ 逻辑状态检查
CheckDeathCondition();
UpdateUI();
}
3.3 LateUpdate------所有Update之后的更新
💡 LateUpdate在所有Update执行完毕后被调用。它的典型用途是摄像机跟随------确保摄像机在所有物体更新完毕后再移动。
csharp
void LateUpdate()
{
// 摄像机跟随(确保角色已经在Update中完成了移动)
Vector3 targetPosition = player.position + offset;
transform.position = Vector3.Lerp(transform.position, targetPosition, followSpeed * Time.deltaTime);
transform.LookAt(player);
}
为什么摄像机跟随要用LateUpdate而不是Update?
- 如果在Update中移动摄像机,无法保证是在角色移动之前还是之后
- 使用LateUpdate确保角色所有Update逻辑完成后再移动摄像机
- 避免画面抖动(摄像机可能在角色移动的中间帧状态更新)
3.4 Update vs FixedUpdate的执行频率
一个帧中可能发生的情况:
Frame 1: FixedUpdate → Update → LateUpdate
Frame 2: Update → LateUpdate(没有FixedUpdate,因为时间还没到)
Frame 3: FixedUpdate → FixedUpdate → Update → LateUpdate(积压了两次物理更新)
这就是为什么不能把输入检测放在FixedUpdate中------某些帧可能没有FixedUpdate,导致输入丢失。
四、禁用与销毁阶段
4.1 OnDisable------禁用时调用
OnDisable在以下情况被调用:
- 脚本组件被禁用(enabled = false)
- GameObject被禁用(SetActive(false))
- GameObject被销毁(OnDisable在OnDestroy之前)
csharp
void OnDisable()
{
// 清理OnEnable中注册的内容
EventManager.OnScoreChanged -= UpdateScoreDisplay;
// 停止协程
StopAllCoroutines();
// 重置状态
isPerformingAction = false;
}
4.2 OnDestroy------销毁时调用
OnDestroy在以下情况被调用:
- GameObject被Destroy()
- 场景被卸载
- 应用退出(但如果物体是DontDestroyOnLoad的,可能在退出时才销毁)
csharp
void OnDestroy()
{
// 清理资源
if (spawnedParticles != null)
{
Destroy(spawnedParticles);
}
// 从管理器中移除
if (GameManager.Instance != null)
{
GameManager.Instance.UnregisterObject(this);
}
Debug.Log($"{gameObject.name} 被销毁了");
}
⚠️ OnDestroy中访问全局单例时要做null检查(GameManager.Instance != null),因为在场景切换或应用退出时,管理器可能已经被销毁了。
五、完整生命周期代码演示
csharp
using UnityEngine;
public class LifecycleDemo : MonoBehaviour
{
// 记录调用次数
private int awakeCount, onEnableCount, startCount;
private int updateCount, fixedUpdateCount, lateUpdateCount;
private int onDisableCount, onDestroyCount;
void Awake()
{
awakeCount++;
Debug.Log($"[{Time.frameCount}] ① Awake 调用(第{awakeCount}次)");
}
void OnEnable()
{
onEnableCount++;
Debug.Log($"[{Time.frameCount}] ② OnEnable 调用(第{onEnableCount}次)");
}
void Start()
{
startCount++;
Debug.Log($"[{Time.frameCount}] ③ Start 调用(第{startCount}次)");
}
void FixedUpdate()
{
fixedUpdateCount++;
if (fixedUpdateCount <= 3)
Debug.Log($"[{Time.frameCount}] FixedUpdate(第{fixedUpdateCount}次)");
}
void Update()
{
updateCount++;
if (updateCount <= 5)
Debug.Log($"[{Time.frameCount}] Update(第{updateCount}次)");
// 10帧后自动禁用
if (updateCount == 10)
{
Debug.Log("------ 10帧了,即将禁用脚本 ------");
enabled = false;
// 注意:这不会停止当前帧的执行!当前Update会继续完成
}
// 15帧后自动销毁
if (updateCount == 15)
{
Debug.Log("------ 15帧了,即将销毁GameObject ------");
Destroy(gameObject);
}
}
void LateUpdate()
{
lateUpdateCount++;
if (lateUpdateCount <= 3)
Debug.Log($"[{Time.frameCount}] LateUpdate(第{lateUpdateCount}次)");
}
void OnDisable()
{
onDisableCount++;
Debug.Log($"[{Time.frameCount}] ⑦ OnDisable 调用(第{onDisableCount}次)");
}
void OnDestroy()
{
onDestroyCount++;
Debug.Log($"[{Time.frameCount}] ⑧ OnDestroy 调用(第{onDestroyCount}次)");
}
}
六、多脚本之间的生命周期顺序
6.1 脚本执行顺序设置
当场景中有多个GameObject,它们各自的Awake调用顺序是不确定的。如果你需要确保某个脚本的Awake在另一个之前执行,可以在 Edit > Project Settings > Script Execution Order 中设置。
- 添加脚本到列表
- 设置执行顺序数值(越小越早执行)
- Default Time是0,设为负数则更早执行
6.2 不同GameObject间的生命周期
GameObject A:
Awake A
OnEnable A
(所有GameObject的Awake都执行完毕)
Start A
...
GameObject B:
Awake B
OnEnable B
Start B
...
执行顺序:Awake_A → OnEnable_A → Awake_B → OnEnable_B → Start_A → Start_B
📝 关键在于:所有物体的Awake和OnEnable都会在任何一个Start之前执行完毕。这就是为什么在Awake中不能依赖其他物体,但在Start中可以。
七、特殊情况
7.1 Instantiate时的生命周期
当使用Instantiate创建物体时:
- 新物体的Awake和OnEnable立即在当前帧内执行(在Instantiate返回之前)
- Start会在当前帧的后续执行
csharp
void Update()
{
GameObject newObj = Instantiate(prefab);
// 此时新物体的Awake和OnEnable已经执行完毕!
// 但Start还没执行(在当前帧稍后执行)
}
7.2 场景加载时的生命周期
场景加载时:
① 所有物体的Awake和OnEnable按不确定顺序执行
② 所有物体的Start执行
③ 进入正常Update循环
7.3 协程与生命周期
协程在Start返回后开始执行(如果将Start声明为IEnumerator):
csharp
IEnumerator Start()
{
// 这部分立即执行(等同于普通Start的开始部分)
Debug.Log("Start开始");
yield return new WaitForSeconds(2f);
// 2秒后执行这部分
Debug.Log("Start的2秒后部分");
}
八、常见Bug与排查
8.1 NullReferenceException在Start中
如果Start中GetComponent返回null:
- 确认组件确实挂载在同一GameObject上
- 考虑将GetComponent移到Awake中会更好(更早发现问题)
8.2 在Awake中访问其他物体的组件返回null
这是经典错误------其他物体的Awake可能还没执行,其组件可能还没初始化。解决方案:将跨物体的引用放在Start中。
8.3 OnDestroy中访问已销毁的物体
在OnDestroy中访问外部引用时,那些引用可能已经被销毁。使用null检查(Unity对已销毁的UnityEngine.Object返回"假null"------使用!= null可以检测)。
九、本篇总结
✅ 本文核心知识点回顾:
- 生命周期顺序:Awake → OnEnable → Start → FixedUpdate → Update → LateUpdate → OnDisable → OnDestroy
- Awake管自身初始化,Start管关联初始化
- OnEnable/OnDisable成对使用,管理事件订阅和取消
- FixedUpdate用于物理,Update用于游戏逻辑,LateUpdate用于摄像机跟随
- 所有物体的Awake/OnEnable在任何一个Start之前完成
- Script Execution Order可以控制脚本之间的执行顺序
- Instantiate时Awake/OnEnable立即执行,Start在当前帧稍后执行
📝 下一篇文章将学习使用Transform控制物体移动------Translate与position的详解。
动手任务 :
① 写一个包含所有生命周期方法的测试脚本,观察执行顺序
② 对比Awake和Start中访问其他物体组件的差异
③ 用LateUpdate实现一个平滑的摄像机跟随
关于作者:拥有多年Unity开发经验的游戏开发者,专注Unity系统化教学与最佳实践分享。