解析设计模式中Prototype原型模式在游戏中的应用

发表于2017-05-19
评论1 711浏览

本篇文章和大家介绍一下关于Prototype原型设计模式在游戏中的应用,先举个例子,假设我们为游戏中的每一种怪物都有不同的类 - 鬼,恶魔,巫师等,如:

  1. class Monster  
  2. {  
  3.   // Stuff...  
  4. };  
  5.   
  6. class Ghost : public Monster {};  
  7. class Demon : public Monster {};  
  8. class Sorcerer : public Monster {};  

一个spawner构造一个特定怪物类型的实例。 为了满足游戏中的每一个怪物,我们可以通过为每个怪物类提供一个spawner类来管理它,从而可以并行类层次结构如下所示:

实现它将如下所示:

  1. class Spawner  
  2. {  
  3. public:  
  4.   virtual ~Spawner() {}  
  5.   virtual Monster* spawnMonster() = 0;  
  6. };  
  7.   
  8. class GhostSpawner : public Spawner  
  9. {  
  10. public:  
  11.   virtual Monster* spawnMonster()  
  12.   {  
  13.     return new Ghost();  
  14.   }  
  15. };  
  16.   
  17. class DemonSpawner : public Spawner  
  18. {  
  19. public:  
  20.   virtual Monster* spawnMonster()  
  21.   {  
  22.     return new Demon();  
  23.   }  
  24. };  

这显然不是一个好设计方法。。。。。。

Prototype模式提供了一个解决方案 关键的问题是,一个对象可以产生类似于自己的其他对象 如果你有一个幽灵,你可以从中制造更多的鬼魂 如果你有一个恶魔,你可以制造其他恶魔 任何怪物都可以被视为用于生成其他版本的原型怪物。

为了实现这一点,我们给我们的基类Monster,一个抽象的clone()方法:

  1. class Monster  
  2. {  
  3. public:  
  4.   virtual ~Monster() {}  
  5.   virtual Monster* clone() = 0;  
  6.   
  7.   // Other stuff...  
  8. };  

每个怪物子类提供了一个实现,它返回一个在类和状态中与自己相同的新对象。 例如:

  1. class Ghost : public Monster {  
  2. public:  
  3.   Ghost(int health, int speed)  
  4.   : health_(health),  
  5.     speed_(speed)  
  6.   {}  
  7.   
  8.   virtual Monster* clone()  
  9.   {  
  10.     return new Ghost(health_, speed_);  
  11.   }  
  12.   
  13. private:  
  14.   int health_;  
  15.   int speed_;  
  16. };  

一旦我们所有的怪物都支持,我们不再需要每个怪物类的spawner类 相反,我们定义了一个:

  1. class Spawner  
  2. {  
  3. public:  
  4.   Spawner(Monster* prototype)  
  5.   : prototype_(prototype)  
  6.   {}  
  7.   
  8.   Monster* spawnMonster()  
  9.   {  
  10.     return prototype_->clone();  
  11.   }  
  12.   
  13. private:  
  14.   Monster* prototype_;  
  15. };  

效果如下所示:


接着上面的代码,要创建一个ghost spawner,我们创建一个原型的ghost实例,然后创建一个持有该原型的spawner:

  1. Monster* ghostPrototype = new Ghost(15, 3);  
  2. Spawner* ghostSpawner = new Spawner(ghostPrototype);  


关于这种模式的一个简单的部分是它不只是克隆原型的类,它也克隆了它的状态,我发现这种模式既优雅又令人惊讶, 我无法想象自己能想出来。

下面介绍它们如何应用:

那么,我们不必为每个怪物创建一个单独的spawner类,所以这很好。 但是我们必须在每个怪物类中实现clone()。

即使我们每个怪物都有不同的, 而不是为每个怪物制作单独的spawner类,我们可以做出生成函数,像这样:

  1. Monster* spawnGhost()  
  2. {  
  3.   return new Ghost();  
  4. }  

一个spawner类可以简单地存储一个函数指针:

  1. typedef Monster* (*SpawnCallback)();  
  2.   
  3. class Spawner  
  4. {  
  5. public:  
  6.   Spawner(SpawnCallback spawn)  
  7.   : spawn_(spawn)  
  8.   {}  
  9.   
  10.   Monster* spawnMonster()  
  11.   {  
  12.     return spawn_();  
  13.   }  
  14.   
  15. private:  
  16.   SpawnCallback spawn_;  
  17. };  

为了创造一个Ghosts,你可以做:

  1. Spawner* ghostSpawner = new Spawner(spawnGhost);  

现在,大多数C ++开发人员都熟悉模板。 我们的spawner类需要构造一些类型的实例,但是我们不想硬编码一些特定的怪物类。 然后,自然的解决方案是使它成为一个类型参数,这些模板让我们做:

  1. class Spawner  
  2. {  
  3. public:  
  4.   virtual ~Spawner() {}  
  5.   virtual Monster* spawnMonster() = 0;  
  6. };  
  7.   
  8. template <class T>  
  9. class SpawnerFor : public Spawner  
  10. {  
  11. public:  
  12.   virtual Monster* spawnMonster() { return new T(); }  
  13. };  

使用它看起来像:

  1. Spawner* ghostSpawner = new SpawnerFor();  

前两个解决方案解决了需要一个类,Spawner,它由一个类型参数化。 在C ++中,类型通常不是类的首先, 如果您使用JavaScript,Python或Ruby等动态类型的语言,那么类可以传递给常规对象,您可以更直接地解决这些问题。

当你做一个spawner时,只需传递它应该构造的怪物类 - 代表怪物类的实际运行时对象,这很容易的。

有了所有这些选项,我真的不能说我发现一个案例,我觉得原型设计模式是最好的答案。 也许你的经验会有所不同,但现在让我们把它放在一边,谈论别的东西:原型作为一种语言范式。

许多人认为“面向对象编程”是“类”的代名词,但是一个相当不争议的做法是,OOP允许您定义将数据和代码捆绑在一起的“对象”。 与C和C语言等结构语言相比,OOP的定义特征是将状态和行为紧密结合在一起。

在纯粹的意义上,自己比面向对象的语言更加面向对象。 我们认为OOP是结婚的状态和行为,但语言与类实际上有一线分隔。考虑您最喜欢的基于类的语言的语义, 要访问对象上的某些状态,请查看实例本身的内存, 状态包含在实例中。

要调用一个方法,你可以查看实例的类,然后查找方法。 行为包含在类中 总是有一定程度的间接访问方法,这意味着字段和方法是不同的。

要查找任何东西,你只要看对象, 一个实例可以包含状态和行为, 您可以拥有一个完全独特的方法的单个对象。

如果这是自己所做的,那就很难使用了, 基于类的语言的继承尽管有其错误,但为您提供了重用多态代码并避免重复的有用机制。

要找到某个字段或调用某个对象的方法,我们首先查看对象本身, 如果有的话,我们完成了, 如果没有,我们看对象的父对象, 这只是一些其他对象的引用, 当我们未能在第一个对象上找到一个属性时,我们尝试其父对象及其父对象等等。 换句话说,失败的查找被委托给对象的父类。

父对象让我们重复使用多个对象的行为(和状态!),所以我们已经介绍了类的实用程序的一部分。 类的其他关键事项是给我们创建实例的方法。 当你需要一个新的东西,你可以做新的Thingamabob(),或任何你喜欢的语言的语法, 一个类是一个工厂的实例。

我非常兴奋地玩纯粹的基于原型的语言,但一旦开始运行,我发现了一个令人不快的事实:它只是没有那么有趣的程序。

也许这是因为我以前的经验是基于类的语言,所以我的思想已经被这个范例所影响。 但我的希望是,大多数人都喜欢定义好的“种类”。
看看有多少游戏具有明确的角色类别,以及精确的不同类型的敌人,物品和技能的名单,每个都有整齐的标签。
虽然原型是一个非常酷的范例,而我希望更多的人知道,我们大多数人并不是每天都在使用它们。 我看到的完全拥抱原型的代码对我来说很困难。

好的,如果基于原型的语言是如此不友好,我该如何解释JavaScript? 这是每天有数百万人使用原型的语言 更多的电脑运行JavaScript比地球上的任何其他语言。
JavaScript的创始人Brendan Eich直接获得灵感,许多JavaScript的语义都是基于原型的。 每个对象都可以有任意的属性集合,包括字段和“方法”(这些都只是存储为字段的函数)。 一个对象也可以有一个称为它的“prototype”的对象,如果一个字段访问失败,它就被委派给它。

但是,尽管如此,我相信JavaScript在实践中与基于类的语言比与原型语言更相似基于原型的语言,克隆的核心操作是无处可见的。
在JavaScript中没有克隆对象的方法。 它最接近的是Object.create(),它允许您创建一个新对象,该对象将委托给现有对象。 即使没有添加到ECMAScript 5之前,JavaScript出现十四年之后
而不是克隆,让我来介绍你定义类型的典型方式,并用JavaScript创建对象 从一个构造函数开始:

  1. function Weapon(range, damage) {  
  2.   this.range = range;  
  3.   this.damage = damage;  
  4. }  

这将创建一个新对象并初始化其字段。 调用方式如下所示:

  1. var sword = new Weapon(10, 16);  

这里使用这个绑定到一个新的空对象来调用Weapon()函数,它 为其添加了一堆字段,然后自动返回填写的对象。
也为你做
另一件事 当它创建该空白对象时,将其连接起来以委托给一个原型对象您可以直接使用Weapon.prototype获取该对象。
当状态被添加到构造函数体中时,为了定义行为,你通常会向原型对象添加方法
,如下所示:

  1. Weapon.prototype.attack = function(target) {  
  2.   if (distanceTo(target) > this.range) {  
  3.     console.log("Out of range!");  
  4.   } else {  
  5.     target.health -= this.damage;  
  6.   }  
  7. }  


这将攻击属性添加到武器原型,其值是一个函数。 由于新的Weapon()返回的每个对象都会委托给Weapon.prototype,所以现在可以调用sword.attack()并调用该函数。 看起来有点像这样:

   我们回顾一下:
创建对象的方式是通过使用表示类型的对象(构造函数)调用的“新”操作状态存储在实例本身上。
行为通过一个间接级别 - 委托给原型 - 并存储在一个单独的对象上,该对象表示某种类型的所有对象共享的一组方法。
您可以使用JavaScript编写原型样式的代码(无需克隆),但语言的语法和习语鼓励了基于类的方法。
就个人而言,我认为这是件好事
就像我说的那样,我发现原型的加倍使得代码更难处理,所以我喜欢这种JavaScript将核心语义包含在更加优雅的东西中。

当你的游戏数据达到一定的大小时,你真的开始想要类似的功能。 数据建模是一个深刻的问题,我不能希望在这里做,但我想抛出一个功能,让您在自己的游戏中考虑:使用原型和委托来重用数据。

在游戏中可能会定义如下:

  1. {  
  2.   "name""goblin grunt",  
  3.   "minHealth": 20,  
  4.   "maxHealth": 30,  
  5.   "resists": ["cold""poison"],  
  6.   "weaknesses": ["fire""light"]  
  7. }  

再看下面的表内容如下所示:

  1. {  
  2.   "name""goblin wizard",  
  3.   "minHealth": 20,  
  4.   "maxHealth": 30,  
  5.   "resists": ["cold""poison"],  
  6.   "weaknesses": ["fire""light"],  
  7.   "spells": ["fire ball""lightning bolt"]  
  8. }  
  9.   
  10. {  
  11.   "name""goblin archer",  
  12.   "minHealth": 20,  
  13.   "maxHealth": 30,  
  14.   "resists": ["cold""poison"],  
  15.   "weaknesses": ["fire""light"],  
  16.   "attacks": ["short bow"]  
  17. }  

现在,如果这是代码, 这些实体之间有很多重复, 它浪费空间,花费更多的时间, 你必须仔细阅读,以确定数据是否相同。
如果这是代码,我们将为它创建一个抽象,并在三个类型中重用它, 但是,JSON不知道什么, 所以我们让它变得更聪明。
我们将声明,如果一个对象有一个“原型”字段,那么它定义了这个代表的另一个对象的名称, 第一个对象上不存在的任何属性都可以回溯到原型上。

为此,我们可以简化JSON配置文件如下

  1. {  
  2.   "name""goblin grunt",  
  3.   "minHealth": 20,  
  4.   "maxHealth": 30,  
  5.   "resists": ["cold""poison"],  
  6.   "weaknesses": ["fire""light"]  
  7. }  
  8.   
  9. {  
  10.   "name""goblin wizard",  
  11.   "prototype""goblin grunt",  
  12.   "spells": ["fire ball""lightning bolt"]  
  13. }  
  14.   
  15. {  
  16.   "name""goblin archer",  
  17.   "prototype""goblin grunt",  
  18.   "attacks": ["short bow"]  
  19. }  

在基于原型的系统中,任何对象都可以用作克隆来创建新的对象, 在游戏中经常有一次性的特殊实体的游戏中,数据特别适合。 这些通常是游戏中更常见的对象的改进,原型代理是很适合定义的。如下所示:

  1. {  
  2.   "name""Sword of Head-Detaching",  
  3.   "prototype""longsword",  
  4.   "damageBonus""20"  
  5. }  


作为游戏引擎的数据建模系统有一点额外的功能可以使设计人员更轻松地为装备游戏世界的武器和野兽添加许多小的变化,丰富性正是让玩家感到高兴的,这也是作为游戏引擎必不可少的部分。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论