C# 中的 Event(事件)详解及其在 Unity 中的衍生

观察者模式 - 游戏设计模式 Design Patterns Revisited

  随便打开电脑中的一个应用,很有可能它就使用了MVC架构, 而究其根本,是因为观察者模式。 观察者模式应用广泛,Java甚至将其放到了核心库之中(java.util.Observer),而C#直接将其嵌入了语法(event关键字)。

  观察者模式是应用最广泛和最广为人知的 GoF 模式,但是游戏开发世界与世隔绝,所以对你来说,它也许是全新的。

前言

上面这一段摘自 Bob Nystrom 的《游戏编程模式》一书的“观察者模式”章节。

什么是观察者模式?可以这么认为:在软件设计的世界里,观察者模式如同一座桥梁,连接着系统中高度松耦合的组件。

通过“发布-订阅”的思想,观察者让对象间无需直接通信,仅通过状态变化的通知即可协同工作——当某个对象(发布者)发生状态改变时,所有订阅其变化的观察者(订阅者)都能收到通知并作出响应。

这种设计模式在用户界面开发、游戏逻辑触发、分布式系统通信等领域中,都是解决“解耦”问题的黄金准则。

由于 C# 在设计之初,高度借鉴了 Java 的面向对象编程思想,因此在 C# 中,Event 事件不再是需要开发者手动实现的抽象概念,而是与类成员同级的“第一公民”。

它通过编译器的语法糖,自动处理了委托的组合与访问权限控制,甚至能通过运算符实现直观的订阅与取消订阅。这种高度集成的设计,让开发者得以专注于业务逻辑本身,而非事件机制的底层实现。

本文将会从 C# 中最简单的 Event 事件开始讲解,逐步了解 Event 与委托、Action 的关联,以及将 C# 原生事件与 Unity 编辑器深度绑定的 UnityEvent 类。

本教程测试环境

硬件与系统:MacOS Sequoia 15.3.2 MacBook Pro 2024

软件:Jetbrains Rider 2024.3.4、Unity 2022.3.48f1c1


基本的 Event 使用

首先,我们在 Unity 中新建一个场景,创建第一个脚本 TestingEvents.cs,并将该脚本绑定在场景中。

在该脚本内,如果需要实现 Event 事件,必须引入 System 库,由于本文的测试方法需要使用到 Unity 的基本操作,因此需要继承自 MonoBehaviour 类,书写基本代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using UnityEngine;

namespace Events
{
// 继承自 MonoBehaviour 类,这里主要使用 Start 和 Update 两个函数
public class TestingEvents : MonoBehaviour
{
public event EventHandler OnSpacePressed;

// 该函数内的脚本在场景被加载时调用一次
private void Start()
{

}

// 该函数内的脚本每帧调用一次
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
OnSpacePressed(this, EventArgs.Empty);
}
}
}
}

在上面的代码中,我们实现了以下功能:

  1. 声明了一个基于 EventHandler 委托的 event 事件,事件的名称为 OnSpacePressed

  2. 通过 Unity 自带的 Update() 函数中,每帧持续检测是否按下空格键,如果按下则执行 if 块内的操作。

  3. 按下空格键后,执行定义的 OnSpacePressed 事件。

Event 的声明

注意到,我们在这里使用了下面这样一句来声明新 Event:

1
public event EventHandler OnSpacePressed;

其中,event 关键字表明这是一个 Event 事件,而 EventHandler 其实是一个 System 库自带的预定义委托类型,其定义如下:

1
public delegate void EventHandler(object sender, EventArgs e);

这个委托需要传递两个形参:事件发送者 sender 和一个 EventArgs 类或派生类,后面也会进一步讲解如何利用这个类。

在我们的代码中,我们给 OnSpacePressed 传递其自身 this 为事件的发送者,携带的类为 EventArgs.Empty(即不携带任何 EventArgs 类):

1
OnSpacePressed(this, EventArgs.Empty);

如果你不知道什么是委托(delegate),不用担心,接下来的内容会进一步讲解 Event 与 Delegate 委托的关系。这两者都是 C# 的面向对象编程中不可或缺的部分,甚至 Event 机制本身就是基于委托实现的。

Event 的调用

启动场景,按下空格,会发现出现了 NullReferenceException 报错:

NullReferenceException: Object reference not set to an instance of an object

这表明我们的事件还并没有任何订阅者(观察者),该事件被触发后,没有对应的订阅者调用任何函数。

所以我们需要在调用前进行判断,如果没有订阅者,则不触发该事件。C# 6.0 也为这一操作提供了更加方便的判断方式:

1
2
3
4
5
6
7
private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
OnSpacePressed?.Invoke(this, EventArgs.Empty);
}
}

通过问号 ? 进行判断,关键字 Invoke 调用,其具体实现类似:

1
2
3
4
5
6
7
private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
if(OnSpacePressed != null) OnSpacePressed(this, EventArgs.Empty);
}
}

再次启动场景,按下空格,这次就不会出现空引用的报错了。

Event 的监听

进行修改后,虽然代码没有报错,但我们的 OnSpacePressed 事件也完全没有被触发。

如果需要触发该 Event,就要为该事件添加一个订阅者。当事件触发后,订阅者接受到信号,才能执行程序设定好的函数。这里我们在 Start 函数中添加订阅者:

1
2
3
4
5
6
7
8
9
private void Start()
{
OnSpacePressed += Event_OnSpacePressed;
}

private void Event_OnSpacePressed(object sender, EventArgs e)
{
Debug.Log("SpacePressed!");
}

直接使用 += 为事件添加一个函数作为订阅者,当事件被触发时,函数 Event_OnSpacePressed 执行。

现在进入场景后,按下空格,就可以看到 SpacePressed! 的 Debug 信息了,由于订阅者在添加后持续存在,所以每次按下空格,都会出现一次 Debug 提示。

当然,对订阅者的编辑操作可以出现在任何地方,比如订阅者触发的函数内部:

1
2
3
4
5
6
7
8
9
10
private void Start()
{
OnSpacePressed += Event_OnSpacePressed;
}

private void Event_OnSpacePressed(object sender, EventArgs e)
{
Debug.Log("SpacePressed!");
OnSpacePressed += Event_OnSpacePressed;
}

在触发的函数内部再次添加一个订阅者,即该 Event 每触发一次,每个订阅者都会再次添加一个订阅者,每按下一次空格,SpacePressed! 的出现次数都是上次的 2 倍。

也可以通过在触发的函数内部删除自身订阅者,从而实现事件只能被触发一次的功能:

1
2
3
4
5
6
7
8
9
10
private void Start()
{
OnSpacePressed += Event_OnSpacePressed;
}

private void Event_OnSpacePressed(object sender, EventArgs e)
{
Debug.Log("SpacePressed!");
OnSpacePressed -= Event_OnSpacePressed;
}

这里按下一次空格后,程序会执行一次输出,同时 Event_OnSpacePressed 会被删除,接下来再次按下空格不会再次触发该事件。

Event 的跨类使用

前面体现的功能,实际上也可以直接通过函数来实现,那 Event 的特点在哪里?我们创建另一个脚本 TestingSubscriber.cs,将其与 TestingEvent 挂载在场景中同一个物体下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;

namespace Events
{
public class TestingSubscriber : MonoBehaviour
{
private void Start()
{
TestingEvents testingEvents = GetComponent<TestingEvents>();
testingEvents.OnSpacePressed += TestingEvents_OnSpacePressed;
}

private void TestingEvents_OnSpacePressed(object sender, EventArgs.Empty)
{
Debug.Log("Subscriber: SpacePressed!");
}
}
}

这里从 TestingEvent 类中获取到定义的 OnSpacePressed 事件,并在新脚本中实现一个订阅者,监测到事件触发时,执行 TestingEvents_OnSpacePressed 函数。

同时,将原本的 TestingEvent 中调用的方法删去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using UnityEngine;

namespace Events
{
public class TestingEvents : MonoBehaviour
{
public event EventHandler OnSpacePressed;

private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
OnSpacePressed?.Invoke(this, EventArgs.Empty);
}
}
}
}

现在,两个脚本互相没有连接,定义事件的 TestingEvents 类自身并没有实现任何订阅者,也不知道是否存在订阅者,但还是会在玩家按下空格时发出事件被触发的信号。

同时 TestingSubscriber 只“知道”一个事件名,它只会持续监听这个事件,如果事件被触发,它会执行 TestingEvents_OnSpacePressed 函数。

现在进入场景,按下空格,还是得到了 Subscriber: SpacePressed! 的信息。

这才是 Event 的真正作用:一个事件可以有多个监听者(订阅者),也可以完全没有监听者。它要做的只是在玩家按下空格时告诉所有脚本:“呃,我不知道有谁感兴趣,但是玩家刚刚按下了空格。做你想做的事吧。”

同时,其他监听者可以在听到这句话后做出任何事情,包括实现跳跃、飞行、冲刺或者任何按下空格后会发生的事情,而完全不需要知道是谁发出的信号,也不需要知道其他监听者的存在。这样,就实现了各个模块的完全解耦,也由此出现了事件驱动的开发模式:不同模块之间互不引用,只通过事件的监听和发送来相互沟通。

在事件驱动的模式下,你完全可以直接删除一整个模块,而不需要担心其他模块会出现问题。其他模块依旧会持续监听 / 激活某个事件,哪怕这个事件已经不再会被触发 / 被监听。


EventArgs 的应用

上文提到,EventHandler 是一个 System 库自带的预定义委托类型,这个委托需要传递两个形参:事件发送者 sender 和一个 EventArgs 类或派生类。

这代表,我们可以创建一个自定义类,通过让自定义类继承自 EventArgs 类,实现在 EventHandler 事件中调用自己的类来传递参数。

EventArgs 是一个包含事件数据的类(正如上文所使用的、不需要传递额外信息的事件,通常使用 EventArgs.Empty)。EventHandler 适用于那些不携带额外信息的事件场景。如果需要在使用 EventHandler 的同时,让事件携带更多信息,可以继承 EventArgs 类来创建自己的事件数据类,并使用泛型版本的 EventHandler

我们在之前的代码基础上继续修改,添加类 OnSpacePressedEventArgs 并继承自 EventArgs,通过一个公共变量 pressCount 记录按下 Space 的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using UnityEngine;

namespace Events
{
public class TestingEvents : MonoBehaviour
{
public event EventHandler<OnSpacePressedEventArgs> OnSpacePressed;

public class OnSpacePressedEventArgs : EventArgs
{
// OnSpacePressedEventArgs 类中记录的按下空格次数
public int pressCount;
}

// TestingEvents 类中记录的空格按下次数
private int spaceCount;

private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
spaceCount++;
OnSpacePressed?.Invoke(this, new OnSpacePressedEventArgs { pressCount = spaceCount });
}
}
}
}

这里我们通过泛型版本的 EventHandler,将自定义的数据类传入,就可以在 Invoke 函数中传入自己的数据类。

在触发事件时,通过创建对象的方式将数据嵌入到事件对象中,这意味着监听者在接收到事件时,可以通过事件内嵌的对象获取到其他脚本提供的数据,从而实现数据的远程接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;

namespace Events
{
public class TestingSubscriber : MonoBehaviour
{
private void Start()
{
TestingEvents testingEvents = GetComponent<TestingEvents>();
testingEvents.OnSpacePressed += TestingEvents_OnSpacePressed;
}

private void TestingEvents_OnSpacePressed(object sender, TestingEvents.OnSpacePressedEventArgs e)
{
Debug.Log("Subscriber: Space: " + e.pressCount);
}
}
}

在监听脚本中,同样将原本接收的 EventArgs.Empty 改为自定义的数据类对象,并在 Debug 信息中使用对象中实现的 pressCount

现在进入场景,每次按下空格后,Debug 显示的按下次数都会自增,这样你就掌握了最基本的事件驱动的信息传递。


事件与委托

现在,你实际上已经知道如何使用事件、定义订阅者、通过 EventArgs 派生类来在项目的各个角落之间传递数据。但这一切是怎么实现的?为什么不添加订阅者时激活事件会报错?为什么订阅者要带有两个如此奇怪的形参?恐怕你还是一头雾水。现在我们继续深入,仔细探究 Event 事件系统的实现原理。

当你声明一个事件时,实际上是声明了一个类型为委托的成员变量,这个委托定义了事件处理方法的签名。想要清楚地理解事件与委托的关系,我们就得从委托本身开始。

什么是委托

委托是 C# 面向对象编程的重要组成部分,可以被看作是对方法的引用。换句话说,它是一种类型安全的回调函数,允许你将一个或多个方法作为参数传递给其他方法,并在适当的时机调用这些方法。定义一个委托就像是定义了一个方法签名模板,任何符合这个签名的方法都可以被分配给该委托类型的变量。

注意:在 C# 中,通常将其他编程语言中的 “函数” 称作 “方法”

可能你还是觉得有点模糊,那么看看下面这两个例子。第一个例子是委托的声明与赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static class Example
{
public delegate void NormalDelegate();

private NormalDelegate normalDelegate;

private void Main()
{
normalDelegate = A; // 将 A 方法赋值给委托
normalDelegate(); // 执行委托 : 实际上是执行 A 方法
normalDelegate = B; // 将 B 方法赋值给委托
normalDelegate(); // 执行委托 : 实际上是执行 B 方法
}

void A() // A 方法 : 与委托定义的传入变量相同,即不传入参数
{
Debug.Log("A");
}

void B() // B 方法 : 与委托定义的传入变量相同,即不传入参数
{
Debug.Log("B");
}
}

先声明一个不需要传入任何变量的委托 NormalDelegate,随后通过 normalDelegate 来实现这个委托,就像在一个类里实例化另一个类一样。

然后,你就可以将任何与委托传入值类型相同(在这里是不传入任何变量)的方法赋值给这个委托。

可能你还是不够熟悉?其实,委托是可以指向多个不同的方法的,比如:

1
2
3
4
5
6
7
private void Main()
{
normalDelegate += A; // 将 A 方法添加到委托
normalDelegate(); // 执行委托 : 实际上是执行 A 方法
normalDelegate += B; // 将 B 方法添加到委托
normalDelegate(); // 执行委托 : 实际上是执行 A 和 B 方法
}

是不是觉得熟悉了一些?没错,这就是向事件添加订阅者的写法:

1
2
3
4
5
private void Start()
{
testingEvents.OnSpacePressed += TestingEvents_OnSpacePressed1;
testingEvents.OnSpacePressed += TestingEvents_OnSpacePressed2;
}

第二个例子是通过委托实现回调方法。

1
2
3
4
5
6
7
8
9
public static class Example
{
public delegate void Callback();

public static void UseCallback(Callback function)
{
function();
}
}

在第二个例子中,首先声明一个名为 Callback 的委托,不传入任何参数。然后将该委托作为参数传入任何方法中,并在方法中使用这个委托。

在这个过程中,UseCallback 方法不知道传入的方法是什么,可能是实现一些算式,也可能是输出一些 Debug 信息。但它完全不需要知道这个传入的方法的具体实现细节,只需要知道这个方法应该被执行。

接下来就可以随意调用这个方法,并传入任意方法作为形参:

1
2
3
4
private void Start()
{
Example.UseCallback(()=>{ Debug.Log("执行委托方法!"); });
}

在这个例子中,我们通过 lambda 表达式,在调用这个方法时定义了 Callback() 的具体内容,可以看成:

1
2
3
4
public void Callback()
{
Debug.Log("执行委托方法!");
}

那么 UseCallback() 就变成了:

1
2
3
4
public static void UseCallback(Callback function)
{
Debug.Log("执行委托方法!");
}

当然,你也可以在声明委托时指定委托需要传入的形参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class Example
{
public delegate void Callback(int a, int b);

public static void UseCallback(Callback function)
{
function(1, 2);
}
}

public static class Program
{
void Main()
{
Example.UseCallback((int a, int b) => { Debug.Log(a + b); });
}
}

在这个情况下,提前定义的委托传入两个 int 变量 1 和 2,但它不需要实现具体的操作,而是将具体的实现交给调用者。在调用 UseCallback 方法时,必须构造或传入一个方法作为参数,我们在这里传入的方法是通过 Debug 信息输出两个整型变量的和

有了委托,我们便可以将整个方法像参数一般传入另一个方法,大大提高使用者在调用函数时的可操作性。

可能你还是对委托的调用有些陌生,那么我们用另一种写法重写一下 UseCallback 方法:

1
2
3
4
public static void UseCallback(Callback function)
{
function?.Invoke(1, 2);
}

这下是不是变得眼熟了?没错,就是我们的 OnSpacePressed 事件的触发方法:

1
2
3
4
5
if(Input.GetKeyDown(KeyCode.Space))
{
spaceCount++;
OnSpacePressed?.Invoke(this, new OnSpacePressedEventArgs { pressCount = spaceCount });
}

泛型委托 EventHandler

看完上面的小节,可能你已经看出 OnSpacePressed 与委托的关系了,我们再来看看之前定义的两种事件,分别是使用 Empty 和自定义类的两种事件:(并非完整代码,只是方便进行对比)

1
2
3
4
5
6
7
8
9
10
11
// 事件 1
public event EventHandler OnSpacePressed;

OnSpacePressed?.Invoke(this, EventArgs.Empty);

// 事件 2
public event EventHandler<OnSpacePressedEventArgs> OnSpacePressed;

public class OnSpacePressedEventArgs : EventArgs { ... }

OnSpacePressed?.Invoke(this, new OnSpacePressedEventArgs { ... });

不难看出,在调用基于 EventHandler 的事件时,必须传入一个 this 作为事件发送者,以及一个继承自 EventArgs 的类作为方法类。这意味着 EventHandler 本身就是一个委托:

1
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

那么我们创建的 OnSpacePressed 也就是一个需要在调用时传入的方法。既然这个方法的声明基于委托,那就必须带有委托的两个参数 object senderTEventArgs e

而我们定义、通过 += 添加给事件的订阅者,正是这个传入的、通过 Invoke 进行调用的方法。

这也就解释了在未添加订阅者时,尝试触发事件会报错 NullReferenceException 的原因:委托不能独立执行,必须传入一个构造好的方法作为参数,就像你不能调用一个需要形参的方法而不传入任何变量。

基于委托的事件

现在我们知道了 EventHandler 就是一个 System 库自带的委托,那我们是不是能直接使用自己的委托来实现一个 event?答案是肯定的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using UnityEngine;

namespace Events
{
public class TestingEvents : MonoBehaviour
{
public delegate void FloatEventDelegate(float value);

public event FloatEventDelegate OnFloatEvent;

private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
OnFloatEvent?.Invoke(5.6f);
}
}
}
}

TestingEvents 中,我们先声明一个带有一个 float 参数的委托 FloatEventDelegate,再用这个委托创建一个事件。玩家按下空格时,触发这个事件,同时传递一个参数 5.6。

然后我们在 TestingSubscriber 中传入一个方法作为订阅者,监听事件的触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;

namespace Events
{
public class TestingSubscriber : MonoBehaviour
{
private void Start()
{
TestingEvents testingEvents = GetComponent<TestingEvents>();
testingEvents.OnFloatEvent += FloatEvent;
}

private void FloatEvent(float value)
{
Debug.Log("Float event received: " + value);
}
}
}

在这里,FloatEvent 接收到传递的参数后,直接通过 Debug 信息打印出参数。

进入场景,按下空格,Debug 即会输出在订阅者中实现的 Float event received: 和在事件中定义的值 5.6

简化委托 Action

好了,你已经学会通过委托来创建事件,并且可以传递任何你想传递的参数,但这样还是显得有些麻烦。

在创建事件之前,你还需要提前声明这个委托,并且需要给委托指定传入的参数和参数名称,这样在参数较多的情况下会略显复杂:

1
2
3
4
5
6
7
8
public delegate void MassEventDelegate(
float fvalue1, float fvalue2,
int ivalue, int ivalue2
bool bvalue,
string svalue1, string svalue2
double dvalue1, double dvalue2);

public event FloatEventDelegate OnMassEvent;

好在 C# 已经给我们提供了一个更加简单的声明委托的方法:Action<T>

Action<T> 是 .NET 框架提供的预定义委托类型之一,用于简化无需返回值的方法引用或匿名方法的使用。

其代表一个不带参数且没有返回值的方法的委托。其最简单的形式是 Action,它对应于一个无参数且返回类型为 void 的方法。而 Action<T1, T2, ..., Tn> 则是带有参数但没有返回值的方法的委托,其中 T1 到 Tn 表示参数的类型。

可能有点难听明白,我们举一个代码作为例子,将上面的 OnMassEvent 进行简化:

1
public event Action<float, float, int, int, bool, string, string, double, double> OnMassEvent;

没错,只需要这样一句,并且不需要指定变量名称。

你可能已经发现 Action<T> 中的 “T” 可以用任何变量类型替代,这就是泛型。如果你还没有学过,在这里只需要明白这一句话就足够了。

现在我们使用 Action 来实现我们的事件,给事件传入三个不同类型的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using UnityEngine;

namespace Events
{
public class TestingEvents : MonoBehaviour
{
public event Action<bool, float, int> OnActionEvent;

private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
OnActionEvent?.Invoke(true, 5.6f, 3);
}
}
}
}

同时创建一个订阅者,负责通过 Debug 输出三个变量的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;

namespace Events
{
public class TestingSubscriber : MonoBehaviour
{
private void Start()
{
TestingEvents testingEvents = GetComponent<TestingEvents>();
testingEvents.OnActionEvent += ActionEvent;
}

private void ActionEvent(bool value, float f, int i)
{
Debug.Log($"Action event received: {value} , float = {f} , int = {i}");
}
}
}

我想你已经知道会输出什么了:

Action event received: true , float = 5.6 , int = 3

UnityEvent 类

在上面的教程中,虽然我们一直在使用 Unity 进行测试,但实际上测试的 Event 事件均为 C# 的原生 Event。事实上,Unity 编辑器内部也集成有一个完整的事件系统,也就是 UnityEvent 类。

UnityEvent 类基于 Event 实现,在实现上面的基础功能的前提下,与 Unity 编辑器实现高度集成,对开发者而言使用更加方便,现在我们来实现一个 UnityEvent。

要实现 UnityEvent 事件的创建,必须要先引入 UnityEngine.Events 库。书写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;
using UnityEngine.Events;

namespace Events
{
public class TestingEvents : MonoBehaviour
{
public UnityEvent OnUnityEvent;

private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
OnUnityEvent?.Invoke();
}
}
}
}

这里定义了一个 OnUnityEvent,同时设定其在按下空格时触发。

现在转到编辑器,查看我们的 TestingEvents 脚本,会发现长得不一样了:

这就是 UnityEvent 的实用之处,现在我们在 TestingSubscriber 中写几个方法来供其使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using UnityEngine;

namespace Events
{
public class TestingSubscriber : MonoBehaviour
{
public void NoParamEvent()
{
Debug.Log("TestUnityEvent : NoParamEvent");
}

public void ParamEventInt(int value)
{
Debug.Log("TestUnityEvent : IntParamEvent with " + value);
}

public void ParamEventBool(bool value)
{
Debug.Log("TestUnityEvent : BoolParamEvent with " + value);
}

public void ParamEventString(string value)
{
Debug.Log("TestUnityEvent : StringParamEvent with " + value);
}
}
}

不需要任何其他的函数来将这些订阅者函数绑定至事件,因为这些都可以由我们手动完成。

现在回到编辑器,点击 On Unity Event() 右下角的加号,手动添加一个订阅者,并将我们的 TestingSubscriber 拖到左下角的框内。

此时,点击 No Function,并在其中找到我们拖进去的 TestingSubscriber,你就能找到刚刚创建的所有方法:

凭喜好添加刚刚创建的方法并设定参数:

进入场景,按下空格,就会发现刚刚添加的方法被全部激活:

总结

C# 的事件系统是一种强大的工具,它可以极大地提高代码的可读性、可维护性和可扩展性,为事件驱动编程提供了充足的实现基础。

通过深入学习和灵活运用 C# 事件,我们可以更加高效地实现类与对象间的通信和交互。也希望本文提供的知识和示例能够激发读者对 C# 事件和 UnityEvent 类的兴趣,并在实际开发中有所应用。