[C#学习总结]从委托到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 senderEventArgs 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案例来进行上手!真正体验观察者模式!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值