[翻译]Game Programming Patterns Chapter4: Prototype

品味人生 • 发布于 2019-02-01 21:37:02

这个家伙很害羞,不想介绍自己!

我第一次听说“原型(Prototype)”这个词是在“设计模式”这本书里。现金好像每个人都在说起原型这个词,但实际上他们并不是在谈论设计模式。我们在这里的讨论将包含这些,并且我将展现给你一些其他更有趣的内容,比如“原型”这个术语本身与其出现后引出的概念。不过首先,让我们来回顾下原型模式。

    The first time I heard the word “prototype” was in Design Patterns. Today, it seems like everyone is saying it, but it turns out they aren’t talking about thedesign pattern. We’ll cover that here, but I’ll also show you other, more interesting places where the term “prototype” and the concepts behind it have popped up. But first, let’s revisit the original pattern.

The Prototype Design Pattern 原型设计模式


    我们假装自己正在开发一款《圣铠传说》类似的游戏。我们已经让各种妖魔鬼怪孵化在英雄的周围,争夺着英雄的新鲜血肉。这些令人厌恶的食客们通过“怪物孵化器”出现在场景区域,每一种不同的怪物孵化器产生一种怪物。


    Pretend we’re making a game in the style of Gauntlet. We’ve got creatures and fiends swarming around the hero, vying for their share of his flesh. These unsavory dinner companions enter the arena by way of “spawners”, and there is a different spawner for each kind of enemy.


   为了方便举例,我们就假设游戏中有“幽灵”,“恶魔”,“妖魔”等不同种类的怪物好了,如:


   For the sake of this example, let’s say we have different classes for each kind of monster in the game — Ghost, Demon, Sorcerer, etc., like:
class Monster
{
// Stuff...
};

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


怪物孵化器构造出某个特定的怪物类型的实例。为了支持游戏中的每一种怪物的生成,我们得很暴力地为每一种怪物创造出一个孵化器,这将导致一种扁平的类结构:

A spawner constructs instances of one particular monster type. To support every monster in the game, we could brute-force it by having a spawner class for each monster class, leading to a parallel class hierarchy:

Parallel class hierarchies. Ghost, Demon, and Sorceror all inherit from Monster. GhostSpawner, DemonSpawner, and SorcerorSpawner inherit from Spawner.


其实现方式如下:


Implementing it would look like this:

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();
}
};

// You get the idea...


除非你是按代码行数拿报酬的,不然这显然不是一个好的编程方式。一堆类,一堆样本代码,一堆冗余,一堆重复,不停地重复性劳动...

Unless you get paid by the line of code, this is obviously not a fun way to hack this together. Lots of classes, lots of boilerplate, lots of redundancy, lots of duplication, lots of repeating myself…


原型模式提供了一种解决方法。其核心思想是“一个对象能够产生出其他与其相仿的对象”。如果你有一个幽灵,你可以通过这个幽灵制作出更多的幽灵。如果你有一个魔鬼,你就能制作出其他魔鬼。如果怪物都能被看作是一个用来制作其自身的不同版本怪物的原型怪物。


The Prototype pattern offers a solution. The key idea is that an object can spawn other objects similar to itself. If you have one ghost, you can make more ghosts from it. If you have a demon, you can make other demons. Any monster can be treated as a prototypal monster used to generate other versions of itself.


为了实现这个模式,我们给出一个基类Monster, 包含一个虚方法clone():


To implement this, we give our base class, Monster, an abstract clone() method:
class Monster
{
public:
virtual ~Monster() {}
virtual Monster* clone() = 0;

// Other stuff...
};


Each monster subclass provides an implementation that returns a new object identical in class and state to itself. For example:
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_;
};


一旦所有的怪物都支持这些,我们就不再需要为每一个monster类创建spawner类了,相对的,我们只定义一个类:

Once all our monsters support that, we no longer need a spawner class for each monster class. Instead, we define a single one:

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

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

private:
Monster* prototype_;
};


其内部保存着一个Monster对象,这个隐藏对象的唯一目的就是而为了作为一个孵化器的模板来制作更多类似的怪物,有点像从不离开蜂巢的蜂后一样。

It internally holds a monster, a hidden one whose sole purpose is to be used by the spawner as a template to stamp out more monsters like it, sort of like a queen bee who never leaves the hive.


A Spawner contains a prototype field referencing a Monster. It calls clone() on the prototype to create new monsters.


为了创建一个幽灵的孵化器,我们创建一个幽灵的原型实例,然后再创建储存这个原型实例的孵化器。


To create a ghost spawner, we create a prototypal ghost instance and then create a spawner holding that prototype:

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

一个很酷的地方就是原型模式并不是克隆类的原型,他会将其状态也克隆出来。这意味着我们可以创造出各种孵化器,跑得飞快地幽灵,虚弱的幽灵,跑得慢的幽灵,只要通过创造出相应的原型幽灵即可。

One neat part about this pattern is that it doesn’t just clone the class of the prototype, it clones its state too. This means we could make a spawner for fast ghosts, weak ghosts, or slow ghosts just by creating an appropriate prototype ghost.


我发现这是这个模式优雅而令人惊讶的地方。我自己实在想不到这一点,但现在我实在无法想象我不知道这个特性会是怎么样。


I find something both elegant and yet surprising about this pattern. I can’t imagine coming up with it myself, but I can’t imagine not knowing about it now that I do.

How well does it work?  原型模式效果如何?


好吧,我们不需要再为了每一个怪物创建单独的孵化器了,这点不错。但是我们不得不为每一个怪物类实现clone()方法。这和实现孵化器需要的代码量其实差不多。

Well, we don’t have to create a separate spawner class for each monster, so that’s good. But we do have to implement clone() in each monster class. That’s just about as much code as the spawners.

当你真正开始去编写一个正确的clone()函数时,会发现一些很不爽的语法陷阱。是要做深度clone还是浅度克隆?换句话说,如果一个魔鬼正拿着一把叉子,那么克隆出来的魔鬼也要拿着叉子么?

There are also some nasty semantic ratholes when you sit down to try to write a correct clone(). Does it do a deep clone or shallow one? In other words, if a demon is holding a pitchfork, does cloning the demon clone the pitchfork too?

而且,不仅仅是这种针对这个描述的问题的实现并没有真正节省我们多少代码量,事实上,这个问题本身就是一个编造出来的问题。我们肯定得为每一个怪物使用单独的类。现今的游戏引擎一定不是这么玩的。

Also, not only does this not look like it’s saving us much code in this contrived problem, there’s the fact that it’s a contrived problem. We had to take as a given that we have separate classes for each monster. These days, that’s definitelynot the way most game engines roll.

我们大多数人都知道管控一个巨大的类结构体是件麻烦事,这也是为什么我们要使用组件(Component)和类型对象(Type Object)来对不同的实体进行建模,而不依赖于干涉其类本身。

Most of us learned the hard way that big class hierarchies like this are a pain to manage, which is why we instead use patterns like Component and TypeObject to model different kinds of entities without enshrining each in its own class.

Spawn functions(孵化器函数)


即使我们真的给不同的怪物创建了不同的类,仍然存在许多其他途径来处理这个麻烦的问题。相比于给每一个怪物分别创建孵化器类,我们也能创建孵化器函数,比如这样:


Even if we do have different classes for each monster, there are other ways to decorticate this Felis catus. Instead of making separate spawner classes for each monster, we could make spawn functions, like so:


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


这样做相较于搞出一个构造某种怪物的类来说,冗余更少。创建完后,一个孵化器类可以简单地存储这个函数指针即可:


This is less boilerplate than rolling a whole class for constructing a monster of some type. Then the one spawner class can simply store a function pointer:

typedef Monster* (*SpawnCallback)();

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

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

private:
SpawnCallback spawn_;
};


在创建幽灵孵化器的时候,就这么写:


To create a spawner for ghosts, you do:

Spawner* ghostSpawner = new Spawner(spawnGhost);

 

Templates(模板)


现在,大多数C++的开发者已经熟悉了使用模板开发。我们的孵化器类需要去构造某些类型的实例,但又不需要硬编码各个怪物类型。那么很自然的做法就是采用模板提供的类型参数:


By now, most C++ developers are familiar with templates. Our spawner class needs to construct instances of some type, but we don’t want to hard code some specific monster class. The natural solution then is to make it a type parameter, which templates let us do:

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

template <class T>
class SpawnerFor : public Spawner
{
public:
virtual Monster* spawnMonster() { return new T(); }
};


使用时形如:


Using it looks like:

Spawner* ghostSpawner = new SpawnerFor<Ghost>();

First-class types(一等类型)


前面两种解决方式着重处理的问题是去得到一个类型参数化的孵化器(Spawner)类。在C++中,类型基本上不被看做是第一等级,所以在使用时需要多出点体力活才能完成。如果你在使用一些动态类型的语言,比如JavaScript,Python,或者Ruby的话,这些语言中类也是一种常规的对象,可以用来传递,这样解决当前的问题就更加直接了。


The previous two solutions address the need to have a class, Spawner, which is parameterized by a type. In C++, types aren’t generally first-class, so that requires some gymnastics. If you’re using a dynamically-typed language like JavaScript, Python, or Ruby where classes are regular objects you can pass around, you can solve this much more directly.


当你创建孵化器时,把构造怪物的类直接传过去就行了,这个类就是表示怪物的一个运行时对象,超简单。

When you make a spawner, just pass in the class of monster that it should construct — the actual runtime object that represents the monster’s class. Easy as pie.

综上所述,我实在不能说我发现了一个对于原型设计模式来说针对性最强的案例。也许你的感觉不同,现在让我们先把这事放一边,来聊聊别的:把原型看做一种语法范式。

With all of these options, I honestly can’t say I’ve found a case where I felt the Prototype design pattern was the best answer. Maybe your experience will be different, but for now let’s put that away and talk about something else: prototypes as a language paradigm.

The Prototype Language Paradigm(原型语法范式)


许多人认为“面向对象编程”等同于“类”。OOP的定义让人感觉像是某个教派的信条一样,但确实毫无争议性地OOP能够让你定义一个将数据和编码打包在一块的“对象”。相比较C这样的结构化语言和Scheme这样的函数式语言,OOP的定义特性将行为和状态结合得更紧密。

Many people think “object-oriented programming” is synonymous with “classes”. Definitions of OOP tend to feel like credos of opposing religious denominations, but a fairly non-contentious take on it is that OOP lets you define “objects” which bundle data and code together. Compared to structured languages like C and functional languages like Scheme, the defining characteristic of OOP is that it tightly binds state and behavior together.

你也许认为类是打到这种紧密性的唯一方法,但也有一些人,包括Dave Ungar,Randall Smith不同意。他们在80年代创造了一种叫Self的语言,非常OOP,但没有类的概念。

You may think classes are the one and only way to do that, but a handful of guys including Dave Ungar and Randall Smith beg to differ. They created a language in the 80s called Self. While as OOP as can be, it has no classes.

Self


In a pure sense, Self is more object-oriented than a class-based language. We think of OOP as marrying state and behavior, but languages with classes actually have a line of separation between them.


Consider the semantics of your favorite class-based language. To access some state on an object, you look in the memory of the instance itself. State iscontained in the instance.