代码改变世界

游戏编程模式之双缓冲模式

2021-10-21 10:23  ZhuSenlin  阅读(161)  评论(0编辑  收藏  举报

问题引入

  尽管计算机的处理能力相比过去有了极大的进步。但对于实时渲染的游戏程序,仍旧不能在一瞬间将同屏的所有物件全部加载出来。即使可以,用户的硬件条件参差不齐,若运行在性能较低的机器,用户将看到破碎断续加载的画面。

  从更底层形象地看断续加载的原因,就要了解画面是如何绘制的。计算机维护着一个帧缓冲区,游戏想要显示画面,就需要将像素颜色信息填写到帧缓冲区中。而显示设备就从帧缓冲区中读取像素信息并会知道屏幕上。这时候就会出现一个同步问题——当显示设备从帧缓冲区读取到计算机未填充的信息时,就会照成原本想要绘制的画面像素缺失,导致画面撕裂,从而表现为断续加载。

  如何解决计算填充像素能力跟不上显示器读取像素的频率的问题,双缓冲给出了一个比较好的解决方案。

双缓冲方案

  两个人赛跑,已知其中一个速度快,一个速度慢。那么如何让那个速度跑的慢的长时间能够保证领先于速度快的人?很简单,就是让速度慢的人提前跑很长一段时间。双缓冲的原理就是,计算机维护两个帧缓冲区,当计算机填充好一个帧缓冲区后,就交给显示设备进行读取,再显示设备读取此帧缓冲区的过程中,计算机已经开始填充另一个缓冲区。这样的错开使得每一次显示设备都能获取到数据完整的帧缓冲区,也不会出现断续加载、画面撕裂的情况。

  双缓冲模式抽象出来,可以总结出它的使用范围:

  • 我们需要维护一些被逐步改变的、数据量较大的状态量
  • 这两个状态可能存在读写同步的问题(可能存在同时进行读和写操作)
  • 我们希望避免访问状态的代码能看到具体的工作过程
  • 我们希望读和写操作不要相互等待,尤其是读操作等待写操作完成

双缓冲注意事项

  • 交换本身需要时间。在图形渲染API中,交换时间就是两个指针交换值的时间。注意这个时间,当交换时间大于等待时间,双缓冲就没有了意义。
  • 双缓冲增加了内存的使用。相比于单缓冲,双缓冲多维护了一个缓冲区。这就是用空间换时间。

示例代码

//帧缓冲区
class FrameBuffer
{
    public:
        static const int screen_width=1920;
        static const int screen_heigh=1080;
    
        void SetPixel(int x,int y,Color c)
        {
            if(x<screen_width&&y<screen_heigh)
                pixels[x][y]=c;
        }
    
        Color GetPixel(int x,int y)
        {
            if(x<screen_width&&y<screen_heigh)
                return pixels[x][y];
            else
                return Color.Default;
        }
        
        void Clear()
        {
            for(int i=0;i<screen_width;i++)
                for(int j=0;j<screen_heigh;j++)
                   pixels[i][j]=Color.White;  
        }
        
        Color* GetPixels()
        {
            return pixels;
        }
    
    private:
        Color pixels[screen_width][screen_heigh];
}


//渲染类
class Render
{
    public:
        void Draw()
        {
            FrameBuffer buffer=frames[drawCount%(FRAME_COUNT-1)];
            //填充数据
            buffer.SetPixel(0,0,Color.red);
            //...
            
            Swap();
        }
        
        void Swap()
        {
            currentFrame=drawCount%(FRAME_COUNT-1);
        }
        
        FrameBuffer GetDisplayBuffer()
        {
            return frames[currentFrame];
        }

    private:
        static const int FRAME_COUNT=2;
        FrameBuffer frames[FRAME_COUNT];
        int currentFrame=0;
        int drawCount=0;
}

//显示设备类
class DisplayAdaptor
{
    public:
        void Display()
        {
            FrameBuffer buffer=GetDisplayBuffer();
            //...
        }
}