Unity 时间方法执行顺序(生命周期)

0x00 Emmmmm

有人用这个搞我,问我知不知道,在一个 update 函数中实例化了一个对象,那么这些函数在后续的几帧中的调用情况是啥样的,让我写个坐标轴,这谁受得了啊?

(其他还有比方说 Unity 协程的原理,我看过,居然忘了!)

所以今天我不管别的了就来看打印输出了,让脚本自己把每一帧,执行的函数,打印一下。其实网上有很多这样的讲解,但是看别人的科普终究不如自己实践,所以我决定花点时间从官方文档到写代码打印,理理清楚这些事件方法执行顺序( Order of Excution for event functions )。

0x01 FlowChart

这是 2012 年,由 Richard Fine 总结的 unity 3.4 版本的生命周期图,很清晰:

这是 unity 官方文档中 2018.4 版本的脚本生命周期流程图(script lifecycle flowchart):

有少部分的区别。

为了比较直观的对整个生命周期有更直观的理解,决定在一个场景中,尽量用到这些常用的函数,然后打印出相关的执行信息,特别是在每一帧具体执行了什么函数,这样也对后面实现游戏逻辑时应该在何处实现,有更深刻的理解哈。

今天的重点主要是常用的几个方法,Awake、OnEnable、Start、Update、OnDisable、OnDestory、OnGUI等,以及协程。

0x02 Conclusion

为了节约时间,更有目的性的阅读实验部分,这里把结论提前,对照结论,可以在实验中找到相应的语句进行验证。

结论:

  • 单个脚本中方法的执行顺序 Awake -> OnEnable -> Start -> Update.
  • 帧更新的顺序 FixedUpdate( xN ) -> Update -> LateUpdate
  • 开始运行时场景中所有有效( active )的对象脚本中的 Awake 和 OnEnable( 脚本组件 enabled )将会在在所有脚本的 Start 执行之前执行。
  • Start 只会在 Update 执行之前,执行一次。
  • Start 中开启的协程 yield 到帧结束,会在第二帧结束时( LateUpdate 完成 )继续。而在 Update 中开启的协程,会在本帧结束时继续。而 yield 一定时间则不会等到目标时刻的帧结束。
  • OnEnable 在每次脚本对象由 disabled 到 enabled,或挂载的对象由 inactive 到 active 时,都会执行。( 但如果脚本本身是 disable 的,对象由 inactive 到 active 也不会执行 OnEnable )
  • 实例化的对象只有在 active 的时候,才会执行 Awake OnEnable Start Update( 无论通过预制体还是场景内对象,本来在用作实例化时就没啥区别 )
  • OnGUI 在暂停时也会执行诶。

补充:

  • 多个脚本中 Awake 方法的执行是随机的,官方没有给出这种随机的规则或理由。(Each GameObject’s Awake is called in a random order between objects. Because of this, you should use Awake to set up references between scripts, and use Start to pass any information back and forth.)
  • Destroy 方法在销毁一个对象时,并不是立即销毁,而是在下一次 Update 执行之前(所有对象的 Update 方法是统一执行,不会出现一个对象开始执行 Update 了,而另一个对象还在处理 FixedUpdate 这种情况)
  • 介于上一条的,在调用 Destroy 方法后,可以继续访问此对象挂载的脚本中的方法或者其他组件,但在下一帧时,对象就会被销毁,指向对象的引用也会为空。
  • 调用 Destroy 方法时会立即调用对象的 OnDisable (如果对象有效),但对于多个对象的销毁这两个方法没有严格的顺序关系(只与执行位置有关系,下一帧之前 OnDestroy)。

0x03 Test

场景中为主要是通过 GameManager 启动协程,以及对 Cube 游戏对象,预制体进行实例化操作。Hierarchy 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
public class GameManager : MonoBehaviour
{
private int updateCount;
private int lateupdateCount;
private int fixedupdateCount;
public GameObject cubeActive;
public GameObject cubeInact;
public GameObject cubeObj;
public GameObject cubePrefab;

void Awake()
{
print(this + " On Awake.");
}

// Start is called before the first frame update
void Start()
{
print(this + " On Start.");
StartCoroutine(CoroutineInStart());
}

IEnumerator CoroutineInStart()
{
print(this + " On CoroutineInStart.");
yield return new WaitForEndOfFrame();

print(this + " On CoroutineInStart. After WaitForEndOfFrame(). yield 1");
var ob = GameObject.Instantiate(cubeObj);
var pb = GameObject.Instantiate(cubePrefab);
print(this + " On CoroutineInStart. " + ob + " is Instantiated inactive.");
yield return new WaitForFixedUpdate();

print(this + " On CoroutineInStart. After WaitForFixedUpdate(). yield 2");
print(this + " On CoroutineInStart. " + cubeActive.GetComponent<CubeController>() + " is disabled now.");
cubeActive.GetComponent<CubeController>().enabled = false;
cubeActive.SetActive(false);
print(this + " On CoroutineInStart. " + ob + " is active now.");
ob.SetActive(true);
pb.SetActive(true);
yield return new WaitForFixedUpdate();

print(this + " On CoroutineInStart. After WaitForFixedUpdate(). yield 3");
print(this + " On CoroutineInStart. " + cubeActive.GetComponent<CubeController>() + " is enabled now.");
cubeActive.SetActive(true);
cubeActive.GetComponent<CubeController>().enabled = true;
//ob.GetComponent<CubeController>().enabled = false;
yield return new WaitForSeconds(0.1f);

print(this + " On CoroutineInStart. After WaitForSeconds(0.1f). yield 4");

print(this + " On CoroutineInStart. " + ob + " is inactive now.");
ob.SetActive(false);
print(this + " On CoroutineInStart. " + ob + " is active now.");
ob.SetActive(true);
print(this + " On CoroutineInStart. " + ob + " is destroyed now.");
GameObject.Destroy(ob);
}

void OnEnable()
{
print(this + " On OnEnable.");
}

void OnDisable()
{
print(this + " On OnDisable.");
}

// Update is called once per frame
void Update()
{
updateCount++;
print(this + " On Update: " + updateCount + ".");
if (updateCount == 3)
{
StartCoroutine(CoroutineInUpdate3());
}
}

IEnumerator CoroutineInUpdate3()
{
print(this + " On CoroutineInUpdate3.");
yield return new WaitForEndOfFrame();

print(this + " On CoroutineInUpdate3. After WaitForEndOfFrame().");

}

void FixedUpdate()
{
fixedupdateCount++;
print(this + " On FixedUpdate: " + fixedupdateCount + ".");
}

void LateUpdate()
{
lateupdateCount++;
print(this + " On LateUpdate: " + lateupdateCount + ".");
}

void OnDestroy()
{
print(this + "On OnDestroy.");
}

void OnGUI()
{
//print(this + "On OnGUI.");
}
}

打印结果:

补充实验:

针对两个个问题:在 Destroy 一个对象之后,即在销毁行后对这个对象的内容进行调用,是否能够正确调用其内容(包括但不限于,当前脚本,对象的某些组件);Awake 方法执行顺序。

针对 Awake 猜测(不重要),做了一些测试,并查阅了官方文档,嗯,随机的。

关于 Destroy 的在协程中做了一些修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
IEnumerator CoroutineInStart()
{
...
yield return new WaitForSeconds(0.1f);

print(this + " On CoroutineInStart. After WaitForSeconds(0.1f). yield 4");

print(this + " On CoroutineInStart. " + ob + " is inactive now.");
ob.SetActive(false);
print(this + " On CoroutineInStart. " + ob + " is active now.");
ob.SetActive(true);
print(this + " On CoroutineInStart. " + ob + " is destroyed now.");
GameObject.Destroy(ob);
// new below
ob.GetComponent<CubeController>().SayHi(); // add a function print "I can hear yaaaa"
var obcol = ob.GetComponent<BoxCollider>(); // a cube has a box collider
print(obcol + " can be find, too.");
yield return new WaitForEndOfFrame();
print(this + " On CoroutineInStart. " + ob + " this Update End now.");
print(ob + ", ob is null? ");
print(obcol + ", ob collider is null?");
}

你是看了结论过来的吧,下面是打印输出:

0x04 Translate

官网文档的翻译:

(翻译完了才发现,Unity 中国现在有了自己的中文版文档( 使用了 Unity Web,需要耐心等待 ),但版本是 2018.1 也可以参考这个中文官方文档

First Scene Load

场景首次加载


These functions get called when a scene starts (once for each object in the scene).

当场景加载后,这些方法会被调用(场景中的每一个对象都会调用一次)。


  • Awake: This function is always called before any Start functions and also just after a prefab is instantiated.(If a GameObject is inactive during start up Awake is not called until it is made active.)

Awake 方法的执行总是先于任意的 Start 方法,也会在一个预制体被实例化的时候。而如果一个游戏对象( GameObject )是无效( inactive )状态的,Awake 方法会等到它有效( active )时被调用。

【ps 这里的 active inactive 是指 GameObject 的那个对勾吧】


  • OnEnable: (only called if the Object is active): This function is called just after the object is enabled. This happens when a MonoBehaviour instance is created, such as when a level is loaded or a GameObject with the script componenet is instantiated.

OnEnable 方法只针对那些有效( active )的对象有效,它会在对象激活的时候调用,这会发生在一个 MonoBehaviour 实例被创建的时候,比如场景加载时,或者挂在了脚本的游戏对象被实例化时。


  • OnLevelWasLoaded: This function is executed to inform the game that a new level has been loaded.

这个方法被用来通知游戏新的场景已经装载好了。


Note that for objects added to the scene, the Awake and OnEnable functions for all scripts will be called before Start, Update, etc. are called for any of them. Naturally, this cannot be enforced when an object is instantiated during gameplay.

注意当对象添加到场景中时,全部脚本中的 Awake 和 OnEnable 方法都将会在 Start、Update 等方法被调用之前执行。当然,在游戏过程中实例化对象时,不能被强制执行此方法。


Editor

编辑器


  • Reset: Reset is called to initialize the script’s properties when it is first attached to the object and also when the Reset command is used.

Reset 方法用来初始化脚本的属性,当脚本初次挂载到对象上或是执行了重置( Reset )命令时,会执行这个方法。


Before the first frame update

在第一帧更新之前


  • Start: Start is called before the first frame update only if the script instance is enabled.

Start 方法在第一帧更新( update )之前被调用,当然,脚本实例必须是激活( enabled )的。


For objects added to the scene, the Start function will be called on all scripts before Update, etc. are called for any of them. Naturally, this cannot be enforced when an object is instantiated during gameplay.

注意当对象添加到场景中时,全部脚本中的 Start 方法都将会在 Update 等方法被调用之前执行。当然,在游戏过程中实例化对象时,不能被强制执行此方法。


In between frames

在帧与帧之间


在帧之间

  • OnApplicationPause: This is called at the end of the frame where the pause is detected, effectively between the normal frame updates. One extra frame will be issued after OnApplicationPause is called to allow the game to show graphics that indicate the paused state.

OnApplicationPause 方法会在暂停时,执行完当前帧后调用。实际上,实在两个正常的帧更新之间。在这个方法被调用后,会有额外的一帧出现,来显示表明游戏被暂停了。


Update Order

更新顺序


When you’re keeping track of game logic and interactions, animations, camera positions, etc., there are a few different events you can use. The common pattern is to perform most tasks inside the Update function, but there are also other functions you can use.

当处理游戏逻辑,交互,动画,相机位置等等具体功能时,这里有一系列不同的事件方法可以使用。常见的模式是在 Update 方法中执行大部分的逻辑任务,当然,也有其他的方法可以使用。


  • FixedUpdate: FixedUpdate is often called more frequently than Update. It can be called multiple times per frame, if the frame rate is low and it may not be called between frames at all if the frame rate is high. All physics calculations and updates occur immediately after FixedUpdate. When applying movement calculations inside FixedUpdate, you do not need to multiply your values by Time.deltaTime. This is because FixedUpdate is called on a reliable timer, independent of the frame rate.

FixedUpdate 的调用比 Update 更加频繁:当帧率( frame rate )较低时,每一帧有可能多次调用 FixedUpdate 方法,但是当帧率较高时,也可能在帧率非常高时,在帧之间根本不会被调用。所有的物理计算和更新会在 FixedUpdate 执行完后立即产生效果。而且,当在 FixedUpdate 中处理移动计算时,也不需要乘上 Time.deltaTime 来弥补帧之间的时间差。因为 FixedUpdate 是基于一个可靠的计时器的定时调用的,与帧率无关。


  • Update: Update is called once per frame. It is the main workhorse function for frame updates.

Update 方法每帧会调用一次,是帧更新中干活的主力。


  • LateUpdate: LateUpdate is called once per frame, after Update has finished. Any calculations that are performed in Update will have completed when LateUpdate begins. A common use for LateUpdate would be a following third-person camera. If you make your character move and turn inside Update, you can perform all camera movement and rotation calculations in LateUpdate. This will ensure that the character has moved completely before the camera tracks its position.

LateUpdate 同样会每帧更新一次,但是在 Update 完成之后执行。也就意味着,当 LateUpdate 开始时,Update 中的任何计算行为都将完成。对于 LateUpdate 的应用有一个比较经典的案例,第三人称摄像机跟随。将人物角色的移动等操作放在 Update 中,将摄像机的运动(平移,旋转等等)放在 LateUpdate 中,这就可以确保每一帧镜头的移动都是在人物移动之后进行的。


Animation update loop

这一部分是动画更新循环,并不常见,因此暂时不做翻译啦。


Rendering

渲染( ,表达,刷墙…… )


  • OnPreCull: Called before the camera culls the scene. Culling determines which objects are visible to the camera. OnPreCull is called just before culling takes place.

在相机进行剔除之前执行。剔除决定哪些对象对于相机来说是可见的。

  • OnBecameVisible/OnBecameInvisible: Called when an object becomes visible/invisible to any camera.

在对象相对于相机可见或不可见时调用。

  • OnWillRenderObject: Called once for each camera if the object is visible.

在对象可见时,每个相机调用一次。

  • OnPreRender: Called before the camera starts rendering the scene.

在相机开始渲染场景前调用。

  • OnRenderObject: Called after all regular scene rendering is done. You can use GL class or Graphics.DrawMeshNow to draw custom geometry at this point.

在场景渲染完成后调用,可以通过使用 GL 类或 Graphics.DrawMeshNow 方法绘出图形。

  • OnPostRender: Called after a camera finishes rendering the scene.

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

  • OnRenderImage: Called after scene rendering is complete to allow post-processing of the image, see Post-processing Effects.

在场景完成渲染后调用来处理图像的后处理。

  • OnGUI: Called multiple times per frame in response to GUI events. The Layout and Repaint events are processed first, followed by a Layout and keyboard/mouse event for each input event.

OnGUI 用来响应 GUI 事件,每一帧会调用多次。首先处理布 Layout 和 Repaint 事件,然后为每一个输入时间处理 Layout 和 keyboard/mouse 事件。

  • OnDrawGizmos Used for drawing Gizmos in the scene view for visualisation purposes.

用来画场景中的小物件(Gizmos)。


Coroutines

协程


Normal coroutine updates are run after the Update function returns. A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes. Different uses of Coroutines:

正常协程的更新在 Update 方法返回之后执行。一个协程实际是一个可以在执行中挂起的方法( yield ),并在给出的 YieldInstruction 完成时继续执行。

  • yield The coroutine will continue after all Update functions have been called on the next frame.

下一帧中所有的 Update 方法完成后继续。

  • yield WaitForSeconds Continue after a specified time delay, after all Update functions have been called for the frame

经过一个特定的时延,并在当前帧的所有 Update 方法完成后继续。

  • yield WaitForFixedUpdate Continue after all FixedUpdate has been called on all scripts

在所有的 FixedUpdate 完成后继续。

在我测试的时候,发现 WaitForFixedUpdate 并不是等到所有的 FixedUpdate 执行完成,而是下一次完成就继续执行协程了( 我测试的版本是 2018.3.6f1,而这篇文档是 2018.4 )。从上面的老图上也可以看到,当时的 WaitForFixedUpdate 是会等到下一个 FixedUpdate 结束。

  • yield WWW Continue after a WWW download has completed.

在一次 WWW 下载完成后继续。

  • yield StartCoroutine Chains the coroutine, and will wait for the MyFunc coroutine to complete first.

连接一个协程,在等待 MyFunc 协程完成后继续。


When the Object is destroyed

当对象被摧毁


  • OnDestroy: This function is called after all frame updates for the last frame of the object’s existence (the object might be destroyed in response to Object.Destroy or at the closure of a scene).

OnDestroy 方法在对象存在的最后一帧中所有的帧更新结束后调用。对象可以是被 Object.Destory 方法摧毁或由于场景结束。


When quitting

当退出


These functions get called on all the active objects in your scene:

这些函数会在场景中所有有效( active )上被调用。

  • OnApplicationQuit: This function is called on all game objects before the application is quit. In the editor it is called when the user stops playmode.

当退出应用时,或用户停止播放。

  • OnDisable: This function is called when the behaviour becomes disabled or inactive.

当变为非激活( disable )或无效( inactive )时。


0xff

最后,我哭我的 surface 笔挂了,随便看看这个 timeline 吧,简单的在第二帧实例化一个无效( inactive )的 cube 对象,在第四帧将 cube 设置为有效( active )的方法调用情况: