一、前言 Preface
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情。不知道大家是否在同一生命周期阶段中引用对象出现过无实例化导致报错的问题;是否不清楚个阶段先后调用顺序以及存在的功能和意义呢?本文就很详细透彻地带你掌握
Unity3D
学习之关键 —— 生命周期(Life Cycle
)。众所周知,Unity
主循环是单线程,游戏脚本MonoBehavior
有着严格的生命周期,不同阶段执行各自代码逻辑可以让我们更加灵活地操纵游戏对象的行为和维护项目的稳定性。
二、生命周期 Life Cycle
Unity
的生命周期是一个 脚本实例化 所要经历的所有阶段,我们可以在不同的阶段为其添加不同的任务,但是这些任务要相互协调循序渐进,不能存在冲突。
新版官方生命周期图示如下:
2.1 编辑器阶段 Editor
Reset
在用户点击检视面板的 Reset
按钮或者 首次 添加该组件时被调用。常用于在检视面板中给定一个默认值。
注意: 此函数只在编辑模式下被调用。
2.2 初始化阶段 Initialization
Awake
游戏物体实例化后并处于 激活状态 时调用,即使脚本组件没有激活也会调用,而且总是在 Start()
函数之前调用。
OnEnable
物体在 Unity
进行实例化完成后需要将其激活,激活的这一刻就是 OnEnable()
函数回调的时机。可以调用多次,只要物体由不被激活状态转向激活状态 OnEnable()
函数就会被调用执行。
Start
触发前提是,游戏物体与脚本组件均处于激活状态,在 Update()
执行前调用,在 Awake
与 OnEnable
全部执行完毕后进行调用,但是在 Unity
中,多脚本的加载顺序是随机的。如果想直接进行对不同脚本 Awake 实例化的顺序控制的话,可以通过 Edit => Project Settings => Script Execution Order 来进行设置(数值越小,执行优先级越高)。
注意:只调用一次,当物体关闭激活状态,再次打开时不会反复触发!
2.3 物理周期阶段 Physics
这个阶段其实是一个追赶性质的游戏循环,具体指的是在每次 Update
和 Render
执行之前进行循环。
如果出现卡顿,即固定物理周期时间小于每帧时间,物理周期可能会在每帧发生多次,直到追赶上游戏帧率。
FixedUpdate
固定帧更新:每隔 Time.fixedDeltaTime
被调用一次(更新的默认频率为 0.02s )
那能不能设置
FixedUpdate
的更新频率呢?
答案是肯定的,在 Unity
导航菜单栏中,依次点击 Edit => Project Settings => Time 菜单项后,在右侧的 Inspector
视图中弹出的时间管理器中有 Fixed Timestep
选项用于设置固定帧调用频率。
协程
协程是一个可暂停执行
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
事件。
例如:你画一个 button
或 label
时常常会用到它,这就侧面反映了 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
状态,则设置对象component
的enable
为false
并不会触发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
看到输出的结果:
结束场景后,我们可以清楚的看到:最先执行的是 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。