Unity脚本生命周期总结——内置事件方法执行顺序详解

前言

这篇贴子的内容主要信息来源是Unity的官方文档Version2019.2:https://docs.unity3d.com/Manual/ExecutionOrder.html 

也会根据这些年自己的开发经验,谈一些自己的看法和小技巧。

 

就算初级Unity程序员也应该知道的

Unity的内置事件方法很多,最基础的也是最常用一些,列举如下:

方法名(按单次执行顺序排列)方法说明
Awake在实例化时首先执行的方法,整个生命周期中只执行一次
OnEnable每次进入激活状态都会执行,对应OnDisable
Start在实例化以后的下一次Update之前调用,整个生命周期只执行一次
FixedUpdate物理更新方法,执行频率可在Project Setting->Time->Fixed Timestep进行设置
OnTriggerXXX触发器检测事件
OnCollisionXXX碰撞器检测事件
OnMouseXXX鼠标检测事件
Update帧更新方法
LateUpdate在Update之后执行的更新方法
OnGUIGUI渲染逻辑执行方法
OnApplicationQuit程序退出时的执行方法
OnDisable每次设置为不激活状态时执行,对应OnDisable
OnDestroy实例被销毁时的执行方法

 

 

 

 

 

 

 

 

 

 

 

 

 

 

官方流程图

首先简单说明一下:

  • 首先强调的时这是MonoBehaviour的生命周期图,不要和Editor等其他的混着来,在这篇博客里我们只讨论MonoBehaviour。
  • 上面提到的所有事件方法,只有白色椭圆框中的是开放给用户的。
  • 具执行的场景和时机可以分为:Initialization(初始化),Editor(编辑器),Physics(物理),Input events(输入),Game logic(游戏循环),Screne rendering(场景渲染),Gizmo rendering(线框渲染,只在编辑器开启了Gizmos选项的视图中显示),GUI rendering(GUI渲染),End of frame(一帧结束),Pausing(暂停),Decommissioning(销毁阶段),标粗的三个阶段是最最常用的,也写关键的事件都属于这三部分。
  •  

当场景被加载后

构造方法: 这里容易被忽视,构造方法是在实例化时执行的,所以在顺序上一定优先于其他的任何方法。

Awake: 当脚本被实例化时首先执行的方法,如果依附的GameObject处于未激活状态,则Awake会在被激活时首先执行,该方法不依赖于enable属性。该方法在组件的生命周期里只会执行一次。

OnEnable: 设置脚本实例的enable属性未true时,该方法被首先调用。在MonoBehaviour派生的类被实例化时、场景被加载时,带有该组件的GameObject被创建时,这个方法都可能被调用,我这里是说的是可能,因为这依赖于生成的实例的enable属性是否为true。严格来说,OnEnable不应该属于初始化方法,因为每次将组件的enable属性设置为true时,该方法都会被调用一次。

Main:这个方法在官方流程里没有,但它的确可以正常的在OnEnable之后自动调用,感兴趣的可以试一下。

Editor

Reset: 该方法的在组件被附加到物体上和调用Reset命令时执行,在编辑器的Inspector窗口的每一个组件的右上角的齿轮,点开以后可以执行Reset命令。

 

在第一帧刷新之前

Start: Start方法在下一次帧刷新之前被调用,需要强调的是,如果实例是在运行中生成的,不同于立即执行的Awake方法,Start方法是在当前帧的Update方法之后,LateUpdate方法执行之前调用的。这一点会在后面的测试中验证。

 

Update方法们,的执行顺序

FixedUpdate: 一般来说,FixedUpdate方法的执行要比Update方法频繁。如果当前帧刷新的很慢,可以在一帧之中被调用好几次,如果当前的帧率的很快,也可能一次都不会被调用。该方法会在所有的物理运算和更新之前被调用。当你在FixedUpdate方法内部应用运动计算的时候,不需要与Time.delaTime进行乘法运算,因为FixedUpdate的调用周期是稳定的,可以在Timer中进行设置,是不依赖于当前帧速率的。但是,要知道在极端情况下,比如主线程被阻塞的时候,FixedUpdate因为也是在主线程中的,所以这种情况下就不能稳定调用了。

Update: 每帧调用一次;这也是帧更新中最常用的方法。

LateUpadate: 每帧一次,从名字也能看出来,是在Update之后执行的方法,最典型的应用是第三人称相机。在Update中执行角色的移动和旋转,在LateUpdate中执行摄像机的跟随,这样可以保证角色的移动和旋转在摄像机跟随之前就已经完成了。根据官方的流程图可以看出 LateUpdate并不是紧跟在Update之后执行的,在以前的工作过程中,还偶然发现了一个小秘密。

 

做个小测试

有三个脚本,Test1:

public class Test1 : MonoBehaviour
{
    private void Awake()
    {
        Debug.Log("Test1 Awake");
    }

    private void OnEnable()
    {
        Debug.Log("Test1 OnEnable");
    }
    
    private void Start()
    {
        Debug.Log("Test1 Start");
        gameObject.AddComponent<Test2>();
    }

    private void Update()
    {
        if (!gameObject.GetComponent<Test3>())
        {
            gameObject.AddComponent<Test3>();    
        }
        Debug.Log("Test1 Update");
    }

    private void LateUpdate()
    {
        Debug.Log("Test1 LateUpdate");
    }

}

Test2(Test3相同):

public class Test2 : MonoBehaviour
{
    private void OnEnable()
    {
        Debug.Log("Test2 OnEnable");
    }
    
    private void Awake()
    {
        Debug.Log("Test2 Awake");
    }

    // Start is called before the first frame update
    void Start()
    { 
        Debug.Log("Test2 Start");   
    }

    void Update()
    {
        Debug.Log("Test2 Update");
    }

    private void LateUpdate()
    {
        Debug.Log("Test2 LateUpdate");
    }

}

场景种只在摄像机上挂了Test1脚本。执行结果如下:

我们重点看Test3,Test3在第一帧的Update中被附加到gameObject,Awake立即执行,OnEnable紧跟Awake执行,但是Test3的Start却是在第一帧Update结束以后,LateUpdate开始之前执行的!!!所以说,在游戏运行过程中生成的组件,其Start还是在当前帧完成的,只不过是在所有Update之后,LateUpdate之前。还有一点需要注意,Test3在第一帧中没有执行Update,因为Test3的初始化是在Update中完成的,所以Test3的Update不可能再被调用,但是在系统执行LateUpdate时,此时的Test3已经完成了注册,所以Test3的LateUpdate被正常调用了!!!

到这里有没有对Start的执行感到困惑呢?Start就行是什么时候执行的???

我们把上面的试验中的Test1稍作改动:

public class Test1 : MonoBehaviour
{
    private void Awake()
    {
        Debug.Log("Test1 Awake");
    }

    private void OnEnable()
    {
        Debug.Log("Test1 OnEnable");
    }
    
    private void Start()
    {
        Debug.Log("Test1 Start");
    }

    private void Update()
    {
        if (!gameObject.GetComponent<Test3>())
        {
            gameObject.AddComponent<Test3>();    
        }
        Debug.Log("Test1 Update");
    }

    private void LateUpdate()
    {
        gameObject.AddComponent<Test2>();
        Debug.Log("Test1 LateUpdate");
    }
}

唯一的修改就是将Test2添加的时机移动到了Test1的LateUpdate内。在运行前选中Pause,我们只看第一帧的数据,执行结果如下:

Test2的Start方法在LateUpdate之后被执行了!!!到这里,你是不是能想到什么?

//-----------------------------------------------------------------

//等我哪天有确凿证据证明我的想法了,再来补充

//-----------------------------------------------------------------

 

两帧之间调用的方法

OnApplicationPause: 检测到Pause请求,会在当前帧执行完以后调用该方法。在调用OnApplicationPause之后,将创建额外的一帧,以允许游戏显示表示暂停状态的图形。

 

动画更新循环

官方给出的流程图中出现了OnStateMachineEnter/Exit,但是这两个方法实在StateMachineBehaviour中定义的,不属于MonoBehaviour,所以在这里不做讨论。但是如果单独的只说OnAnimatorMove和OnAnimatorIK又好像没什么意义。这里动画作为相对独立的一部分,以后有机会再做详细讨论,这里我只明确一下,动画更新是在Update之后,LateUpdate之前调用的。

 

渲染

渲染作为我个人的主攻方向之一,可以讲的东西非常多,与渲染有关的事件方法的调用顺序从图中看就很明确了。所以我觉得详细的用法和经验,也作为之后的单独的一个贴子去讨论。我们在这里只明确,渲染相关的方法调用是在LateUpdate之后依次执行的。

 

协程Coroutines

协程是可以在用户设置的延迟指令结束以后被延迟执行的方法。

Unity中协程的本质是迭代器(IEnumerator),开启一个协程后,在yield return之前的的代码会立即执行,遇到yield return则协程被挂起,等待之后的调用,流程图中的调用就是协程继续执行的条件判断触发的过程,大部分条件判断是在Update之后,动画更新之前。如果判断条件成立,则执行后面的代码。

很多时候我们只是需要等待一帧,这种情况下使用

yield return null;

要比下面的这两种方式节省一次实例化的开销

yield return 0;
yield return new WaitForSeconds(Time.deltaTime);

协程作为Unity开发很重要的一部分,这里只简单一说,后面有机会还是要单独开一篇的。

 

被销毁时执行

OnDestroy 的调用是在实例存在的最后一帧被调用的,实例的销毁可能时由于Object.Destroy()方法的调用或者场景的销毁。顺便一提,实例真正的被销毁是在OnDestroy执行之后进行的,OnDestroy()的执行又是在,而不是Destroy()方法调用时立即执行,所以也可以说,Destroy方法有一定的延迟。如果是使用DestroyImmediate()方法要求立即销毁该对象,则OnDestroy也会被立即执行,而不是到当前执行的事件(如Update,LateUpdate)结束以后,该方法的调用机制类似于Start方法。

 

当程序退出时

OnApplicationQuit 每一个实现的该方法在程序退出前都会被调用,编辑器中调用Application.Quit()方法是无效的。

OnDisable 同OnEnable一样,在设置更改enable值和所依附物体的active属性,都会执行该方法。强调一下,在被销毁时,也会执行该方法,如果是同Destroy方法销毁对象,OnDisable方法会被立即执行,而OnDestory方法要在Update之后。

 

结语:

MonoBehaviour中的方法并不止这些,但这些都是最常用的一些。像OnApplicationFocus(bool hasFocus)这样的方法还有很多,具体的执行方式,还是自己多去探索。

 

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值