Flyweight 蝇量级
Game Programming PatternsDesign Patterns Revisited
游戏编程模式重新审视设计模式
The fog lifts, revealing a majestic old growth forest. Ancient hemlocks,
countless in number, tower over you forming a cathedral of greenery. The stained
glass canopy of leaves fragments the sunlight into golden shafts of mist.
Between giant trunks, you can make out the massive forest receding into the
distance.
雾气散去,露出一片雄伟的古老森林。古老的铁杉,数量不计其数,耸立在你面前,形成了一座绿色的大教堂。树叶的彩色玻璃树冠将阳光分割成金色的薄雾。在巨大的树干之间,您可以看到向远处退去的巨大森林。
This is the kind of otherworldly setting we dream of as game developers, and
scenes like these are often enabled by a pattern whose name couldn’t possibly be
more modest: the humble Flyweight.
这就是我们作为游戏开发者梦寐以求的超凡脱俗的场景,而像这样的场景往往是由一种名字再谦虚不过的模式实现的:不起眼的 Flyweight。
Forest for the Trees 树木之林
I can describe a sprawling woodland with just a few sentences, but actually
implementing it in a realtime game is another story. When you’ve got an entire
forest of individual trees filling the screen, all that a graphics programmer
sees is the millions of polygons they’ll have to somehow shovel onto the GPU
every sixtieth of a second.
我可以用几句话来描述一片广阔的林地,但实际上
在实时游戏中实现它是另一回事。当屏幕中填满了一整片树木森林时,图形程序员所看到的只是他们每六十分之一秒就必须以某种方式铲到 GPU 上的数百万个多边形。
We’re talking thousands of trees, each with detailed geometry containing
thousands of polygons. Even if you have enough memory to describe that forest,
in order to render it, that data has to make its way over the bus from the CPU
to the GPU.
我们谈论的是数千棵树,每棵树都有包含数千个多边形的详细几何体。即使您有足够的内存来描述该森林,为了渲染它,该数据也必须通过从 CPU 到 GPU 的总线。
Each tree has a bunch of bits associated with it:
每个树都有一堆与之关联的位:
- A mesh of polygons that define the shape of the trunk, branches, and greenery.
定义树干、树枝和绿色植物形状的多边形网格。 - Textures for the bark and leaves.
树皮和树叶的纹理。 - Its location and orientation in the forest.
它在森林中的位置和方向。 - Tuning parameters like size and tint so that each tree looks different.
调整大小和色调等参数,使每棵树看起来都不同。
If you were to sketch it out in code, you’d have something like this:
如果你要在代码中草拟出来,你会得到这样的结果:
class Tree { private: Mesh mesh_; Texture bark_; Texture leaves_; Vector position_; double height_; double thickness_; Color barkTint_; Color leafTint_; };
That’s a lot of data, and the mesh and textures are particularly large. An entire
forest of these objects is too much to throw at the GPU in one frame.
Fortunately, there’s a time-honored trick to handling this.
这是大量的数据,而且网格和纹理特别大。这些对象的整个森林对于在一帧中扔给 GPU 来说太多了。幸运的是,有一个历史悠久的技巧来处理这个问题。
The key observation is that even though there may be thousands of trees in the
forest, they mostly look similar. They will likely all use the same mesh and textures. That means most of the fields in
these objects are the same between all of those instances.
关键的观察结果是,尽管森林中可能有数千棵树,但它们大多看起来相似。它们可能都使用相同的网格和纹理。这意味着这些对象中的大多数字段在所有这些实例之间都是相同的。
We can model that explicitly by splitting the object in half. First, we pull
out the data that all trees have in common and move it
into a separate class:
我们可以通过将对象一分为二来显式建模。首先,我们提取所有树共有的数据并将其移动到一个单独的类中:
class TreeModel { private: Mesh mesh_; Texture bark_; Texture leaves_; };
The game only needs a single one of these, since there’s no reason to have the
same meshes and textures in memory a thousand times. Then, each instance of a
tree in the world has a reference to that shared TreeModel
. What remains in
Tree
is the state that is instance-specific:
游戏只需要其中的一个,因为没有理由在内存中将相同的网格和纹理一千次。然后,世界中树的每个实例都有对该共享 TreeModel
的引用。剩下什么
Tree
是特定于实例的状态:
class Tree { private: TreeModel* model_; Vector position_; double height_; double thickness_; Color barkTint_; Color leafTint_; };
You can visualize it like this:
您可以像这样可视化它:
This is all well and good for storing stuff in main memory, but that doesn’t
help rendering. Before the forest gets on screen, it has to work its way over to
the GPU. We need to express this resource sharing in a way that the graphics
card understands.
这对于在主内存中存储内容来说很好,但这对渲染没有帮助。在森林出现在屏幕上之前,它必须经过 GPU。我们需要以图形卡能够理解的方式表达这种资源共享。
A Thousand Instances 1000 个实例
To minimize the amount of data we have to push to the GPU, we want to be able to
send the shared data — the TreeModel
— just once. Then, separately, we
push over every tree instance’s unique data — its position, color, and scale.
Finally, we tell the GPU, “Use that one model to render each of these
instances.”
为了最大限度地减少我们必须推送到 GPU 的数据量,我们希望能够只发送一次共享数据 — TreeModel
。然后,我们分别推送每个树实例的唯一数据 — 它的位置、颜色和比例。最后,我们告诉 GPU,“使用该模型来渲染每个实例。
Fortunately, today’s graphics APIs and cards
support exactly that. The details are fiddly and out of the scope of this book,
but both Direct3D and OpenGL can do something called instanced
rendering.
幸运的是,今天的图形 API 和卡
正是支持这一点。细节很繁琐,超出了本书的范围,
但 Direct3D 和 OpenGL 都可以执行称为实例化渲染的操作。
In both APIs, you provide two streams of data. The first is the blob of common
data that will be rendered multiple times — the mesh and textures in our
arboreal example. The second is the list of instances and their parameters that
will be used to vary that first chunk of data each time it’s drawn. With a
single draw call, an entire forest grows.
在这两个 API 中,您都提供了两个数据流。第一个是将多次渲染的常见数据块 — 树栖示例中的网格和纹理。第二个是实例及其参数的列表,这些参数将用于在每次绘制第一个数据块时改变该数据块。使用单个绘制调用,整个林就会增长。
The Flyweight Pattern Flyweight 模式
Now that we’ve got one concrete example under our belts, I can walk you through
the general pattern. Flyweight, like its name implies, comes into play when you
have objects that need to be more lightweight, generally because you have too
many of them.
现在我们已经有一个具体的例子,我可以向您介绍一般模式。顾名思义,Flyweight 在您需要更轻量级的对象时发挥作用,通常是因为您拥有太多的对象。
With instanced rendering, it’s not so much that they take up too much memory as
it is they take too much time to push each separate tree over the bus to the
GPU, but the basic idea is the same.
使用实例化渲染时,与其说它们占用了太多内存,不如说它们需要花费太多时间将每个单独的树通过总线推送到 GPU,但基本思想是相同的。
The pattern solves that by separating out an object’s data into two kinds. The
first kind of data is the stuff that’s not specific to a single instance of
that object and can be shared across all of them. The Gang of Four calls this
the intrinsic state, but I like to think of it as the “context-free” stuff. In
the example here, this is the geometry and textures for the tree.
该模式通过将对象的数据分为两种来解决这个问题。第一种数据是不特定于该对象的单个实例并且可以在所有实例之间共享的内容。四人帮称其为内在状态,但我喜欢将其视为“无上下文”的东西。在此示例中,这是树的几何图形和纹理。
The rest of the data is the extrinsic state, the stuff that is unique to that
instance. In this case, that is each tree’s position, scale, and color. Just
like in the chunk of sample code up there, this pattern saves memory by sharing
one copy of the intrinsic state across every place where an object appears.
其余的数据是 extrinsic state,即该实例独有的东西。在本例中,这是每棵树的位置、比例和颜色。就像上面的示例代码块一样,此模式通过在对象出现的每个位置共享内部状态的一个副本来节省内存。
From what we’ve seen so far, this seems like basic resource sharing,
hardly worth being called a pattern. That’s partially because in this example
here, we could come up with a clear separate identity for the shared state:
the TreeModel
.
从我们目前所看到的情况来看,这似乎是基本的资源共享,几乎不值得被称为模式。这部分是因为在这个例子中,我们可以为共享状态想出一个明确的单独身份:TreeModel
。
I find this pattern to be less obvious (and thus more clever) when used in cases
where there isn’t a really well-defined identity for the shared object. In those
cases, it feels more like an object is magically in multiple places at the same
time. Let me show you another example.
我发现这种模式在共享对象没有真正明确定义标识的情况下使用时不太明显(因此更聪明)。在这些情况下,感觉更像是一个物体同时神奇地出现在多个地方。让我给你看另一个例子。
A Place To Put Down Roots
一个扎根的地方
The ground these trees are growing on needs to be represented in our game too.
There can be patches of grass, dirt, hills, lakes, rivers, and whatever other
terrain you can dream up. We’ll make the ground tile-based: the surface of the
world is a huge grid of tiny tiles. Each tile is covered in one kind of terrain.
这些树木生长的地面也需要在我们的游戏中得到体现。可以有成片的草地、泥土、丘陵、湖泊、河流以及您能想到的任何其他地形。我们将使地面图块基于:世界表面是一个由小图块组成的巨大网格。每个图块都覆盖在一种地形中。
Each terrain type has a number of properties that affect gameplay:
每种地形类型都有许多影响游戏玩法的属性:
- A movement cost that determines how quickly players can move through it.
一种移动消耗,决定玩家通过它的速度。 - A flag for whether it’s a watery terrain that can be crossed by boats.
一面旗帜,表明它是否是船只可以穿越的水域地形。 - A texture used to render it.
用于渲染它的纹理。
Because we game programmers are paranoid about efficiency, there’s no way we’d
store all of that state in each tile in the world.
Instead, a common approach is to use an enum for terrain types:
因为我们游戏程序员对效率很偏执,所以我们不可能将所有这些状态存储在世界中的每个图块中。相反,一种常见的方法是对地形类型使用枚举:
enum Terrain { TERRAIN_GRASS, TERRAIN_HILL, TERRAIN_RIVER // Other terrains... };
Then the world maintains a huge grid of those:
然后世界维护着一个巨大的网格:
class World { private: Terrain tiles_[WIDTH][HEIGHT]; };
To actually get the useful data about a tile, we do something like:
要实际获取有关瓦片的有用数据,我们执行以下操作:
int World::getMovementCost(int x, int y) { switch (tiles_[x][y]) { case TERRAIN_GRASS: return 1; case TERRAIN_HILL: return 3; case TERRAIN_RIVER: return 2; // Other terrains... } } bool World::isWater(int x, int y) { switch (tiles_[x][y]) { case TERRAIN_GRASS: return false; case TERRAIN_HILL: return false; case TERRAIN_RIVER: return true; // Other terrains... } }
You get the idea. This works, but I find it ugly. I think of movement cost and
wetness as data about a terrain, but here that’s embedded in code. Worse, the
data for a single terrain type is smeared across a bunch of methods. It would be
really nice to keep all of that encapsulated together. After all, that’s what
objects are designed for.
你明白了。这很有效,但我觉得它很丑陋。我认为移动成本和湿度是有关地形的数据,但在这里它嵌入在代码中。更糟糕的是,单个 terrain 类型的数据被涂抹在一系列方法中。如果把所有这些封装在一起,那就太好了。毕竟,这就是 Objects 的设计目的。
It would be great if we could have an actual terrain class, like:
如果我们能有一个实际的地形类,那就太好了,比如:
class Terrain { public: Terrain(int movementCost, bool isWater, Texture texture) : movementCost_(movementCost), isWater_(isWater), texture_(texture) {} int getMovementCost() const { return movementCost_; } bool isWater() const { return isWater_; } const Texture& getTexture() const { return texture_; } private: int movementCost_; bool isWater_; Texture texture_; };
But we don’t want to pay the cost of having an instance of that for each tile in
the world. If you look at that class, you’ll notice that there’s actually
nothing in there that’s specific to where that tile is. In flyweight terms,
all of a terrain’s state is “intrinsic” or “context-free”.
但是我们不想为每个 tile 支付一个实例的成本
世界。如果你查看该类,你会注意到实际上有
那里没有特定于该磁贴所在位置的内容。用蝇量级术语来说,
地形的所有状态都是 “Intrinsic” 或 “context-free” 的。
Given that, there’s no reason to have more than one of each terrain type. Every
grass tile on the ground is identical to every other one. Instead of having the
world be a grid of enums or Terrain objects, it will be a grid of pointers to
Terrain
objects:
鉴于此,没有理由每种地形类型都具有多个。地上的每块草都与其他草块相同。世界不是枚举或 Terrain 对象的网格,而是指向
地形
对象:
class World { private: Terrain* tiles_[WIDTH][HEIGHT]; // Other stuff... };
Each tile that uses the same terrain will point to the same terrain instance.
使用相同地形的每个图块将指向相同的地形实例。
Since the terrain instances are used in multiple places, their lifetimes would
be a little more complex to manage if you were to dynamically allocate them.
Instead, we’ll just store them directly in the world:
由于 terrain 实例在多个位置使用,因此如果要动态分配它们,则它们的生命周期管理起来会稍微复杂一些。相反,我们只将它们直接存储在 world:
class World { public: World() : grassTerrain_(1, false, GRASS_TEXTURE), hillTerrain_(3, false, HILL_TEXTURE), riverTerrain_(2, true, RIVER_TEXTURE) {} private: Terrain grassTerrain_; Terrain hillTerrain_; Terrain riverTerrain_; // Other stuff... };
Then we can use those to paint the ground like this:
然后我们可以使用它们来粉刷地面,如下所示:
void World::generateTerrain() { // Fill the ground with grass. for (int x = 0; x < WIDTH; x++) { for (int y = 0; y < HEIGHT; y++) { // Sprinkle some hills. if (random(10) == 0) { tiles_[x][y] = &hillTerrain_; } else { tiles_[x][y] = &grassTerrain_; } } } // Lay a river. int x = random(WIDTH); for (int y = 0; y < HEIGHT; y++) { tiles_[x][y] = &riverTerrain_; } }
Now instead of methods on World
for accessing the terrain properties, we can
expose the Terrain
object directly:
现在,我们可以直接公开 Terrain
对象,而不是在 World
上访问地形属性的方法:
const Terrain& World::getTile(int x, int y) const { return *tiles_[x][y]; }
This way, World
is no longer coupled to all sorts of details of terrains. If
you want some property of the tile, you can get it right from that object:
这样,World (世界
) 不再与地形的各种细节耦合。如果需要磁贴的某些属性,可以直接从该对象获取它:
int cost = world.getTile(2, 3).getMovementCost();
We’re back to the pleasant API of working with real objects, and we did this
with almost no overhead — a pointer is often no larger than an enum.
我们回到了使用真实对象的令人愉快的 API,我们做到了这一点,几乎没有开销——指针通常不大于枚举。
What About Performance? 性能如何?
I say “almost” here because the performance bean counters will rightfully want
to know how this compares to using an enum. Referencing the terrain by pointer
implies an indirect lookup. To get to some terrain data like the movement cost,
you first have to follow the pointer in the grid to find the terrain object and
then find the movement cost there. Chasing a pointer like this can cause a cache miss, which can slow things down.
我在这里说“几乎”,因为性能 bean 计数器理所当然地想知道这与使用 enum 相比如何。通过指针引用地形意味着间接查找。要获取一些地形数据(如移动成本),您首先必须按照网格中的指针找到地形对象,然后在其中找到移动成本。像这样跟踪指针可能会导致缓存未命中,从而减慢速度。
As always, the golden rule of optimization is profile first. Modern computer
hardware is too complex for performance to be a game of pure reason anymore. In
my tests for this chapter, there was no penalty for using a flyweight over an
enum. Flyweights were actually noticeably faster. But that’s entirely dependent
on how other stuff is laid out in memory.
与往常一样,优化的黄金法则是 profile 优先。现代计算机硬件太复杂了,性能不再是纯粹理性的游戏。在我本章的测试中,使用 flyweight 而不是 enum 没有惩罚。蝇量级实际上明显更快。但这完全取决于其他内容在内存中的布局方式。
What I am confident of is that using flyweight objects shouldn’t be dismissed
out of hand. They give you the advantages of an object-oriented style without
the expense of tons of objects. If you find yourself creating an enum and doing
lots of switches on it, consider this pattern instead. If you’re worried about
performance, at least profile first before changing your code to a less
maintainable style.
我确信的是,使用享元对象不应该被随意否定。它们为您提供了面向对象样式的优势,而无需花费大量对象。如果你发现自己正在创建一个 enum 并在其上执行大量 switch,请考虑此模式。如果您担心性能,至少在将代码更改为更难维护的样式之前先进行分析。
See Also 另请参阅
-
In the tile example, we just eagerly created an instance for each terrain type and stored it in
World
. That made it easy to find and reuse the shared instances. In many cases, though, you won’t want to create all of the flyweights up front.
在瓦片示例中,我们只是急切地为每种地形类型创建一个实例并将其存储在World
中。这使得查找和重用共享实例变得容易。但是,在许多情况下,您不希望预先创建所有轻量级。If you can’t predict which ones you actually need, it’s better to create them on demand. To get the advantage of sharing, when you request one, you first see if you’ve already created an identical one. If so, you just return that instance.
如果您无法预测您实际需要哪些,最好按需创建它们。要获得共享的优势,当您请求共享时,您首先要查看是否已经创建了相同的共享。如果是这样,您只需返回该实例。This usually means that you have to encapsulate construction behind some interface that can first look for an existing object. Hiding a constructor like this is an example of the Factory Method pattern.
这通常意味着您必须将构造封装在一些可以首先查找现有对象的接口后面。像这样隐藏构造函数是 Factory Method 模式的一个示例。 -
In order to return a previously created flyweight, you’ll have to keep track of the pool of ones that you’ve already instantiated. As the name implies, that means that an object pool might be a helpful place to store them.
为了返回之前创建的享元,您必须跟踪已实例化的享元池。顾名思义,这意味着对象池可能是存储它们的有用位置。 -
When you’re using the State pattern, you often have “state” objects that don’t have any fields specific to the machine that the state is being used in. The state’s identity and methods are enough to be useful. In that case, you can apply this pattern and reuse that same state instance in multiple state machines at the same time without any problems.
当您使用 State 模式中,您经常会遇到没有任何特定字段的 “state” 对象 添加到正在使用该状态的计算机中。该州的 身份和方法就足够有用了。在这种情况下,您可以申请 this 模式,并在多个状态机中重用同一 state 实例 同时没有任何问题。