原型模式 Prototype Pattern 《游戏编程模式》学习笔记

原型的定义

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

举个例子

假设我现在要做一款游戏,这个游戏里有许多不同种类的怪物,鬼魂,恶魔和巫师。这些怪物通过"生产者"进入这片区域,每种敌人有不同的生产者。

假设每种怪物都有不同的类,同时他们都继承怪兽这个基类,那么我们的代码就会是这样

cpp 复制代码
class Monster
{
  // 代码......
};

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

为了能够产生这些怪物,我们需要不同的生产者类,这些类都继承spawner这个基类,那么我们就得写如下的代码:

cpp 复制代码
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();
  }
};


// 你知道思路了......

那么这么一来,我们的架构就类似于这样:

每个怪物类都有生产者类,得到平行的类结构

已经闻到臭味了......你的代码里有一堆特定的spawner,将来要是有一个新怪物,你就得多些一堆代码。 众多类,众多引用,众多冗余,众多副本,众多重复自我......

那么这个时候,原型模式就可以派上用场了! 我们来看看实现

原型模式提供了一个解决方案。 关键思路是一个对象可以产出与它自己相近的对象。 如果你有一个恶灵,你可以制造更多恶灵。 如果你有一个恶魔,你可以制造其他恶魔。 任何怪物都可以被视为原型怪物,产出其他版本的自己。

我们给基类Monster增加一个Clone()的方法

cpp 复制代码
class Monster
{
public:
  virtual ~Monster() {}
  virtual Monster* clone() = 0;

  // 其他代码......
};
每个怪兽子类提供一个特定实现,返回与它自己的类和状态都完全一样的新对象。就像这样
class Ghost : public Monster {
public:
  Ghost(int health, int speed)
  : health_(health),
    speed_(speed)
  {}

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

private:
  int health_;
  int speed_;
};

然后我们就不需要那么多特定的spawner了,我们只需要一个spawner()如下:

cpp 复制代码
class Spawner
{
public:
  Spawner(Monster* prototype)
  : prototype_(prototype)
  {}

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

private:
  Monster* prototype_;
};

可以看到这个Spawner内部有一个Monster*类型的prototype,这就是原型,一个隐藏的怪物, 它唯一的任务就是被生产者当做模板,去产生更多一样的怪物, 有点像一个从来不离开巢穴的蜂后。

当你要使用的时候,你只需要这么做:

cpp 复制代码
Monster* ghostPrototype = new Ghost(15, 3);
Spawner* ghostSpawner = new Spawner(ghostPrototype);

这个模式的灵巧之处在于它不但拷贝原型的类,也拷贝它的状态。 这就意味着我们可以创建一个生产者,生产快速鬼魂,虚弱鬼魂,慢速鬼魂,而只需创建一个合适的原型鬼魂。

如果你还是觉得麻烦,懒得写Clone()方法的话,这里还有一种思路,使用生产函数来代替生产者类,我们这么写一个生产函数:

cpp 复制代码
Monster* spawnGhost()
{
  return new Ghost();
}

这比构建怪兽生产者类更简洁。生产者类只需简单地存储一个函数指针:

cpp 复制代码
typedef Monster* (*SpawnCallback)();

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

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

private:
  SpawnCallback spawn_;
};

而你调用的时候,就这样写就行:

cpp 复制代码
Spawner* ghostSpawner = new Spawner(spawnGhost);

原型还可以做什么?

原型模式不仅仅可以应用在代码之中,我们还可以将其用在一些别的地方,比如数据存储中。

再举个例子,我们在游戏中经常使用Json来存储一些数据,比如怪物的各种属性。

所以游戏中的哥布林也许被定义为像这样的东西:

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

接下来,如果策划和你说,我还要别的哥布林,例如哥布林巫师,哥布林弓箭手,即使他们的很多属性都是一样的,我们还是不得不这么写:

cpp 复制代码
{
  "name": "goblin wizard",
  "minHealth": 20,
  "maxHealth": 30,
  "resists": ["cold", "poison"],
  "weaknesses": ["fire", "light"],
  "spells": ["fire ball", "lightning bolt"]
}

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

太重复了,我们讨厌重复,太多重复的数据意味着我们要话更多的时间去维护和管理,这是我们都不想看到的。

如果这是代码,我们会为"哥布林"构建抽象,并在三个哥布林类型中重用。 但是无能的JSON没法这么做。所以让我们把它做得更加巧妙些。

我们可以为对象添加"prototype"字段,记录委托对象的名字。 如果在此对象内没找到一个字段,那就去委托对象中查找。

这样,我们可以简化我们的哥布林JSON内容:

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

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

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

这样不就好多了?只需在游戏引擎上多花点时间,你就能让设计者更加方便地添加不同的武器和怪物,而增加的这些丰富度能够取悦玩家。

这一节的内容有好多关于原型模式的思想方面的介绍,作者还介绍了原型模式在编程语言方面的应用,我认为这些都是暂时对游戏编程帮助不大,所以没有记录下来,有兴趣的同学请翻阅原文。

原文链接:https://gpp.tkchu.me/prototype.html

相关推荐
晨非辰1 小时前
#C语言——刷题攻略:牛客编程入门训练(十一):攻克 循环控制(三),轻松拿捏!
c语言·开发语言·经验分享·学习·visual studio
xiaoxiaoxiaolll2 小时前
期刊速递 | 《Light Sci. Appl.》超宽带光热电机理研究,推动碳纳米管传感器在制药质控中的实际应用
人工智能·学习
励志码农3 小时前
JavaWeb 30 天入门:第二十三天 —— 监听器(Listener)
java·开发语言·spring boot·学习·servlet
DisonTangor3 小时前
字节开源 OneReward: 通过多任务人类偏好学习实现统一掩模引导的图像生成
学习·ai作画·开源·aigc
黎宇幻生4 小时前
Java全栈学习笔记33
java·笔记·学习
2501_926227945 小时前
.Net程序员就业现状以及学习路线图(五)
学习·.net
siy23339 小时前
[c语言日记] 数组的一种死法和两种用法
c语言·开发语言·笔记·学习·链表
在路上`11 小时前
前端学习之后端java小白(三)-sql外键约束一对多
java·前端·学习
尚久龙11 小时前
安卓学习 之 用户登录界面的简单实现
android·运维·服务器·学习·手机·android studio·安卓
yb0os111 小时前
RPC实战和核心原理学习(一)----基础
java·开发语言·网络·数据结构·学习·计算机·rpc