在Unity中,MonoBehaviour 是所有脚本组件的基类,它定义了一系列生命周期方法,这些方法在脚本的生命周期中按特定顺序自动调用。理解这些生命周期方法对于编写高效、正确的Unity脚本至关重要。本文将详细介绍MonoBehaviour的生命周期函数,包括每个函数的作用、触发时机以及相似函数之间的区别。

MonoBehaviour 生命周期概述

MonoBehaviour的生命周期从脚本实例创建开始,到对象销毁结束。生命周期方法按以下顺序执行(某些方法可能在特定条件下不调用):

  1. Awake()
  2. OnEnable()
  3. Start()
  4. FixedUpdate() (如果启用)
  5. Update() (如果启用)
  6. LateUpdate() (如果启用)
  7. OnDisable()
  8. OnDestroy()

此外,还有一些其他方法如 OnApplicationQuit() 等,但本文重点介绍核心生命周期方法。

MonoBehaviour 生命周期顺序示意图

详细介绍每个生命周期函数

1. Awake()

作用
Awake() 方法用于初始化脚本。它在脚本实例被创建后立即调用,用于设置脚本的初始状态、获取组件引用或进行一次性初始化。

触发时机

  • 当脚本组件被添加到GameObject上时(在编辑器中或运行时)。
  • 在所有脚本的Awake()方法执行完毕后,才会开始执行其他生命周期方法。
  • 即使GameObject被禁用(inactive),Awake()也会被调用。

什么时候会触发

  • 场景加载时,所有MonoBehaviour的Awake()会在Start()之前被调用。
  • 运行时动态实例化对象时。

注意

  • Awake()在对象生命周期中只调用一次。
  • 不要在Awake()中进行依赖于其他脚本的初始化,因为其他脚本的Awake()可能还未执行。

性能优化建议

  • 避免在Awake()中进行耗时操作,如同步加载资源或复杂计算,以免影响场景加载性能。
  • 缓存组件引用(如GetComponent()的结果)以减少后续查找开销。
  • 如果需要异步加载,使用协程或异步方法,但避免阻塞主线程。

2. OnEnable()

作用
OnEnable() 用于启用脚本时的初始化。它类似于Awake(),但可以被多次调用,用于重新启用对象时的设置。

触发时机

  • 当脚本组件被启用时(GameObject.activeSelf = true 或脚本.enabled = true)。
  • 在Start()之后,如果对象被禁用再启用,OnEnable()会再次调用。

什么时候会触发

  • 场景开始时,如果对象是启用的。
  • 运行时启用GameObject或脚本组件。
  • 从禁用状态切换到启用状态。

与Awake()的区别

  • Awake()只调用一次,而OnEnable()可以多次调用。
  • Awake()在对象创建时调用,无论是否启用;OnEnable()只在启用时调用。

性能优化建议

  • 由于OnEnable()可多次调用,避免重复执行耗时初始化;使用标志位检查是否已初始化。
  • 缓存必要的引用,避免每次启用时重新查找组件。
  • 如果启用/禁用频繁,考虑使用对象池来减少实例化开销。

3. Start()

作用
Start() 用于在脚本开始执行前的最后初始化。它通常用于设置依赖于其他脚本或场景中其他对象的引用。

触发时机

  • 在第一次Update()方法调用之前。
  • 确保所有脚本的Awake()和OnEnable()都已执行完毕。

什么时候会触发

  • 场景加载后,第一次帧更新前。
  • 对象被启用后,如果之前未调用过Start()。

与Awake()的区别

  • Awake()用于早期初始化,Start()用于依赖其他组件的初始化。
  • Awake()在Start()之前调用,且Start()可能在Awake()执行后延迟调用。

性能优化建议

  • Start()只调用一次,适合进行依赖其他对象的初始化,但避免同步加载大量资源。
  • 使用异步加载或预加载资源,以防止阻塞游戏启动。
  • 缓存跨脚本引用,避免在Start()中频繁使用FindObjectOfType()等耗时方法。

4. FixedUpdate()

作用
FixedUpdate() 用于处理物理相关的更新。它以固定的时间间隔调用,与帧率无关,适合处理刚体运动、碰撞检测等物理计算。

调用 FixedUpdate 的频度常常超过 Update。如果帧率很低,可以每帧调用该函数多次;如果帧率很高,可能在帧之间完全不调用该函数。在 FixedUpdate 之后将立即进行所有物理计算和更新。在 FixedUpdate 内应用运动计算时,无需将值乘以 Time.deltaTime。这是因为 FixedUpdate 的调用基于可靠的计时器(独立于帧率)。

触发时机

  • 以固定的时间步长(默认0.02秒,可在Time.fixedDeltaTime中设置)调用。
  • 在Update()之前或之后,取决于物理更新顺序。

什么时候会触发

  • 每当物理引擎更新时,通常每秒50次(如果fixedDeltaTime=0.02)。
  • 即使帧率变化,FixedUpdate()的调用频率保持不变。

与Update()的区别

  • Update()与帧率相关,FixedUpdate()与时间相关。
  • FixedUpdate()适合物理计算,Update()适合一般更新。

性能优化建议

  • FixedUpdate()以固定频率调用,保持逻辑简单高效,避免复杂循环或大量计算。
  • 使用Time.fixedDeltaTime进行时间相关计算,确保物理行为一致。
  • 如果物理对象过多,考虑使用对象池或LOD(细节层次)来减少计算量。

5. Update()

作用
Update() 是最常用的更新方法,用于处理每帧的逻辑,如输入处理、动画更新、AI行为等。

触发时机

  • 每帧调用一次,与渲染帧率同步。
  • 在FixedUpdate()之后,LateUpdate()之前。

什么时候会触发

  • 游戏运行时,每渲染一帧。
  • 如果帧率是60FPS,每秒调用60次。

注意

  • Update()的调用频率不固定,取决于硬件性能。

性能优化建议

  • Update()每帧调用,是性能瓶颈的主要来源;避免耗时操作,如复杂数学计算或大量字符串操作。
  • 缓存变量和组件引用,避免每帧调用GetComponent()或Find()。
  • 使用Time.deltaTime进行平滑插值,但注意在低帧率下可能导致不一致。
  • 考虑使用协程或事件驱动来替代频繁的Update()检查。

6. LateUpdate()

作用
LateUpdate() 用于在所有Update()方法执行完毕后的更新,常用于相机跟随、动画调整等需要最后执行的操作。

触发时机

  • 每帧在所有Update()方法之后调用。
  • 在渲染之前。

什么时候会触发

  • 每帧一次,在Update()之后。
  • 适合处理依赖于其他对象位置更新的逻辑。

与Update()的区别

  • LateUpdate()确保在所有Update()之后执行,避免更新顺序问题。
  • 例如,相机跟随玩家时,应在LateUpdate()中更新相机位置。

性能优化建议

  • LateUpdate()类似Update(),保持轻量,避免复杂逻辑。
  • 用于最终调整,如相机位置,确保所有对象已更新。
  • 如果相机跟随复杂,考虑使用Cinemachine等优化工具。

7. OnDisable()

作用
OnDisable() 用于清理或保存状态,当脚本被禁用时调用。它是OnEnable()的反操作。

触发时机

  • 当脚本组件被禁用时(GameObject.activeSelf = false 或脚本.enabled = false)。
  • 在对象被销毁前调用。

什么时候会触发

  • 运行时禁用GameObject或脚本。
  • 场景结束时。
  • 对象被销毁前。

与OnDestroy()的区别

  • OnDisable()在对象禁用时调用,可多次调用;OnDestroy()只在销毁时调用一次。
  • OnDisable()不意味着对象被销毁,可能只是临时禁用。

性能优化建议

  • OnDisable()可多次调用,快速执行清理,避免耗时操作。
  • 取消订阅事件,释放临时资源,但保留可重用状态。
  • 如果禁用频繁,使用对象池避免重复初始化/销毁开销。

8. OnDestroy()

作用
OnDestroy() 用于最终清理,当脚本实例被销毁时调用。用于释放资源、取消订阅事件等。

触发时机

  • 当GameObject被销毁时,或脚本组件被移除时。
  • 在对象生命周期结束时。

什么时候会触发

  • 运行时调用Destroy()方法。
  • 场景卸载时。
  • 应用程序退出时(但不保证)。

注意

  • OnDestroy()不保证在应用程序退出时调用,因为Unity可能强制退出。
  • 不要依赖OnDestroy()进行关键保存操作。

性能优化建议

  • OnDestroy()用于最终清理,确保释放所有资源,如取消事件订阅、销毁手动创建的对象。
  • 避免在OnDestroy()中进行耗时操作,以免影响场景切换性能。
  • 使用对象池可以减少Destroy()和Instantiate()的开销。

相似函数之间的区别总结

  • Awake() vs Start():Awake()用于早期初始化,Start()用于依赖初始化;Awake()总是在Start()前调用。
  • OnEnable() vs Start():OnEnable()可多次调用,用于启用时的初始化;Start()只调用一次,在首次启用后。
  • Update() vs FixedUpdate():Update()每帧调用,适合非物理逻辑;FixedUpdate()固定间隔,适合物理计算。
  • Update() vs LateUpdate():LateUpdate()在所有Update()后执行,适合相机跟随等最后更新。
  • OnDisable() vs OnDestroy():OnDisable()在禁用时调用,可恢复;OnDestroy()在销毁时调用,不可恢复。

最佳实践

下面写一段简单的代码示例,可以尝试在Unity编辑器中创建一个GameObject并附加此脚本,思考如何合理使用这些生命周期方法,通过不断尝试了解不同节点这些函数的启动时机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class GameMono : MonoBehaviour
{
private void Awake()
{
Debug.Log("Awake执行了");
}
private void OnEnable()
{
Debug.Log("OnEnable执行了");

}
private void Start()
{
Debug.Log("Start执行了");
}

private void FixedUpdate()
{
Debug.Log("FixedUpdate正在执行");
}
private void Update()
{
Debug.Log("Update正在执行");
}
private void LateUpdate()
{
Debug.Log("LateUpdate正在执行");
}

private void OnDisable()
{
Debug.Log("OnDisable执行了");
}
private void OnDestroy()
{
Debug.Log("OnDestroy执行了");
}
}

  • 在Awake()中进行初始化,避免依赖其他脚本。
  • 在Start()中处理跨脚本依赖。
  • 使用FixedUpdate()处理物理,使用Update()处理输入和动画。
  • 在OnDisable()和OnDestroy()中清理资源。
  • 避免在Update()中进行耗时操作,以保持帧率稳定。
  • 性能优化扩展
    • 缓存所有组件引用,避免重复GetComponent()调用。
    • 使用对象池管理频繁创建/销毁的对象,减少GC压力。
    • 最小化Update()和LateUpdate()中的逻辑,使用事件或状态机优化。
    • 监控Profiler,识别性能瓶颈,如过多的Draw Calls或物理计算。
    • 对于大量对象,使用批处理或LOD系统。
    • 异步加载资源,避免阻塞主线程。

通过理解这些生命周期方法,可以更好地控制脚本的行为,确保游戏逻辑正确执行。