Mediator(中介者)模式在MonoGame游戏开发中的应用
背景
游戏开发中,一个非常常见的行为就是,游戏中的角色需要对周围发生的事物做出反应:飞机在被子弹打中后要出现爆炸效果并从屏幕上消失;台球在撞击到桌面边缘时应能够根据速度和角度反弹等等。在这些场景中,至少会有两个参与者(飞机与子弹,台球与桌面)进行交互,以共同完成一个游戏行为。两个参与者的情况是最为简单的:一个对象调用另一个对象的某个方法即可完成交互,然而即便是一款最为简单的游戏,一个场景中,也会无时不刻地产生这种角色与角色之间的交互,而且往往同一个场景下参与交互的角色都不止两个。 就拿《贪吃蛇》游戏来说,在蛇头碰到食物的时候,会发生这些事情:
- 蛇尾需要根据规则向后增加骨节
- 蛇头碰到的食物会消失
- 场景在棋盘的可选位置上,随机派发一颗食物
- 用户得分增加并显示在场景上
注意上面这段话的关键部分:“当....的时候,会发生....”。这让我们自然而然地想到了消息派发和处理机制,很明显,应对这种复杂的对象交互行为,我们可以选用事件模型来处理,在某个条件成熟时,发出事件消息,而整个场景中的每个对象,都可以选择订阅(或者不订阅)这个事件消息,来决定是否应该成为(或者不成为)整个行为的参与者,以及对于这种事件的发生,应该如何应答(如何处理)。
初步设计
于是,可以参考GoF95设计模式中的Mediator(中介者)模式来实现这样的设计:引入一个负责事件派发的组件,游戏中的对象可以使用这个组件派发消息,也可以将定义在内部的事件消息处理函数,以委托的形式注册到这个组件中,以便当消息被派送时,这些委托函数都能够被正确调用。下面的UML类图大致表达了这样的设计:
在上图中:
- IMessageDispatcher是一个消息派发组件,它有两个方法:RegisterHandler,用于注册消息处理函数;DispatchMessage,用于派发消息
- IVisibleComponent是游戏中所有可见对象的接口定义,它与IMessageDispatcher有关联关系,内部包含一个IMessageDispatcher的实现。所有实现了该接口的类,都可以使用IMessageDispatcher的实例来派发消息,或者使用自己内部的某个事件处理函数来订阅消息
- MessageDispatcherImpl是IMessageDispatcher的实现类,它会通过IVisibleComponent的具体类中的委托,将事件处理函数与所要处理的消息类型对应起来,所以,它与每个IVisibleComponent接口的实现类之间是依赖关系
职责的分离
根据GRASP原则,面向对象软件系统需要明确对象职责。因此,在设计这个游戏框架的时候,可以考虑对事件消息的产生对象和事件消费对象进行职责分离:
- 有些参与者只负责发送消息,比如游戏中的服务(例如:FPS服务向游戏场景发送FPS Updated事件,它并不接收和处理来自其它参与者的消息
- 有些参与者只接收消息
- 有些参与者既接收消息,也发送消息。绝大多数的游戏参与者都属于此类
区分职责的一个重要原因是进行关注点分离,以满足开-闭原则的基本需求(不应该暴露出来的接口,就不能暴露出来)。在这样的设计思想指导下,我们的游戏框架大致会有如下的结构:
简单介绍一下:
- 游戏中的所有参与者都是Component
- 游戏中所有能够被看到并且操作的参与者,都是VisibleComponent
- 游戏由多个场景(Scene)组成,每个场景由1到多个Component组成,Scene负责管理这些Component的生命周期,在需要的时候,发出事件消息,Scene也可以接收来自其它组件的消息,因此,Scene成为了Mediator模式实现中的Colleague角色,它聚合IMessageDispatcher的实现
- VisibleComponent和GameService都需要被加载到某个Scene才能正常执行,因此,它们都会使用Scene所聚合的IMessageDispatcher实现来完成事件消息的发送和接收
- IMessageDispatcher的实现类型,会通过委托方式,将来自GameService、Scene和Sprite的事件处理委托注册到消息处理器队列中
实现效果
下面的例子中:
- 当足球Sprite与另一个足球Sprite碰撞时,碰撞检测服务就会发出CollisionDetectedMessage,通知参与碰撞的两个足球向相反方向弹开
- 当足球Sprite与界面边界碰撞时,碰撞检测服务就会发出BoundaryReachedMessage,通知触碰边界的足球Sprite弹回
- FPS服务会不时地将FPS参数(Frames Per Second)以FpsMessage发出,当前场景接到通知后,将FPS的数值显示在屏幕左上角
下面的《俄罗斯方块》游戏中,通过碰撞检测服务,判断下落方块是否应该与棋盘融合:
参考链接
- 本文提到的自研MonoGame游戏开发框架:https://github.com/daxnet/ovow
- 基于该框架开发的俄罗斯方块源代码:https://github.com/daxnet/tetris-sharp
- 《使用C#和MonoGame开发俄罗斯方块游戏》