【透彻】学习 Unity3D 关键之生命周期

2,225 阅读9分钟

一、前言 Preface

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情。不知道大家是否在同一生命周期阶段中引用对象出现过无实例化导致报错的问题;是否不清楚个阶段先后调用顺序以及存在的功能和意义呢?本文就很详细透彻地带你掌握 Unity3D 学习之关键 —— 生命周期(Life Cycle)。众所周知,Unity 主循环是单线程,游戏脚本 MonoBehavior 有着严格的生命周期,不同阶段执行各自代码逻辑可以让我们更加灵活地操纵游戏对象的行为和维护项目的稳定性。

二、生命周期 Life Cycle

Unity 的生命周期是一个 脚本实例化 所要经历的所有阶段,我们可以在不同的阶段为其添加不同的任务,但是这些任务要相互协调循序渐进,不能存在冲突。

新版官方生命周期图示如下:

image.png

2.1 编辑器阶段 Editor

Reset

在用户点击检视面板的 Reset 按钮或者 首次 添加该组件时被调用。常用于在检视面板中给定一个默认值。

注意: 此函数只在编辑模式下被调用。

2.2 初始化阶段 Initialization

Awake

游戏物体实例化后并处于 激活状态 时调用,即使脚本组件没有激活也会调用,而且总是在 Start() 函数之前调用。

OnEnable

物体在 Unity 进行实例化完成后需要将其激活,激活的这一刻就是 OnEnable() 函数回调的时机。可以调用多次,只要物体由不被激活状态转向激活状态 OnEnable() 函数就会被调用执行。

Start

触发前提是,游戏物体与脚本组件均处于激活状态,在 Update() 执行前调用,在 AwakeOnEnable 全部执行完毕后进行调用,但是在 Unity 中,多脚本的加载顺序是随机的。如果想直接进行对不同脚本 Awake 实例化的顺序控制的话,可以通过 Edit => Project Settings => Script Execution Order 来进行设置(数值越小,执行优先级越高)。

注意:只调用一次,当物体关闭激活状态,再次打开时不会反复触发!

image.png

2.3 物理周期阶段 Physics

这个阶段其实是一个追赶性质的游戏循环,具体指的是在每次 UpdateRender 执行之前进行循环。

如果出现卡顿,即固定物理周期时间小于每帧时间,物理周期可能会在每帧发生多次,直到追赶上游戏帧率。

FixedUpdate

固定帧更新:每隔 Time.fixedDeltaTime 被调用一次(更新的默认频率为 0.02s

那能不能设置 FixedUpdate 的更新频率呢?

答案是肯定的,在 Unity 导航菜单栏中,依次点击 Edit => Project Settings => Time 菜单项后,在右侧的 Inspector 视图中弹出的时间管理器中有 Fixed Timestep 选项用于设置固定帧调用频率。

image.png

协程

协程是一个可暂停执行 yield 直到给定的 YieldInstruction 达到完成状态的函数。

  • yield WaitForFixedUpdate :在所有脚本上调用所有 FixedUpdate 后继续运行

触发/碰撞检测

即一些 OnTriggerXXX()OnCollisionXXX() 等触发/碰撞检测函数,Unity 物理计算和部分动画都在这个追赶循环中执行的,物理速度和动画速度都是按固定时间去走的。

2.4 事件输入阶段 Input Events

OnMouseXXX

鼠标手势检测,所有输入事件全在这个阶段调用处理。

2.5 游戏逻辑阶段

这一阶段最主要的是 Update自定义协程 部分。

Update

正常帧更新: 用于更新游戏逻辑,每一帧调用执行。

每一帧的间隔是否跟 FixedUpdate 一样是固定的呢?

不然,这个跟设备的性能以及被渲染的物体 (多边形数量) 有关,这就导致同一个游戏在不同的机器上运行速度有快有慢,因为 Update 的执行间隔不同。

FixedUpdate 渲染帧执行,如果你的渲染效率低,就会导致 FixedUpdate 调用次数下降,FixedUpdate 比较适用于物理引擎的计算。而 Update 它跟每帧渲染有关,比较适合做控制。所以当处理 Rigidbody 刚体这种物理受力时,就需要用 FixedUpdate 代替 Update

LateUpdate

在每帧 Update 调用执行完毕后被调用。

例如:当物体在 Update 里移动时,跟随物体的相机可以在 LateUpdate 里实现,在所有 Update 操作完才跟进摄像机,不然就有可能出现摄像机已经推进了,但是视角里还没有角色的空帧出现,同时也会伴随着抽搐抖动等情况 (没有在这一帧逻辑完全结束后调用跟随)。

协程

Update() 函数返回后将运行正常协程更新。

协程概念见 2.2 节。

协程的不同用法:

  • yield null :等待一帧,在下一帧上调用所有 Update 函数后,协程将继续
  • yield WaitForSeconds :在为帧调用所有 Update 函数后,在指定的时间延迟后继续运行
  • yield WWW :当网络任务执行完成后,会在当前帧的这个时间点执行 WWW 之后的操作。一般用于异步加载资源
  • yield StartCoroutine :连接协同程序,并等待 MyFunc coroutine 首先结束

也就是说,将代码段分散在不同的帧中,每次执行一段,下一帧再执行 yield 挂起的地方。如此一来,实时操作控制将变得更加灵活了。

举个栗子:在 Start() 函数中调用 StartCoroutine(TestCoroutine()) 来开启一个协程程序,当满足一定条件时就会触发执行 TestCoroutine() 函数中 yield xxxx; 语句之后的代码了。

这样就能实现一个时间分片的 异步 效果,而不是像线程那样在操作系统层面分 CPU 时间片去执行。

2.6 场景渲染阶段 Scene Rendering

场景渲染阶段,Unity 提供了一些函数回调:

OnWillRenderObject

如果对象可见,则为每个摄像机调用这个函数。

注意: UI 元素除外!

OnPreCull

当此摄像机剔除了某个渲染场景时调用触发。

注意: 这个函数仅用于宿主为摄像机的脚本。

OnBecameVisible

当物体 即将进入 摄像机时调用一次,类似触发器 OnTriggerEnter()

注意: 添加给物体,而不是摄像机。

OnBecameInvisible

当物体 即将离开 摄像机会调用一次,类似触发器 OnTriggerExit()

注意: 添加给物体,而不是摄像机。

OnPreRender

在摄像机开始渲染场景之前调用。

OnRenderObject

所有常规场景渲染完成之后调用。

此时,可以使用 UnityEngine.GL 类或 Graphics-DrawMeshNow 来绘制自定义几何形状。

OnPostRender

在摄像机完成场景渲染后调用。

OnRenderImage

在场景渲染完成后调用以允许对图像进行后期处理。

后期处理请参阅 后期处理 - Unity 手册

2.7 Gizmo 渲染阶段 Gizmo Rendering

Gizmos 一般是为开发者使用的,指的是开发时场景编辑器中所展示的那些 相机线框 之类的物体。

OnDrawGizmos

用于在场景视图中绘制小图示 Gizmos ,以达到可视化目的。

注意: 此方法里的内容一般不会发布到生产环境。

2.8 GUI 渲染阶段 GUI Rendering

OnGUI

首先处理布局和重新绘制事件 (用户界面渲染),然后为每个输入事件处理布局和键盘/鼠标事件。

每一帧更新时调用多次以响应 GUI 事件。

例如:你画一个 buttonlabel 时常常会用到它,这就侧面反映了 OnGUI 是每帧调用多次执行的。

2.9 帧结束阶段 End Of Frame

协程

  • yield return WaitForEndOfFrame :当前帧彻底结束后会执行此协程

2.10 暂停阶段 Pausing

OnApplicationPause

应用暂停时会调用触发,取消暂停后会从物理周期阶段中的 FixedUpdate 开始重新执行。

2.11 正式停用阶段 Decommissioning

OnApplicationQuit

在退出应用时调用,但有时会失效,此方法为不稳定的方法,正常情况下可以用于保存退出前的信息,但最好使用更稳妥的方式,因为此方法有时不会被调用,比如 Android 环境。

OnDisable

只有当脚本在帧期间被 禁用 时,OnDisable 才被调用。

如果它被 再次启用OnEnable 将被调用。

注意:

  • OnDisable 不能用于协同程序。
  • 当将 component 对象的 enable 设置为 false 时,或对象的 gameObject 被设置 SetActive(false) 时调用
  • 假设对象 gameObject 处于 disable 状态,则设置对象 componentenablefalse 并不会触发 OnDisable

OnDestroy

当物体对象将被销毁时调用,一般用于清理逻辑。

注意: OnDestroy 也不能用于协同程序,同时它只会在预先已经被激活的游戏物体上被调用。

三、代码体会 Experiment

3.1 事件函数执行顺序

在脚本中实现了几个 主要且常用的生命周期方法

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LifeCycle : MonoBehaviour {
    void Awake () {
        Debug.Log("Awake");
    }
    void Start () {
        Debug.Log("Start");
    }
    void FixedUpdate(){
        Debug.Log("FixedUpdate");
    }
    void Update () {
        Debug.Log("Update");
    }
    void LateUpdate () {
        Debug.Log("LateUpdate");
    }
    void OnDestroy () {
        Debug.Log("Destroy");
    }
}

在场景中新建 EmptyGameObject ,并将脚本挂载到该对象上。

再运行场景,然后你会在控制台 Console 看到输出的结果:

image.png

结束场景后,我们可以清楚的看到:最先执行的是 Awake() 方法,随后是 Start() 方法,这两个方法都执行了一次。

Update()LateUpdate() 方法执行的频率一样,都是每帧执行一次,FixedUpdat() 则是按照固定帧去走的,和它们俩执行频次不同。

最后结束场景时,调用到了 OnDestroy() 方法执行对对象的销毁。

3.2 协程的执行

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ExecCoroutine : MonoBehaviour {
    void Awake()
    {
        Debug.Log("Awake-0");
        StartCoroutine(CoroutineTest());
        Debug.Log("Awake-2");
    }

    void FixedUpdate()
    {
        Debug.Log("FixedUpdate-3");
    }

    IEnumerator CoroutineTest()
    {
        Debug.Log("CoroutineTest-1");
        yield return new WaitForFixedUpdate();
        Debug.Log("CoroutineTest-4");
    }
}

触发 WaitForFixedUpdate 后,要等到物理帧 FixedUpdate 结束后才会执行 Debug.Log("CoroutineTest-4"); 这条语句。

四、结尾 Ending

Unity Events 不仅仅包含脚本生命周期,还包含一些由系统自动调用的方法,也就是 回调 。只需明白这些方法不是由我们开发者自己调用的,而是由 Unity 在适当的时候自行调用,开发者只要实现这些方法即可。

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。

五、参考 Reference

【Unity】Unity 生命周期_是嘟嘟啊的博客-CSDN博客_unity 生命周期

Unity生命周期 - 知乎 (zhihu.com)