通过在您的 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事件。然后,其他几个带有AudioObserver和ParticleSystemObserver组件的 GameObjects 就能以各自的方式对事件做出响应。
确定哪一个客体是主体,哪一个客体是观察者,只是因用法而异。任何引发事件的事物都是主体,任何对事件做出反应的事物都是观察者。同一 GameObject 上的不同组件可以是主体,也可以是观察者。即使是同一个组件,在某种情况下也可能是主体,而在另一种情况下则可能是观察者。
例如,示例中的AnimObserver会在点击按钮时为按钮添加一点动作。尽管它是 ButtonSubject GameObject 的一部分,但它仍充当观察者的角色。
Unity 还包含一个独立的 UnityEvents系统,它使用 UnityAction委托。它可以在检查器(为观察者模式提供图形界面)中进行配置,允许开发人员指定事件发生时应调用哪些方法。
如果您使用过 Unity 的用户界面系统(例如,创建用户界面按钮的 OnClick 事件),那么您已经对此有了一些经验。
在上图中,按钮的 OnClick 事件调用并触发了两个 AudioObservers 的OnThingHappened方法的响应。因此,您可以在不使用代码的情况下设置主题事件。
如果您想让设计师或非程序员创建游戏事件,UnityEvents 将非常有用。不过要注意,它们可能比系统命名空间中的相应事件或操作要慢。UnityActions 还有一个额外的好处,就是可以用来调用带参数的方法,而 UnityEvents 则仅限于调用不带参数的方法。
在考虑 UnityEvents 和 UnityActions 时,请权衡性能与使用。UnityEvents 更简单易用,但在可调用的方法类型方面受到了更多限制。有些人可能还会说,暴露检查器中的所有事件会更容易出错。
有关示例,请参阅 Unity Learn 上的利用事件创建简单消息系统模块。
实施事件会增加一些额外的工作,但它确实有很多优点:
观察者模式有助于解耦对象: 事件发布者无需了解事件订阅者本身的任何信息。主体和观察者在保持一定程度的分离(松散耦合)的同时进行交流,而不是在一个类和另一个类之间建立直接的依赖关系。
你不必建造它: C# 包含一个成熟的事件系统,您可以使用System.Action委托来代替定义自己的委托。此外,Unity 还包括UnityEvents和UnityActions。
每个观察者都实现了自己的事件处理逻辑: 这样,每个观察对象都能保持做出响应所需的逻辑。这使得调试和单元测试更加容易。
它非常适合用户界面: 核心游戏代码可以与用户界面逻辑分开。然后,用户界面元素会监听特定的游戏事件或条件,并做出适当的响应。为此,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 应用程序中使用设计模式以及 SOLID 原则的技巧。 使用游戏编程模式提升代码水平.