目录
- 0 前言
- 1 Unity生命周期图
- 2 Unity生命周期
- 2.1 初始化阶段
- 1 加载第一个场景
- 2 Editor
- 3 第一次帧更新之前
- 2.2 物理更新阶段
- 1 物理帧
- 2 触发/碰撞
- 3 协程:物理帧结束
- 2.3 输入事件阶段
- 2.4 游戏逻辑阶段
- 1 帧更新
- 2 多协程点判断
- 3 后更新
- 2.5 渲染阶段
- 2.6 暂停阶段
- 2.7 退出阶段
- 3 面试题
参考:Unity官方手册以及Unity生命周期
0 前言
Unity的生命周期指的是Unity脚本从唤醒到销毁的过程,在这个过程里,Unity脚本会按预定顺序执行大量事件函数。
这些函数全部都是系统定义好的,需要继承MonoBehaviour类才能调用。脚本需要挂在任意游戏对象上,并且一个游戏对象可以挂载不同的脚本,各个脚本执行自己的生命周期。
本文将介绍这些事件函数,并说明它们的执行顺序。
1 Unity生命周期图
见官方手册:脚本生命周期流程图
2 Unity生命周期
2.1 初始化阶段
1 加载第一个场景
场景开始时将调用以下函数:
Awake:
调用时间:当一个脚本实例被载入时,即场景加载时或实例化预制体时,Awake被调用,仅调用一次。
调用条件:
游戏对象处于激活状态(如果游戏对象在启动期间处于非激活状态,则在激活之后才会调用Awake。
基于这个调用条件,我们发现,Awake函数执行时并不保证所有对象都已经被加载和初始化,因此不能在其中进行与对象初始化相关的操作,例如在Awake函数中寻找其他对象或设置依赖其他对象的属性,这些操作应该放在Start函数中进行。
OnEnable:
调用时间:当对象每次变为可用[1]或激活[2]状态时此函数被调用,可多次调用。
调用条件:游戏对象处于激活状态;游戏脚本处于启用状态。
注释:
可用:对象是否处于场景中。如果对象处于可用状态,则它存在于场景中并且可以被访问。可以使用gameObject.activeSelf属性来检查对象是否处于可用状态。(对象突然可用的情况:在游戏运行时被实例化。
激活:对象是否处于启用状态。当对象处于启用状态时,它将被渲染并响应输入事件。可以使用SetActive()函数来启用或禁用对象。
2 Editor
编辑器模式下才会被调用的函数:
Reset:
调用时间:在用户点击检视面板的Reset按钮或者首次添加该组件时被调用。
OnValidate:
调用时间:每当设置脚本的属性时都会调用,包括反序列化对象时,这可能发生在不同的时间,例如在编辑器中打开场景时和域重新加载后。
3 第一次帧更新之前
Start:
调用时间:当一个MonoBehaviour实例被载入时,即场景加载时或实例化预制体时,Start被调用,仅调用一次,且在第一次帧更新前调用。
调用条件:游戏对象处于激活状态;游戏脚本处于启用状态。
2.2 物理更新阶段
1 物理帧
FixedUpdate:
FixedUpdate基于一个可靠的定时器被调用,独立于帧率之外(project->setting->Time,默认为0.02s,使用Time.fixedDeltaTime访问该值)。处理物体的物理属性(Rigidbody、Force、Collider)或者输入事件时,需要用FixedUpdate代替Update,以使物体的物理表现更平滑。
不过实际上,FixedUpdate并不是真的按照现实时间间隔执行的,它按照Timer时间间隔执行,而Timer并不是真正意义上的现实时间,它的作用是在运行环境下创造一个与现实时间高度相近的变量来实现物理帧的逻辑稳定。因为FixedUpdate的这个特质,建议在此环节只做物理相关的处理,不要把其他类型(如网络帧同步)的处理也放入此步骤。
2 触发/碰撞
OnTriggerXXX:
触发器被触发时调用。
OnCollisionXXX:
产生碰撞事件时调用。
3 协程:物理帧结束
Yield WaitForFixedUpdate:
当物理帧执行完毕后会跳转到此协程,协程的调用跟方法是不同的,可以理解为在一段代码中设置一个卡点,当程序执行到这个卡点所匹配的时机时卡点后面的代码才会继续执行。
public class Test : MonoBehaviour{ void Awake(){ //数字代表执行顺序 Debug.Log("1"); StartCoroutine(TestCoroutine()); Debug.Log("3"); } void FixedUpdate(){ Debug.Log("4"); } //协程 IEnumerator TestCoroutine(){ Debug.Log("2"); yield return new WaitForFixedUpdate(); //卡点:物理帧结束 Debug.Log("5"); } }
2.3 输入事件阶段
鼠标、键盘、触屏、手柄等各类输入事件会在这个阶段触发,这个时间点物理更新已经执行(如果需要物理更新的话),而逻辑更新和渲染并未执行,要了解这个触发的时机,才能更好的掌握代码逻辑。
2.4 游戏逻辑阶段
1 帧更新
Update:
每帧调用1次,但由于系统性能以及游戏体量的区别,每一帧的刷新频率也是不同的,所以不要过分期待在Update方法中按时完成任务。
Update与FixedUpdate实际上是使用同一个线程的,update在loop中的处理方式是本次更新完毕再根据上一帧到现在的偏移时间判断是否进行下一次更新,Update的本质就是回调函数。只要是回调函数就存在上下文传递的损耗,所以如果想减少回调,可以考虑自己实现一套Update机制,使用虚函数来代替Update。
2 多协程点判断
1.yield null
2.yield WaitForSeconds
3.yield WWW:当网络任务执行完成后,会在当前帧的这个时间点执行WWW之后的操作。一般用于异步加载资源。
4.yield StartCoroutine
3 后更新
LateUpdate:
每帧Update方法调用之后会调用本方法。LateUpdate一般用于二次计算,因为LateUpdate 开始时,在 Update 中执行的所有计算便已完成。
LateUpdate 的常见用途是第三人称摄像机跟随。如果在 Update 内让角色移动和转向,可以在 LateUpdate 中执行所有摄像机移动和旋转计算。这样可以确保角色在摄像机跟踪其位置之前已完全移动。
2.5 渲染阶段
OnGUI:
OnGUI()方法是在Unity的GUI系统渲染之前被调用的,一般用于在游戏界面中显示UI元素,例如按钮、标签、滑动条等。当需要在游戏界面中显示UI元素时,可以在OnGUI()方法中编写相关的代码。OGUI()每帧调用多次。
需要注意的是,OnGUI()方法只能在主线程中调用,不能在其他线程中调用,否则会导致程序异常。另外,由于OnGUI()方法的调用是由Unity的GUI系统自动触发的,因此不需要手动调用该方法。
另外,OnGUI函数在Unity的新版本中已被标记为“过时”(Deprecated),不推荐在新的项目中使用。推荐使用新的UI系统,如UGUI(Unity GUI)和IMGUI(Immediate Mode GUI)。
2.6 暂停阶段
OnApplicationPause:
在帧的结尾处调用此函数(在正常帧更新之间有效检测到暂停)。在调用 OnApplicationPause 之后,将发出一个额外帧,从而允许游戏显示图形来指示暂停状态。
2.7 退出阶段
ApplicationQuit:
当应用程序退出时,所有正在运行的场景和对象都将被销毁,包括挂在这些对象上的MonoBehaviour脚本。在这种情况下,如果需要在应用程序退出之前执行一些特定的操作,可以使用OnApplicationQuit函数来实现。在编辑器中,用户停止播放模式时,调用函数。
需要注意的是,当应用程序被强制关闭或崩溃时,OnApplicationQuit函数不会被调用。
OnDisable:
在脚本或游戏对象被禁用时被调用,通常用于在游戏对象被禁用时执行一些清理操作或停止某些动作,例如停止音频播放、停止动画播放等。一般情况下,这些操作应该在OnEnable函数中启动,而在OnDisable函数中停止。这样可以保证在游戏对象被启用和禁用时,动作的开启和停止是对称的,避免出现意外的行为。
需要注意的是,OnDisable函数不会在游戏对象被销毁时被调用,因为在游戏对象被销毁时,所有与其相关的组件都会被销毁,包括脚本上的OnDisable函数。如果需要在游戏对象被销毁时执行一些清理操作,可以使用OnDestroy函数来实现。
OnDestory:
在游戏对象被销毁时被调用。OnDestroy函数通常用于在游戏对象被销毁时执行一些清理操作或释放资源,例如关闭文件、释放内存等。
3 面试题
1. Awake和Start的区别?
① 调用时机不同。Awake在脚本实例被载入时就被调用,而Start在第一次帧更新前被调用。换句话说,Awake是在脚本实例出生时就被调用了,而Start是在帧上调用。举个例子,当我们在Update里实例化一个GameObject,Awake会在同一帧里马上被执行,而Start会在下一帧执行。
② 调用条件不同。Awake在脚本实例被载入时就被调用,无论脚本是否处于启用状态。而Start在脚本处于启用状态时才可被调用。
③ 用法不同。一般来说,我们在Awake中初始化变量或状态,但当我们需要延迟初始化或者获取其他游戏对象的组件时,可以使用Start。
2. Awake、Start和OnEnable的区别?
区别主要在于Awake和Start在一个物体的生命周期中仅被调用一次,当这个物体被取消激活再重新激活时,脚本里的Awake和Start都不会重新执行,而OnEnable则会在每次激活时执行。
3.现在场景内有两个物体,当场景载入时,它们的Awake函数会同时执行吗?
不会同时执行,会顺序执行,因为Unity是单线程的。而它们的执行顺序由Unity决定,满足一定条件时我们可以指定执行顺序。
而是否能够指定执行顺序主要取决于这两个对象是否为同一脚本的不同实例。
如果是:不能指定,这两个物体的事件函数的执行顺序是不一定的。
如果不是:可以指定。使用 Project Settings 窗口的 Script Execution Order 面板)。例如,如果有两个脚本,EngineBehaviour 和 SteeringBehaviour,可以设置 Script Execution Order,使 EngineBehaviours 始终在 SteeringBehaviours 之前更新。
另外,如果是一个物体的两个继承于MonoBehaviour的脚本,它们的Awake会按照脚本的添加顺序执行。
4.Update和FixedUpdate的区别?
Update()方法是在每一帧被渲染之前调用的,通常用于更新游戏对象的状态或行为,例如玩家输入、动画控制、音频播放等。由于它的调用时间不是固定的,取决于帧率和计算机性能,因此适合于处理非物理相关的计算。
而FixedUpdate()方法是根据固定时间间隔被调用的,一般默认为每秒50次。由于物理引擎的固定时间间隔是固定的,因此FixedUpdate()方法在每一帧中的调用次数是固定的,这使得它适合进行物理相关的计算,例如运动、碰撞检测等。
另外,与Update()方法不同,FixedUpdate()方法不能被暂停、恢复或禁用,因为它是基于物理引擎的固定时间间隔调用的。
5. 如果有多个物体需要进行碰撞检测,应该使用什么函数?
最常用的做法是使用物理引擎提供的碰撞检测机制,即给游戏对象添加Collider组件和Rigidbody组件,然后在脚本里使用OnCollisionXXX等事件函数来处理相关逻辑。
如果不使用物理引擎提供的碰撞检测机制,也可以使用Raycast或者OverlapSphere等函数来进行碰撞检测,不过这些函数需要手动调用,不属于Unity生命周期中的事件函数。
6. 什么是协程?
在Unity中,协程是一种特殊的函数,它可以在执行过程中暂停,并在指定的时间或条件满足时继续执行。Unity的协程是基于迭代器(yield)实现的,可以通过yield return语句来暂停协程的执行,并通过yield break语句来结束协程的执行。
协程的用途:
1.延时执行:协程可以在一定时间后执行指定的操作,例如延迟一定时间后执行动画播放、音效播放等操作。
2.动态加载资源:协程可以异步加载资源,并在资源加载完成后执行指定的操作,例如在游戏运行时动态加载场景、模型、贴图等资源。
3.动画控制:协程可以用来控制动画的播放,例如在一定时间内逐渐改变动画的状态、循环播放动画等。
4.多线程模拟:协程可以模拟多线程的效果,在主线程中执行协程并暂停,在另一个线程中执行指定的操作,例如下载文件、解压文件等操作。
5.协同工作:协程可以在不同的脚本之间进行协同工作,例如在一个脚本中控制另一个脚本的执行顺序和参数传递。
例子:
当玩家拿到一个道具时,我们想要显示一个“+1”的文字动画,然后等待一秒后自动消失。我们可以使用协程来实现这个效果。
public class PickupItem : MonoBehaviour{ public GameObject pickupEffect; private void OnTriggerEnter(Collider other){ if (other.CompareTag("Player")){ // 播放拾取特效 Instantiate(pickupEffect, transform.position, Quaternion.identity); // 开始协程 StartCoroutine(DisplayText()); } } IEnumerator DisplayText(){ // 显示“+1”文本 var text = GameObject.Instantiate(Resources.Load
("Text")); text.transform.position = transform.position; text.GetComponent ().text = "+1"; // 等待1秒 yield return new WaitForSeconds(1f); // 销毁文本 Destroy(text); } } 7.如何在场景切换时管理生命周期?
①在场景中使用不同的游戏对象管理不同的功能和状态,从而使生命周期管理更加清晰和简单。
②在需要在场景切换时保留的游戏对象上使用DontDestroyOnLoad方法,这样这些游戏对象就不会在场景切换时被销毁。
③在需要在场景切换时销毁的游戏对象上使用SceneManager.LoadScene方法,这样这些游戏对象就会在场景切换时被销毁,从而避免资源泄漏和性能问题。
④在游戏对象上使用Awake和OnDestroy方法,在游戏对象被创建时执行一些初始化操作,在游戏对象被销毁时执行一些清理操作,以确保游戏对象在生命周期内始终处于正确的状态。
8.Unity生命周期与内存管理的关系?
在Unity中,当一个游戏对象或组件被创建时,它会被分配一定的内存空间来存储它的数据和状态。当这个游戏对象或组件不再需要时,Unity会自动将它们的内存空间释放,以便其他对象或组件可以使用这些内存空间。在这个过程中,生命周期起到了关键作用。例如,当一个游戏对象被销毁时,Unity会自动调用这个对象的OnDestroy方法,在这个方法中释放这个游戏对象占用的内存空间。类似地,当一个组件被禁用或销毁时,Unity也会自动释放这个组件占用的内存空间。
除了自动内存管理外,Unity还提供了一些手动内存管理的功能,例如资源管理、对象池等。这些功能可以帮助开发者更好地控制内存的使用和释放,从而提高游戏的性能和稳定性。
猜你喜欢
网友评论
- 搜索
- 最新文章
- 热门文章