官方文档:事件函数的执行顺序
脚本的生命周期
如图:
脚本的生命周期主要经历以下几个阶段:
初始化
初始化阶段,(包括初始化Awake,OnEnable,然后Editor的Reset被穿插在着初始化过程之间,因此我们可以在脚本里重写Reset方法,这将在编辑器中的game的start之前执行。最后Start),当我们开始游戏的时候,初始化顺序就是Awake,OnEnable,Reset(如果定义了且在编辑器内),Start
Awake:在实例化预制件之后调用此函数。(如果游戏对象在启动期间处于非活动状态,则在激活之后才会调用 Awake。)
OnEnable :(仅在对象处于激活状态时调用)在启用对象后立即调用此函数。在创建 MonoBehaviour
实例时(例如加载关卡或实例化具有脚本组件的游戏对象时)会执行此调用。
Start:仅当启用脚本实例后,才会在第一次帧更新之前调用 Start。
以上时刻都是在第一帧更新之前执行的,一般来说要物体经历Awake,OnEnable之后才会进入Start,例如AB两物体,假设A在B之前执行,执行顺序是A.Awake,A.OnEnable,B.Awake,B.OnEnable,A.Start,B.Start.
更新顺序
在初始化结束后,则进入更新阶段,其中不同更新方式的执行顺序是不同的:
FixedUpdate:该阶段更新时长是固定不变的,大约0.02f一次,主要为了物理系统使用,调用 FixedUpdate 的频度常常超过Update。如果帧率很低,可以每帧调用该函数多次;如果帧率很高,可能在帧之间完全不调用该函数。在 FixedUpdate之后将立即进行所有物理计算和更新。在 FixedUpdate 内应用运动计算时,无需将值乘以 Time.deltaTime。这是因为FixedUpdate 的调用基于可靠的计时器(独立于帧率)。
Update:每帧调用一次 Update。这是用于帧更新的主要函数。Update由于受到帧率影响,因此通常一些游戏控制逻辑会放在这里
LateUpdate:每帧调用一次 LateUpdate__(在 Update__ 完成后)。LateUpdate 开始时,在 Update 中执行的所有计算便已完成。LateUpdate 的常见用途是跟随第三人称摄像机。如果在 Update 内让角色移动和转向,可以在 LateUpdate 中执行所有摄像机移动和旋转计算。这样可以确保角色在摄像机跟踪其位置之前已完全移动。
从上述生命周期图可以看到,更新阶段中三种更新方式的执行顺序应当是依次的,其中第一种方式不受帧率影响,而Update和LateUpdate受帧率影响,且LateUpdate在Update后执行,因此我们可以从执行顺序和执行频率的角度来考虑什么时候使用怎样的更新方式。
动画更新循环
图中灰色区块是动画更新执行的顺序,在物理阶段的FixedUpdate中更新和在游戏主逻辑阶段的Update中更新的流程都是差不多的。
首先执行状态机的固定更新,随后进入状态机的Entry或Exit状态(如果状态机中执行了的话),随后去处理其他动画事件
StateMachineBehaviour (OnStateEnter/OnStateUpdate/OnStateExit):一个层最多可以有 3 个活动状态:当前状态、中断状态和下一个状态。使用一个定义 OnStateEnter、OnStateUpdate 或 OnStateExit 回调的 StateMachineBehaviour 组件为每个活动状态调用此函数。依次针对当前状态、中断状态和下一个状态调用此函数。
可以看到StateMachineBehaviour ,也就是状态机之间的状态切换之后也会给出回调,可以在这些状态切换时定义回调函数。
各类事件
在生命周期中,我们可以看到事件的执行顺序大致都是按照它们的类别处理的:
OnState,OnAnimation等动画事件,最优先
OnTrigger,OnCollistion等物理事件,后于动画事件
OnMouse,OnKey(猜测有,不确信)等输入事件,后于物理事件
OnState,OnAnimation等动画事件,在Update时再次更新
OnRender…等一些场景渲染事件,在LateUpdate后更新
OnDrawGizmos,Gizmos渲染,在场景渲染之后,用于在场景视图中绘制辅助图标以实现可视化。
OnGUI,GUI渲染,在Gizmos渲染之后。每帧调用多次以响应 GUI 事件。首先处理布局和重新绘制事件,然后为每个输入事件处理布局和键盘/鼠标事件。
OnApplicationPause暂停应用,在发生暂停的帧之后调用,但在实际暂停之前发出另一帧。
可以看到整个中间事件处理主要流程就是:先处理动画事件,然后处理物理事件,随后是输入事件,然后动画再更新,随后是一些渲染事件,而GUI的渲染是在一切之后的。
结束阶段
在场景中的所有活动对象上调用以下函数:
OnApplicationQuit:在退出应用程序之前在所有游戏对象上调用此函数。在编辑器中,用户停止播放模式时,调用函数。
OnDisable:行为被禁用或处于非活动状态时,调用此函数。
Object.Destroy 对象存在的最后一帧完成所有帧更新之后,调用此函数(可能应 Object.Destroy 要求或在场景关闭时销毁该对象)。
上述事件例如OnDisable,Destroy等都是在游戏中可以执行的,而OnApplicationQuit方法只能在执行退出时调用。整个退出过程就是OnApplicationQuit,OnDisable,Destroy 。而执行退出的方法应当是Application.Quit。
阶段分析
上述过程大致可分为:初始化阶段,物理阶段,输入检测阶段,游戏逻辑阶段,渲染阶段,结束阶段。
初始化阶段在游戏开始时进行一次,而结束阶段也只会在游戏结束时进行一次,其余阶段在每帧都会顺序地循环进行一次。
其中内部动画更新穿插在物理阶段的FixedUpdate之后,物理事件之前,以及游戏逻辑阶段的Update之后,LaterUpdate之间。输入检测阶段在物理阶段和游戏逻辑阶段之间,即FixedUpdate结束,Update之前。渲染阶段在游戏逻辑阶段之后,处于整个帧的最后,当渲染阶段结束意味着该帧结束。
协程返回
不难发现,实际上协程的返回时刻可以视为一些阶段起始点和结束点的标志。例如yield WaitForFixedUpdate
标志FixedUpdate结束的时刻,yield null
和其他一些随后的返回则在Update之后,yield WaitForEndOfFrame
则代表了每帧结束的时刻。(中间英文翻译:一些本来应该返回的协程如果中断了,在之后恢复执行也要在Update执行yield,可能翻译有误)
总结
整个生命周期重要时刻的执行顺序:
- 初始化阶段
- Awake ,OnEnable ,Restart(编辑器),Start
- 物理阶段
- OnFixedUpdate
- (内部动画执行)
- yield return WaitForFixedUpdate
- 输入阶段
- OnMouseXXX
- 游戏逻辑阶段
- Update
- yield return null
- yield return WaitForSeconds
- yield return WWW
- yield return StartCoroutine
- (内部动画执行)
- LaterUpdate
- 渲染阶段
- SceneRender场景渲染
- Gizmos渲染
- GUIRender(GUI渲染最后)
- yield return WaitForEndOfFrame 标志帧的结束
- (暂停,帧结束回到OnFixedUpdate或者结束游戏)
- 结束阶段
- OnApplicationQuit,OnDisable,Destory