前两篇文章介绍了创建封装的模式和结构封装的模式,这次我们接着来看剩下的模式。分别有:策略模式、迭代器模式、命令模式、模板方法模式、观察者模式、责任链模式、状态模式、解析器模式、备忘录模式、访问者模式、中介者模式。这些模式的共同点是职责的封装。他们都是把一部分职责从原有的对象中抽离出来,封装成一个单独的对象,达到不同的目的。
最简单的就是策略(Strategy)模式:
策略模式就是把某一部分职责,例如角色的攻击抽离出来,单独封装成一个类,然后派生不同的实例,实现不同的攻击方式。这样原有的角色类就不再需要为攻击的多样化而操心了,利用接口的形式解耦后角色可以动态配上不同的攻击行为。
另一个常见的迭代器(Iterator)模式也是同样的思路,把遍历管理器内元素的职责从管理器中抽离出来,单独封装成一个迭代器类,这样可以使得管理器接口更加简单,并且只专注于管理元素。不同的是,根据管理器使用的容器结构,迭代器会有对应的一个实现,所以管理器与迭代器是绑定的,不会像上面策略模式那样动态替换。
以上两个模式都致力于对象与行为解耦,再推进一步,操作可变,并且操作的对象也可变,这样的需求该怎么组织呢?例如游戏正常状态下,按下键盘w键是控制角色往前移动,而在剧情模式下,按下键盘w键是控制摄像机围绕剧情所在场景转动。对于这种需求,命令(Command)模式可以派上用场。
命令模式把操作的对象,以及对象的行为封装成一个新对象。调用者持有命令对象的接口,并且可以动态的更换命令。就像上面的例子,键盘响应对象通过动态更换命令接口,实现对不同对象(Player和Camera)执行不同(直走和环绕)操作。
除了通过组合达到行为解耦,从而实现职责封装,还有可以通过继承来实现,这就是模板方法(Template Method)模式。
模板方法把算法固定在基类中,通过抽象方法让子类决定不同的细节。就像上面类图所示,基类Sort已经实现了排序的算法,把比较函数定义为抽象接口让派生类实现,这样派生类只要实现Compare细节,就可以实现不同的排序功能,也不用管排序到底如何实现。
应对类似关注键盘消息这种,当事件触发时通知关心的对象的需求,大名鼎鼎的观察者(Observer)模式是不二的选择。观察者模式把所有关心数据的类实现一个Observer接口,当数据发生变化时,掌握数据的Subject通过Observer接口定义的方法回调关注数据的对象。Subject有Register(),Remove(),和Notify()方法,可以动态的添加删除关注数据的对象,管理Observer的引用。
另一个与观察者模式很相似的模式是责任链(Chain Of Responsibility)模式,它也是要求关心某个消息的对象实现同一套接口,然后用链式结构串起来。这样不但能达到发送与接收解耦,还能动态的改变响应顺序。这个模式在UI界面中广泛的应用,在之前的文章《浅谈游戏UI架构》中有详细讲解,这里就不再重复了。
游戏AI的设计最常用的是状态机,状态机的核心就是状态(State)模式。它把角色拥有的所有行为都封装在状态里面,然后在不同的状态下有不同的行为实现。
如图所示,角色有巡逻、攻击、协助3种行为,在不同的状态下这3种行为有不同的实现。例如愤怒状态下,角色攻击会很凶猛,但忽略其他角色的协助请求;冷静状态下会仔细巡逻并积极响应协助请求;害怕状态下则巡逻攻击协助都不会执行。利用状态模式组织他们之间的关系非常直观并且可以动态改变。
在运用状态模式时我们有一个需要考虑的点是状态之间的切换逻辑放在哪里。通常是放在Client(也就是例子中的Character)里,这样Client就要对所有状态了解。另一种做法是放到各个状态中,这样做虽然避免所有状态切换逻辑都塞到Client中,但却导致各个子状态相互耦合。两种做法各有利弊,运用时要根据实际情况取舍。
看完状态模式我们来看看解析器(Interpreter)模式,其实他们是同一个东西。解析器模式把每一个语法规则的解析封装成一个类,然后解析时上层做语法匹配,发现是哪这一种语法就交由该语法规则解析类解析。其实每一个语法解析就是一种状态,上层就是通过语法匹配来切换不同的状态,整个语法解析器其实就是一个状态机。
游戏开发中我们都会遇到一个问题——游戏进度的保存与恢复。如果是在同一个模块中,游戏状态直接转换成数值保存就可以了。但是如果我们的模块是交付第三方使用的,我们要提供进度保存恢复的接口,但我们又不想把游戏状态直接以数值导出,因为这样外层就可以随意修改了。应对这种需求,备忘录(Memento)模式可以帮到我们。备忘录模式就是把系统的状态封装成一个类,外界持有这个类的对象,但对于它如何表示系统状态,如何生成和如何使用都不可知,只能通过我们提供的接口使用。
GameMemento pMen = GameControl::Save(); // 保存进度但Client对于数据并不可见
GameControl::Restore(pMen); // 恢复进度
最后剩下两个模式,他们其实可以分到结构封装一类,因为他们都是在现有类的基础上再封装了一层,不过他们没有包裹关系,所以看作是访问职责的封装更合适。首先介绍的是访问者(Visitor)模式,访问者模式的思路是当一个系统中不同对象已经有不同的访问接口,但面对陆续新加的访问需求,不应该在对象上直接修改满足需求,而应该把访问职责抽离出来封装成访问者类,由该类来处理各种各样的访问需求。
如图所示,场景中不同的对象都实现一个访问接口,在接受访问时把自己当作参数传递给访问者。而不同的访问者实现不同的访问接口,获取所需信息。对于取位置的不同接口细节,交给访问者完成就可以了,而且再新加访问需求时只要派生访问者,实现其他访问细节即可,被访问的对象不需要改变。
最后介绍的中介者(Mediator)模式是在两个模块的对象需要互相访问时,不让他们各自交互,而是通过同一个中介者来访问。就像游戏中的表现模块与UI模块,可能UI模块的角色面板需要访问表现模块主角模型的一些信息,他们之间不应该相互可见直接访问,而是通过一个UI到表现的中介者来实现,其他UI模块需要访问表现也同样如此。这样相当于在两个模块之间竖起一堵墙,中间只能通过中介者交互。这样做避免了不同模块下的对象之间相互关联,让依赖关系降到最低,简化了程序结构。
到此24个经担的设计模式都粗略介绍过一遍了,可能有的一下带过,有的以偏概全,就当作抛砖引玉。设计模式也是重在引发大家对于结构设计的思考,不必生搬硬套,灵活运用即可。