笔者语
翻译自开放项目的 Wiki ,翻译时版本是 ad3e009,诸君若看到较新的版本,或笔者文章有何疏漏,可留言或电邮 zhangqrr@qq.com / ricey54560@gmail.com
如何建立一个稳固的、能让 Objects 相互通信并且避免使用单例模式的事件系统?我们的解决方案是使用 ScriptableObject。
我们不用单例模式的原因有很多。单例模式在两个系统中建立死板的连接,导致他们不能单独存在,必须依赖于对方。显而易见,这会让项目变得难以维护、模块难以复用,如果你想单独测试一个系统也是很困难的,必须要测试整个游戏才可以。
工作原理
在事件系统底层我们创建了一系列的 Scriptable Object 叫做“事件频道”(Event Channels),他们就像是一个收音机频道一样,在这些频道上广播其他脚本(图中Action/Trigger)想要广播的事件。另一些脚本(图中Event Listener)可以反过来收听一个自己关心的频道,并将自己的回调方法(图中Call response(s))注册进频道所广播的事件中。上面的图描绘了这一运作过程。
发送事件和监听事件的都是 Monobehaviours。Scriptobject Event Channels 是一种资源,自从我们使用它们去连接两个系统之后,这些 Monobehaviours 就可以以完全独立的方式存在于两个不同的场景中了。
举一个例子,当按下某个按钮的时候会发送一个事件,并且广播在一个叫做 “Button_X_Pressed” 的 Event Channels Scriptobject 上。这时,我们可以让一个或者多个物体监听这个事件,并且当事件发生的时候做出不同的反映:其中一个孵化出很多粒子,其中一个播放了声音,另一个开始播放过场动画。
如何使用
项目中的 Event Channel
Events 可以有参数或者没有参数。下面是一些我们在项目中用到的 Event Channels 的例子。
- Void Events 是没有参数的事件。一个很好地应用是需要广播退出游戏的时候。
- Int Events 是在广播时携带一个 int 参数的事件。当我们解锁了一个成就时,我们可以使用这种事件进行广播,参数为成就 ID。
- Load Events 是要传递两个参数的事件,一个是 GameScene 的数组,包含了我们想要加载的场景的所有数据,另一个是一个 bool 值,表明我们是否想要显示 ”加载中......“ 的界面。
会有更多类型的 Channels 被添加进项目。你可以在 /Scripts/Events/Scriptableobjects/ 文件夹中找到我们定义的所有 Event Channels ScriptableObjects。
创建一个 Event Channel ScriptableObject
在项目窗口右键,在 “Game Event” 中选择一种最适合你需求的 Event Channel,并给它取一个形象的名字。这样一个 Event Channel 就创建好了,可以随时使用。
使用 Event Channel 进行广播
要先持有作为 Channel 的 SO 的引用,然后只需要一行代码就可以在代码的任何地方发送一个事件。下面是一个例子,当一个物体进入当前物体的碰撞体时发送一个事件。
public VoidEventChannelSO OnTriggerEnterEventChannel;
private void OnTriggerEnter(Collider other)
{
OnTriggerEnterEventChannel.RaiseEvent();
}
设置一个事件监听者
有很多方式可以监听一个 Event Channel 上的事件。负责监听的物体可以是一个单独的 Monobehaiour,它只用来做这一件事,或者把监听部分包含在一个脚本中。
在项目中,一个混合监听者的例子就是 LocationLoader 脚本。在这个例子中,负责监听的部分包含在了脚本内,也就是说这个脚本不仅是场景加载方法的容器也是一个监听者。当我们在项目中使用某个监听者不超过一次时,这种方式非常有用。(它在 Scripts/SceneManagement/LocationLoader.cs)(笔者:?)
你也可以找一个通用监听者的例子在 Scripts/Events/VoidEventsListener.cs 。这个脚本可以被挂载到任何 GameObject 并且监听一个 VoidEventChannelSO,它有一个无参的 UnityEvent,你可以将游戏中的任何行为与该 UnityEvent 关联,当事件发生时,UnityEvent 关联的所有行为都会回应。
创建一个新类型的 Event Channel
如果你需要在调用 Raise 方法的时候传递特定数量/类型的参数,那么你可以创建新类型的 Event Channel。下面是 IntEventChannelSO 的例子:
[CreateAssetMenu(menuName = "Events/Int Event Channel")]
public class IntEventChannelSO : ScriptableObject
{
public UnityAction<int> OnEventRaised;
public void RaiseEvent(int value)
{
OnEventRaised.Invoke(value);
}
}
将 int 变量换成任何你想要传递的参数类型。你也可以添加不止一个参数。
创建一个新类型的监听者
你可以根据事件触发时传递的参数数量来新建一个类型的监听者。下面是 Int Event Listener 的例子:
[System.Serializable]
public class IntEvent : UnityEvent<int>
{
}
public class IntEventListener : MonoBehaviour
{
public IntEventChannelSO IntGameEvent;
public IntEvent OnEventRaised;
private void OnEnable()
{
//Check if the event exists to avoid errors
if (IntGameEvent == null)
{
return;
}
IntGameEvent.eventRaised += Respond;
}
private void OnDisable()
{
if (IntGameEvent == null)
{
return;
}
IntGameEvent.eventRaised -= Respond;
}
public void Respond(int value)
{
if (OnEventRaised == null)
{
return;
}
OnEventRaised.Invoke(value);
}
}
更多信息
- 可以通过 2nd Devlog Video 来快速回顾我们是如何使用 ScriptableObjects 来驱动 Event Channels。
- 我们最初介绍事件系统是在 Episode 2 of the Livestream (37.55)。注意,有些细节在那之后已经改变了。在 Wiki 里的是较新的。
- 如果你想知道更多关于 UnityEvent 的多参数版本,你可以看 examples in the documentation ,这里它使用了四个参数。