Skip to content

Latest commit

 

History

History
977 lines (746 loc) · 59.2 KB

05.1-Component.md

File metadata and controls

977 lines (746 loc) · 59.2 KB

组件模式

Intent 意图

Allow a single entity to span multiple domains without coupling the domains to each other.

允许一个单独的实体跨多个不同域而不耦合它们。

Motivation 动机

Let's say we're building a platformer. The Italian plumber demographic is covered, so ours will star a Danish baker, Bjørn. It stands to reason that we'll have a class representing our friendly pastry chef, and it will contain everything he does in the game.

举个例子,假设我们准备要制作一个平台类游戏。意大利水管工(译者注:作者指的是超级玛丽,超级玛丽是个水管工,在最初设计时被设定为了意大利人)已经有了,那我们设计一个丹麦面包师 Bjorn。显而易见,我们将设计一个能够很好的代表面包师的类,这个类包含了面包师的所有动作跟特性。

注:我之所以是个程序猿而非设计师就是因为我总想要去实现这些很棒的想法。

Since the player controls him, that means reading controller input and translating that input into motion. And, of course, he needs to interact with the level, so some physics and collision go in there. Once that's done, he's got to show up on screen, so toss in animation and rendering. He'll probably play some sounds too.

因为玩家控制他,这就意味着需要读取控制器的输入并且将输入转换成动作。当然,角色类还需要跟平台相互作用,所以还需要一些物理和碰撞方面的东西。当这些都完成了,角色通过动画和渲染就显示在屏幕上了。角色可能还会播放一些音效。

Hold on a minute; this is getting out of control. Software Architecture 101 tells us that different domains in a program should be kept isolated from each other. If we're making a word processor, the code that handles printing shouldn't be affected by the code that loads and saves documents. A game doesn't have the same domains as a business app, but the rule still applies.

且慢,事情似乎在往失控的方向发展。在第一章软件架构中我们曾经提到,一个程序中的不同域应该互相隔离。在我们设计一个文字处理器时,处理打印部分的代码不应该受到保存,读取文档的代码的任何影响。也许游戏的域与应用的域不完全相同,但这个道理是相通的。

As much as possible, we don't want AI, physics, rendering, sound and other domains to know about each other, but now we've got all of that crammed into one class. We've seen where this road leads to: a 5,000-line dumping ground source file so big that only the bravest ninja coders on your team even dare to go in there.

所以尽可能的,我们不应让AI,物理,渲染,声效已经其他域互相影响,但我们又必须将所有这些包含在一个类中。我们已经看到了,这个是一个代码量能达5000行以上的巨大的源文件,以至于只有最勇敢的程序员才胆敢去尝试。

This is great job security for the few who can tame it, but it's hell for the rest of us. A class that big means even the most seemingly trivial changes can have far-reaching implications. Soon, the class collects bugs faster than it collects features.

如此庞大的工作量对于那些能够驯服它的人来说这件很棒的事情,但是对无能为力的其余我们来说则如同地狱。一个如此大的类意味着即使最微不足道的修改都可能产生深远的影响。所以很快,在这个类中你将会得到比功能更多的错误。

The Gordian knot 难题

Even worse than the simple scale problem is the coupling one. All of the different systems in our game have been tied into a giant knotted ball of code like:

比简单规模问题更复杂的是耦合问题。我们游戏中所有不同的系统都被绑成一个像巨大的结球一样的代码,就像:

if (collidingWithFloor() && (getRenderState() != INVISIBLE))
{
  playSound(HIT_FLOOR);
}

Any programmer trying to make a change in code like that will need to know something about physics, graphics, and sound just to make sure they don't break anything.

任何试图想要修改以上代码的程序都必须要知道相关物理,图像以及声音的知识以避免破坏任何功能。

注:While coupling like this sucks in any game, it's even worse on modern games that use concurrency. On multi-core hardware, it's vital that code is running on multiple threads simultaneously. One common way to split a game across threads is along domain boundaries -- run AI on one core, sound on another, rendering on a third, etc.当这种耦合的设计在任何游戏中都是一种糟糕的设计,但是在使用并发性的现代游戏中尤其糟糕。代码是否能够运行在多个线程上对拥有多核的硬件来说至关重要。而一个常见的实现多线程并行设计的方法就是设置域隔阂,比如让AI计算在一个核中完成,声效在另外一核,渲染在第三个,以此类推。

Once you do that, it's critical that those domains stay decoupled in order to avoid deadlocks or other fiendish concurrency bugs. Having a single class with an UpdateSounds() method that must be called from one thread and a RenderGraphics() method that must be called from another is begging for those kinds of bugs to happen.而要实现以上所说的设置不同域之间的隔阂,最至关重要的就是让不同的域之间保持去耦来避免产生死锁以及其他致命的并发错误。一个单类中,尝试在一个线程上调用 UpdateSounds() 方法而在另一个线程上调用 RenderGraphics() 方法,这无疑就是自取灭亡。

These two problems compound each other; the class touches so many domains that every programmer will have to work on it, but it's so huge that doing so is a nightmare. If it gets bad enough, coders will start putting hacks in other parts of the codebase just to stay out of the hairball that this Bjorn class has become.这两个问题互相复合,一个包含了很多域的类将要求每个想要修改他的程序员做大量的工作,而这毫无疑问就是个噩梦。当代码变得足够糟糕,程序员都开始因为不想去理这团杂乱的毛团而开始放弃它。

Cutting the knot 解决难题

We can solve this like Alexander the Great -- with a sword. We'll take our monolithic Bjorn class and slice it into separate parts along domain boundaries. For example, we'll take all of the code for handling user input and move it into a separate InputComponent class. Bjorn will then own an instance of this component. We'll repeat this process for each of the domains that Bjorn touches.

想要解决这个问题,我们应该像执剑的亚历山大大帝一样。将独立的Bjorn 类依着不用的域切成相互独立的部分。举个例子,我们将所有用来处理用户输入的代码放到一个类中。而Bjorn将拥有这个类的一个实例。我们将循环对所有Bjorn类包含的领域做同样的工作。

When we're done, we'll have moved almost everything out of Bjorn. All that remains is a thin shell that binds the components together. We've solved our huge class problem by simply dividing it up into multiple smaller classes, but we've accomplished more than just that.

但我们完成这件工作后,我们几乎将Bjorn类中的所有东西都清除了。剩下的是一个将所有组件绑在一起的外壳。我们通过简单的将代码分割成小类的方式解决了这个复杂的巨大类问题。但是我们却又不仅仅只是解决了这个问题。

Loose ends 宽松的末端

Our component classes are now decoupled. Even though Bjorn has a PhysicsComponent and a GraphicsComponent, the two don't know about each other. This means the person working on physics can modify their component without needing to know anything about graphics and vice versa.

现在我们的内容类是去耦的了。尽管Bjorn类仍然有物理以及图像不同两块的内容,但是这两块内容互不干涉。这意味着想要修改物理块内容的程序员不再需要了解图像块的知识,反之亦然。

In practice, the components will need to have some interaction between themselves. For example, the AI component may need to tell the physics component where Bjørn is trying to go. However, we can restrict this to the components that do need to talk instead of just tossing them all in the same playpen together.

在实践中,这些组件需要互相之间有一些互动。例如,AI组件可以会告知物理组件 Bjorn将去哪里。然而,我们可以限制的只让组件之间进行交流而不是将他们全部放到一起。

Tying back together 捆绑在一起

Another feature of this design is that the components are now reusable packages. So far, we've focused on our baker, but let's consider a couple of other kinds of objects in our game world. Decorations are things in the world the player sees but doesn't interact with: bushes, debris and other visual detail. Props are like decorations but can be touched: boxes, boulders, and trees. Zones are the opposite of decorations -- invisible but interactive. They're useful for things like triggering a cutscene when Bjørn enters an area.

这个设计的另一种特性是可重用的组件包。到目前为此,我们都只是考虑了面包师一个角色,但游戏中可能出现的别的物品。例如灌木,碎片和其他的视觉细节等装饰是游戏中玩家能看到却不能交互的对象。而像盒子、巨石、树木等道具则是玩家可以与之交互的对象。分区则与装饰品正好相反——玩家看不到却能与之交互。上述这些对象将会在玩家的角色Bjorn进入一个区域时起作用。

注:When object-oriented programming first hit the scene, inheritance was the shiniest tool in its toolbox. It was considered the ultimate code-reuse hammer, and coders swung it often. Since then, we've learned the hard way that it's a heavy hammer indeed. Inheritance has its uses, but it's often too cumbersome for simple code reuse.当我们使用面对对象编程的时候,继承就是我们手上最有力的武器。它被认为是程序猿最喜欢用的终极武器。然而我们发现这个武器很多时候是吧双刃剑,继承有它的用途,但是对某些代码重用来说实现起来太麻烦了。

Instead, the growing trend in software design is to use composition instead of inheritance when possible. Instead of sharing code between two classes by having them inherit from the same class, we do so by having them both own an instance of the same class.相反,软件设计的趋势应该是尽可能的使用组合而不是继承。 我们应该让两个类拥有同一个类的实例而不是继承同一个类。

Now, consider how we'd set up an inheritance hierarchy for those classes if we weren't using components. A first pass might look like:

现在我们考虑如何在不用组件的情况下建立这些类的继承层次结构,它应该像:

We have a base GameObject class that has common stuff like position and orientation. Zone inherits from that and adds collision detection. Likewise, Decoration inherits from GameObject and adds rendering. Prop inherits from Zone, so it can reuse the collision code. However, Prop can't also inherit from Decoration to reuse the rendering code without running into the Deadly Diamond.

我们有一个基本游戏类,它包含像位置跟方向这种基本的元素。而区域继承了这个基本类并在其基础上加了碰撞。另外的,装饰也继承了基本类但是却添加了渲染。支柱继承区域,但是它重用了碰撞的代码。然后支柱不能继承装饰并重用渲染的代码,否则程序可能产生“致命的钻石”。

注:The "Deadly Diamond" occurs in class hierarchies with multiple inheritance where there are two different paths to the same base class. The pain that causes is a bit out of the scope of this book, but understand that they named it "deadly" for a reason.“致命的钻石”发生在对同一基类有不用路径的多重继承的类层次结构中。但是该错误的诱因不在这本书的讨论范畴内,但是请相信叫它致命的不是没有原因的。

We could flip things around so that Prop inherits from Decoration, but then we end up having to duplicate the collision code. Either way, there's no clean way to reuse the collision and rendering code between the classes that need it without resorting to multiple inheritance. The only other option is to push everything up into GameObject, but then Zone is wasting memory on rendering data it doesn't need and Decoration is doing the same with physics.

我们可以做些转变让支柱能够继承装饰类。但是我们将不能够复制碰撞的代码。无论如何,都没有办法不通过多重继承而在同一个类中重用碰撞跟渲染部分的代码。唯一的选择就是将这两段代码同时放到基本类中,然后这么做的结果就是区域类将会不需要渲染以及装饰类的代码而浪费了不少的内存。

Now, let's try it with components. Our subclasses disappear completely. Instead, we have a single GameObject class and two component classes: PhysicsComponent and GraphicsComponent. A decoration is simply a GameObject with a GraphicsComponent but no PhysicsComponent. A zone is the opposite, and a prop has both components. No code duplication, no multiple inheritance, and only three classes instead of four.

现在,让我们试着用组件的方法。所有的子类完全消失,取而代之的是一个简单的基本类和两个组件:物理组件以及图像组件。所以装饰类就是一个同时包含基本类和图像组件的类,而区域则恰恰相反,支柱则是同时包含这两个组件,没有代码重复,没有多重继承,只有简单的三个类而不是四个。

注:A restaurant menu is a good analogy. If each entity is a monolithic class, it's like you can only order combos. We need to have a separate class for each possible combination of features. To satisfy every customer, we would need dozens of combos.这好比一个餐厅的菜单,如果每个实体都是一个单独的类,那么也许你就只能点设定好的几个套餐。但是我们需要的一个能够结合不同特性的独立类。为了满足客户,我们可能需要数十个套餐。

Components are à la carte dining -- each customer can select just the dishes they want, and the menu is a list of the dishes they can choose from.而组件就像按着菜单点菜用餐,每个客户能够选择那些他们喜欢的菜,而菜单则是一个他们选择菜品的列表。

Components are basically plug-and-play for objects. They let us build complex entities with rich behavior by plugging different reusable component objects into sockets on the entity. Think software Voltron.

The Pattern 模式

A single entity spans multiple domains. To keep the domains isolated, the code for each is placed in its own component class. The entity is reduced to a simple container of components.

样式是一个跨多个域的单一实体。为了能够保持域之间相互隔离,每个域的代码都独立的放在自己的组件类中。实体则可以减少到一个的组件的容器。

注:"Component", like "Object", is one of those words that means everything and nothing in programming. Because of that, it's been used to describe a few concepts. In business software, there's a "Component" design pattern that describes decoupled services that communicate over the web.“组件”就如同“对象”,它即代表所有事情,但是也不代表任何事情。就因为如此,它用来描述一些概念。在商业软件中,有一种“组件”设计模式,它描述了解耦服务,并能够通过网络进行通讯。

I tried to find a different name for this unrelated pattern found in games, but "Component" seems to be the most common term for it. Since design patterns are about documenting existing practices, I don't have the luxury of coining a new term. So, following in the footsteps of XNA, Delta3D, and others, "Component" it is.我试图找到一个不同的名字来跟本文所说的游戏中的样式进行区别,但是“组件”仍然是最合适的名称。既然设计模式是用于记录已经存在的的东西,我也没有那个荣幸能够创造一个新的术语。所以“组件”就随XNA,Delta3D之后,变成它现在的这个含义。

When to Use It

Components are most commonly found within the core class that defines the entities in a game, but they may be useful in other places as well. This pattern can be put to good use when any of these are true:

组件一个最普遍的用法是在核心类中定义了一个游戏的实体,但是它们也能够用在别的地方。当如下条件成立时,样式就能够发挥它的作用: 你有一个涉及到多个域的类,但是你想保持互相解耦。

  • You have a class that touches multiple domains which you want to keep decoupled from each other.你有一个涉及到多个域的类,但是你想保持互相解耦。
  • A class is getting massive and hard to work with.一个类越来越庞大,很难处理。
  • You want to be able to define a variety of objects that share different capabilities, but using inheritance doesn't let you pick the parts you want to reuse precisely enough.你希望定义能够共用不同功能的不同的类但却不通过继承来精确的重用代码。

Keep in Mind 注意事项

The Component pattern adds a good bit of complexity over simply making a class and putting code in it. Each conceptual "object" becomes a cluster of objects that must be instantiated, initialized, and correctly wired together. Communication between the different components becomes more challenging, and controlling how they occupy memory is more complex.

组件模式每次通过简单添加一点点的复杂的代码的方法来构成一个类。每个概念上的“对象”成为一个集群是必须被实例化,初始化以及正确地连接在一起。不同组件之间的通讯变得更具挑战性,并且限制它们占用内存将更加复杂。

For a large codebase, this complexity may be worth it for the decoupling and code reuse it enables, but take care to ensure you aren't over-engineering a "solution" to a non-existent problem before applying this pattern.

对于一个大型代码库,它的复杂性会让解耦以及代码重用变得有价值,但是请注意你没有在不存在问题的代码库中过度设计而使用这样一个“解决方案”。

Another consequence of using components is that you often have to hop through a level of indirection to get anything done. Given the container object, first you have to get the component you want, then you can do what you need. In performance-critical inner loops, this pointer following may lead to poor performance.

使用组件的另外一个后果是如果你需要经常跳过一个间接层来处理任何事情,考虑到容器对象,首先你必须得到你需要的组件,然后你才可以做你需要做的事情,在一些性能要求较高的代码中,这个指针可能会导致低劣的性能。

注:There's a flip side to this coin. The Component pattern can often improve performance and cache coherence. Components make it easier to use the Data Locality pattern to organize your data in the order that the CPU wants it.就如硬币有正反面,而这就如同硬币的另外一面。组件模式通常能够提高性能和缓存一致性。组件让使用数据本地化模型来组织CPU所需要的数据顺序变得简单。

Sample Code 范例代码

One of the biggest challenges for me in writing this book is figuring out how to isolate each pattern. Many design patterns exist to contain code that itself isn't part of the pattern. In order to distill the pattern down to its essence, I try to cut as much of that out as possible, but at some point it becomes a bit like explaining how to organize a closet without showing any clothes.

写这本书对我来说最大的挑战是找到如何隔离每个模式的方法。许多设计模式都包含了不属于与本模式无关的代码。为了提取模式的精华,我试着尽可能的简化,但是这就变得有点像是在展示一个没有任何衣服的衣柜

The Component pattern is a particularly hard one. You can't get a real feel for it without seeing some code for each of the domains that it decouples, so I'll have to sketch in a bit more of Bjørn's code than I'd like. The pattern is really only the component classes themselves, but the code in them should help clarify what the classes are for. It's fake code -- it calls into other classes that aren't presented here -- but it should give you an idea of what we're going for.

而组件模式则尤其困难。如果你没有见过任何解耦的代码,你将可能对组件的设计模式毫无头绪。所以我在我们Bjorn的代码上扩展开来向你们描述下。模式的确是只有组件本身,但是这其中的代码应该能够解释这个类。它是一段伪代码,它调用了其它不属于这里的类,但是它应该能够让你明白我们正在干什么。

A monolithic class 一个单类

To get a clearer picture of how this pattern is applied, we'll start by showing a monolithic Bjorn class that does everything we need but doesn't use this pattern:

以便更清楚的了解这种模式是如何应用的,我们将从一个完成所有事情但是不使用组件模式的巨大单类Bjorn开始。

注:I should point out that using the actual name of the character in the codebase is usually a bad idea. The marketing department has an annoying habit of demanding name changes days before you ship. "Focus tests show males between 11 and 15 respond negatively to ‘Bjørn’. Use ‘Sven’ instead."我应该指出,使用实际名称中的字符代码库通常都是一个糟糕的想法。市场部有一个恼人的习惯就是要求你在发布应用前修改名字。“专注力测试的结果表示11到15岁之间的男生对Bjorn反响平平,请改为Sven。”

This is why many software projects use internal-only codenames. Well, that and because it's more fun to tell people you're working on "Big Electric Cat" than just "the next version of Photoshop."这也是为什么许多软件项目使用只面向内部的代号。因为告诉别人你正在开发一个“大电力猫”的程序比“新版本的Photoshop”要有趣多了。

class Bjorn
{
public:
  Bjorn()
  : velocity_(0),
    x_(0), y_(0)
  {}

  void update(World& world, Graphics& graphics);

private:
  static const int WALK_ACCELERATION = 1;

  int velocity_;
  int x_, y_;

  Volume volume_;

  Sprite spriteStand_;
  Sprite spriteWalkLeft_;
  Sprite spriteWalkRight_;
};

Bjorn has an update() method that gets called once per frame by the game:

Bjorn中有个update()方法来调用游戏中的每一帧:

void Bjorn::update(World& world, Graphics& graphics)
{
  // Apply user input to hero's velocity.
  switch (Controller::getJoystickDirection())
  {
    case DIR_LEFT:
    velocity_ -= WALK_ACCELERATION;
    break;

    case DIR_RIGHT:
    velocity_ += WALK_ACCELERATION;
    break;
  }

  // Modify position by velocity.
  x_ += velocity_;
  world.resolveCollision(volume_, x_, y_, velocity_);

  // Draw the appropriate sprite.
  Sprite* sprite = &spriteStand_;
  if (velocity_ < 0)
  {
    sprite = &spriteWalkLeft_;
  }
  else if (velocity_ > 0)
  {
    sprite = &spriteWalkRight_;
  }

  graphics.draw(*sprite, x_, y_);
}

It reads the joystick to determine how to accelerate the baker. Then it resolves its new position with the physics engine. Finally, it draws Bjørn onto the screen.它通过读取操纵杆来决定如何加速面包师。然后通过物理引擎来解决新位置的问题。最后将面包师显示到屏幕上。

The sample implementation here is trivially simple. There's no gravity, animation, or any of the dozens of other details that make a character fun to play. Even so, we can see that we've got a single function that several different coders on our team will probably have to spend time in, and it's starting to get a bit messy. Imagine this scaled up to a thousand lines and you can get an idea of how painful it can become.这个示例实现非常简单。没有重力,动画或者其他几十个能够让游戏变得有趣的细节。但即便如此,我们可以看到,我们有一个单一的函数让团队中不同的程序员都得花点时间,而且也开始有点混乱。试想下如果代码扩展到一千行这将会是多么痛苦的一件事情。

Splitting out a domain 分割域

Starting with one domain, let's pull a piece out of Bjorn and push it into a separate component class. We'll start with the first domain that gets processed: input. The first thing Bjorn does is read in user input and adjust his velocity based on it. Let's move that logic out into a separate class:从一个域开始,我们将一部分的Bjorn代码抽离出来并将它封装到一个独立的组件类中。我们首先从输入这个域开始。Bjorn类做的第一件事情就是读入用户的输入并相应的调整速度。让我们将这个逻辑封装到一个独立的类中:

class InputComponent
{
public:
  void update(Bjorn& bjorn)
  {
    switch (Controller::getJoystickDirection())
    {
      case DIR_LEFT:
        bjorn.velocity -= WALK_ACCELERATION;
        break;

      case DIR_RIGHT:
        bjorn.velocity += WALK_ACCELERATION;
        break;
    }
  }

private:
  static const int WALK_ACCELERATION = 1;
};

Pretty simple. We've taken the first section of Bjorn’s update() method and put it into this class. The changes to Bjorn are also straightforward:非常简单,我们只需要将Bjorn类中update方法放到一个新的类中就好了,而更改Bjorn类也相当简单:

class Bjorn
{
public:
  int velocity;
  int x, y;

  void update(World& world, Graphics& graphics)
  {
    input_.update(*this);

    // Modify position by velocity.
    x += velocity;
    world.resolveCollision(volume_, x, y, velocity);

    // Draw the appropriate sprite.
    Sprite* sprite = &spriteStand_;
    if (velocity < 0)
    {
      sprite = &spriteWalkLeft_;
    }
    else if (velocity > 0)
    {
      sprite = &spriteWalkRight_;
    }

    graphics.draw(*sprite, x, y);
  }

private:
  InputComponent input_;

  Volume volume_;

  Sprite spriteStand_;
  Sprite spriteWalkLeft_;
  Sprite spriteWalkRight_;
};

Bjorn now owns an InputComponent object. Where before he was handling user input directly in the update() method, now he delegates to the component:现在Bjorn拥有一个输入组件类,之前它通过调用update方法来处理用户的输入,现在它只需代理组件即可:

input_.update(*this);

We've only started, but already we've gotten rid of some coupling -- the main Bjorn class no longer has any reference to Controller. This will come in handy later.我们仅仅才开始就已经摆脱了一些耦合——我们将逐步使得核心Bjorn类不再涉及到任何控制器。

Splitting out the rest 分割其余部分

Now, let's go ahead and do the same cut-and-paste job on the physics and graphics code. Here's our new PhysicsComponent:现在,让我们对物理以及图形的代码继续做同样的工作。这里给出了新的物理组件的代码:

class PhysicsComponent
{
public:
  void update(Bjorn& bjorn, World& world)
  {
    bjorn.x += bjorn.velocity;
    world.resolveCollision(volume_,
        bjorn.x, bjorn.y, bjorn.velocity);
  }

private:
  Volume volume_;
};

In addition to moving the physics behavior out of the main Bjorn class, you can see we've also moved out the data too: The Volume object is now owned by the component.除了将物理行为从核心类Bjorn中移除外,你还能看到我们同时将数据也移除了:现在音量对象是组件所拥有的。

Last but not least, here's where the rendering code lives now:最后但是同样重要的,是渲染部分的代码:

class GraphicsComponent
{
public:
  void update(Bjorn& bjorn, Graphics& graphics)
  {
    Sprite* sprite = &spriteStand_;
    if (bjorn.velocity < 0)
    {
      sprite = &spriteWalkLeft_;
    }
    else if (bjorn.velocity > 0)
    {
      sprite = &spriteWalkRight_;
    }

    graphics.draw(*sprite, bjorn.x, bjorn.y);
  }

private:
  Sprite spriteStand_;
  Sprite spriteWalkLeft_;
  Sprite spriteWalkRight_;
};

We've yanked almost everything out, so what's left of our humble pastry chef? Not much:我们几乎将所有东西都移除了,只剩下没有多少代码的Bjorn类:

class Bjorn
{
public:
  int velocity;
  int x, y;

  void update(World& world, Graphics& graphics)
  {
    input_.update(*this);
    physics_.update(*this, world);
    graphics_.update(*this, graphics);
  }

private:
  InputComponent input_;
  PhysicsComponent physics_;
  GraphicsComponent graphics_;
};

The Bjorn class now basically does two things: it holds the set of components that actually define it, and it holds the state that is shared across multiple domains. Position and velocity are still in the core Bjorn class for two reasons. First, they are "pan-domain" state -- almost every component will make use of them, so it isn't clear which component should own them if we did want to push them down.现在Bjorn类只做两件很基础的事情,拥有一些能够真正工作起来的组件,以及拥有能够在不同的域共享的状态信息。位置和速度的信息之所以还保留在Bjorn类中主要有两个原因,首先它们是“pan-domain(最基础?)”状态,几乎所有的组件都必须使用它们,所以如果将它们放到组件中是不明智的。

Secondly, and more importantly, it gives us an easy way for the components to communicate without being coupled to each other. Let's see if we can put that to use.第二点也是最重要的点是将位置与速度这两个状态信息保留在Bjorn类中使得我们轻松的在组件中传递信息而不需要耦合组件。让我们来看看应该如何使用。

Robo-Bjørn 重构Bjorn

So far, we've pushed our behavior out to separate component classes, but we haven't abstracted the behavior out. Bjorn still knows the exact concrete classes where his behavior is defined. Let's change that.到目前为止,我们已经将行为封装到单独的组件类中,但是我们没有将这些行为抽象化。Bjorn仍然精确的知道行为是在哪个类中被定义的。让我们来修改下。

We'll take our component for handling user input and hide it behind an interface. We'll turn InputComponent into an abstract base class:我们将处理用户输入的组件隐藏到一个接口下,这样就能够将输入组件变成一个抽象的基类:

class InputComponent
{
public:
  virtual ~InputComponent() {}
  virtual void update(Bjorn& bjorn) = 0;
};

Then, we'll take our existing user input handling code and push it down into a class that implements that interface:然后,我们将已经存在的用于处理用户输入的代码封装到一个实现了接口的类中:

class PlayerInputComponent : public InputComponent
{
public:
  virtual void update(Bjorn& bjorn)
  {
    switch (Controller::getJoystickDirection())
    {
      case DIR_LEFT:
        bjorn.velocity -= WALK_ACCELERATION;
        break;

      case DIR_RIGHT:
        bjorn.velocity += WALK_ACCELERATION;
        break;
    }
  }

private:
  static const int WALK_ACCELERATION = 1;
};

We'll change Bjorn to hold a pointer to the input component instead of having an inline instance:我们改变Bjorn类,让它拥有一个指向输入组件的指针而不是拥有一个内联实例:

class Bjorn
{
public:
  int velocity;
  int x, y;

  Bjorn(InputComponent* input)
  : input_(input)
  {}

  void update(World& world, Graphics& graphics)
  {
    input_->update(*this);
    physics_.update(*this, world);
    graphics_.update(*this, graphics);
  }

private:
  InputComponent* input_;
  PhysicsComponent physics_;
  GraphicsComponent graphics_;
};

Now, when we instantiate Bjorn, we can pass in an input component for it to use, like so:现在,当我们实例化Bjorn是,我们可以通过一个输入组件使用,像这样

Bjorn* bjorn = new Bjorn(new PlayerInputComponent());

This instance can be any concrete type that implements our abstract InputComponent interface. We pay a price for this -- update() is now a virtual method call, which is a little slower. What do we get in return for this cost?这个实例可以是任何实现了我们抽象输入组件接口的具体类型。但是我们也因此付出代价,现在update方法是一个抽象方法调用方法,相对有点慢。我们应该反思,付出了这个代价我们得到了什么?

Most consoles require a game to support "demo mode." If the player sits at the main menu without doing anything, the game will start playing automatically, with the computer standing in for the player. This keeps the game from burning the main menu into your TV and also makes the game look nicer when it's running on a kiosk in a store.大多数主机需要游戏支持“演示模式”。如果玩家停留在主菜单并且不做任何事情,电脑则会代替玩家让游戏则会自动的播放起来。这么做的目的是为了不要让游戏长时间的停留在主菜单画面,同时也为了在销售商店展示给顾客留下更好的印象。

Hiding the input component class behind an interface lets us get that working. We already have our concrete PlayerInputComponent that's normally used when playing the game. Now, let's make another one:而将输入组件类隐藏到一个接口下有助于我们完成这个工作。我们已经有一个可供玩家正常游戏时的玩家输入组件。现在我们来制作另外一个输入组件:

class DemoInputComponent : public InputComponent
{
public:
  virtual void update(Bjorn& bjorn)
  {
    // AI to automatically control Bjorn...
  }
};

When the game goes into demo mode, instead of constructing Bjørn like we did earlier, we'll wire him up with our new component:当游戏进入演示模式时,我们像之前那样构建Bjorn类,取而代之的是将它连接到新的组件上:

^code 13

And now, just by swapping out a component, we've got a fully functioning computer-controlled player for demo mode. We're able to reuse all of the other code for Bjørn -- physics and graphics don't even know there's a difference. Maybe I'm a bit strange, but it's stuff like this that gets me up in the morning.现在,仅仅只是交换了一个组件,我们就得到了一个功能完备的完全由电脑控制的展示模式。我们能够重用所有其他Bjorn的代码,包括物理以及图形而让Bjorn知道这两者这件有什么区别。也许是我有些奇怪,但是像这样的东西能让我在早上精神起来。

注:That, and coffee. Sweet, steaming hot coffee.这个,还有甜的热气腾腾的咖啡。

No Bjørn at all? 删掉Bjorn

If you look at our Bjorn class now, you'll notice there's nothing really "Bjørn" about it -- it's just a component bag. In fact, it looks like a pretty good candidate for a base "game object" class that we can use for every object in the game. All we need to do is pass in all the components, and we can build any kind of object by picking and choosing parts like Dr. Frankenstein.现在让我们看看的Bjorn类,你会发现基本上没有Bjorn独有的代码,它更像是个组件包。事实上,它是一个能够用到游戏中所有对象身上的游戏基本类的最佳候选。我们需要做的只是将它传递给所有的组件,然后我们就像选择博士Frankenstein一起去构建任何类型的对象。

Let's take our two remaining concrete components -- physics and graphics -- and hide them behind interfaces like we did with input:让我们把剩下的两个具体组件—物理以及图形隐藏到接口下:

class PhysicsComponent
{
public:
  virtual ~PhysicsComponent() {}
  virtual void update(GameObject& obj, World& world) = 0;
};

class GraphicsComponent
{
public:
  virtual ~GraphicsComponent() {}
  virtual void update(GameObject& obj, Graphics& graphics) = 0;
};

Then we re-christen Bjorn into a generic GameObject class that uses those interfaces:然后我们重构Bjorn类,并将它改造成一个使用了以上接口的通用游戏类

class GameObject
{
public:
  int velocity;
  int x, y;

  GameObject(InputComponent* input,
             PhysicsComponent* physics,
             GraphicsComponent* graphics)
  : input_(input),
    physics_(physics),
    graphics_(graphics)
  {}

  void update(World& world, Graphics& graphics)
  {
    input_->update(*this);
    physics_->update(*this, world);
    graphics_->update(*this, graphics);
  }

private:
  InputComponent* input_;
  PhysicsComponent* physics_;
  GraphicsComponent* graphics_;
};

注:Some component systems take this even further. Instead of a GameObject that contains its components, the game entity is just an ID, a number. Then, you maintain separate collections of components where each one knows the ID of the entity its attached to.有一些组件系统在此基础上更发展一步,整个游戏就是一个ID,一个数字而不是一个包含组件的游戏类。然后你可以使用ID来维持一个实体所拥有的组件套之间互相独立。

These entity component systems take decoupling components to the extreme and let you add new components to an entity without the entity even knowing. The Data Locality chapter has more details.这些实体组件系统将解耦组件的设计发挥到了极限。让你能够对一个实体进行添加新的组件而不让实体知道。局部数据的章节将更详细的讲述这个细节。

Our existing concrete classes will get renamed and implement those interfaces:我们将现有的具体类重命名并且实现以上接口:

class BjornPhysicsComponent : public PhysicsComponent
{
public:
  virtual void update(GameObject& obj, World& world)
  {
    // Physics code...
  }
};

class BjornGraphicsComponent : public GraphicsComponent
{
public:
  virtual void update(GameObject& obj, Graphics& graphics)
  {
    // Graphics code...
  }
};

And now we can build an object that has all of Bjørn's original behavior without having to actually create a class for him, just like this:现在我们可以构建一个拥有所有Bjorn原本行为的对象,但是却不需要因此生成一个类,就像:

GameObject* createBjorn()
{
  return new GameObject(new PlayerInputComponent(),
                        new BjornPhysicsComponent(),
                        new BjornGraphicsComponent());
}

注:This createBjorn() function is, of course, an example of the classic Gang of Four Factory Method pattern.当然,createBjorn方法是一个典型的四种工厂设计模式的实例

By defining other functions that instantiate GameObjects with different components, we can create all of the different kinds of objects our game needs.通过定义其他的函数来实例化拥有不同组件的游戏类,我们能够创建所有我们游戏中所需的对象。

Design Decisions 设计决策

The most important design question you'll need to answer with this pattern is, "What set of components do I need?" The answer there is going to depend on the needs and genre of your game. The bigger and more complex your engine is, the more finely you'll likely want to slice your components.关于这个设计模式的最重要的问题是:我需要的组件套是什么?答案是取决于你游戏的需求与风格。更大更复杂的引擎需要你将组件切分的更细。

Beyond that, there are a couple of more specific options to consider:除此之外,有一些更具体的选择需要考虑:

How does the object get its components? 对象如何获得组件

Once we've split up our monolithic object into a few separate component parts, we have to decide who puts the parts back together.一旦我们将一个单独的对象分割成数个独立的组件,我们就必须决定谁在背后来联系这些组件。

  • **If the object creates its own components:**如果这个类创建了自己的组件:

    • It ensures that the object always has the components it needs. You never have to worry about someone forgetting to wire up the right components to the object and breaking the game. The container object itself takes care of it for you.它确保了这个类一定有它所需要的组件。你不要担心有人忘记了将类链接到组件上而导致游戏崩溃。容器类将会负责这件事。

    • It's harder to reconfigure the object. One of the powerful features of this pattern is that it lets you build new kinds of objects simply by recombining components. If our object always wires itself with the same set of hard-coded components, we aren't taking advantage of that flexibility.但是这么做将导致很难再重新配置这个类。此设计模式一个强大的特性就是能够让你通过简单的组合组件来构建任何你需要的对象。如果我们的对象总是连着一组组件,我们将失去了这种灵活性。

  • If outside code provides the components: 如果由外部代码提供组件:

    • The object becomes more flexible. We can completely change the behavior of the object by giving it different components to work with. Taken to its fullest extent, our object becomes a generic component container that we can reuse over and over again for different purposes.对象将变得灵活。我们完全可以通过添加不同的组件来改变类的行为。我们甚至能把这个类当做一个通用的组件容器,一遍又一遍的为不同的目重用代码。

    • The object can be decoupled from the concrete component types. If we're allowing outside code to pass in components, odds are good that we're also letting it pass in derived component types. At that point, the object only knows about the component interfaces and not the concrete types themselves. This can make for a nicely encapsulated architecture.对象可以从具体的组件类型中解耦出来,we’re allowing outside code to pass in components, odds are good that we’re also letting it pass in derived component types. 对象只是知道组件的接口而不知道具体的类型,这能够很好的封装结构。

How do components communicate with each other?组件之间如何传递信息?

Perfectly decoupled components that function in isolation is a nice ideal, but it doesn't really work in practice. The fact that these components are part of the same object implies that they are part of a larger whole and need to coordinate. That means communication.完美的将组件互相解耦并且保证功能隔离是个很好的想法,但它通常是不现实的。事实就是组件是相同对象的一个部分,所以组件与组件之间需要传递信息。

So how can the components talk to each other? There are a couple of options, but unlike most design "alternatives" in this book, these aren't exclusive -- you will likely support more than one at the same time in your designs.所以组件之间又是如何传递信息的呢?有好几个选择,但是不像大多数这边书中设计模式,它们不是唯一的,所以你可以同时使用好几种不同的方法。

  • **By modifying the container object's state:**通过修改容器对象的状态:

    • It keeps the components decoupled. When our InputComponent set Bjørn's velocity and the PhysicsComponent later used it, the two components had no idea that the other even existed. For all they knew, Bjørn's velocity could have changed through black magic.它是的组件保持解耦。当我们的输入组件设置Bjorn的速度和物理组件使用它的时候,这两个组件甚至都不知道对方的存在,它们知道的是Bjorn类的速度已经改变了。

    • It requires any information that components need to share to get pushed up into the container object. Often, there's state that's really only needed by a subset of the components. For example, an animation and a rendering component may need to share information that's graphics-specific. Pushing that information up into the container object where every component can get to it muddies the object class.它需要任何组件需要知道的信息都在更高一级的容器中进行共享。通常,某些状态只是一少部分组件所需要。举个例子,动画已经渲染的组件可能需要与图形组件共享信息,但是将这些信息放到所有组件都能够获取到的容器类中则会混乱了这个对象类。

    Worse, if we use the same container object class with different component configurations, we can end up wasting memory on state that isn't needed by any of the object's components. If we push some rendering-specific data into the container object, any invisible object will be burning memory on it with no benefit.更糟糕的是,如果我们使用相同的容器类以及不同的组件配置,我们将会把宝贵的内存浪费在可能完全不需要的那些状态信息的对象组件上。如果我们将一些特定的渲染数据放到容器类汇总,任何不可见的对象就会浪费大量内存,而且这不带来任何好处。

    • It makes communication implicit and dependent on the order that components are processed. In our sample code, the original monolithic update() method had a very carefully laid out order of operations. The user input modified the velocity, which was then used by the physics code to modify the position, which in turn was used by the rendering code to draw Bjørn at the right spot. When we split that code out into components, we were careful to preserve that order of operations.这使得信息传递变得隐秘以及依赖组件执行的顺序。在我们的样例代码中,最原始的update方法有一个非常仔细的操作顺序。用户输入修改了速度,然后物理代码修改位置,然后渲染代码在屏幕上显示Bjorn。当我们将代码分割成不同的组件后,我们需要小心翼翼的保留操作的顺序。

      If we hadn't, we would have introduced subtle, hard-to-track bugs. For example, if we'd updated the graphics component first, we would wrongly render Bjørn at his position on the last frame, not this one. If you imagine several more components and lots more code, then you can get an idea of how hard it can be to avoid bugs like this.如果我们不这么做的话,可以会导致一些很细小的难以追踪的bug。举个例子,如果我们首先加载了图形组件,我们极有可能会将Bjorn显示在错误的位置上。即便不是这个,如果加入更多的组件已经更大的代码,你就会发现避免执行顺序发生错乱是件多么困难的事情。

注:Shared mutable state like this where lots of code is reading and writing the same data is notoriously hard to get right. That's a big part of why academics are spending time researching pure functional languages like Haskell where there is no mutable state at all.大量的像这样共享可变的状态信息的代码无论对阅读还是写来说都是非常难保持正确的。这也是为什么学者会花时间研究像Haskell这样没有可变状态纯函数语言的主要原因。

  • By referring directly to each other: 直接联系

    The idea here is that components that need to talk will have direct references to each other without having to go through the container object at all.有一个想法就是当组件需要与其他的组件进行信息传递时,它不通过容器类而是直接将信息传递到目标组件。

    Let's say we want to let Bjørn jump. The graphics code needs to know if he should be drawn using a jump sprite or not. It can determine this by asking the physics engine if he's currently on the ground. An easy way to do this is by letting the graphics component know about the physics component directly:假设我们想让Bjorn跳起来。图形代码需要知道它是否应该使用一个跳跃的动作。一个比较简单方法就是让图形组件与物理组件取得直接的联系:

    class BjornGraphicsComponent
    {
    public:
      BjornGraphicsComponent(BjornPhysicsComponent* physics)
      : physics_(physics)
      {}
    
      void Update(GameObject& obj, Graphics& graphics)
      {
        Sprite* sprite;
        if (!physics_->isOnGround())
        {
          sprite = &spriteJump_;
        }
        else
        {
          // Existing graphics code...
        }
    
        graphics.draw(*sprite, obj.x, obj.y);
      }
    
    private:
      BjornPhysicsComponent* physics_;
    
      Sprite spriteStand_;
      Sprite spriteWalkLeft_;
      Sprite spriteWalkRight_;
      Sprite spriteJump_;
    };

    When we construct Bjørn's GraphicsComponent, we'll give it a reference to his corresponding PhysicsComponent.当我们构建Bjorn的图形组件时,我们给它一个对应的物理组件的引用。

    • It's simple and fast. Communication is a direct method call from one object to another. The component can call any method that is supported by the component it has a reference to. It's a free-for-all.这很简单而且很快捷。组件之间的信息传递是通过一个对象调用另一个对象的方法。组件能够调用其代码中含有引用的组件的所有方法。这是个混战。

    • The two components are tightly coupled. The downside of the free-for-all. We've basically taken a step back towards our monolithic class. It's not quite as bad as the original single class though, since we're at least restricting the coupling to only the component pairs that need to interact.组件之间紧密耦合。缺点就是会变得相当混乱。我们好像又回到了当初一个巨大的单类的时候,但其实这远没有那来的糟糕,起码我们将耦合限制在需要交流的组件之间。

  • **By sending messages:**通过传递信息

    • This is the most complex alternative. We can actually build a little messaging system into our container object and let the components broadcast information to each other.这是选项中最复杂的一个。我们可以在容器类中建立一个小的消息传递系统,让需要传递信息的组件通过广播的方式去建立组件间的联系。

      Here's one possible implementation. We'll start by defining a base Component interface that all of our components will implement:以下是一个实现的可能。我们将首先定义一个所有组件都能实现的基本组件接口:

      class Component
      {
      public:
        virtual ~Component() {}
        virtual void receive(int message) = 0;
      };

      It has a single receive() method that component classes implement in order to listen to an incoming message. Here, we're just using an int to identify the message, but a fuller implementation could attach additional data to the message.它有一个receive方法,组件通过实现它来监听传入信息。在这里我们将信息定义成int型,通过更加全面的实现我们也可以将额外的数据附加到信息中。

      Then, we'll add a method to our container object for sending messages:然后,我们在容器类中添加一个方法来发送消息

      class ContainerObject
      {
      public:
        void send(int message)
        {
          for (int i = 0; i < MAX_COMPONENTS; i++)
          {
            if (components_[i] != NULL)
            {
              components_[i]->receive(message);
            }
          }
        }
      
      private:
        static const int MAX_COMPONENTS = 10;
        Component* components_[MAX_COMPONENTS];
      };

      Now, if a component has access to its container, it can send messages to the container, which will rebroadcast the message to all of the contained components. (That inclues the original component that sent the message; be careful that you don't get stuck in a feedback loop!) This has a couple of consequences:现在,如果一个组件访问它的容器,它能够将信息发送给容器,并且通过容器将信息广播给容器所包含的所有组件。

      注:If you really want to get fancy, you can even make this message system queue messages to be delivered later. For more on this, see Event Queue.如果你真的乐意,你甚至可以将这个消息洗头膏改成成可以延迟发送的队列消息。更多细节请查看事件队列这一个章节。

    • Sibling components are decoupled. By going through the parent container object, like our shared state alternative, we ensure that the components are still decoupled from each other. With this system, the only coupling they have is the message values themselves.兄弟组件之间是解耦的。就好像我们之前选择状态传递信息的方法一样,我们通过上层容器类来确保组件之间是解耦的。使用传递信息系统的方法,唯一的耦合就是消息本身。

      注:The Gang of Four call this the Mediator pattern -- two or more objects communicate with each other indirectly by routing the message through an intermediate object. In this case, the container object itself is the mediator.“四人帮”称之为中介模式,两个或两个以上的对象通过将信息传递到一个中介的方法来取得相互之间的联系。而本章节中,容器类则充当了中间的角色。

    • The container object is simple. Unlike using shared state where the container object itself owns and knows about data used by the components, here, all it does is blindly pass the messages along. That can be useful for letting two components pass very domain-specific information between themselves without having that bleed into the container object.容器对象十分简单。不像状态共享那样容器类能够获知应该传递给组件的信息,在这里,容器类的工作只是将信息发送出去。这对两个类之间传递非常特定的信息而不让容器类获知来说是个非常有用的方法。

Unsurprisingly, there's no one best answer here. What you'll likely end up doing is using a bit of all of them. Shared state is useful for the really basic stuff that you can take for granted that every object has -- things like position and size.意料之外的是,没有那个选择是最好的。你最终有可能将上述所说的三种方法都使用到。状态共享对每个对象都拥有的基本状态如位置和尺寸等非常管用。

Some domains are distinct but still closely related. Think animation and rendering, user input and AI, or physics and collision. If you have separate components for each half of those pairs, you may find it easiest to just let them know directly about their other half.有些域虽然不同但是仍然紧密相关。就比如说动画和渲染,用户输入与AI,又或者物理与碰撞。如果你有上述这些强关联的组件的话,最简单的方法就是在他们之间建立直接的联系。

Messaging is useful for "less important" communication. Its fire-and-forget nature is a good fit for things like having an audio component play a sound when a physics component sends a message that the object has collided with something.消息传递是个对“不太重要”的通信有用的机制。其“即发即弃”的特性非常适合类似于当对象与物体发生碰撞时,物理组件想让声音组件发出一个声音的这种行为。

As always, I recommend you start simple and then add in additional communication paths if you need them.与往常一样,我建议你从简单的开始,然后在你需要组件通信的时候再考虑应该添加那种信息传递的方法。

See Also 其他参考

  • The Unity framework's core GameObject class is designed entirely around components. Unity)框架的核心GameObject类完全由组件设计完成。

  • The open source Delta3D engine has a base GameActor class that implements this pattern with the appropriately named ActorComponent base class.开源引擎Delta3D 有一个实现了这种设计模式的基类GameActor和一个ActorComponent的基类。

  • Microsoft's XNA game framework comes with a core Game class. It owns a collection of GameComponent objects. Where our example uses components at the individual game entity level, XNA implements the pattern at the level of the main game object itself, but the purpose is the same.微软的XNA游戏框架附带了一个核心游戏类。它拥有一系列游戏组件对象。本文中的举例是在单个游戏层面上使用组件,而XNA则实现了主要游戏对象的设计模式,但是本质是一样的。

  • This pattern bears resemblance to the Gang of Four's Strategy pattern. Both patterns are about taking part of an object's behavior and delegating it to a separate subordinate object. The difference is that with the Strategy pattern, the separate "strategy" object is usually stateless -- it encapsulates an algorithm, but no data. It defines how an object behaves, but not what it is.这种设计模式与“四人帮”中的战略模式很类似。都是通过将对象的行为委托给一个独立的从对象。不同的是战略模式的“战略”对象通常都是无状态的,它封装了一个算法,但是没有数据。它定义了一个对象的行为方式,而不是对象本身。

    Components are a bit more self-important. They often hold state that describes the object and helps define its actual identity. However, the line may blur. You may have some components that don't need any local state. In that case, you're free to use the same component instance across multiple container objects. At that point, it really is behaving more akin to a strategy.组件是有点高傲。它们经常保持状态,描述和定义对象。然而,这可能有点模糊。你可能有一些不知道任何状态的组件。在这种情况下,你可以在跨多个容器对象的情况下使用相同的组件实例。在这一点上,它的确表现的像是一个策略对象。