在线阅读:
参考文章:
目录
序列模式
游戏循环是时钟的中心轴。 对象通过更新方法来聆听时钟的滴答声。 我们可以用双缓冲模式存储快照来隐藏计算机的顺序执行,这样看起来世界可以进行同步更新。
双缓冲模式
用序列的操作模拟瞬间或者同时发生的事情。
定义缓冲类封装了缓冲:一段可改变的状态。 这个缓冲被增量地修改,但我们想要外部的代码将修改视为单一的原子操作。 为了实现这点,类保存了两个缓冲的实例:下一缓冲和当前缓冲。
当信息从缓冲区中读取,它总是读取当前的缓冲区。 当信息需要写到缓存,它总是在下一缓冲区上操作。 当改变完成后,一个交换操作会立刻将当前缓冲区和下一缓冲区交换, 这样新缓冲区就是公共可见的了。旧的缓冲区成为下一个重用的缓冲区。
何时使用
- 我们需要维护一些被增量修改的状态。
- 在修改到一半的时候,状态可能会被外部请求。
- 我们想要防止请求状态的外部代码知道内部的工作方式。
- 我们想要读取状态,而且不想等着修改完成。
提醒:
交换本身需要时间
我们得保存两个缓冲区
class Scene
{
public:
Scene()
: current_(&buffers_[0]),
next_(&buffers_[1])
{}
void draw()
{
next_->clear();
next_->draw(1, 1);
// ...
next_->draw(4, 3);
swap();
}
Framebuffer& getBuffer() { return *current_; }
private:
void swap()
{
// 只需交换指针
Framebuffer* temp = current_;
current_ = next_;
next_ = temp;
}
Framebuffer buffers_[2];
Framebuffer* current_;
Framebuffer* next_;
};
缓冲区是如何被交换的
在执行交换操作的时候,我们必须锁住两个缓冲区上的读取和修改,为了让性能最优,我们需要它进行的越快越好
交换缓冲区的指针或引用:
-
速度快。 不管缓冲区有多大,交换都只需赋值一对指针。很难在速度和简易性上超越它。
-
外部代码不能存储对缓存的永久指针。 这是主要限制。 由于我们没有真正地移动数据,本质上做的是周期性地通知代码库的其他部分到别处去寻找缓存, 就像前面的舞台类比一样。这就意味着代码库的其他部分不能存储指向缓冲区中数据的指针—— 它一段时间后可能就指向了错误的部分。
这会严重误导那些期待缓冲帧永远在内存中的固定地址的显卡驱动。在这种情况下,我们不能这么做。
-
缓冲区中的数据是两帧之前的数据,而不是上一帧的数据。
在缓冲区之间拷贝数据
-
下一帧的数据和之前的数据相差一帧。 拷贝数据与在两块缓冲区间跳来跳去正相反。 如果我们需要前一帧的数据,这样我们可以处理更新的数据。
-
交换也许更花时间。 这个当然是最大的缺点。交换操作现在意味着在内存中拷贝整个缓冲区。 如果缓冲区很大,比如一整个缓冲帧,这需要花费可观的时间。 由于交换时没有东西可以读取或者写入任何一个缓冲区,这是一个巨大的限制。
缓冲的粒度如何
如果缓存是一整块
- 交换操作更简单。 由于只有一对缓存,一个简单的交换就完成了。 如果可以改变指针来交换,那么不必在意缓冲区大小,只需几部操作就可以交换整个缓冲区。
如果很多对象都持有一块数据
交换操作更慢。 为了交换,需要遍历整个对象集合,通知每个对象交换。
游戏循环
将游戏的进行和玩家的输入解耦,和处理器速度解耦。
游戏循环的第一个关键部分:它处理用户输入,但是不等待它
模式:
一个游戏循环在游玩中不断运行。 每一次循环,它无阻塞地处理玩家输入,更新游戏状态,渲染游戏。 它追踪时间的消耗并控制游戏的速度。
更新方法
通过每次处理一帧的行为模拟一系列独立对象。
游戏世界管理对象集合。 每个对象实现一个更新方法模拟对象在一帧内的行为。每一帧,游戏循环更新集合中的每一个对象。
更新方法适应以下情况:
-
你的游戏有很多对象或系统需要同时运行。
-
每个对象的行为都与其他的大部分独立。
-
对象需要跟着时间进行模拟。