书中总结出这种 Subclass Sandbox 的设计模式

Game Design Patterns: Subclass Sandbox

这种模式要点有两点:

  1. 在基类中实现各种功能性方法供子类调用
  2. 定义沙盒接口供子类重载

书中的例子,基类中定义三个实用方法,和沙盒接口 activate:

class Superpower
{
public:
virtual ~Superpower() {} protected:
virtual void activate() = 0; void move(double x, double y, double z)
{
// Code here...
} void playSound(SoundId sound, double volume)
{
// Code here...
} void spawnParticles(ParticleType type, int count)
{
// Code here...
}
};

然后在子类中调用

class SkyLaunch : public Superpower
{
protected:
virtual void activate()
{
// Spring into the air.
playSound(SOUND_SPROING, 1.0f);
spawnParticles(PARTICLE_DUST, 10);
move(0, 0, 20);
}
};

这两个要点其实是独立的。书中主要在讨论第一点。第二点,沙盒接口的作用,一个是执行入口,另一个是调用时机可控,这些大多是和具体业务相关。

看书中的例子,很明显能看出一个问题:

void move(double x, double y, double z) 和其他两个方法playSoundspawnParticles并不是一类功能。假设基类抽象的是场景节点,move是实例本身的功能,放到基类作为公用代码以及公用接口是OOP的标准用法。因此主要考虑的是,实用的功能性方法,如播放声音,播放特效等,应该放到哪里?

主要有几种方案:

  1. 直接写:每次在用到的地方编写一遍功能。显然这会造成代码的冗余,给维护造成麻烦。但是往往很多功能一开始写的时候就是写在用到的地方,在开发过程中逐渐由更多的地方需要同样的功能时,再考虑把功能重构到通用的地方。

  2. 功能单例:不同类功能封装到不同的单例。如播放音乐时调用AudioEngine::getInstance()->playSound()。这样做的缺点,如书中所说,是程序中多个地方造成了对AudioEngine这样的系统的引用耦合。我们可以用加一层门面来抵消这种负面效果,如加一个AudioUtils类对原始的AudioEngine进行封装。

  3. 超级单例:不同类功能封装到同一个单例。如一个GameUtils类或直接放到GameRuntime类中,这样与“功能单例”相比的好处是,从依赖多个系统到依赖一个系统,并且减少整个程序单例的数量,调用的时候非常方便。缺点也是十分严重,就是GameUtils会是一个非常庞大的类,一方面增加这个类本身的阅读和维护成本,另一方面对降低了重用性。

    比如一个game有audio, graphics, physics功能,现在有另一个游戏要重用原有代码但是不需要physics特性,但是如果由于GameUtils依赖了physics系统,虽然在业务流程里没有任何调用任何物理相关的功能,要去除对physics系统的依赖也是很困难的。

    很多大型系统里,这种通用全能的类往往会变成一个泥潭,没人敢改动和删减其中的功能,只有不断往上加新的功能。

  4. 超级基类:把功能封装到基类中。就是书中介绍的方法。我对这种方法持保留态度。它实际是超级单例的一种特殊实现,缺点与其类似,甚至更糟。

    如果说超级单例方案造就了一个泥潭,超级基类把这坨泥潭引导每个实例中。它引入了继承这一大耦合,并且使得基类是一个会经常改动的类,这些都是反设计原则的。

    最可怕的是,如果你要用基类中的功能,就需要把自己变成它的子类。如果上一种方法是你要一片树叶给你一个森林,这个方法就是要把自己变成一个森林。

    一般基类中要有一些真正属于公有的代码,如引用计数,生成id,初始化等等,还有一些真正属于对象功能的方法。如果把功能方法和实用方法混在一起,对象真正的功能就会淹没在大量无关代码里,是阅读和维护代码的人很难抓住真正的功能。

    但这种方法在实践中还是用的很多的。我觉得最大的好处是实用方便,大多数语言里调用基类方法比调用其他全局实例方法的语法简洁的多。这常常意味着易用性,用 playSound() 显然比 AudioEngine::getInstance()->playSound() 爽快的多。特别是对初学者,或者非专业人员,比如封装给策划用的脚本,还有现在很多引擎面向的是非专业编程人员,写代码的时候感觉要什么有什么,根本不用考虑这些功能是哪里来的,反正是引擎提供的,只要专注游戏逻辑就好了。这些受众也不会太在意代码架构方面的东西。

  5. 消息系统:基于消息调用功能。这是依赖最低的方法。模块间的功能使用消息系统调用,如AudioEngine监听 PLAY_SOUND 消息,播放声音时发送 sendMessage(PLAY_SOUND, "a.mp3"),不用真正依赖 audio 系统。这样只要消息接口一致,调用方和被调用方可以独立重构。如果去掉某个模块,只是对应消息无效,不会代码崩溃或者无法编译。如果不想 AudioEngine 依赖消息系统,可以用建立一个 AudioManager 监听消息和调用 AudioEngine

    缺点也很多。1. 这种方法调用太重,有一些很小的功能或者实用函数不适合这种模式;2. 代码流会变得更乱,不容易阅读和调试;3. 调用变成纯动态,跳过编译器检查,容易出错;4. 效率会更低

游戏设计模式:Subclass Sandbox模式,以及功能方法集的设计思考的更多相关文章

  1. 设计模式---对象创建模式之工厂方法模式(Factory Method)

    前提:“对象创建”模式 通过“对象创建”模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定.它是接口抽象之后的第一步工作. 典型模式(表现最为突出) 工 ...

  2. 【C#设计模式——创建型模式】工场方法模式

    工场方法模式对简单工场模式进行了乔庙的扩展,不是用一个专门的类来决定实例化哪一个子类.相反,超类把这种决定延迟到每个子类.这种模式实际上没有决策点,就是没有直接选择一个子类实例化的决策. 看书上的例子 ...

  3. java设计模式之模板模式以及钩子方法使用

    1.使用背景 模板方法模式是通过把不变行为搬到超类,去除子类里面的重复代码提现它的优势,它提供了一个很好的代码复用平台.当不可变和可变的方法在子类中混合在一起的时候, 不变的方法就会在子类中多次出现, ...

  4. 设计模式之工厂模式之工厂方法(php实现)

    github: git@github.com:ZQCard/design_pattern.git /** * 工厂方法 * 使用开闭原则来分析下工厂方法模式.当有新的产品产生时,只要按照抽象产品角色. ...

  5. Delphi 设计模式:《HeadFirst设计模式》Delphi2007代码---工厂模式之工厂方法[转]

      1   2{<HeadFirst设计模式>工厂模式之工厂方法 }   3{ 产品类                              }   4{ 编译工具 :Delphi20 ...

  6. 游戏编程技巧 - Subclass Sandbox

    Subclass Sandbox 使用场景 你正在开发一款类似LOL的游戏,里面有许多英雄角色,你决定把这些英雄类交给小弟们实现.因为在这些英雄中,释放放技能时,有的要使用粒子系统造成炫酷的效果,有的 ...

  7. 游戏开发设计模式之状态模式 & 有限状态机 & c#委托事件(unity3d 示例实现)

    命令模式:游戏开发设计模式之命令模式(unity3d 示例实现) 对象池模式:游戏开发设计模式之对象池模式(unity3d 示例实现) 原型模式:游戏开发设计模式之原型模式 & unity3d ...

  8. 游戏开发设计模式之原型模式 & unity3d JSON的使用(unity3d 示例实现)

    命令模式:游戏开发设计模式之命令模式(unity3d 示例实现) 对象池模式:游戏开发设计模式之对象池模式(unity3d 示例实现) 实现原型模式 原型模式带来的好处就是,想要构建生成任意独特对象的 ...

  9. 游戏开发设计模式之命令模式(unity3d 示例实现)

    博主才学尚浅,难免会有错误,尤其是设计模式这种极富禅意且需要大量经验的东西,如果哪里书写错误或有遗漏,还请各位前辈指正. 打 算写设计模式的目的就是,首先自己可以理清思路,还有就是国内的设计模式资料很 ...

随机推荐

  1. java使用Apache POI操作excel文件

    官方介绍 HSSF is the POI Project's pure Java implementation of the Excel '97(-2007) file format. XSSF is ...

  2. lintcode 中等题:find the missing number 寻找缺失的数

    题目 寻找缺失的数 给出一个包含 0 .. N 中 N 个数的序列,找出0 .. N 中没有出现在序列中的那个数. 样例 N = 4 且序列为 [0, 1, 3] 时,缺失的数为2. 注意 可以改变序 ...

  3. lintcode :Invert Binary Tree 翻转二叉树

    题目: 翻转二叉树 翻转一棵二叉树 样例 1 1 / \ / \ 2 3 => 3 2 / \ 4 4 挑战 递归固然可行,能否写个非递归的? 解题: 递归比较简单,非递归待补充 Java程序: ...

  4. *[hackerrank]Jim Beam

    https://www.hackerrank.com/contests/infinitum-aug14/challenges/jim-beam 学习了线段相交的判断法.首先是叉乘,叉乘的几何意义是有向 ...

  5. HTML5入门1---Canvas画布

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  6. Qt网络通信骨架解析,QtClient QtServer QtSerialPort

    http://blog.csdn.net/Dr_Abel/article/details/52469134#t18

  7. C++:默认的构造函数

    注意:如果类中用户没有定义构造函数,系统会自动提供一个函数体为空的默认构造函数. 但是,只要类中定义了一个构造函数(不一定无参构造函数),系统将不再给它提供 默认的构造函数.因为,默认的构造函数被类中 ...

  8. Android 类似时间轴的实现

    想要实现图片中的的时间轴的效果,设定了三种颜色,但是出来的只有一个黑色,还不是设定好的,而且长度很长的话不能滚动,下面上代码: 布局文件: <LinearLayout xmlns:android ...

  9. Android 拦截短信

    public class SMSMess extends BroadcastReceiver { @Override public void onReceive(Context arg0, Inten ...

  10. 安卓开发44:解决 INSTALL_FAILED_UID_CHANGED 等问题

    apk无法卸载,一般可以下面的方法试一下: 1. 删除/data/app/(filename) 文件夹下的apk包 2. 删除/system/app/(filename) 文件夹下的apk包 3. 将 ...