C#事件与UnityEvent

C#中的事件系统

在C#中,事件系统是一种发布/订阅模式(Pub/Sub),允许对象通过事件来传递信息。事件本质上是委托(delegate)的扩展,委托是一种引用类型,可以保存对方法的引用。事件的主要角色是事件发布者和事件订阅者:

  1. 事件发布者:定义并触发事件。
  2. 事件订阅者:订阅并响应事件。

在某些地方,“发布”和“订阅”也会被叫做“广播”和“监听”

事件声明通常使用event关键字,表示只有事件发布者可以触发事件,而事件订阅者可以订阅该事件。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Publisher
{
public event EventHandler MyEvent;

protected virtual void OnMyEvent()
{
MyEvent?.Invoke(this, EventArgs.Empty);
}
}

public class Subscriber
{
public void Subscribe(Publisher publisher)
{
publisher.MyEvent += HandleEvent;
}

private void HandleEvent(object sender, EventArgs e)
{
// 处理事件
}
}

Unity的UnityEvent

在Unity中,UnityEvent是一种特殊的事件类型,提供了一种在编辑器中无需编写代码即可设置事件的机制。UnityEvent位于UnityEngine.Events命名空间下,可以在脚本中定义,也可以通过Unity Inspector窗口进行配置。例如:

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

public class MyComponent : MonoBehaviour
{
public UnityEvent myUnityEvent;

void Start()
{
if (myUnityEvent != null)
{
myUnityEvent.Invoke();
}
}
}

在Inspector中,你可以为myUnityEvent添加监听器(Listeners),选择对象并设置要调用的方法。

区别中的UnityEvent(事件)和UnityAction(委托)

UnityEvent可以在Inspector中配置,适合需要在设计时灵活管理事件和监听器的情况。
UnityAction不能在Inspector中配置,适合完全通过代码管理事件的情况。

在Unity中,UnityEventUnityAction都是用于事件系统的类型,但它们有不同的用途和实现方式。理解它们的区别有助于你更好地选择和使用它们。

UnityEvent

UnityEvent是Unity提供的一种事件系统的实现,主要用于方便地在Inspector窗口中进行事件的配置和管理。它是基于Unity的序列化系统,可以在Inspector中添加和配置监听器(Listeners)。

特点
  1. Inspector配置UnityEvent可以在Inspector中进行配置,允许你在设计时通过拖拽和选择来添加监听器。
  2. 支持多参数UnityEvent可以支持多参数的事件,可以在事件触发时传递参数。
  3. 序列化支持UnityEvent可以被序列化,这意味着你可以在Unity的场景和预制体中保存它们的状态。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using UnityEngine.Events;

public class EventExample : MonoBehaviour
{
[SerializeField]
private UnityEvent myEvent;

private void Start()
{
myEvent.Invoke(); // 触发事件
}
}

在Inspector中,你可以看到myEvent,并可以通过Inspector添加和配置监听器。

UnityAction

UnityAction是一个委托(Delegate),它是Unity事件系统的一部分。它用于定义无返回值的方法签名,可以用来代替C#的Action委托。

特点
  1. 无Inspector配置UnityAction不能在Inspector中进行配置,它完全通过代码来管理。
  2. 简单轻量UnityAction更轻量,适合在代码中直接定义和使用事件处理。
  3. 灵活UnityAction可以用于任意需要无返回值的方法签名,可以通过标准的C#事件机制或其他方式来调用。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;
using UnityEngine.Events;

public class ActionExample : MonoBehaviour
{
private UnityAction myAction;

private void Start()
{
myAction += MyMethod;
myAction.Invoke(); // 触发事件
}

private void MyMethod()
{
Debug.Log("MyMethod called");
}
}

在这个示例中,myAction是一个UnityAction类型的委托,当MyMethod被添加到myAction中并调用myAction.Invoke()时,MyMethod会被执行。

关键区别

  1. 配置方式

    • UnityEvent可以在Inspector中配置,适合需要在设计时灵活管理事件和监听器的情况。
    • UnityAction不能在Inspector中配置,适合完全通过代码管理事件的情况。
  2. 序列化和保存

    • UnityEvent可以被序列化和保存,可以在场景和预制体中保存其配置。
    • UnityAction是委托,不支持序列化和Inspector配置。
  3. 参数支持

    • UnityEvent支持多参数,可以在事件触发时传递多个参数。
    • UnityAction的参数取决于它的泛型签名,可以是无参数或带一个或多个参数的委托。

事件驱动编程有什么优点

事件驱动编程(Event-Driven Programming, EDP)有以下几个优点:

  1. 解耦:事件发布者和订阅者之间的耦合度很低,使得代码更易于维护和扩展。
  2. 提高可读性和可维护性:通过事件机制,逻辑分离更加明确,事件处理逻辑独立于主业务逻辑。
  3. 灵活性:订阅者可以在运行时动态地订阅或取消订阅事件,提供更大的灵活性。
  4. 简化复杂性:适用于处理异步操作和回调,简化了复杂的回调逻辑。(例如使用Addressable.LoadSceneAsync()异步加载场景)

演示使用事件驱动简化异步操作和回调

下面通过Unity的Addressable.LoadSceneAsync()方法的一个示例,展示如何使用事件系统注册和处理异步操作的完成事件。

示例:使用Addressable.LoadSceneAsync()进行异步加载场景并处理完成事件

在Unity中,Addressable.LoadSceneAsync()是一个异步方法,用于加载场景。在加载完成时,我们可以注册一个回调方法来处理完成事件。以下是一个具体的示例:

  1. 添加必要的命名空间
1
2
3
4
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
  1. 定义加载场景的方法
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
29
30
31
32
public class SceneLoader : MonoBehaviour
{
public string sceneAddress; // 场景地址,在Inspector中设置

void Start()
{
LoadScene();
}

void LoadScene()
{
// 调用Addressable.LoadSceneAsync()来异步加载场景
AsyncOperationHandle<SceneInstance> handle = Addressable.LoadSceneAsync(sceneAddress, LoadSceneMode.Single, true);

// 注册回调方法,在加载完成时调用
handle.Completed += OnSceneLoaded;
}

void OnSceneLoaded(AsyncOperationHandle<SceneInstance> handle)
{
// 检查加载是否成功
if (handle.Status == AsyncOperationStatus.Succeeded)
{
Debug.Log("Scene loaded successfully.");
// 这里可以添加更多的处理逻辑,例如初始化场景中的对象等
}
else
{
Debug.LogError("Failed to load the scene.");
}
}
}
解释
  1. 命名空间:首先,我们导入了必要的命名空间,用于访问Addressables、异步操作和场景管理功能。

  2. 场景加载方法:在LoadScene方法中,调用Addressable.LoadSceneAsync()来异步加载场景。该方法返回一个AsyncOperationHandle<SceneInstance>对象,用于跟踪异步操作的进度和状态。

  3. 注册回调:使用handle.Completed += OnSceneLoaded;OnSceneLoaded方法注册为回调方法。当场景加载完成时,无论成功还是失败,都会调用OnSceneLoaded方法。

  4. 处理加载完成事件:在OnSceneLoaded方法中,检查加载状态。如果加载成功,输出相应的日志信息;如果失败,输出错误信息。这里你可以添加更多的逻辑来处理场景加载完成后的初始化工作。

优点

使用事件驱动编程和回调机制简化了复杂的异步操作处理:

  • 解耦:异步操作的逻辑与完成事件的处理逻辑分离,增强代码的可读性和可维护性。
  • 灵活性:可以轻松地注册和解除回调方法,灵活处理不同的加载场景和操作。
  • 简化代码:避免了嵌套的回调地狱(Callback Hell),使代码更加简洁明了。

通过这个示例,可以看到使用事件驱动编程和回调机制如何有效地处理异步操作,从而简化复杂的回调逻辑。

事件对于性能的消耗

事件的性能消耗主要体现在以下几个方面:

  1. 委托调用开销:每次触发事件都会遍历并调用所有订阅者的方法,这会产生一定的性能开销。
  2. 内存开销:事件委托的内存占用随着订阅者的增加而增加,特别是长时间存在的事件,可能会导致内存泄漏。
  3. 垃圾回收:如果事件订阅者没有及时取消订阅,会导致对象无法被垃圾回收,造成内存泄漏和性能问题。

为了优化性能,可以参考以下几点:

  1. 避免频繁触发事件:对于高频事件,可以考虑批量处理或减少事件触发的频率。
  2. 及时取消订阅:确保在不需要时及时取消事件订阅,以避免内存泄漏。
  3. 使用弱引用:在某些情况下,可以使用弱引用(WeakReference)来避免订阅者对象无法被垃圾回收。

通过合理的设计和管理,事件系统可以在性能和灵活性之间找到平衡。


C#事件与UnityEvent
http://example.com/事件/
作者
李小基
许可协议