这看起来很直观,但如果我们就保持这样,会带来问题。问题在于显卡驱动可以在任意时刻调用getPixels,甚至是在这里:
- buffer_.draw(1, 1);
- buffer_.draw(4, 1);
- // <- Video driver reads pixels here!
- buffer_.draw(1, 3);
- buffer_.draw(2, 4);
- buffer_.draw(3, 4);
- buffer_.draw(4, 3);
现在滚动Harry来驱动这些东西,然后看会发生什么:
- harry->slap();
-
- stage.update();
-
舞台其他的代码不变,我们只是改变一下添加喜剧演员代码的顺序:
- stage.add(harry, 2);
- stage.add(baldy, 1);
- stage.add(chump, 0);
- stage.add(baldy, 1);
让我们看一下再次运行之后的结果:
- Stage updates actor 0 (Chump)
- Chump was not slapped, so he does nothing
- Stage updates actor 1 (Baldy)
- Baldy was not slapped, so he does nothing
- Stage updates actor 2 (Harry)
- Harry was slapped, so he slaps Baldy
- Stage update ends
- Harry was slapped, so he slaps Baldy
- Stage updates actor 2 (Harry)
- Baldy was not slapped, so he does nothing
- Stage updates actor 1 (Baldy)
- Chump was not slapped, so he does nothing
厄,完全不一样。问题也很明显。更新这些演员时,我们修改了他们的“slapped”状态,然后在同一次更新中读取这个状态。因此,先在update中改变状态,然后在同一个update中去影响下面的部分。
最终的结果是一个演员可能在同一次update中去响应滚动操作,也可能在下一帧,这取决于这两个演员在舞台上的顺序。这违反了我们的一个需求,就是演员要表现出并行运行的状态——不应该关心他们更新的顺序。
被缓冲的滚动
幸运的是,双缓冲模式可以帮助我们。这次不再维护两份整个的缓冲区对象,而是采用更合适的粒度:每一个演员的slaped状态:
- 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_;
- };
- bool nextSlapped_;
- bool currentSlapped_;
-
- bool wasSlapped() { return currentSlapped_; }
-
- }
- nextSlapped_ = false;
-
- currentSlapped_ = nextSlapped_;
- // Swap the buffer.
- {
-
- virtual void update() = 0;
-
- Actor() : currentSlapped_(false) {}
- public:
- {
不再只有一个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();
- }
- }
- }
- actors_[i]->swap();
- {
-
- }
- actors_[i]->update();
- {
- for (int i = 0; i < NUM_ACTORS; i++)
- {
update()函数现在更新所有的演员,然后统一翻转他们的状态。结果是一帧里只能看到一个被slap演员的slap状态,而不再与他们在舞台上的顺序有关。只要玩家或者任意的外部代码调用,都是所有的演员在一帧中同时被更新。
设计决策
双缓冲模式很直观,上面的例子也基本覆盖了大多数你会遇到的情况。当实现这个模式时,有两个决策点你需要关注。
如何翻转缓冲区?
翻转操作是这个过程最重要的一步,因为当它发生的时候,我们必须锁住两个缓冲区的所有读写操作。为了得到最高的效率,我们希望越快越好。
1、交换缓冲区的指针:
这是我们图形例子中用到的,也是双缓冲图形中通用的解决方案。
1)它很快。与缓冲区多大没有关系,这种交换只是简单的两个指针引用。很难再有比它更快更简单的了。
2)外部代码不能保存这个指向缓冲区的指针。这是主要的局限性。我们没有真正地移动数据,本质上是我们定时通知外部代码去缓冲区的其他部分寻找数据,跟前面舞台例子中类似。这意味着外部的代码不能保存那个直接指向缓冲区的指针——因为他们过一会可能就会指向一个错误的位置。
在一个显卡希望帧缓冲区是一个固定位置的系统中,这可能是个大问题。我们不会再选择这样的方式。
3)当前缓冲区的数据是来自两帧前的,而不是前一帧的。连续的帧被写入两个交替的缓冲区中,而且他们直接也不存在数据拷贝,就像这样:
- Frame 1 drawn on buffer A
- Frame 2 drawn on buffer B
- Frame 3 drawn on buffer A
- ...
- Frame 3 drawn on buffer A
- Frame 2 drawn on buffer B
你会发现当我们要写入第三帧数据时,缓冲区中的数据是来自第一帧,而不是更接近的第二帧。大多数情况下,没问题——我们通常在绘制之前清理掉当前的缓冲区。如果我们试图重用缓冲区中的某些数据,那就需要考虑到数据是按照帧序列的,这超出了我们的期望。
2、缓冲区之间复制数据:
如果我们不能重新指向其他缓冲区,那唯一能做的就是老老实实地从下一个缓冲区把数据拷贝到当前缓冲区。这就是我们前面喜剧的例子中用到的。在这个例子中,我们用这种方法,因为状态——只是一个布尔值——不会比复制一个指针消耗更大。
1)下一个缓冲区的数据是唯一的份老数据。这就就是在两个缓冲区像乒乓球一样来回来去复制数据的好处。如果我们要访问上一个缓冲区的数据,这将是最接近的老数据。
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];
- };
-
- static int next(){ return 1 - current_; }
- static int current_;
-
- bool wasSlapped(){ return slapped_[current_]; }
-
- static void swap() { current_ = next(); }
- static void init() { current_ = 0; }
- public:
- {
演员使用current_去从状态数组中索引当前的状态。下一个状态永远是数组中的另一个值,所以我们可以用next()函数计算。翻转状态就变成了简单的更换current_索引。这里聪明一点是swap()现在是静态函数——只需要执行一次,所有的演员就会翻转状态。
扩展阅读
你会发现双缓冲模式被几乎所有的图形API使用。如OpenGL的swqpBuffers(), Direct3D 有swapChains,还有微软的XNA framework 在它的endDraw()方法中翻转缓冲区。
Wantgame编译 【原译文】
相关阅读:游戏编程设计模式-state
![锐亚教育](http://www.insideria.cn/files/default/2017/03-19/164933d4e4bd117214.jpg)
锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛
- 还没有人评论,欢迎说说您的想法!