Unity脚本的生命周期
Unity脚本的生命周期
前言:Unity中定义了10个重要的事件函数,按照执行的先后顺序依次为以下的内容:
(1):Reset:重置函数,编辑期当脚本赋值给游戏对象时触发,仅执行一次。
(2):Awake:唤醒函数,最先执行的事件函数,用于优先级最高的事件处理,仅执行一次。
(3):OnEnable:启用函数,当脚本启动的时候触发,随着脚本的不断启用与禁用可以执行多次。
(4):Start:开始函数,一般用于给脚本字段赋初值使用,仅执行一次。
(5):FixedUpdate:固定更新函数,以默认0.02s的时钟频率执行,常用于物体学模拟中处理刚体的移动等,每秒执行多次。
(6):Update:更新函数,执行的频率不固定,与计算机当前的性能消耗乘反比,常用于逻辑计算,每秒执行多次。
(7):LateUpdate:后更新函数,在其余两个更新函数之后执行,常用于摄像机的控制情形中,每秒执行多次。
(8):OnGUI:图形绘制函数:绘制系统UI界面,每秒执行多次。
(9):OnDisable:脚本禁用函数,当脚本禁用的时候触发,随着脚本的不断启用和禁用,可以多次的执行。
(10):OnDestroy:销毁函数,本脚本所属游戏对象销毁的时候执行本脚本。仅执行一次。
1.写下来我们编写脚本来测试一下各个函数的生命周期:
public class Demo10_ScriptLifeCycle : MonoBehaviour { void Reset() { print("重置函数Reset"); } void Awake() { print("唤醒事件函数Awake"); } void OnEnable() { print("脚本启用事件函数OnEnable"); } void Start() { print("开始函数Start"); //开启调用函数 InvokeRepeating("InvokeTest", 0f, 1f); //开始协成 StartCoroutine("CoroutineTest"); } void FixedUpdate() { print("固定更新函数FixedUpdate"); } void Update() { print("更新函数Update"); //停止调用函数 if (Input.GetKey(KeyCode.I)) { CancelInvoke("InvokeTest"); } //停止协成 else if (Input.GetKey(KeyCode.C)) { StopCoroutine("CoroutineTest"); } } void LateUpdate() { print("延迟更新函数LateUpdate"); } void OnGUI() { print("界面绘制函数OnGUI"); } void OnDisable() { print("脚本禁用函数OnDisable"); } void OnDestroy() { print("销毁函数OnDestory"); } void InvokeTest() { print("被测试的调用函数的方法"); } IEnumerator CoroutineTest() { while (true) { yield return new WaitForSeconds(1f); print("被测试的协成"); } } }//class_end
当我们把脚本赋值给层级视图的一个空对象的时候,注意我们Console视图中会出现一个信息:
以上的输出的结果是Unity生命周期中的Reset()函数在编辑期间就触发的(当我们给空对象赋值我们的脚本的时候触发的)。好的,接下来执行我们的程序:
运行的结果我们可以看到脚本中的部分事件函数的执行的顺序与执行的次数。但是我们发现没有OnDisable和OnDestroy事件函数的输出。我们开始禁用这个脚本:
当我们禁用完脚本后会发现“被测试的调用函数的方法”和“被测试的协成”依然还是会执行:
当我们启用脚本的时候会执行OnEnable()事件函数:
我们用结构图表示各个函数的执行的顺序:
Unity伪线程揭秘:
前言:Unity是不支持多线程的,也就是说我们必须要在主线程中进行操作。但是我们在Unity中可以创建多个脚本。并且可以分别绑定在不同的游戏物体上。他们各自都在执行自己的生命周期,给我们的感觉像是多线程并执行脚本的。我们接下来测试一下脚本是如何执行的,第一个脚如下:
public class Demo10_ScriptLifeCycle_2 : MonoBehaviour { void Awake() { print("第一个脚本——唤醒函数Awake"); } void Update() { print("第一个脚本——更新函数Update"); } void LateUpdate() { print("第一个脚本——后更新函数LateUpdate"); } }//class_end
第二个脚本和第三个脚本类似,只不过就是方法名不同。这时候我们在Hierarchy视图中创建三个空挂点来挂载脚本。并在Project视图中创建三条脚本。然后按照顺序将脚本绑定在对用的游戏对象上。
当程序运行的时候我们截图看下面的结果:
关于脚本赋值给游戏对象的顺序也是影响到最终输出结果的,即先赋值给游戏对象的脚本后执行,后赋值给游戏对象的脚本先执行【这属于数据结构中“栈”的特性】。
这时候我们发现脚本的执行的顺序并不是杂乱无章的。它会先执行Awake(),在执行Start(),最后执行LateUpdate()。当我们删掉脚本一中的Update(),之后:
public class Demo10_ScriptLifeCycle_2 : MonoBehaviour { void Awake() { print("第一个脚本——唤醒函数Awake"); } //void Update() //{ // print("第一个脚本——更新函数Update"); //} void LateUpdate() { print("第一个脚本——后更新函数LateUpdate"); } }//class_end
这时候我们发现:
当我们播放的时候,他们的执行的顺序时没有任何的改变的。即使我们删掉了脚本一中的Update()的方法,但是它也不会执行LateUpdate(),而是等待脚本二和脚本三种的Update()方法执行完成之后,在执行所有的LateUpdate()。
通过上面的例子说明:每个脚本的Awake、Update、LateUpdate等等方法在后台都有一个总汇。
void Awake() { 脚本1中的Awake(); 脚本2中的Awake(); 脚本3中的Awake(); }
void Awake() { 脚本1中的Update(); 脚本2中的Update(); 脚本3中的Update(); }
我们接下来在给出一组例子,我们在脚本一Awake获取立方体对象:
void Awake() { print("第一个脚本——唤醒函数Awake"); GameObject go = GameObject.Find("Cube"); print(go.name); }
在脚本二Awake中创建一个游戏对象:
void Awake() { print("第二个脚本——唤醒函数Awake"); GameObject.CreatePrimitive(PrimitiveType.Cube); }
如果我们脚本的执行的顺序时:先执行脚本二在执行脚本一的话,我们就可以获取游戏对象的名字。根据我们的测试我们可以得到游戏对象的名字:
如果当我们的脚本一中是先创建一个游戏对象,然后再脚本二中得到游戏对象的名称就会报空指针的错误。
/// <summary> /// 脚本一 /// </summary> public class Demo10_ScriptLifeCycle_2 : MonoBehaviour { void Awake() { print("第一个脚本——唤醒函数Awake"); GameObject.CreatePrimitive(PrimitiveType.Cube); } void Update() { print("第一个脚本——更新函数Update"); } void LateUpdate() { print("第一个脚本——后更新函数LateUpdate"); } }//class_end
/// <summary> /// 脚本二 /// </summary> public class Demo10_ScriptLifeCycle_3 : MonoBehaviour { void Awake() { print("第二个脚本——唤醒函数Awake"); GameObject go = GameObject.Find("Cube"); print(go.name); } void Update() { print("第二个脚本——更新函数Update"); } void LateUpdate() { print("第二个脚本——后更新函数LateUpdate"); } }//class_end
我们测试如下:
所以我们发现在实际的项目中我们的脚本是非常的多的,他们的先后的顺序我们也不知道,所有建议一般把创建游戏对象的过程放在Awake()方法中,在Start()方法中获取游戏对象或者获取游戏组件,这样就可以确保不会报空指针的错误。