游戏编程模式-子类沙盒

  “使用基类提供的操作集合来定义子类中的行为。“

动机

  在游戏中,我们可以实现各种各样的想法,比如说创造一个超级英雄, 我们为超级英雄创造各种能力。这个时候我们可以怎么做了?建立一个superpower的基类,然后使用派生的想法,构建各种派生类来实现超能力。但这里会很快的出现问题,因为超能力的多种多样,我们可能需要在派生类中做各种可能的事情:比如播放音效、产生视觉效果、与AI交互、创建和销毁其它游戏实体以及产生物理效果。它们可能触及代码库的每一个角落。很明显,这样会:

  •   产生大量的冗余代码。虽然各种超能力不同,但它们非常可能以同样的方式来产生视觉效果和播放音效,如果人们在实现它们的时候没有整合起来,那么将会产生大量的重复代码和重复劳动;
  •   游戏引擎的每个部分都将与这些类产生耦合;
  •   当这些外部系统需要改变时,superpower类的代码很可能遭到随机性的破坏。因为我们的superpower类与外部代码存在很强的耦合关系或者说依赖关系,一旦外部系统发生改变,不可避免的会影响到superpower类;
  •   定义所有superpower都遵循的不变量时很困难的。例如说我们像保证所有的power类的音效都能得到合理的优先级划分和排队,如果这些派生类都直接调用音效引擎的话,这将很难实现。

  这个时候我们应该怎么办了?对于播放音效,我们可以提供一个playSound的方法,这个方法置于superpower类中,派生的子类需要播放音效的时候都调用这个方法,这样我们就能对音效的播放进行一个统一的管理,比如说调整优先级。而这就引申出了一个做法:把子类需要的功能封装成方法放到基类中,然后让子类访问这些方法。这个时候就有一个问题了,如何安放这些方法了?也就是说子类应该如何来组织使用这些方法实现功能了?为此我们定义一个沙盒方法,这个时子类必须实现的抽象保护方法。所以接下来你要做的就是:

  1.   创建一个类继承基类;
  2.   重写沙盒函数activate();
  3.   通过调用基类提供的方法来实现子类的功能;

  也就是我们把基础的操作代码提取到更高的层次来解决冗余的问题。一旦我们在子类中发现大量的重复代码,我们就会把它上移到基类中作为一个新的基本方法。也就是说我们把子类的耦合都提取到父类中,这样耦合的地方就只有一处,每个子类仅与基类耦合。一旦游戏的某个部分发生变化时,我们只需要修改基类即可,不会牵扯到子类的修改。这样的设计会催生一种扁平的类层次架构。你的继承链不会太深,但会有大量的子类,这些子类与基类挂钩。通过一个类派生大量的子类,我们限制了该代码在代码库中的影响范围。

沙盒模式

  一个基类定义一个抽象的沙盒方法和一些预定义的操作集合。通过将它们设置为受保护的状态以确保它们仅供子类使用。每个派生出的沙盒子类根据父类提供的操作来实现沙盒函数。

使用情境

  沙盒模式是运用在多数代码库里、甚至游戏之外的一种非常简单通用的模式。如果你正在部署一个非虚的受保护方法,那么你很可能正在使用与之类似的模式。沙盒模式适用于一下情况:

  •   你有一个带有大量子类的基类;
  •   基类能够提供子类所有可能需要执行的操作集合;
  •   在子类之间有重叠代码,你希望在它们之间更简单的共享代码;
  •   你希望使这些继承类与程序的其它代码之间的耦合最小化。

使用须知

  这些年“继承”一词被部分程序圈所诟病,原因之一使基类会衍生越来越多的代码。这个模式尤其受这个因素影响。

  由于子类使通过它们的基类来完成各自功能的,因此基类最终会与那些需要与其子类交互的任何系统产生耦合。当然,这些子类也与它们的基类密切相关。这个蜘蛛网式的耦合使得无损的改变基类使很困难的——你遇到类脆弱的基类问题。但从另一个角度来说,你的所有耦合都被聚集到了基类,子类现在与其它部分划清了界限。理想的情况下,你的绝大部分操作都在子类中。这样意味着你的大量代码库使独立的,并且更容易维护。

  如果你仍然发现本模式正在把你的基类变得庞大不堪,那么请考虑一些提供的操作提取到一个基类能管理的独立类中。这里可以借鉴组件模式。

示例

  superpower基类:

class Superpower
{
 public:
    virtual ~Superpower(){}

 protected:
    virtual void activate() = 0;

    void move(double x, double y, double z)
    {
         //move code
    }

    void playSound(SoundId sound)
    {
          //play code
    }

     //other methods....
};
    

  这里activate就是沙盒函数。由于它是抽象虚函数,因此子类必须要重写它。这是为了让子类实现者能够明确它们该对子类做什么。接下来让我们实现一些子类来说明子类是如何创建的。

class SkyLaunch:public Superpower
{
 protected:
    virtual void activate()
    {
        move(0,0,30);
        playSound(SOUND_SPROING);
     }
};

  这里,子类做的事很简单,移动,然后播放音乐。因为操作都放到了基类中,子类没有与外部代码有任何的耦合。当然,我们可以做其它更复杂的事,只需要在基类中提供相应的基本操作,你可以放飞你的想象力。

 沙盒模式就是如此的简单,代码并不太多,它描述的是一个基本的思想,但并没有给出过于详细的机制。所以这里你还是要面临一些抉择:

设计决策

  需要提供什么操作

  这里有两个极端,一种是基类什么操作都不提供,只提供一个沙盒方法;而另一个就是基类提供子类所有需要的操作。子类仅与基类耦合,不同调用任何外部系统。前者基类不提供任何操作,所以基类与外部系统的耦合度低,随着基类提供的操作多,与外部系统的耦合就越来越高。如果我们把所有的操作都聚集到基类,那么基类就会变得很大,维护起来也就会越来越困难,所以我们应该如何做出选择了?

  •   如果所提供的操作仅仅被一个或者少数的子类使用,那么不必将它加入到基类。这只会给基类增加复杂度,同时影响每个子类,但只有少数子类受益;将该操作与其它提供的操作保持一致或许值得,但让特殊子类直接调用外部系统或许更为简单和清晰;
  •   当你在游戏的其它模块进行某个方法调用时,如果它不修改任何状态,那么它就不具备侵入性。它仍然产生耦合,但这是一个“安全”的耦合,不会给游戏带来破坏;
  •   如果提供的操作,其实现仅仅是对一些外部系统调用的二次封装,那么它并由带来多少价值。这种情况下直接调用外部系统更简单。然而,有时极其简单的转向调用也仍有用——这些函数通常访问基类不想直接暴露给子类的状态。

  是直接提供函数,还是有包含它们的对象提供

  这个设计模式的挑战在于最终你的基类可能塞满了方法。你能够通过转移一些函数到其它类中来缓解这种情况,并于基类的相关操作中返回相应的类对象即可。就像这样:

class SoundPlayer
{
Protected:
    SoundPlayer& getSoundPlayer()
    {
        return soundPlayer_;
    }

private:
    SoundPlayer soundPlayer_;
};

  把提供的操作分流到一个像这样的辅助类中能给你带来些好处。

  •   减少基类的函数数量。
  •   在辅助类中,代码通常更容易维护;
  •   减低基类和其它系统之间的耦合。

  基类如何获取所需状态

  你的基类通常希望封装一些数据以对子类保持隐藏。比如,我们想在系统中添加一些例子特效,那我们如何把粒子系统对象传递给基类了?

  •   把它传递给基类的构造函数

  像这样:

class Superpower
{
 public:
    Superpower(ParticleSystem* particles):particles_(particles)
    {}

private:
    ParticleSystem* particles_;
};

  这样虽然解决了问题,但同时带来了另一个问题。就是每个继承类都需要一个构造函数来调用基类的构造函数并传递那个粒子系统参数。这样就向每个子类暴露了一些我们并不希望暴露的状态。而且,这样也存在维护负担。如果后面我们添加另一个状态,那么我们不得不修改每个继承类的构造函数来传递它。

  •   进行分段初始化

  为了避免通过构造函数传递所有的东西,我们可以把初始化拆分为两个步骤。构造函数不带参数仅仅负责创造对象,然后我们通过一个直接定义在基类中的函数来传递它所需要的其它数据。比如像这样:

Superpower* power = new SkyLaunch();
power->init(particles);

  这里可能发生的就是我们忘记调用init函数,那样我们就只能得到构造了一半的对象。对于这个问题可以通过封装一个方法来解决。

Superpower* createSkyLaunch(ParticleSystem *particles)
{
    Superpower* power = new SkyLaunch();
    powwer->init(particles);
    return power;
}

  如果你想控制只能使用这个函数来创建SkyLaunch对象,可把SkyLaunch的构造函数声明为私有的来实现,类似单例模式的实现方式。

  •   将类静态化

  我们可以把我们不想暴露的状态声明为基类的私有成员,同时也是静态的,游戏将不得不保证初始化这个状态,但它仅需要整个游戏初始化一次,只要保证尽早调用初始化函数即可。这中做法还带来一个好处就是因为是静态变量,所有实例共用一个,所以占用的内存更少。

  •   使用服务定位器

  前面的方法严格要求外部代码必须在基类使用相关状态之前将这些状态传递给基类,这给周围的代码的初始化工作带来了负担。另外一个选择是让基类把它们需要的状态拉进去处理。一个实现方法是使用服务定位器。

class Superpower
{
 protected:
    void spawnParticles(ParticleType type,int count)
    {
        ParticleSystem& particles = Locator::getParticles();
        particles.spawn(type,count);
    }
};

  这里spawenParticles需要一个粒子系统。它从服务定位器获取了一个,而不是由外部代码主动提供。

  

  

原文地址:https://www.cnblogs.com/xin-lover/p/11576081.html

时间: 2024-11-10 17:06:42

游戏编程模式-子类沙盒的相关文章

游戏开发设计模式之子类沙盒模式(unity3d 示例实现)

积累提供所有操作(的实现)来定义子类的行为用一个最简单的例子来讲解这个模式玩家操纵的英雄也就是这个游戏的主角会有许多技能,我们想定义许多不同的技能,来让玩家使用.首 先我们定义一个skillBase类作为基类,我们所有技能的动作都在这里实现.我们可以从这些基本元动作中组合出各种各样的技能,甚至成百上千种,可以 设计一个doc文档来设计各种技能的操作,及操作顺序.这就是之所以为什么叫子类沙盒的原因,把实现技能的方法作为沙盒,向这个沙盒里加入各种各样的元动 作来组成各种各样的技能.以传说系列的凤凰天

Game Programming Patterns(游戏编程模式)

Game Programming Patterns(游戏编程模式) 大部分游戏开发者在他们游戏项目上总是一个巨大的挑战,总是东拼西凑,修修补补.很多游戏项目常常以失败告终,或者是被淹没在复杂而繁琐的代码中.如何解决这些问题? 各位看官,不管你是对游戏开发感兴趣,或者正在饱受代码不断增长带来的灾难,这本书将是你们的福音! 这本Game Programming Patterns 是由Bob Nystrom(一位在EA待过7年,有着20年游戏开发经历的工程师编写).本书将告诉你,什么模式能够帮你理清和

【游戏设计模式】之四 《游戏编程模式》读书笔记:全书内容梗概总结

本系列文章由@浅墨_毛星云 出品,转载请注明出处.   文章链接:http://blog.csdn.net/poem_qianmo/article/details/53240330 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 本文的Github版本:QianMo/Reading-Notes/<游戏编程模式>读书笔记 这是一篇超过万字读书笔记,总结了<游戏编程模式>一书中所有章节与内容的知识梗概. 我们知道,游戏行业其实一直很缺一本系

《游戏编程模式》(5)

Chatper 11 字节码 通过将行为编码成虚拟机指令,而使其具备数据的灵活性. 解释器模式(慢): 1 class Expression 2 { 3 4 public: 5 virtual ~Expression() {} 6 virtual double evaluate() = 0; 7 8 }; 9 10 class NumberExpression : public Expression 11 { 12 13 public: 14 NumberExpression(double va

《游戏编程模式》记录

写在前面 这本书长这样 我还没有看过“GOF”,我所读到的设计模式都是这本书(游戏角度)给出的定义,害怕GOF中的定义过于抽象. 没有在项目代码晃来晃去经历的,或者没有工作至少半年的,不用着急买这本书,因为估计看不懂. 本文用来重点记录“我觉得XX设计模式是什么”,以及“当我在看XX设计模式时,我在想什么” 这本书的写作方式,属于我喜欢的“谈话口吻” 命令模式 书面定义:“将一个请求封装成一个对象,从而允许你使用不同的请求.队列或日志将客户端参数化,同时支持请求操作的撤销和恢复” 他的举例:按键

Game Programming Patterns(游戏编程模式)-简介

游戏编程模式-简介 本系列博客是:Game Programming Patterns 的中文翻译版本. 翻译的github地址: cyh24. 如有兴趣,可联系博主共同翻译,一起造(wu)福(dao)他人. 博客虽然水分很足,但是也算是博主的苦劳了, 如需转载,请附上本文链接http://blog.csdn.net/cyh_24/article/details/46868419,不甚感激! 本系列博客 目录,可点击进入. 简介 ============================ 在我五年级

《游戏编程模式》(8)

<游戏编程模式>最后一篇,刚从英国玩了一圈,春节又要到啦 Chapter 19 对象池 使用固定的对象池重用对象,取代单独地分配和释放对象,达到提升性能和优化内存使用的目的. 使用情境: 频繁创建销毁对象: 对象大小基本一致: 堆上分配内存较慢或可能产生内存碎片: 粒子类: 用union节省内存:粒子使用时用live结构体,不使用时用next指针 1 class Particle 2 { 3 4 public: 5 Particle() 6 : framesLeft_(0) 7 {} 8 9

Game Programming Patterns(游戏编程模式)-架构,性能与游戏

游戏编程模式- 架构,性能与游戏 本系列博客是:Game Programming Patterns 的中文翻译版本. 翻译的github地址: cyh24. 如有兴趣,可联系博主共同翻译,一起造(wu)福(dao)他人. 博客虽然水分很足,但是也算是博主的苦劳了, 如需转载,请附上本文链接,不甚感激! 本系列博客 <游戏编程模式>– 目录,可点击进入. 架构,性能与游戏 ============================ 在我们埋头研究一堆的设计模式之前,我想先告诉你,对于软件架构,我个

游戏编程模式--单例模式

单例模式 定义:确保一个类只有一个实例,并为其提供一个全局的访问入口. 那么什么情况下使用单例?最常见的情况就是一个类需要与一个维持自身状态的外部系统进行交互,比如说打印机.大多数情况下都是多人共用一个打印机,这意味着可能由多个人同时向这个打印机发送打印任务,这个时候管理打印机的类就必须熟悉打印机的当前状态并协调这些任务的执行.这个时候就不允许存在多个打印机的实例,因为实例无法知道其他的实例所做的操作,也就无法进行整体的管理. 我们先看看最常见的单例的实现方式: class FileSystem