Unity认识常用的生命周期函数(AwakeStartUpdate...)

Posted _ElecSheep

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity认识常用的生命周期函数(AwakeStartUpdate...)相关的知识,希望对你有一定的参考价值。

一、了解帧的概念

游戏的本质就是一个死循环
每一次循环都会处理游戏逻辑 并 更新一次游戏画面
之所以能看到画面在动 是因为
切换画面速度达到一定速度时
人眼就会认为画面是动态且流畅的
一帧就是执行了一次循环
Unity底层已经封装好了这个死循环
我们只需要利用Unity的生命周期函数的规则来执行游戏逻辑即可

FPS(Frames Per Second)
即每秒钟帧数
一般我们说60帧30帧
意思是1秒更新60次、30次画面
1s = 1000ms
60帧:1帧为 1000ms/60 ≈ 16.66ms
30帧:1帧为 1000ms/30 ≈ 33.33ms

游戏卡顿的原因:
跑1帧游戏逻辑的计算量过大,或者硬件性能过低,无法在一帧的时间内处理完所有游戏逻辑

二、生命周期函数的概念

所有继承MonoBehavior的脚本 最终都会挂载到GameObject游戏对象上
生命周期函数就是该脚本对象依附的GameObject对象从出生到消亡整个生命周期中
会通过反射自动调用的一些特殊函数

Unity帮助我们记录了一个GameObject对象依附了哪些脚本
会自动地得到这些对象,通过反射去执行一些固定名字的函数(就是生命周期函数)

三、生命周期函数

注意:
生命周期函数的访问修饰符一般为private和protected
因为不需要在外部手动调用生命周期函数,都是Unity自动帮我们调用

3-1.Awake

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson1 : MonoBehaviour

    //当一个对象(自己这个类对象 而不是依附的GameObject)被创建时,会调用该生命周期函数
    //作用:Awake是类似构造函数的存在,我们可以在一个类对象刚被创建时,进行一些初始化操作
    //Awake只会被执行一次
    private void Awake()
    
        //补充知识点:在Unity中打印信息的两种方式
        //1.如果没有继承MonoBehaviour,可以使用debug.Log();
        Debug.Log("我是打印的信息");
        Debug.LogWarning("警告!");
        Debug.LogError("出错了!");

        //2.如果继承了MonoBehaviour 有一个现成的方法可以实现打印
        print("我是打印的信息");
    

3-2.OnEnable

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson1 : MonoBehaviour

    //依附的GameObject对象每次被激活时 会被调用
    //作用:想要当一个对象被激活时 进行一些逻辑处理,就可以写在本函数中
    private void OnEnable()
    
        print("我依附的GameObject被激活了");
    

3-2.Start

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson1 : MonoBehaviour

    //从自己被创建出来后,第一次帧更新之前被调用
    //作用:还是用于初始化信息的,但是它相对Awake来说,要执行的晚一些
    //      因为它是在对象进行帧更新之前才会被执行
    //一个对象只会调用一次
    private void Start()
    
        print("我在第1帧更新前被执行");
    

3-4.FixedUpdate

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson1 : MonoBehaviour

    //固定间隔时间执行,间隔的时间可以设置
    //作用:用于进行物理相关的更新(如碰撞检测)
    //      它是每一帧都会执行的,但是这里的帧和游戏帧有点不同
    private void FixedUpdate()
    
        print("我会固定间隔时间循环执行");
    


3-5.Update

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson1 : MonoBehaviour

    //逻辑帧更新
    //每秒更新多少次是可以设置的,如果不设置 默认会以最快的速度更新
    //作用:用于处理游戏核心逻辑更新
    private void Update()
    
        print("我一帧被执行一次");
    

3-6.LateUpdate

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson1 : MonoBehaviour

    //每帧执行 于Update之后执行
    //作用:一般用来处理摄像机位置更新相关内容
    //      在Update和LateUpdate之间,Unity进行了一些处理,处理动画相关的更新
    private void LateUpdate()
    
        print("我每针都会被执行,但晚于Update");
    

3-7.OnDisable

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson1 : MonoBehaviour

    //依附的GameObject对象每次失活时被调用(对象被销毁时也会被调用)
    //作用:想要当一个对象失活时 进行一些逻辑处理,就可以写在本函数中
    private void OnDisable()
    
        print("我依附的GameObject失活了");
    

3-8.OnDestroy

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson1 : MonoBehaviour

    //对象被销毁时被调用(依附的GameObject对象被删除时)
    private void OnDestroy()
    
        print("我被销毁了");
    

四、生命周期函数支持继承和多态


Lesson1的脚本如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson1 : MonoBehaviour

    //把Awake写成一个虚函数
    protected virtual void Awake()
    
        print("父类的Awake");
    

Lesson1Son的脚本如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson1Son : Lesson1

    //可以重写父类Lesson1的虚函数
    protected override void Awake()
    
        base.Awake();
        print("子类的Awake");
    

运行:可以看到,父类的Awake和子类的Awake都被执行了,所以生命周期函数支持继承和多态

五、补充:关于继承Mono的类的构造函数

要知道,虽然不建议在继承MonoBehavior的类中写构造函数
但是不意味着不能写,当在继承MonoBehavior的类中写无参构造函数时
会发现在编辑模式下或者运行后,只要该脚本挂载在场景中,那么该无参构造函数是会被自动执行
因为Unity的工作原理中提到的反射机制,实际上Unity通过反射帮助我们实例化了该脚本对象
既然要实例化那么肯定是需要new的,只不过Unity中不需要我们自己new继承了MonoBehavior的类,只要挂载后Unity就帮助我们做了这件事

那么为什么不建议继承MonoBehavior的类写构造函数呢?
1.Unity的规则就是,继承MonoBehavior的脚本不能new只能挂载
2.生命周期函数的Awake是类似构造函数的存在,当对象出生就会自动调用
3.写构造函数反而在结构上会破坏Unity设计上的规范
总结:
如果继承MonoBehavior的脚本想要进行初始化相关,可以在Awake或者Start中进行,搞清这两个生命周期函数的执行时机,根据需求选择在哪里进行初始化。
切记!!继承MonoBehavior的脚本不要new,不要new,不要new!!

六、补充:不同对象的声明周期函数是在同一个线程中执行的吗?

Unity中所有对象上挂载的生命周期函数都是在一个主线程中按先后执行的
Unity会主动把场景上的对象,对象上挂载的脚本都统统记录下来,
在主线程的死循环中,按顺序按时机的通过反射,执行记录的对象身上挂载的脚本的对应生命周期函数

一文读懂Unity常用生命周期函数! 超级详细不服来辩~

一,初始化

1.1 函数描述

运行状态初始化:(也是执行顺序)

  1. Awake:初始化时调用,在Start函数之前调用
  2. OnEnable:在对象启用时调用
  3. Start:仅当启用脚本实例时,才会在第一帧调用

编辑器状态初始化

  1. Reset:编辑器下调用,当脚本第一次附加到物体上或者点击Reset时执行,来初始化脚本属性。

实际应用

  1. Awake:通常使用为需要提前初始化的逻辑。比如单例赋值
    private void Awake(){Instance = this;}
  2. OnEnable:处理每次显示时都需要进行初始化的逻辑,通常和OnDisable配合使用。
    比如: 游戏逻辑使用并修改了变量Number,而下次显示时使用是又需要Number = 1,此时就可以写private void Awake(){Number = 1;}
  3. Start:通常使用为一些变量初始化逻辑。
    比如:获取指定物体:private void Start(){child1 = transform.GetChild(0);}
  4. Reset:通常在游戏测试调试时,使用编辑器下的初始化。通过点击Reset执行一个逻辑。

1.2 示例解析

运行状态初始化示例:
  搭建场景新建两个UI -> Image,一个作为背景,一个作为弹窗。新建脚本并挂载到弹窗Image上。

脚本内容如下:

using UnityEngine;

public class LifeCycleFunction : MonoBehaviour
{
    #region Initialization 初始化
    public int Number = 1;
     
    private void Awake()
    {
        Debug.Log("Awake 初始化时调用,在Start函数之前调用,...");
    }
    private void OnEnable()
    {
        Debug.Log("OnEnable 每次对象启用都会被调用...");
    }
    private void Reset()
    {
        Number = 0;
        Debug.Log("Reset 编辑器模式下才可被调用...Number: " + Number);
    }
    private void Start()
    {
        Debug.Log("Start 仅当启用脚本实例时,才会在第一帧调用...");
    }
    
    #endregion
}

此次运行结果展示了Awake,OnEnable,Start 的执行顺序,运行结果:

编辑器下的初始化:
1.脚本拖拽附加时自动执行

2.点击Reset调用: 可以看到我手动Number设置为2,Reset后被重置为代码中写的0;


二,更新

2.1 函数描述

三个函数

  1. FixedUpdate:固定时间调用,FixedUpdate通常比Update更频繁地调用
  2. Update:每帧调用一次
  3. LateUpdate:在Update完成后,每帧调用一次

实际应用

  1. FixedUpdate:所有物理计算和更新都在FixedUpdate中处理。它是固定时间调用,不会受到帧率影响。比如:一些物理属性的更新操作Force,Collider,Rigidbody等。
  2. Update:每帧调用一次,根据帧率的快慢影响执行速度。通常的游戏逻辑都写在这里,比如:和玩家交换,当用户按下空格时进行执行什么操作。
  3. LateUpdate:每次Update完成后调用移除。常见用处是相机跟随主角,比如:主角在Update中移动,则可以在LateUpdate执行相机的移动,这将可以保证摄像机跟着的时候之前的逻辑一起完全执行完成。

2.2 示例解析

将如下脚本添加到上面新建的脚本中:(为了方便查看,将上面初始化的函数先注释掉)

#region 更新

// 固定时间调用,FixedUpdate通常比Update更频繁地调用
private void FixedUpdate()
{
    Debug.Log("FixedUpdate 固定时间调用...");
}
// 每帧调用一次
private void Update()
{
    Debug.Log("Update 每帧调用一次...");
}
// 在Update后,每帧调用一次 
private void LateUpdate()
{
    Debug.Log("LateUpdate 在Update后,每帧调用一次...");
}
#endregion

运行后,可以看到结果如下:


三,鼠标交互

3.1 函数描述

几个函数

  1. OnMouseEnter: 鼠标进入时调用一次
  2. OnMouseOver: 鼠标停留(经过)时一直调用
  3. OnMouseExit: 鼠标退出时调用一次
  4. OnMouseDown: 鼠标按下时调用一次
  5. OnMouseDrag: 鼠标拖拽(按住)时一直调用
  6. OnMouseUp: 鼠标抬起时调用一次

实际使用:使用时一般都是成对使用

  1. OnMouseEnter,OnMouseOver,OnMouseExit 一组。比如模拟选中状态:鼠标进入时物体变色,鼠标退出时再变回来。
  2. OnMouseDown,OnMouseDrag,OnMouseUp 一组。比如射击游戏:鼠标按下拖拽时调整方向,抬起时发射子弹。
  3. 当鼠标按下并停留在当前游戏对象上时,OnMouseOver,OnMouseDrag会同时触发。

检测原理

  1. 只能检测当前脚本挂载的游戏对象。
  2. 当前游戏对象需要有碰撞体。
  3. 不能有其他物体(UI)遮挡到此游戏对象。
  • 总结为一局话就是:OnMouseXXX的原理是通过鼠标的射线检测来判断鼠标当前位置是否碰到了挂载脚本游戏对象的碰撞体。

勾选IsTrigger:

  若需要不检测勾选IsTrigger的碰撞体,Edit => Project Settings => Physics中的Queries Hit Triggers,将这个✅ 取消,即可不触发勾选IsTrigger的。【注意:默认是✅ 勾选状态,不需要触发则取消勾选】


3.2 示例解析

  场景中创建一个Cube,将其位置调整在摄像机先显示即可【示例中调整位置(0,0,-1),缩放为(3,3,3)】。

测试功能:

  • 鼠标进入\\退出,触发Cube颜色变化
  • 鼠标移动到Cube上,触发旋转
  • 鼠标在Cube按下并拖拽,触发Cube跟随移动
  • 鼠标抬起Cube回归到原来位置

创建并挂载到Cube代码如下:

using UnityEngine;

public class OnMouseXXXTest : MonoBehaviour
{
    #region 鼠标相关

    // 鼠标和Collider(碰撞体)之间的触发
    
    // 鼠标进入
    private void OnMouseEnter()
    {
        Debug.Log("OnMouseEnter 鼠标进入...");
        GetComponent<MeshRenderer>().material.color = Color.blue;
    }
    
    // 鼠标停留
    private void OnMouseOver()
    {
        transform.Rotate(Vector3.up * 50 * Time.deltaTime);
    }

    // 鼠标退出
    private void OnMouseExit()
    {
        Debug.Log("OnMouseExit 鼠标退出...");
        GetComponent<MeshRenderer>().material.color = Color.green;
    }

    // 鼠标按下
    private void OnMouseDown()
    {
        Debug.Log("OnMouseDown 鼠标按下...");
    }

    // 鼠标拖动
    private void OnMouseDrag()
    {
        Vector3 target = Input.mousePosition;
        target.z = Mathf.Abs(Camera.main.transform.position.z);
        transform.localPosition = Camera.main.ScreenToWorldPoint(target);
    }

    // 鼠标抬起
    private void OnMouseUp()
    {
        Debug.Log("OnMouseUp 鼠标抬起...");
        transform.localPosition = new Vector3(0, 0, -1);
    }

    #endregion
}

测试效果:


四,碰撞检测

4.1 函数描述

碰撞函数

  1. OnCollisionEnter: 进入碰撞时触发一次。
  2. OnCollisionStay: 在碰撞体中停留时每帧触发一次。
  3. OnCollisionExit: 离开碰撞体时触发一次。

触发函数

  1. OnTriggerEnter: 进入碰撞体时触发一次。
  2. OnTriggerStay: 在碰撞体中停留时每帧触发一次。
  3. OnTriggerExit: 离开碰撞体是触发一次。

PS:上面这六个方法,还有对应2D碰撞体的六个方法(如:OnCollisionEnter2D) 函数后面添加2D接口,触发条件和使用方式和3D一致。 使用时注意碰撞体和检测函数同步接口,即用2D碰撞体必须用2D函数。

函数执行条件

  1. 两个物体需要都有碰撞体(Collider)组件。
  2. 检测方(挂载脚本物体)需要有刚体(Rigidbody)组件。
  3. Collider上都不勾选IsTrigger(有一方勾选则执行触发函数)。

4.2 示例解析

在三例上继续操作,在Cube上添加Rigidbody组件,并取消勾选Use Gravity 属性(避免其受到重力影响)。

然后再创建两个Cube,位置大小随意能在Game视图看到就行。将其中一个BoxCollider的IsTrigger 属性勾选 ✅ 上。这样就可以一个用来测试碰撞,一个用来测试触发了。

将代码添加到OnMouseXXXTest类中,添加内容如下:

#region 碰撞,触发检测

// 在方法名后加上2D(如:OnCollisionEnter2D), 即可触发2D碰撞体对应函数
// 进入碰撞时触发
private void OnCollisionEnter(Collision other)
{
    Debug.Log("开始碰撞..." + other.collider.gameObject.name);
    other.collider.GetComponent<MeshRenderer>().material.color = Color.black;
}

// 在碰撞体中停留时每帧触发一次
private void OnCollisionStay(Collision other)
{
    Debug.Log("持续碰撞中..." + other.collider.gameObject.name);
    other.collider.GetComponent<MeshRenderer>().material.color = Color.red;
}

// 离开碰撞体时触发
private void OnCollisionExit(Collision other)
{
    Debug.Log("碰撞结束..." + other.collider.gameObject.name);
    other.collider.GetComponent<MeshRenderer>().material.color = Color.white;
}

// 碰撞体勾选is Trigger 选项: 取消碰撞器,开启触发器
// 进入碰撞体时触发
private void OnTriggerEnter(Collider other)
{
    Debug.Log("触发开始...");
    other.transform.GetComponent<MeshRenderer>().material.color = Color.black;
}

// 在碰撞体中触发
private void OnTriggerStay(Collider other)
{
    Debug.Log("持续触发中...");
    other.transform.GetComponent<MeshRenderer>().material.color = Color.yellow;
}

// 离开碰撞体是触发
private void OnTriggerExit(Collider other)
{
    Debug.Log("触发结束...");
    other.transform.GetComponent<MeshRenderer>().material.color = Color.white;
}
#endregion

运行后得到如下效果:


五,应用程序

5.1 函数描述

三个函数

  1. OnApplicationPause: 检测到暂停的帧结束 --> 切换到后台和回来时调用。
  2. OnApplicationFocus: 当屏幕 获得/失去 焦点时调用
  3. OnApplicationQuit: 当程序退出时调用。

实际应用

  1. OnApplicationPause: 游戏停止保存数据/游戏继续数据初始化。
  2. OnApplicationFocus: 失去焦点关闭背景音乐/获得焦点继续播放音乐。
  3. OnApplicationQuit: 在移动端大退时也会对调用,但不会触发上面两个方法。

5.2 示例解析

使用时将下面代码复制到需要处理检测逻辑的部分即可:

#region Application 应用程序

// 检测到暂停的帧结束时调用,有效地在正常帧更新之间
private void OnApplicationPause(bool pauseStatus)
{
    Debug.Log("OnApplicationPause ... " + pauseStatus);
    if (pauseStatus) // 切换到后台时执行
    {
    }
    else // 切换到前台执行一次
    {
    }
}

// 当屏幕 获得/失去 焦点时调用
private void OnApplicationFocus(bool hasFocus)
{
    Debug.Log("OnApplicationFocus ... " + hasFocus);
    if (hasFocus) // 获得焦点 -- 切换到前台
    {
    }
    else // 失去焦点
    {
    }
}

// 当程序退出时调用 -- Application.Quit();触发,不会触发上面两个方法
private void OnApplicationQuit()
{
    Debug.Log("OnApplicationQuit ... ");
}

由下图可以看到执行逻辑:当我点击Hierarchy面板时触发失去焦点,再次点击Game视图则触发了获得焦点


六,禁用销毁

6.1 函数描述

三个函数

  1. OnDisable: 当对象被禁用时调用此函数(其父物体被禁用也会触发)。
  2. OnDestroy: 在对象存在的最后一帧的所有帧更新之后调用此函数。

实际应用

  1. OnDisable: 通常和OnEnable配合使用。比如:在OnEnable添加监听,在OnDisable移除监听
  2. OnDestroy: 当物体销毁或者场景关闭时触发。比如:子弹打到墙壁时,需要销毁子弹并触发一个打击音效。

6.2 示例解析

1.游戏物体禁用触发OnDisable

2.游戏物体销毁时触发OnDisableOnDestroy


Unity 官方图解:
图解

怎么样?是不是收获颇丰。本文对你有帮助的话,欢迎小伙伴们三连支持一下。有问题的话我们评论见吧~

以上是关于Unity认识常用的生命周期函数(AwakeStartUpdate...)的主要内容,如果未能解决你的问题,请参考以下文章

Unity3D之常用生命周期

一文读懂Unity常用生命周期函数! 超级详细不服来辩~

Unity的生命周期函数

Unity---脚本事件函数和生命周期

Vue的生命周期

Vue的生命周期