Event作为C#语言特性的一部分,在.Net开发中具有比较重要的作用。当我们使用C#作为脚本语言编写Unity游戏时,也经常会需要使用Event解决各种问题。然而,相比于传统C#语言的Event,UnityEvent基于Unity引擎做了一些改变,并且更加适合我们的游戏开发。为了帮助读者深入理解UnityEvent,本文会从Delegate讲起,并逐步介绍C# Event 与UnityEvent的相似与不同。
本文参考了Unity论坛以及其他前辈的文章和视频,想要进一步了解的可以自行查阅:
要理解Event是什么,首先必须得知道它们的前身——Delegate是啥,中文翻译即“委托”。用一句话让你理解Delegate的作用就是“Delegate是一个可以存放函数的容器”。众所周知,变量是程序在内存中为数据开辟的一块空间,面向对象语言中变量可以存放一个具体的数值,或者某个对象的引用。C#则在该基础上更进一步,使用Delegate的机制让存放“函数(Function)”成为可能。
使用Delegate一般分为三步:
下面是在Unity中使用Delegate的一个实例:
using UnityEngine;
public class DelegateExample : MonoBehaviour {
//Step1. 为Delegate定义一种函数原型
public delegate void MyDelegate(int args);
//Step2. 声明一个Delegate变量
public MyDelegate myDelegate;
private void Start()
{
//Step3. 引用Delegate变量实现相应的函数
myDelegate = PrintNum;
myDelegate(5);
myDelegate = PrintDoubleNum;
myDelegate(5);
}
public void PrintNum(int num)
{
print("Print number: " + num);
}
public void PrintDoubleNum(int num)
{
print("Print double number: " + num*2);
}
}
运行上述代码,你会发现使用一个delegate变量让我们执行了两种函数实现。这就是Delegate的妙处所在, Delegate定义了一个用于存放相同函数原型(相同参数类型,相同返回值)的容器。因为他们的函数原型相等,所以向delegate传递一次参数,就可以让所有添加到delegate上的函数正常执行。
在上述例子中,我们第二次向myDelegate赋值时覆盖掉了第一次赋值的函数,所以第二次引用myDelegate只会调用PrintDoubleNum(int num)函数。实际上,delegate作为函数容器,并不仅仅只能容纳一个函数,而是可以同时被委任多个函数。例如,当你把上述代码中的第二次赋值改为 myDelegate += PrintDoubleNum;
就可以实现同时打印两条语句的效果。这种delegate一般被称为multicast delegate。
如果你基本理解了delegate是什么,那么理解event基本不需要花费什么时间,因为event就是在multicast delegate的基础上演变来的。关于Event,一个比较形象的比喻就是广播者和订阅者。把Event想象成一个视频作者,并且他还具有一大堆热情的粉丝,每天都在等待新视频的发布。为了在第一时间收看到新发布的视频,粉丝们大多会选择订阅视频作者(大多数视频网站的套路),这样作者更新时你就会收到一条即时消息。在程序的世界里,event可能不再是一个做视频的,毕竟做视频赚不到什么钱,但是他依然为喜爱他的观众(具有相同函数类型的函数)提供了订阅他的途径(即把自身加入到event的函数容器中),这样无论他有什么动向,都可以直接通知所有他知道的粉丝(调用event会立即引用所有函数容器中的函数)。当然,一千个观众心中就有一千个哈姆雷特,就如同真爱粉无论如何都会支持自己的偶像,而黑粉无时无刻不在带节奏一样,event只负责告诉每个函数什么时候被调用,这些函数到底干了什么,event并不关心。
下面以代码的形式演示上述过程:
Idol.cs
using UnityEngine;
public class Idol : MonoBehaviour {
public delegate void IdolBehaviour(string behaviour);
public static event IdolBehaviour IdolDoSomethingHandler;
private void Start()
{
//Idol 决定搞事了, 如果他还有粉丝的话, 就必须全部都通知到
if (IdolDoSomethingHandler != null)
{
IdolDoSomethingHandler("Idol give up writing.");
}
}
}
SubscriberA.cs
using UnityEngine;
public class SubscriberA : MonoBehaviour {
///
/// OnEnable在该脚本被启用时调用,你可以把它看做路转粉的开端
///
private void OnEnable()
{
//粉丝通过订阅偶像来获取偶像的咨询, 并在得到讯息后执行相应的动作
Idol.IdolDoSomethingHandler += LikeIdol;
}
///
/// OnEnable在该脚本被禁用时调用,你可以把它看做粉转路的开端
///
private void OnDisable()
{
Idol.IdolDoSomethingHandler -= LikeIdol;
}
///
/// 粉丝A是一个脑残粉
///
///
public void LikeIdol(string idolAction)
{
print(idolAction + " I will support you forever!");
}
}
SubscriberB.cs
using UnityEngine;
public class SubscriberB : MonoBehaviour {
///
/// OnEnable在该脚本被启用时调用,你可以把它看做路转粉的开端
///
private void OnEnable()
{
//粉丝通过订阅偶像来获取偶像的咨询, 并在得到讯息后执行相应的动作
Idol.IdolDoSomethingHandler += HateIdol;
}
///
/// OnEnable在该脚本被禁用时调用,你可以把它看做粉转路的开端
///
private void OnDisable()
{
Idol.IdolDoSomethingHandler -= HateIdol;
}
///
/// 粉丝B是一个无脑黑
///
///
public void HateIdol(string idolAction)
{
print(idolAction + " I will hate you forever!");
}
}
把他们分别绑定在一个GameObject上,并运行游戏,你就可以在Console上看到两种粉丝对爱豆发表的见解。这里有几点使用时的注意事项希望引起你的重视:
public Idol idol;
然后在Editor上直接绑定。(这点是Unity特有的)经过上一节的解释,你应该对Event是什么,怎么用有了一个大概的体会,那么这一节我们就接着讲一讲Unity在Event的基础上进行的改良,即UnityEvent。Event设计之初并不会想到应用于Unity游戏开发,所以它的弊端就在于纯代码编程,没有通过使用Unity Editor提高工作效率。而UnityEvent就可以看做是发挥Editor作用的正确改良。还记得上一节中粉丝是怎么订阅的嘛?你必须在每个粉丝对象中访问Idol的IdolDoSomethingHandler,然后把自己将采取的行动添加上去。这样有两个坏处——其一就是你必须时刻提防订阅的时机,假如不小心在Idol发动态之后才订阅,那你就永远收不到那条动态了。其二就是不方便管理,想要查看订阅偶像的所有粉丝,我们就得查找项目中所有IdolDoSomethingHandler的引用,然后再把每个粉丝的文件打开,可以说是非常麻烦了。
为了避免上述的缺点,UnityEvent使用Serializable让用户可以在Editor中直接绑定所有粉丝的调用,即一目了然又不用担心把握不准订阅的时机。
话不多说,直接上代码:
Idol.cs
using UnityEngine;
using UnityEngine.Events;
//使用Serializable序列化IdolEvent, 否则无法在Editor中显示
[System.Serializable]
public class IdolEvent : UnityEvent<string> {
}
public class Idol : MonoBehaviour {
//public delegate void IdolBehaviour(string behaviour);
//public event IdolBehaviour IdolDoSomethingHandler;
public IdolEvent idolEvent;
private void Start()
{
//Idol 决定搞事了, 如果他还有粉丝的话, 就必须全部都通知到
if (idolEvent == null)
{
idolEvent = new IdolEvent();
}
idolEvent.Invoke("Idol give up writing.");
}
}
SubscriberA.cs
using UnityEngine;
public class SubscriberA : MonoBehaviour {
///
/// 粉丝A是一个脑残粉
///
///
public void LikeIdol(string idolAction)
{
print(idolAction + " I will support you forever!");
}
}
SubscriberB.cs
using UnityEngine;
public class SubscriberB : MonoBehaviour {
///
/// 粉丝B是一个无脑黑
///
///
public void HateIdol(string idolAction)
{
print(idolAction + " I will hate you forever!");
}
}
把上面三个脚本绑定到三个GameObject上,但是不要着急立刻运行游戏,因为我们还没有让两个粉丝实现订阅。和使用Event时不同,UnityEvent在序列化后可以在Editor上显示,并且可以让我们在Editor阶段就设置好需要执行的函数。选中Idol所在的GameObject,然后就可以在Inspector中设置IdolEvent可以引用的函数。设置完成后应该如图所示。
此时再运行游戏,你会得到和使用基于delegate的Event时相同的效果。
除此之外,UnityEvent依然提供和C# Event 类似的运行时绑定的功能,不过不同的是,UnityEvent是一个对象,向其绑定函数是通过AddListener()方法实现的。以SubscriberB为例,我们可以在代码中实现同等效果的绑定:
SubscriberB.cs
using UnityEngine;
public class SubscriberB : MonoBehaviour {
public Idol myIdol;
///
/// OnEnable在该脚本被启用时调用,你可以把它看做路转粉的开端
///
private void OnEnable()
{
//粉丝通过订阅偶像来获取偶像的咨询, 并在得到讯息后执行相应的动作
myIdol.idolEvent.AddListener(HateIdol);
}
///
/// OnEnable在该脚本被禁用时调用,你可以把它看做粉转路的开端
///
private void OnDisable()
{
myIdol.idolEvent.RemoveListener(HateIdol);
}
///
/// 粉丝B是一个无脑黑
///
///
public void HateIdol(string idolAction)
{
print(idolAction + " I will hate you forever!");
}
}
由于UnityEvent是一个对象,所以自然可以允许我们通过继承实现自己的Event,实际上Unity中包括Button在内的许多UI组件的点击事件都是通过继承自UnityEvent来复写的。
可访问性(public/private)决定了UnityEvent的默认值,当可访问性为public时,默认会为其分配空间(new UnityEvent());当可访问性为private时,默认UnityEvent为null,需要在Start()中为其分配内存。