游戏编程模式--原型模式

原型模式

  使用特定原型实力来创建特定种类的对象,并且通过拷贝原型来创建新的对象。

创建怪物

  设想我们正在做一款RPG游戏,游戏中场景充斥着大量的怪物,这些怪物随时准备抢夺主角的新鲜血肉。我们可以通过怪物生成器来生成怪物,且每一种怪物都有一个怪物生成器。比如我们要生成三种怪物:幽灵、恶魔、术士。我们分别设计了三个类:

class Monster
{
    //Stuff...
};

class Ghost:public Monster{};
class Demon:public Monster{};
class Sorcerer:public Monster{};

  同时,我们为其构建三个生成器:

class Spawner
{
public:
    virtual ~Spawner(){}
    virtual Monster* SpawnMonster()=0;    
};

class GhostSpawner:public Spawner
{
public:
    virtual Monster* SpawnMonster()
    {
        return new Ghost();
    }
};

class DemonSpawner:public Spawner
{
public:
    virtual Monster* SpawnMonster()
    {
        return new Demon();
    }
};

//other Spawner...

  这种方法显得太暴力了,里面包含了太多的重复代码、太多的类、太多的冗余。显然,这不是一个好的解决方案。这个时候让我们来看看原型模式是怎么解决这个问题的。

  原型模式的核心思想就是通过一个已有的对象生成一个与其近似的新对象。如果你有一个幽灵,则可以使用这个幽灵创建一个更多的幽灵。为了实现这个功能,我们给Monster类添加一个clone的方法:

class Monster
{
public:
    virtual ~Monster(){}
    virtual Monster* clone()=0;

    //other stuff...
};

  每个子类都会提供clone的特定实现,这个实现返回一个与自身类型和状态相同的新对象。例如:

class Ghost:public Monster
{
public:
    Ghost(int health,int speed)
    :health_(health),
    speed_(speed);
    {

    }

    Monster* clone()
    {
        return new Ghost(health_,speed_);
    }

private:
    int health_;
    int speed_;
};

  而一旦我们对所有的类都实现了clone接口,我们就不需要给每一个类都构建一个生成器类,相反只需要定义一个类,代码如下:

class Spawner
{
public:
    Spawner(Monster* prototype)
    :prototype_(prototype)
    {

    }

    Monster* SpawnMonster()
    {
        return prototype_->clone();
    }

private:
    Monster* prototype_;
};

  原型模式虽然很好的解决了怪物创建的问题,但与前面的方法相比,其实代码量也差不了太多。而且要为每一个类正确实现其clone的方法,也是一种挑战,这其中会由很多的陷阱,比如深拷贝和浅拷贝的问题。所以原型模式的效果,还需要你在项目中自己体会,老实说,我还无法找到一个场景,再这个场景下只有应用原型模式才是最佳解决方案。

生成器函数

  而对于怪物生成器,我们也可以不同定义类的方式来实现,而是通过定义孵化函数来实现:

Monster* SpawnGhost()
{
    return new Ghost();
}

typedef Monster* (*SpawnCallback)();

class Spawner
{
public:
    Spawner(SpawnCallback spawn)
    :spawn_(spawn)
    {

    }

    Monster* SpawnMonster()
    {
        return spawn_();
    }

private:
    SpawnCallback spawn_;
};

模板

  另外,我们也可以使用模板的方式来定义怪物类和生成器类,代码如下

class Spawner
{
public:
    virtual ~Spawner(){}
    virtual Monster* SpawnMonster()=0;    
};
template<typename T>
class SpawnerFor:public Spawner
{
public:
    virtual Monster* SpawnMonster(){
        return new T();
    }
};

  以上是原型模式的设计和应用,下面我们介绍一些与原型概念相关的其它应用。

原型语言范式

  许多人认为”面向对象编程“等同于”类“。面向对象的定义看起来是某个教派的信条一样,当然确实毫无争议的是OOP让你可以定义包含数据和方法的对象。让我们把结构化的C语言同函数式的Scheme相比,OOP的特征是它将状态和行为结合的更紧密。你可能认为”类“是实现这种方式的唯一方法。但也有一些人,像Dave Ungar和Randall Smith并不认为这样,它们在20世纪80年代的时候创造了一个叫Self的语言,非常的OOP,但没有类的概念。

Self

  拿你最拿手的基于类的语言来说,它们为了获取对象的某些状态,需要获取该对象在内存中的实例,状态被包含在实例当中。为了调用该实例的方法,你需要从类的声明中查找这个方法,然后再调用这个方法。实例的行为被包含在类中,总是由很多的方式可以间接调用一个方法,但同时也意味着属性和方法是不同的。

  但self语言不同。不管是查找方法还是域,你都是直接到对象中去找。一个实例可以包含状态和行为,你也可以直接构建一个只包含一个方法的对象。但如果这就是Self语言的全部,那将会难以使用。继承在基于类的语言里,除去它的一些缺点,还是一种非常有用的重用代码和消除重复代码的工具。Self语言没有类,但是它可以使用委托来完成类似的功能。

  为了查找一个对象的属性和方法,我们先在这个对象中查找,如果找到,则直接返回,否则在父类中继续查找,如果还是没有,则继续查找父类的父类,直到没有父类为止。也就是说,如果自身查找属性和方法失败,则委托父类进行查找。

  父类可以让我们在多个对象之间重用行为(甚至状态),但对类而言还欠缺一个重要的功能,就是以类为模板创建一些实例对象。但如果没有类,我们如何创建新事物了?特别是创建一系列具有相同行为的事物呢?在Self语言中我们可以使用clone。

  在Self语言中,每一个对象都自动支持原型模式,任意的随想都可以被克隆,如果你想要创建一系列相似的对象,可以这样:

  1.   从一个基础Object对象克隆一个新对象,然后向这个新对象中添加属性和方法;
  2.   然后再使用这个新对象克隆出更多的对象,只要内存够用,你想克隆多少就克隆多少。

  可以看出Self语言是设计的很酷,思路也很清晰,它提供了非常大的灵活性,但与之对应的是它把编程的复杂性留给了用户,这会让程序员觉得不开心。也许这是因为我们被OOP的思想所固化,但我的预感是大多数人只会喜欢定义清楚的事物。

  原型是一种很酷的编程范式,我希望由更多的人去了解它,虽然基于原型的代码看起来很怪异而且可读性不高。

原型数据建模

  如果你仔细观察,你会发现游戏中只有代码和数据,而且数据所占的比例一直在稳步增加。早期的游戏,会存储在磁盘和老游戏墨盒中,但是,今天的大部分游戏,代码仅仅是驱动游戏的引擎,游戏的玩法全部定义在数据中。但是把简单的内容都放到数据文件中并不能解决大项目难于组织的问题。我们使用编程语言的原因就是因为它可以管理复杂性。我们使用函数和类来避免重复代码,对于数据,我们也使用相似的特性。但数据建模是一个很宏大的主题,在这里我们不会详细展开。但我们可以抛砖引玉,让你可以在自己的游戏里面,使用原型和委托来重用数据。

  比如在上述的例子中,我们想为怪物设计其属性,并把它们存放与文件中,一般的做法是使用JSON数据实体,其实就是字典和属性的集合。那么,在游戏中,哥布林的属性可能会被定义成这样:

{
    "name":"goblin grunt",
    "minHealth":20,
    "maxHealth":30,
    "resists":["cold","poison"],
    "weaknesses":["fire","light"]
}

  数据简单易懂,就算是最讨厌文字的设计师也可以读懂它们,我们可以设计更多的哥布林类型:

{
    "name":"goblin wizard",
    "minHealth":20,
    "maxHealth":30,
    "resists":["cold","poison"],
    "weaknesses":["fire","light"],
    "spells":["fire ball","lighting bolt"]
}

{
    "name":"goblin ancher",
    "minHealth":20,
    "maxHealth":30,
    "resists":["cold","poison"],
    "weaknesses":["fire","light"],
    "attacks":["short bow"]
}

   可以看出,这些类型间由太多的重复数据,对于维护这些代码的人来说简直是噩梦。作为一个专业的程序员是很讨厌重复代码的,我们可以为“哥布林”创建一个抽象,然后再3个不同的哥布林类型间重用。做法是给对象定义一个“prototype”的属性,该属性指向另一个对象,如果访问的属性不在对象内部,那么回去它的原型对象中去找。使用这样的设计,我们可以把JSON代码简化为:

{
    "name":"goblin grunt",
    "minHealth":20,
    "maxHealth":30,
    "resists":["cold","poison"],
    "weaknesses":["fire","light"]
}

{
    "name":"goblin wizard",
    "prototype":"goblin grunt",
    "spells":["fire ball","lighting bolt"]
}

{
    "name":"goblin archer",
    "prototype":"goblin grunt",
    "attacks":["short bow"]
}

  可以看出,通过原型的引入,在定义新的“哥布林”的时候就不需要为每一个新类型定义重复的数据了,每当在对象里没有找到相应的属性时,则委托其原型继续查找,直到最顶层。

  在基于原型的系统中,任意对象都可以被用来克隆并创建出一个新的对象,我觉得数据建模也是这样。它特别适合于游戏里面的数据建模,在那里,你经常需要一系列特殊的游戏实体。

  考虑下boss和某些特殊的物品。它们经常是游戏里面某一种对象的重定义版本,而原型委托就是针对此问题的一个很好的解决方案。假设我们有一个物品,叫做“Sword of Head-Detaching",它是一柄长剑,但它是boss掉落的特殊的物品,附带了特殊的属性,那我们可以定义成这样:

{
    "name":"Sword of Head-Detaching",
    "prototype":"longsword",
    "damageBonus":"20"
}

  很自然,很强大。从这可以看出,你只需要一点额外的努力就可以在游戏中建立一个数据建模系统,这将会极大的方便游戏设计者们添加好玩的武器和怪物,为游戏带来更好的提样。

结语

  原型设计模式相比生成器模式(工厂模式),在代码量上相差不是很多,但避免了大量的类的产生。而使用c++模板,则可以大大的减少我们的编码量,具体如何使用,取决于项目的设计和需要。这里需要记住一句话:好的设计模式用在的错误的场景中,会使问题变得更糟糕。

  同时原型也是一种很酷的编程范式,给了我们另一个思考“面向对象”编程途径,与现在流行的“面向对象”编程互相印证,可以加深“面向对象”的理解。

  而数据建模则为游戏的设计带来的巨大的便利,通过引入原型,我们只需要一点额外的努力就可以建立一个数据建模系统,避免大量的重复代码,提供代码空间的利用率和可维护性。

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