通过对所有对象实例同时进行帧更新来模拟一系列相互独立的游戏对象。
动机
它要为游戏中的每个实体封装其自身的行为。这将使游戏循环保持整洁并便于往循环中增加或移除实体。
为了做到这一点,我们需第十章 更新方法.md要一个抽象层,为此定义一个update()的抽象方法。游戏循环维护对象集合,但它并不关心这些对象的具体类型。它只是更新他们。这将每个对象的行为从游戏循环以及其他对象那里分离了出来。
模式
游戏世界维护一个对象集合。每个对象实现一个更新方法以在每帧模拟自己的行为。而游戏循环在每帧对集合中所有的对象调用其更新方法,以实现和游戏世界同步更新。
使用情境
如果这个游戏更加抽象,比如是西洋棋子,你并不需要同时模拟所有对象,而且你不需要也不必要让棋子们逐帧地更新自身,这一模式就不那么适用了。
更新方法在如下情境最为适用:
- 你的游戏中含有一系列对象或系统需要同步地运转。
- 各个对象之间的行为几乎是相互独立的。
- 对象的行为与时间相关。
使用须知
1. 将代码划分至单帧之中使其变得更加复杂
2. 你需要在每帧结束前存储游戏状态以便下一帧继续
3. 所有对象都在每帧进行模拟,但并非真正同步
这意味着,游戏循环遍历更新对象的顺序意义重大。
每次增量式的更新会改变游戏世界,从一个有效的状态到下一个,不会产生对象状态的歧义而需要去协调。
4. 在更新期间修改对象列表时必须谨慎
1 | int numObjectsThisTurn = numObjects_; |
objects是游戏中可更新对象的数组,而numObjects是它的长度,在增加新的对象时,这个长度变量增长。我们在循环的一开始将长度缓存在numObjectsThisTurn变量中,从而使这一帧的循环迭代在遍历到任何新增对象之前停止。
一个令人担忧的问题是在迭代时移除对象。在上述代码中,通过下标的访问形式,这会有几率跳过一个对象的更新。
一个方法是更新时从表的末尾开始遍历。
另外一个方法是可以标志对象为“死亡”,等到遍历更新结束之后,再次遍历列表来移除这些“尸体”。
假如在更新循环中你加入了多线程,则采用延迟修改的方法较好,因为这可以避免更新期间线程同步带来巨大的开销。
示例程序
1 | class Entity |
我们维护一个游戏世界,所有entity对象都在游戏世界中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class World
{
public:
World() : numEntities_(0) {}
void gameLoop();
private:
Entity* entities_[MAX_ENTITIES];
int numEntities_;
};
void World::gameLoop()
{
while(true)
{
// Handle user input...
// Update each entity.
for(int i = 0; i < numEntities_; i++)
{
entities_[i]->update();
}
// Physis and rendering...
}
}
后续可以直接继承Enitity类实现update方法,另外可以使用组件模式来改进继承的方案。
设计决策
update方法依存于何类中
- 实体类中
每当希望有新的表现就创建子类,这会积累大量的类而导致项目难以维护。你最终会发现你希望通过一种单一继承层次的优雅映射方式来复用代码块,那时候你就该傻眼了。 - 组件类中
- 代理类中
可以使用状态模式或者对象类型模式。这么做让你能在代理类之外定义新的行为方式。正像使用组件模式那样这为不得不定义新类和新的行为方式带来灵活性。
那些未被利用的对象该如何处理
一种方法是单独维护一个需要被更新的“存活”对象表。
- 假如你使用单个集合来存储所有游戏对象
你在浪费时间。对于暂时无用的对象,你需要检查他们死否死亡的标志,或者调用一个空方法。 - 假如你使用一个单独的集合来维护活跃的对象
你将使用额外的内存来维护这2个集合。
另外一个方法是,同样维护两个集合,但另外一个只维护那些未被激活的对象,而不是维护所有对象。
你必须保持两个集合同步。当对象被创建或者销毁时,你需要同时修改两个集合。
Unity的Monobehavior以及XNA的Game以及GameComponent均使用了这一模式。