从委托到UnityEvent
文章目录
- 提要
- 委托
- 介绍
- 体验委托
- 委托的多播,以及改变引用
- Action
- Func
- 事件
- 介绍
- 体验事件
- EventHandler
- 设计模式-观察者模式
- 关于Unity:Event与Action
- 初步体验UnityEvent
- 继承UnityEvent
提要
- 在学习C#的过程中,委托与事件的概念是比较重要的(观察者模式),还整合了UnityAction与UnityEvent的使用。仅为个人学习记录,如有错误等还请指正
委托
介绍
- 委托(Delegate)类似于C中的函数指针,是存有对某个方法的引用的一种引用类型变量,并且还可以在运行时随时更改它的引用
- 委托特别用于实现事件,是其基础
体验委托
- 接下来,写一个数字改变器的委托(NumberChanger),它可以指向一些方法,通过参数
p
来改变变量的num
值 - 首先我们需要声明一个委托类型,使用
delegate
关键词加上我们需要引用方法的返回值及其参数
public delegate int NumberChanger(int n);
- 加入一些委托引用的方法测试,注意返回值和参数都必须与刚刚声明的委托完全一致
class DelegateTest1
{
public static int num = 10;
public int AddNum(int p)
{
num += p;
return num;
}
public int MulNum(int p)
{
num *= p;
return num;
}
}
- 使用new()来实例化委托(所以严格意义上,刚刚的是委托类型,它可以有很多实例,就像下面的代码),构造函数中初始化我们就需要加入一个委托引用的方法,注意要让委托引用到某个方法,只需要带方法明就行了
static void Main(string[] args)
{
DelegateTest1 d = new DelegateTest1();
NumberChanger nc1 = new NumberChanger(d.AddNum);
NumberChanger nc2 = new NumberChanger(d.MulNum);
Console.WriteLine(nc1(5));
Console.WriteLine(nc2(5));
}
- 接下来测试,分别得到第一行15,第二行75的结果
委托的多播,以及改变引用
- 我们可以使用
+
,-
,+=
,-=
来操作委托所引用的方法,当要改变引用也可以直接用委托 = 方法名
(直接赋值)来直接改变,总的来说都非常方便
nc1 += MulNum;
Console.WriteLine(nc1(2));
- 得到154的结果,因为75 + 2 再乘以 2
Action
- Action是一个特殊的委托类型,它通常用于封装那些没有返回值且带有参数的方法
- 下面来改写一下之前的NumberChanger
class DelegateTest1
{
//Action - 没有返回值的委托
public Action<int> numberChangerAction;
public static int num = 10;
public void AddNumAction(int p)
{
num += p;
}
}
static void Main(string[] args)
{
DelegateTest1 d = new DelegateTest1();
d.numberChangerAction += d.AddNumAction;
d.numberChangerAction(5);
Console.WriteLine(DelegateTest1.num);
}
- 输出结果15
- 可以看出Action委托的定义格式是用泛型,
Action<T>
,这个T可以是任何类型,这样的参数最多可以有16个
Func
- Func是一个特殊的委托类型,它通常用于封装那些带有返回值且带有参数的方法
- 同样,用Func来改写数字改变器
class DelegateTest1
{
//Func - 有返回值的委托
public Func<int, int> numberChangerFunc;
public static int num = 10;
public int AddNumFunc(int p)
{
num += p;
return num;
}
}
static void Main(string[] args)
{
DelegateTest1 d = new DelegateTest1();
d.numberChangerFunc += d.AddNumFunc;
Console.WriteLine(d.numberChangerFunc(5));
}
- 同样也可以看出Func的格式,
Func<T,TResult>
,TResult
即返回值类型,当有多个参数时,格式就如Func<T1,T2,T3,..,TResult>
以此类推,Func最多也为16个参数
事件
介绍
- 在C#中,事件是一种特殊的委托,可以理解为对委托的封装,并且更安全
- 对于事件的理解:田径比赛时候,裁判员枪声响起,即为发起了一个事件,而所有参加比赛的运动员奔跑,即为响应该事件。事件通常需要发布者和订阅者,如字面意思,我们需要编写一个发布者类(裁判),以及一个或多个订阅者(运动员)类,并且让相应的方法订阅发布者类中的事件。
- 下面,来写一个关于田径比赛的事件
体验事件
- 在这之前我们需要明白,事件只能在类中声明,不能像委托一样暴露在外,并且事件依托于委托而存在,我们需要先声明一个委托,再声明一个事件。
注意事件的声明方式,先用event
关键字再加上对应的委托
//发布者
class Judgement
{
//先声明一个委托
public delegate void JudgeDelegate();
//再声明一个事件
public static event JudgeDelegate RunEvent;
//发布事件的方法
public static void RaceBegin()
{
RunEvent?.Invoke();
}
}
-
?.Invoke()
表示当Judge
不为空,那么Invoke()
通知所有订阅了该事件的方法:你们需要作出回应了,也就是执行对应的方法 - 这里也可以不设置成静态的事件,也可以实例化Judgement对象来使用
- 现在来编写订阅者类,订阅者类中要做的即是定义对事件的处理方法,当枪声响起,运动员该作何回应呢?
//订阅者
class Athlete
{
public Athlete()
{
Judgement.RunEvent += StartRun;
}
public void StartRun()
{
Console.WriteLine("Running!!!");
}
}
- 在构造函数中,实例化的同时直接订阅事件
- 现在来实际使用一下事件
static void Main(string[] args)
{
Athlete athlete = new Athlete();
Athlete athlete2 = new Athlete();
Judgement.RaceBegin();
}
- 我们实例化了两个运动员对象,于是当
RaceBegin
触发事件时,会有两个运动员响应,可以看到输出了两行Running!!!,还可以再完善一点,比如每个运动员有自己的名字:
class Athlete
{
public string name;
public Athlete()
{
Judgement.RunEvent += StartRun;
}
public void StartRun()
{
Console.WriteLine($"{name} is Running!!!");
}
}
class Application
{
static void Main(string[] args)
{
Athlete athlete = new Athlete();
athlete.name = "XiaoMing";
Athlete athlete2 = new Athlete();
athlete2.name = "XiaoHong";
Judgement.RaceBegin();
}
}
EventHandler
- EventHanler是C#提供的已经写好的事件,它可以配合泛型来实现事件信息(参数),下面来简单使用一下
class Info
{
public string info;
}
//发布者
class PostOffice
{
public EventHandler<Info> PostEventHandler;
public void PostStart(Info e)
{
PostEventHandler?.Invoke(this,e);
}
}
//订阅者
class Subscriber
{
public void GetNews(object sender,Info e)
{
Console.WriteLine($"Get News!The information is {e.info}");
}
}
class Application
{
static void Main(string[] args)
{
PostOffice postTest = new PostOffice();
Subscriber subTest = new Subscriber();
Info iTest = new Info();
iTest.info = "test OK!";
postTest.PostEventHandler += subTest.GetNews;
postTest.PostStart(iTest);
}
}
- 标准的触发事件形式其实需要两个参数:
object sender
和EventArgs e
,我们可以这样来理解:当鼠标点击按钮时,第一个参数触发事件的对象就是按钮,而第二个参数可以是我们要传递的信息,比如鼠标点击的位置,按下释放的信息等,在上述代码的Invoke()
(触发这个事件的所有订阅者)中,我们直接使用了this
,也就是报亭发售报刊,报亭就是引发事件的对象
设计模式-观察者模式
观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式(Publish-Subscribe)、模型-视图(Model-View)模式、源 监听器(Source-Listener)或者从属者模式,它是对象行为型模式。
- 其实,刚刚的几个例子已经有用到观察者模式了,具体来讲,被观察者就是状态发生了改变,通知那些观察者们作出相应动作的角色,比如点击一个按钮播放音乐,这个按钮就是被观察者,播放音乐的管理器或者api就是观察者,也就是说,所有的观察者都在监听被观察者。
关于Unity:Event与Action
- UnityAction是Unity封装的一个委托,也能像Action一样使用泛型参数,而UnityEvent可以通过
AddListener()
注册UnityAction(订阅),RemoveListener()
来取消注册UnityAction,以及像event一样用Invoke()
来调用注册的UnityAction,直接使用UnityEvent时是无参数,若要写有参数的,需要自己写一个类来继承UnityEvent。其实就是Unity为我们封装好了一套,下面来尝试一下
初步体验UnityEvent
- 首先创建EventTest对象以及绑定对应脚本
[外链图片转存中…(img-ypWXxLFs-1683447108931)] - 编写如下代码,注意需要引入命名空间
using UnityEngine.Events;
public class EventTest : MonoBehaviour
{
public UnityAction testAction;
public UnityEvent testEvent;
private void Start()
{
testAction += Test;
testEvent.AddListener(testAction);
}
private void Update()
{
if(Input.GetKeyDown(KeyCode.K))
{
testEvent?.Invoke();
}
}
public void Test()
{
Debug.Log("Event OK!");
}
}
- 运行,在场景中按K键几次:
[外链图片转存中…(img-72fDefHC-1683447108932)] - 另外,刚刚我们将UnityEvent设置成了public,那么我们还可以在面板中为它添加监听器,比如添加一个对象,就会自动显示这个对象上的方法,脚本,组件等内容,比如创建一个cube,并添加到面板中,选择
MeshRenderer.material
,随便改变一种材质:
[外链图片转存中…(img-Xz53rnZP-1683447108932)]
[外链图片转存中…(img-TIeidQrS-1683447108932)] - 然后运行,按K键盘,测试结果:
[外链图片转存中…(img-qwKjb0m7-1683447108933)]
继承UnityEvent
- 上文提到若要写带参数的事件,则需要继承UnityEvent,并加入泛型参数,下面我们来改写之前的代码。不过继承前要注意,继承后的类
MyEvent
不会自动序列化到面板上,我们需要引入命名空间using System;
,然后加入[Serializable]
才可以显示到面板上去:
using UnityEngine;
using UnityEngine.Events;
using System;
[Serializable]
public class MyEvent : UnityEvent<Vector3> { }
public class EventTest : MonoBehaviour
{
public MyEvent testEvents;
public UnityAction<Vector3> testAction;
private void Start()
{
testAction += Test;
testEvents.AddListener(testAction);
}
private void Update()
{
if(Input.GetKeyDown(KeyCode.K))
{
testEvents?.Invoke(new Vector3(1,0,0));
}
}
public void Test(Vector3 info)
{
Debug.Log($"The Info is : {info}");
}
}
- 这次我们传递的参数是
Vector3
类型的一个向量,我们再到面板上去添加刚刚的cube对象,发现transform选项里能选的方法多了一些,比如位移旋转等,这是因为这些方法恰好都需要一个Vector3
的参数,这次我们加一个Translate方法,让事件触发时cube可以向X方向位移一个单位
[外链图片转存中…(img-04COTShb-1683447108933)] - 运行自测就OK了,到这里从委托到UnityEvent的一些简单的上手就完成了除开上述的这些,继承UnityEvent之后我们还可以重写
AddListener
方法,进行更多操作,可以多做一些相关的实验。然而这只是最表面的一些应用,具体的话还是建议多写一些Unity案例来进行上手!真正体验观察者模式!