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

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系统化教学与最佳实践分享。

相关推荐
qq_213157892 小时前
其域lcc2丢失部分lod/远处显示空洞的解决方式
unity·lcc2
辻弋2013 小时前
自动检测硬件生成驱动清单、实时监控FPS、120秒性能记录——30+项优化模块化开关,所有改动可精确复原
服务器·windows·游戏引擎·电脑·开源软件
点心的游戏开发世界5 小时前
GDScript 入门笔记:目录
开发语言·笔记·游戏引擎·godot
XR技术研习社5 小时前
关于 PICO 串流测试中报错 IndexOutOfRangeException: renderPassIndex 的两个排障方法
unity·ar·xr·vr
想做后端的前端6 小时前
Unity · 性能优化:内存管理完全指南:从托管堆到跨桥开销
unity·性能优化·游戏引擎
平行云8 小时前
实时云渲染信创架构解析:从GPU池化到全栈适配的技术演进
linux·unity·docker·ue5·webgl·数字孪生·实时云渲染
玖玥拾10 小时前
Lua 基础语法(五)Unity xLua基础配置与 C# 访问 Lua
开发语言·unity·c#·lua
玖玥拾1 天前
Lua 基础语法(二)
开发语言·unity·lua
点心的游戏开发世界1 天前
GDScript 入门笔记(十六):文件读写与数据持久化
开发语言·笔记·游戏引擎·godot