游戏设计模式读书笔记:观察者模式与事件队列

Posted by 蔡华的博客 on February 25, 2017

观察者模式

定义

  • 定义:在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。
  • 看图

模式的理解

  • 观察者模式由来已久,其目的就是为了解耦。观察对象保存观察者的链接,使得观察者与被观察者解耦。这一点与C#中的事件刚好相反。
  • 传统的观察者模式也有其弊端。比如书中提到的使用链表作为观察者集合时,如果删除某个观察者需要遍历。并且每个观察者只有一个next指针的情况,这就使得它只能观察一个对象。当然作者也给出来解决的办法,就是使用链表节点池来解决一个观察者注册多个被观察者的问题。不过在C#的事件系统中这个是不需要的。
  • 当出现被观察者或者观察者销毁时需要额外的注意。一个被观察者销毁产生的问题比较小,因为最多就是不发送消息了。而如果一个观察者销毁但是被观察者还不知道,那么被观察者会发送一个消息给空指针,这样问题就大。 在C#中这个问题也是一样的,当要dispose的时候,需要使用-=操作来去掉观察引用。

观察则模式和事件的区别

  • 长久以来我都认为C#中的事件就是观察者模式,当然实际上有所差别。因为按照经典的设计模式来说观察者是需要类的支持的,当你需要让某个对象可被观察,并为此创建几个观察者时你需要执行诸如继承这样的典型的面向对象编程,需要做一大摊子的事情。但是,很多情况下我们只是希望一个类中的某个对象被观察,也就是一个对象中可能发生多个事件。这也是C#中事件系统所作的事情,在C#中我们观察的是一个对象,而不是一个事情。(参考原文的旁白说明。)

在unity中的使用

  • 这个其实没啥说的,就按照标准的C#程序中使用事件的方式即可,只不过我比较习惯用Action或者Func来替代delegate+event。

事件队列

  • 对于事件队列,不要只认为是事件,它也包含消息或者请求等等。按照我的理解来说就是一个具体要做的事情。
  • 为何要把事件队列和观察者模式写到一起,因为在我看了它们的作用是一样的,就是为了解耦消息的发送者(被观察者)和接受者(观察者)。事件队列在复杂度上要高于观察者模式,这个是因为事件队列在时间上做了进一步的解耦。
  • 怎么理解时间上的解耦?事件队列在接收到一个事件后,何时执行是不确定的。考虑到事件会带上发生时的数据,那么在执行时是不需要依赖时间的。
  • 时间的解耦有个弊端,就是如果你需要实时反馈,那么这个做不到。
  • 还有一个需要注意的是不要形成消息和处理者的循环。

消息汇总与合并

  • 书中作者提到了一个声音播放系统,当队列中存在多个相同音频的请求时需要进行合并。当然这个是因为同一音频短时间内容播放会出现音爆。不过这个也提醒了我们,如果在实际的业务中有这样需要合并的时候,在每个update(也就是要开始执行某个事件时)中要进行合并。

如何实现队列

  • 作者提到了循环缓冲区的问题,通过使用一个数组来实现消息队列,这样可以有效的减少内存的使用。不过我认为弊端就不好定一个数组的大小,如果过小可能来不及做循环,而如果太大一样存在内存的浪费。
  • 从C#角度看,这个队列可以Queue来实现,可以说是完美契合的。

设计决策

  1. 入队的是什么:
    • 事件:如果是事件需要考虑多监听器的情况下让监听器做过滤。其实这个就是一个观察者模式。在这里就有点像异步观察者模式。
    • 消息:一般消息都是一对一的,这个其实也是异步的意思。
  2. 谁能从队列里读取:
    • 事件的话轮到了执行就好,有没有监听者无所谓。如果是一个固定类型的消息,那么可能就是一个具体的业务的方法来获取队列中的消息了。
  3. 谁能写入队列:
    • 如果只有一个写入者,那么这个东西在时间上会执行的很快,和同步的观察者模式差不多。
    • 如果是多个写入者,需要将发送方本身的引用加入到事件的数据当中。就像我们在C#中开发基于UI的程序一样,如果你有经验会知道每个事件处理函数中第一个参数总是某个具体的控件。
  4. 队列中对象的生命周期
    • 在我看来应该就是事件出队列时就是其周期的结束,当然因为GC的问题这个对象也许并不会立刻被销毁。
    • 书中提到了可以将对象的所有权进行转移,到具体的执行方去。也可以让队列一直拥有它。这个方面我还没有觉得有这个必要保留对象,也许是没有遇到一个典型的例子吧。