正式开始学习Unity了。当然,第一个遇到的问题就是Awake和Start的问题,之前在网上查过一下这两者的区别,简单记忆了一下,认为自己知道了两者的区别。不过实际用起来,发现对于这两者到底是什么区别,心里还是没底,而且最关键的是木有Unityt的源代码,所以我们只能是通过文档或者是别人的blog来了解,当然,还有一个办法就是自己做一下实验,实践是检验真理的唯一标准。
一.官方解释
先来看看Unity官方对于这两个函数的解释:
Awake is called when the script instance is being loaded.
Awake is used to initialize any variables or game state before the game starts. Awake is called only once during the lifetime of the script
instance. Awake is called after all objects are initialized so you can safely speak to other objects or query them using
eg. GameObject.FindWithTag. 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. Awake is always called before any
Start functions. This allows you to order initialization of scripts. Awake can not act as a coroutine.
Start is called on the frame when a script is enabled just before any of the Update methods is called the first time.
Like the Awake function, Start is called exactly once in the lifetime of the script. However, Awake is called when the script object is
initialised, regardless of whether or not the script is enabled. Start may not be called on the same frame as Awake if the script is not
enabled at initialisation time.
The Awake function is called on all objects in the scene before any object‘s Start function is called. This fact is useful in cases
where object A‘s initialisation code needs to rely on object B‘s already being initialised; B‘s initialisation should be done in Awake
while A‘s should be done in Start.Where objects are instantiated during gameplay, their Awake function will naturally be called after
the Start functions of scene objects have already completed.
解释一下:
Awake在脚本被实例化的时候就会被调用(不管脚本是不是enable的),而且在脚本的生命周期中只会被调用一次。Awake是在所有对象实例化之后,所以我们可以放心大胆地去使用诸如GmeObject.Fine之类的方法来在Awake中给各个组件之间添加引用 关系。Awake会在所有对象的Start之前调用,但是注意不同对象之间的Awake顺序是不得而知的。
Start是在对象被第一次enable之后,在Update之前调用的,Start在脚本的生命周期中也只可能被调用一次。Start可能不会被立刻调用,比如我们之前没有让其enable,当脚本被enable时,Start才会被调用。
官方文档的建议是:尽量在Awake函数中进行初始化操作,除非有A依赖B,B必须在A实例化之前完成初始化,那么A在Start,B放在Awake中可以保证A在B之后才被初始化(不过个人感觉还是应该尽量都在Awake中进行对象间的引用,然后手动调用Init函数进行初始化,这样可以自己控制初始化的顺序)。
二.几个实验
1.关于Awake,Start,Update的执行时机,以及不被enable时的情况
在场景中创建一个空的Object,把下面的脚本挂上去:
public class StartAwakeTest : MonoBehaviour { // Use this for initialization void Start () { Debug.Log("Start is called!"); } void Awake() { Debug.Log("Awake is called!"); } // Update is called once per frame void Update () { Debug.Log("Update is called!"); } }
直接运行游戏时,输出如下:
和官方文档所说的一致,这个我们早就知道了。不过,如果我们一开始让脚本对象不被激活,最简单的方法就是在编辑器的Inspector面板上,找到对应的脚本前面有一个小的勾选框,默认是被勾选的,就是被激活的,如果取消勾选,那么这个脚本组件就不会被激活。
我们试一下,取消脚本的激活,然后运行:
我们看到,Start和Update函数都没有被执行,而Awake函数仍然被执行了。可见,不管Object被不被激活,Awake函数都会被执行。这时,我们手动勾选一下Start Awake Test前面的勾选框,结果就和第一幅图一样啦,Start和Update都开始被执行了。
2.通过脚本创建的对象的Awake和Start运行情况
我们把之前的脚本去掉,然后在对象上挂上这样一个脚本:
using UnityEngine; using System.Collections; public class CreateObj : MonoBehaviour { //此处通过一个引用来保存对象,因为被取消激活的对象是不能被find函数找到的!!! private GameObject go = null; void Awake() { go = new GameObject("game object"); } void Update() { //添加脚本组件,默认不激活 if (Input.GetKeyUp(KeyCode.F1)) { go.AddComponent<StartAwakeTest>(); //只让StartAwakeTest Component 不激活 = 在编辑器里面取消脚本前面的勾选 //go.GetComponent<StartAwakeTest>().enabled = false; //直接让Obj不激活 go.SetActive(false); } //将其激活 if (Input.GetKeyUp(KeyCode.F2)) { if (go == null) return; go.SetActive(true); } //将其取消激活 if (Input.GetKeyUp(KeyCode.F3)) { if (go == null) return; go.SetActive(false); } } }
运行之后,对象虽然创建了,但是没有挂上脚本,我们通过F1按钮,控制其动态添加脚本,这时,会输出Awake,但是由于我们设置了对象是非Active的,所以Start函数并没有调用:
当我们按下F2时,该object被激活,这时Start函数和Update函数会开始执行。输出“Start is called!","Update is called"当我们按下F3之后,对象被取消激活,这时,Update不会再被执行。
3.对象第二次被激活时Start会被再次调用吗
当我们再次按下F2,让对象第二次被激活,Start函数还会调用吗?(这也是我最关心的)
可见,虽然Object被第二次激活,但是Start函数不会再被调用了!说明Start函数只有在第一次被激活的时候才会被调用!!!
三.对象初始化的时机
Awake和Start两者都只能在生命周期中被调用一次,而且都是最先调用的,所以研究Awake和Start就是为了研究Unity对象的初始化机制,我们进一步地看一下Uniyt初始化时的流程。
1.Find方法
先来看一个函数,Find。我们知道Unity的Find函数可以根据名字查询到场景中的物体,但是这个物体必须是被激活的,如果我们把这个物体SetActive(false)了,那么这个函数是找不到对应物体的。比如下面的一段代码:
void Awake() { GameObject go = new GameObject("game object"); go.SetActive(false); GameObject go1 = GameObject.Find("game object"); if (go1 == null) Debug.Log("Can't find!"); else Debug.Log("Find!"); }
结果:
为什么要看Find方法呢,因为很多情况下,我们都是通过Find来找到对象之间的对应关系。我们通过Find可以进行下一步的实验:
2.Awake的调用时机
我之前有点搞不懂Awake的调用时机,担心会出现在一个Obj被Awake了,其他的没有被Awake,会造成对象空引用的错误,但是事实上并不是这样,看这样一个例子:
以下两段脚本分别被绑定在两个对象上:
Obj1对象上的脚本:
using UnityEngine; using System.Collections; public class Component1 : MonoBehaviour { void Awake() { GameObject go = GameObject.Find("Obj2"); if (go != null) Debug.Log("Obj2 is found!"); go.GetComponent<Component2>().Test(); } public void Test() { Debug.Log("Test in Component1 is called"); } }
Obj2对象上的脚本:
using UnityEngine; using System.Collections; public class Component2 : MonoBehaviour { void Awake() { GameObject go = GameObject.Find("Obj1"); if (go == null) Debug.Log("Obj1 is not found!"); Debug.Log("Obj1 is found!"); go.GetComponent<Component1>().Test(); } public void Test() { Debug.Log("Test in Component2 is called"); } }
运行结果如下:
可见,在两个对象的Awake函数中,都通过名字查找到了对方的对象,并且调用了对方的函数。这说明在Awake函数调用之前,所有的对象已经创建完毕了!所以我们可以通过这种方式来在Awake函数中放心大胆的设置对象之间的引用关系。
3.Start,Awake,和自定义函数的调用顺序
还有一个疑问,就是如果我们自己通过脚本建立一个对象,然后马上调用其中的一个函数,那么,Start和Awake还会在之前被调用吗?不多说,上代码。
在场景中建立一个对象,挂上下面的脚本:
using UnityEngine; using System.Collections; public class Component1 : MonoBehaviour { void Awake() { Debug.Log("Awake in Original GameObject is called!"); } void Start() { Debug.Log("Start in Original GameObject is called!"); GameObject go = new GameObject("new Obj"); go.AddComponent<Component2>(); go.GetComponent<Component2>().Test(); } }
然后准备另一个脚本,供动态生成的对象使用:
using UnityEngine; using System.Collections; public class Component2 : MonoBehaviour { void Awake() { Debug.Log("Awake in new GameObject is called!"); } void Start() { Debug.Log("Start in new GameObject is called!"); } public void Test() { Debug.Log("Test in new GameObject is called"); } }
结果如图:
Look!Awake函数最先被调用了,然后接着是我们自定义的Test函数,最后才是Start函数!!!这里应该是很容易出现问题的地方,比如Test函数中要用到一些值,而这些值应该被初始化,如果我们把初始化放在了Start函数中,那么此处这些值还没有被初始化,那么就会出现空引用异常等错误。我之前也是遇到了很多次,查了半天发现都是把对象的初始化放在了Start函数中,结果浪费了大量的时间,这也是我写这篇文章的重要原因之一,希望大家少走弯路!
四.总结
最后总结一下Awake和Start的异同点:
相同点:
1)两者都是对象初始化时调用的,都在Update之前,场景中的对象都生成后才会调用Awake,Awake调用完才会调用Start,所有Start调用完才会开始Update。
2)两者在对象生命周期内都只会被调用一次,即初始化时被调用,之后即使是在被重新激活之后也不会再次被调用。
不同点:
1)Awake函数在对象初始化之后立刻就会调用,换句话说,对象初始化之后第一调用的函数就是Awake;而Start是在对象初始化后,第一次Update之前调用的,
在Start中进行初始化不是很安全,因为它可能被其他自定义的函数抢先。
2)Awake不管对象是否是Active,脚本是否enabled都会被调用,可以说是无论如何都会被调用的;而Start如果对象被SetAcive(false)或者enabled= false了是
不会被调用的。