Double-Buffer双缓冲——游戏编程模式(下)

来源:互联网 发布:淘宝评价页面代码 编辑:程序博客网 时间:2024/06/07 09:27

Double Buffer(下)

原文:Double Buffer-Game Programming Patterns

不单单是对图形绘制

双缓冲的核心是解决了区块被访问时不能被修改的问题。这里有两个通用的情况。我们通过图形绘制的例子已经讲了第一种情况——区块可以被另外一个线程直接的访问或中断。

这里有另外一个相同的例子,思考:当代码在做修改操作,并且在相同的地方存在访问。这可以存在一个可变的地方,尤其是物理和AI中,这种情况,你已经进入和其他物体的交互中。双缓冲通常是有帮助的。

人工低能

我们正在建立一个行为系统,对任何事物,一个游戏基于一个喜剧, 包含了一个舞台,包括一批跑来跑去的演员,并且有各种狂欢和闹剧。这里有一个基本的行为:

<span style="font-size:18px;">class Actor{public:  Actor() : slapped_(false) {}  virtual ~Actor() {}  virtual void update() = 0;  void reset()      { slapped_ = false; }  void slap()       { slapped_ = true; }  bool wasSlapped() { return slapped_; }private:  bool slapped_;};</span>

每一帧,这个游戏有责任区调用Actor中的update()方法,所以Actor有机会去做一些处理。从用户眼光来看,所有演员的行为都应该同时表现出来。

演员也可以同其他演员交互,通过“交互”指令,这个指令在当前的含义是“他们可以呼(掴掌)周围的演员“。当更新的时候,演员可以调用slap()方法去呼其他人,之后调用wasSlapped()来决定他是否呼过了别人。

演员需要一个舞台,这儿他们可以交互,所以让我们建一个:

<span style="font-size:18px;">class Stage{public:  void add(Actor* actor, int index)  {    actors_[index] = actor;  }  void update()  {    for (int i = 0; i < NUM_ACTORS; i++)    {      actors_[i]->update();      actors_[i]->reset();    }  }private:  static const int NUM_ACTORS = 3;  Actor* actors_[NUM_ACTORS];};</span>

舞台可以让我们添加演员,提供一个单一 的update()调用,更新每一个演员。对于用户,演员的移动是立刻的,但是内部来看,这是每次进行一个更新操作。

另外一点需要注意,每个演员的”呼“状态会在每次更新后被清除。这就是每次响应,演员只会”呼“一次。

继续,让我们定义一个具体的演员的子类。每当他获得一个“呼”的行为——被任何人“呼”过之后,他会给“呼”一个指定的人。

<span style="font-size:18px;">class Comedian : public Actor{public:  void face(Actor* actor) { facing_ = actor; }  virtual void update()  {    if (wasSlapped()) facing_->slap();  }private:  Actor* facing_;};</span>


现在,让我们丢出来一些喜剧演员在这个舞台上,让我们看看发生了什么。我们会设定三个喜剧演员,每一个呼下一个。最后一个呼第一个,一个大的循环:

<span style="font-size:18px;">Stage stage;Comedian* harry = new Comedian();Comedian* baldy = new Comedian();Comedian* chump = new Comedian();harry->face(baldy);baldy->face(chump);chump->face(harry);stage.add(harry, 0);stage.add(baldy, 1);stage.add(chump, 2);</span>


这个舞台的设定可以看做以下的图片。箭头展示了演员会呼谁,数字代表了舞台数组中他们的索引。


我们开始呼 Harry让事情进行下去,当开始处理时,看看发生了什么:

<span style="font-size:18px;">harry->slap();stage.update();</span>

记住update()方法在舞台的更新每个演员会做一次处理。所以如果我们跟踪代码,我们会发现将发生以下事情:

<span style="font-size:18px;">Stage updates actor 0 (Harry)  Harry was slapped, so he slaps BaldyStage updates actor 1 (Baldy)  Baldy was slapped, so he slaps ChumpStage updates actor 2 (Chump)  Chump was slapped, so he slaps HarryStage update ends</span>

在一个单个帧中,我们最初呼在Harry脸上传播到了所有的其他的喜剧演员。现在,让事情乱一些,我们重新排列了喜剧演员在舞台的数组顺序,但是他们想要呼的人依然是相同的顺序。

我们让舞台建立起来,但是我们代替了之前的一块代码,这儿我们在舞台上添加演员的代码为:

<span style="font-size:18px;">stage.add(harry, 2);stage.add(baldy, 1);stage.add(chump, 0);</span>
重新执行一次,让我们看看,什么事情发生了:
<span style="font-size:18px;">Stage updates actor 0 (Chump)  Chump was not slapped, so he does nothingStage updates actor 1 (Baldy)  Baldy was not slapped, so he does nothingStage updates actor 2 (Harry)  Harry was slapped, so he slaps BaldyStage update ends</span>
完全不一样!这个问题很直接。当我们更新演员时,我们修改了他们的“slapped”的状态,这个相同的状态,我们也会在更新时读取。因此,update导致的状态变动,会影响后续update的步骤。

最终的结果是,一个演员响应被掌掴的方式要依赖于演员在舞台上的顺序。这违反了我们在运行时声明的并行性——他们更新的顺序不应该在一帧中受到影响。


掴掌缓冲区

幸运的是,我们的双缓冲模式可以起到帮助。在这里,代替两块“缓冲”体,我们会缓存一个更好的粒度:每个演员的“掴掌”状态:

class Actor{public:  Actor() : currentSlapped_(false) {}  virtual ~Actor() {}  virtual void update() = 0;  void swap()  {    // Swap the buffer.    currentSlapped_ = nextSlapped_;    // Clear the new "next" buffer.    nextSlapped_ = false;  }  void slap()       { nextSlapped_ = true; }  bool wasSlapped() { return currentSlapped_; }private:  bool currentSlapped_;  bool nextSlapped_;};
代替单一的slapped_状态,每一个演员现在有两个该状态。就像先前的图形显示的例子,当前状态被用来读,下一个状态被用来写。

reset()方法已经被swap()方法代替。现在,正如指尖的交换状态,它会复制下一个状态到当前的状态中,使它变成一个新的状态。这也需要Stage舞台类做一个小的改变:

void Stage::update(){  for (int i = 0; i < NUM_ACTORS; i++)  {    actors_[i]->update();  }  for (int i = 0; i < NUM_ACTORS; i++)  {    actors_[i]->swap();  }}
update()方法现在更新了所有的演员,并且之后会交换了他们所有的状态。结果是演员会看到掴掌的行为是在实际应该掴掌的下一帧中出现。这个方式,演员的行为与他们在舞台数组上的顺序无关。到目前为止,用户或者外部代码看起来所有的演员会在同意时刻更新自己的行为。

设计讨论

双缓冲是一个相当直接的模式,以上的例子覆盖了会遇到的大部分情况。这里有两个主要的讨论需要提及。

缓存区是如何被交换?

交换操作是一个最具有讨论性的步骤。当需要交换时,我们必须锁上说有读和修改的缓冲区。为了达到最好的性能表现,我们想让它尽可能的快。

1.交换缓冲区的指针或者引用

这就是我们图形显示中的例子所做的事情,它是解决双缓冲区域最通用的方案。

(1)它很快,不管多大的缓冲区,这个交换只是简单的指针赋值。它在速度和简单程度上没什么要评论的。

(2)外部代码不能保存指向内存的指针。这是一个主要的限制。因为我们并没有真正的移动数据,我们所做的最主要的事情是定时告诉剩下的基本代码去看其他地方的缓冲区,就像最初的分析一样。这里的意思是剩下的代码不能存贮一个直接指向缓冲区的指针—— 他们可能在之后指向一个错误的地方。

系统中可能会出现一些麻烦,显示驱动器的缓存区域框架总是在一个指定的内存位置。在这种情况我们

不能使用这个方式。

(3)存进的缓存的数据将会是两帧之前的数据,而不是最新的。连续的帧被画在可选的缓存区中,在他们之间没有数据拷贝操作的出现,就像:

Frame 1 drawn on buffer AFrame 2 drawn on buffer BFrame 3 drawn on buffer A...
你会注意到,当我们绘制第三帧的时候,数据已经在这个缓存区里面了,不是最近的第二帧。在大多数情况下,这不是一个问题——我们经常在绘制之前清除整个缓冲区。但是如果我们有意的重用一些存在的数据的话,显示先前的帧数据就显得比较重要了。


在两个缓冲区之间复制数据:

如果我们不能让用户重新指向另外一个缓冲区,唯一的一个其他的方式是从另外一个帧中复制数据到当前帧。这也是我们的喜剧演员们掴掌的工作方式。在这种情况下,我们选择这个方式,因为这个状态——单一的Boolen标记,它的复制操作不会超过更改指针的操作所用的时间。

(1)数据在下一个缓冲区仅仅是单一的旧的帧。对于复制数据一件好事,使用来回循环的方在两个缓冲区之间变换。如果我们访问了之前的缓冲区数据,会给我们最新的数据。

(2)交换可能花费更多的时间。当谈,这是一个很大的缺点。我们的交换操作现在意味着复制复制整个缓冲区的内存数据。如果缓冲区很大,可能导致一个明显的卡顿时间。因为这期间不能有数据的读或者写,这是一个很大的限制。


2.什么是内存的颗粒度?

另外一个问题是,这块内存自己应该如何被组织起来。这是一个单个很大块的数据护着物体的收集吗?或者图形显示的例子使用了前者演员使用了后者。

大多数情况,你所缓存的东西将会引导你找到答案。但是这里有些灵活的东西。例如,我们的演员会存放他们的信息到一个单一的信息块中,他们能获得这些信息快的引用。

(1)如果缓存是一大块:

交换是很简单的。如果只是一块缓存,那就做一个简单的交换,如果你可以通过指针进行换,那么就能够交换整个缓冲区,不管有多大。

(2)如果很多对象都有一小块缓存:
交换是很慢的。为了交换,我们需要把他们放到一个集合中,一个一个的进行迭代并执行交换操作。


在我们的喜剧演员的例子中,这是没问题的,我们无论如何都需要清理下一个掴掌状态——每一个缓存块都需要被重新描述。如果我们不需要重新考虑旧数据,这里有一个简单的优化方式,我们能够做在性能上和大数据块一样,那么:

class Actor{public:  static void init() { current_ = 0; }  static void swap() { current_ = next(); }  void slap()        { slapped_[next()] = true; }  bool wasSlapped()  { return slapped_[current_]; }private:  static int current_;  static int next()  { return 1 - current_; }  bool slapped_[2];};

演员通过current_索引访问数组,以获得他们当前的掴掌状态。下一个状态总是数组的另外一个索引,所以我们可以通过next()来计算。交换状态通过简单的使用current_即可。这点灵巧使swap()变成了一个静态的方法——它仅仅需要被调用一次——仅仅需要被调用一次,所有的状态都会被交换。


参考

你可以发现在使用任何图形显示的API中发现双缓冲模式。例如,OpenGL有sqapBuffers(),Direct3D有"交换链",微软的XNA框架通过endDraw()方法,交换缓冲区。


双缓冲翻译(上)http://blog.csdn.net/u012289636/article/details/46864043

0 0
原创粉丝点击