C#事件与UnityEvent
C#中的事件系统
在C#中,事件系统是一种发布/订阅模式(Pub/Sub),允许对象通过事件来传递信息。事件本质上是委托(delegate)的扩展,委托是一种引用类型,可以保存对方法的引用。事件的主要角色是事件发布者和事件订阅者:
- 事件发布者:定义并触发事件。
- 事件订阅者:订阅并响应事件。
在某些地方,“发布”和“订阅”也会被叫做“广播”和“监听”
事件声明通常使用event
关键字,表示只有事件发布者可以触发事件,而事件订阅者可以订阅该事件。例如:
1 |
|
Unity的UnityEvent
在Unity中,UnityEvent
是一种特殊的事件类型,提供了一种在编辑器中无需编写代码即可设置事件的机制。UnityEvent
位于UnityEngine.Events
命名空间下,可以在脚本中定义,也可以通过Unity Inspector窗口进行配置。例如:
1 |
|
在Inspector中,你可以为myUnityEvent
添加监听器(Listeners),选择对象并设置要调用的方法。
区别中的UnityEvent(事件)和UnityAction(委托)
UnityEvent可以在Inspector中配置,适合需要在设计时灵活管理事件和监听器的情况。
UnityAction不能在Inspector中配置,适合完全通过代码管理事件的情况。
在Unity中,UnityEvent
和UnityAction
都是用于事件系统的类型,但它们有不同的用途和实现方式。理解它们的区别有助于你更好地选择和使用它们。
UnityEvent
UnityEvent
是Unity提供的一种事件系统的实现,主要用于方便地在Inspector窗口中进行事件的配置和管理。它是基于Unity的序列化系统,可以在Inspector中添加和配置监听器(Listeners)。
特点
- Inspector配置:
UnityEvent
可以在Inspector中进行配置,允许你在设计时通过拖拽和选择来添加监听器。 - 支持多参数:
UnityEvent
可以支持多参数的事件,可以在事件触发时传递参数。 - 序列化支持:
UnityEvent
可以被序列化,这意味着你可以在Unity的场景和预制体中保存它们的状态。
示例
1 |
|
在Inspector中,你可以看到myEvent
,并可以通过Inspector添加和配置监听器。
UnityAction
UnityAction
是一个委托(Delegate),它是Unity事件系统的一部分。它用于定义无返回值的方法签名,可以用来代替C#的Action
委托。
特点
- 无Inspector配置:
UnityAction
不能在Inspector中进行配置,它完全通过代码来管理。 - 简单轻量:
UnityAction
更轻量,适合在代码中直接定义和使用事件处理。 - 灵活:
UnityAction
可以用于任意需要无返回值的方法签名,可以通过标准的C#事件机制或其他方式来调用。
示例
1 |
|
在这个示例中,myAction
是一个UnityAction
类型的委托,当MyMethod
被添加到myAction
中并调用myAction.Invoke()
时,MyMethod
会被执行。
关键区别
-
配置方式:
UnityEvent
可以在Inspector中配置,适合需要在设计时灵活管理事件和监听器的情况。UnityAction
不能在Inspector中配置,适合完全通过代码管理事件的情况。
-
序列化和保存:
UnityEvent
可以被序列化和保存,可以在场景和预制体中保存其配置。UnityAction
是委托,不支持序列化和Inspector配置。
-
参数支持:
UnityEvent
支持多参数,可以在事件触发时传递多个参数。UnityAction
的参数取决于它的泛型签名,可以是无参数或带一个或多个参数的委托。
事件驱动编程有什么优点
事件驱动编程(Event-Driven Programming, EDP)有以下几个优点:
- 解耦:事件发布者和订阅者之间的耦合度很低,使得代码更易于维护和扩展。
- 提高可读性和可维护性:通过事件机制,逻辑分离更加明确,事件处理逻辑独立于主业务逻辑。
- 灵活性:订阅者可以在运行时动态地订阅或取消订阅事件,提供更大的灵活性。
- 简化复杂性:适用于处理异步操作和回调,简化了复杂的回调逻辑。(例如使用
Addressable.LoadSceneAsync()
异步加载场景)
演示使用事件驱动简化异步操作和回调
下面通过Unity的Addressable.LoadSceneAsync()
方法的一个示例,展示如何使用事件系统注册和处理异步操作的完成事件。
示例:使用Addressable.LoadSceneAsync()进行异步加载场景并处理完成事件
在Unity中,Addressable.LoadSceneAsync()
是一个异步方法,用于加载场景。在加载完成时,我们可以注册一个回调方法来处理完成事件。以下是一个具体的示例:
- 添加必要的命名空间:
1 |
|
- 定义加载场景的方法:
1 |
|
解释
-
命名空间:首先,我们导入了必要的命名空间,用于访问Addressables、异步操作和场景管理功能。
-
场景加载方法:在
LoadScene
方法中,调用Addressable.LoadSceneAsync()
来异步加载场景。该方法返回一个AsyncOperationHandle<SceneInstance>
对象,用于跟踪异步操作的进度和状态。 -
注册回调:使用
handle.Completed += OnSceneLoaded;
将OnSceneLoaded
方法注册为回调方法。当场景加载完成时,无论成功还是失败,都会调用OnSceneLoaded
方法。 -
处理加载完成事件:在
OnSceneLoaded
方法中,检查加载状态。如果加载成功,输出相应的日志信息;如果失败,输出错误信息。这里你可以添加更多的逻辑来处理场景加载完成后的初始化工作。
优点
使用事件驱动编程和回调机制简化了复杂的异步操作处理:
- 解耦:异步操作的逻辑与完成事件的处理逻辑分离,增强代码的可读性和可维护性。
- 灵活性:可以轻松地注册和解除回调方法,灵活处理不同的加载场景和操作。
- 简化代码:避免了嵌套的回调地狱(Callback Hell),使代码更加简洁明了。
通过这个示例,可以看到使用事件驱动编程和回调机制如何有效地处理异步操作,从而简化复杂的回调逻辑。
事件对于性能的消耗
事件的性能消耗主要体现在以下几个方面:
- 委托调用开销:每次触发事件都会遍历并调用所有订阅者的方法,这会产生一定的性能开销。
- 内存开销:事件委托的内存占用随着订阅者的增加而增加,特别是长时间存在的事件,可能会导致内存泄漏。
- 垃圾回收:如果事件订阅者没有及时取消订阅,会导致对象无法被垃圾回收,造成内存泄漏和性能问题。
为了优化性能,可以参考以下几点:
- 避免频繁触发事件:对于高频事件,可以考虑批量处理或减少事件触发的频率。
- 及时取消订阅:确保在不需要时及时取消事件订阅,以避免内存泄漏。
- 使用弱引用:在某些情况下,可以使用弱引用(
WeakReference
)来避免订阅者对象无法被垃圾回收。
通过合理的设计和管理,事件系统可以在性能和灵活性之间找到平衡。