您想找什么?
Hero background image
使用观察者模式创建模块化、可维护的代码

通过在您的 Unity 项目中实施常见的游戏编程设计模式,您可以高效地构建和维护一个简洁、有序和可读的代码库。设计模式不仅能减少重构和测试时间,还能加快入职和开发流程,为游戏、开发团队和业务的发展奠定坚实的基础。

不要把设计模式看成是你可以复制粘贴到代码中的成品解决方案,而应把它看成是正确使用时可以帮助你构建更大、可扩展应用程序的额外工具。

本页将解释观察者模式,以及该模式如何帮助支持对象之间的松散耦合原则。

这里的内容基于免费电子书、 用游戏编程模式提升代码水平该电子书解释了众所周知的设计模式,并分享了在您的 Unity 项目中使用这些模式的实用示例。

Unity 游戏编程设计模式系列中的其他文章可在Unity 最佳实践中心查阅,或点击以下链接:

主题观察员类比
观察员模式

在运行时,游戏中可能会发生各种情况。玩家消灭敌人后会发生什么?或者当他们获得能量或等级提升时?您通常需要一种机制,允许某些对象在不直接引用其他对象的情况下通知其他对象。不幸的是,随着代码库的增加,这会增加不必要的依赖性,从而导致代码维护的不灵活性和过多开销。

观察者模式是解决这一问题的常用方法。它允许对象进行通信,但使用 "一对多 "的依赖关系保持松散耦合。当一个对象改变状态时,所有从属对象都会自动收到通知。

有一个比喻可以帮助我们形象地理解这一点,那就是向许多不同听众广播的无线电塔。它不需要知道谁在收听,只需要知道广播在正确的时间、正确的频率上进行直播。

广播的对象称为主体。其他监听对象称为观察者,有时也称为订阅者(本页始终使用观察者这一名称)。

这种模式的好处在于,它将主体与观察者分离开来,观察者并不真正了解观察者,也不关心观察者收到信号后会做什么。虽然观察者对主体有依赖性,但观察者本身并不了解彼此。

每当实验对象的状态发生变化时,观察者就会收到通知,从而进行相应的更新。这样,修改或扩展代码就变得更加容易,而不会影响系统的其他部分。

另一个好处是,观察者模式鼓励开发可重复使用的代码,因为观察者可以在不同的上下文中重复使用,而无需修改。最后,由于明确定义了对象之间的依赖关系,它通常还能提高代码的可读性。

主题观察员
了解事件

您可以设计自己的主体-观察者类,但这通常是不必要的,因为 C# 已经使用事件实现了这种模式。观察者模式是如此普遍,以至于它已内置于 C# 语言中,这是有充分理由的:它可以帮助你创建更多模块化、可重用和可维护的代码。

什么是活动?这是一个通知,表示发生了一些事情,涉及几个步骤:

发布者(也称为主体)根据建立特定函数签名的委托创建事件。事件只是主体在运行时将执行的一些操作(例如,受到伤害、点击按钮等)。发布者维护着其隶属者(观察者)的列表,并在其状态发生变化时向它们发送通知。

然后,观察者各自创建一个称为事件处理程序的方法该方法必须与委托的签名相匹配。观察者是接收发布者通知并相应更新自己的对象。

每个观察者的事件处理程序都会订阅发布者的事件。您可以根据需要让尽可能多的观察员加入订阅。它们都将等待事件触发。

当发布者在运行时发出事件发生的信号时,就称为事件触发。这反过来又会调用观察者的事件处理程序,后者会运行自己的内部逻辑作为响应。

通过这种方式,您可以让许多组件对来自主体的单个事件做出反应。如果被试表示点击了某个按钮,观察者就可以回放动画或声音、触发剪辑或保存文件。它们的响应可以是任何内容,这就是为什么你会经常发现观察者模式被用来在对象之间发送信息。

代表与活动

委托是一种定义方法签名的类型。这样就可以将方法作为参数传递给其他方法。把它想象成一个变量,用来保存一个方法的引用,而不是一个值。

另一方面,事件本质上是一种特殊类型的委托,它允许类之间以松散耦合的方式进行通信。有关委托和事件之间区别的一般信息,请参阅《区分 C# 中的委托和事件》。

代码中的简单主题

让我们看看如何在下面的代码中定义基本的主题/发布者。

在下面代码示例中的Subject类中,我们继承了 MonoBehaviour,以便更容易地将其附加到 GameObject 上,尽管这并不是必须的。

您可以自由定义自己的自定义委托,也可以使用System.Action,它适用于大多数使用情况。在代码示例中,不需要随事件发送参数,但如果需要的话,使用Action<T>委托并将它们作为List<T>在角括号内传递(最多 16 个参数)也很简单。

在代码片段中,ThingHappened是主体在DoThing方法中调用的实际事件。

?"操作符是一个空条件操作符,这意味着只有当事件不为空时才会被调用。Invoke方法用于引发事件,这意味着它将执行订阅该事件的任何事件处理程序。在这种情况下,DoThing方法将引发ThingHappened事件(如果该事件不为空),从而执行订阅该事件的所有事件处理程序。

您可以下载演示观察者和其他设计模式的示例项目。该代码示例可在此处获取。

代码中的简单观测器

要监听事件,可以创建一个观察者类示例,就像下面的精简代码示例(也可在Github 项目中找到)。

将此脚本作为组件附加到 GameObject 上,并在检查器中引用subjectToObserver以监听ThingHappened事件。

OnThingHappened方法可包含观察者为响应事件而执行的任何逻辑。开发人员通常会添加前缀 "On "来表示事件处理程序(使用样式指南中的命名约定)。

在 "唤醒 "或 "开始 "中,您可以使用+=操作符订阅事件。这就观察者的OnThingHappened方法与主体的ThingHappened 方法结合起来

如果有任何东西运行了主体的DoThing方法,就会引发事件。然后,观察者的OnThingHappened事件处理程序会自动调用并打印调试语句。

注意:如果在运行时删除或移除观察者,而观察者仍订阅ThingHappened,则调用该事件可能会导致错误。因此,在对象生命周期的适当时间,使用-=操作符在 MonoBehaviour 的OnDestroy方法中取消订阅事件非常重要。

观察员样本项目
观察者模式的用例

如果下载示例项目并转到名为11 Observer 的文件夹,您会发现一个示例,其中显示了一个简单的按钮(ExampleSubject)、扬声器(AudioObserver)、动画(AnimObserver)和粒子效果(ParticleSystemObserver)。

当您点击按钮时,ExampleSubject 会调用 ThingHappened 事件。AudioObserver、AnimObserver 和 ParticleSystemObserver 会响应调用各自的事件处理方法。

观察者可以存在于相同或不同的游戏对象上。请注意,AnimObserver 在 ExampleSubject 上制作按钮动画,而 AudioObservers 和 ParticleSystemObserver 占用不同的 GameObject。

ButtonSubject允许用户通过鼠标按钮调用Clicked事件。然后,其他几个带有AudioObserverParticleSystemObserver组件的 GameObjects 就能以各自的方式对事件做出响应。

确定哪一个客体是主体,哪一个客体是观察者,只是因用法而异。任何引发事件的事物都是主体,任何对事件做出反应的事物都是观察者。同一 GameObject 上的不同组件可以是主体,也可以是观察者。即使是同一个组件,在某种情况下也可能是主体,而在另一种情况下则可能是观察者。

例如,示例中的AnimObserver会在点击按钮时为按钮添加一点动作。尽管它是 ButtonSubject GameObject 的一部分,但它仍充当观察者的角色。

团结活动和行动
UnityEvents vs UnityActions

Unity 还包含一个独立的 UnityEvents系统,它使用 UnityAction委托它可以在检查器(为观察者模式提供图形界面)中进行配置,允许开发人员指定事件发生时应调用哪些方法。

如果您使用过 Unity 的用户界面系统(例如,创建用户界面按钮的 OnClick 事件),那么您已经对此有了一些经验。

在上图中,按钮的 OnClick 事件调用并触发了两个 AudioObservers 的OnThingHappened方法的响应。因此,您可以在不使用代码的情况下设置主题事件。

如果您想让设计师或非程序员创建游戏事件,UnityEvents 将非常有用。不过要注意,它们可能比系统命名空间中的相应事件或操作要慢。UnityActions 还有一个额外的好处,就是可以用来调用带参数的方法,而 UnityEvents 则仅限于调用不带参数的方法。

在考虑 UnityEvents 和 UnityActions 时,请权衡性能与使用。UnityEvents 更简单易用,但在可调用的方法类型方面受到了更多限制。有些人可能还会说,暴露检查器中的所有事件会更容易出错。

有关示例,请参阅 Unity Learn 上的利用事件创建简单消息系统模块。

优点和缺点

实施事件会增加一些额外的工作,但它确实有很多优点:

观察者模式有助于解耦对象: 事件发布者无需了解事件订阅者本身的任何信息。主体和观察者在保持一定程度的分离(松散耦合)的同时进行交流,而不是在一个类和另一个类之间建立直接的依赖关系。

你不必建造它: C# 包含一个成熟的事件系统,您可以使用System.Action委托来代替定义自己的委托。此外,Unity 还包括UnityEventsUnityActions

每个观察者都实现了自己的事件处理逻辑: 这样,每个观察对象都能保持做出响应所需的逻辑。这使得调试和单元测试更加容易。

它非常适合用户界面: 核心游戏代码可以与用户界面逻辑分开。然后,用户界面元素会监听特定的游戏事件或条件,并做出适当的响应。为此,MVP 和 MVC 模式使用了观察者模式。

但是,您也应该注意这些注意事项:

它增加了复杂性: 与其他模式一样,创建事件驱动架构在一开始需要进行更多的设置。另外,删除研究对象或观察者时要小心。确保在 OnDestroy 中取消注册观察者,以便在不再需要观察者时正确释放内存引用。

观察者需要引用定义事件的类: 观察者仍然依赖于发布事件的类。使用静态 EventManager(见下一节)来处理所有事件,有助于将对象与对象之间区分开来。

性能可能是个问题: 事件驱动架构增加了额外的开销。大型场景和许多游戏对象会影响性能。

改进图案

虽然这里只介绍了观察者模式的基本版本,但您可以将其扩展到满足游戏应用程序的所有需求。

在设置观察者模式时,请考虑这些建议:

使用 ObservableCollection 类: C# 提供了一个动态的 ObservableCollection来跟踪特定的变化。它可以在项目添加、删除或刷新列表时通知观察者。

将唯一的实例 ID 作为参数传递: 层次结构中的每个 GameObject 都有一个唯一的实例 ID。如果您触发的事件可能适用于多个观察者,请在事件中传递唯一 ID(使用Action<int> 类型。然后,只有当 GameObject 与唯一 ID 匹配时,才运行事件处理程序中的逻辑。

创建静态 EventManager: 由于事件可以驱动游戏的大部分玩法,许多 Unity 应用程序都使用静态或单例 EventManager。这样,您的观察者就可以将游戏事件的中心来源作为主题,使设置更加容易。

FPS Microgame很好地实现了静态事件管理器(EventManager),该管理器可实现自定义 GameEvents,并包含用于添加或移除监听器的静态辅助方法。

Unity 开放项目还展示了一种由可脚本对象(ScriptableObjects)中继 UnityEvents 的游戏架构。它使用事件来播放音频或加载新场景。

创建事件队列: 如果您的场景中有很多对象,您可能不希望同时触发事件。将观察者模式与命令模式相结合,可以将事件封装到事件队列中。然后,您可以使用命令缓冲区逐个回放事件,或根据需要选择性地忽略它们(例如,如果您有可同时发出声音的对象数量上限)。

观察者模式在模型视图演示器(MVP)架构模式中占有重要地位,电子书《用游戏编程模式提升代码水平》中介绍了这一模式。 利用游戏编程模式提升代码水平.

电子书封面
更多有关 Unity 编程的高级资源

您可以在免费电子书中找到更多关于如何在您的 Unity 应用程序中使用设计模式以及 SOLID 原则的技巧。 使用游戏编程模式提升代码水平.

所有高级 Unity 技术电子书和文章均可在最佳实践中心获取。电子书籍也可在文档中的高级最佳实践页面获取。

您喜欢本文吗?
是的!
还行。