游戏编程模式08-序列模式-双缓冲模式

序列模式-双缓冲模式

参考章节:https://gpp.tkchu.me/double-buffer.html

脑内画面

双缓冲模式准备两份数据:一份给外界稳定读取,一份在幕后写入。写完后交换两者。它像剧场换景:观众看到的是前台布景,工作人员在后台准备下一幕,准备好后一次性切换。
#mermaid-svg-SiV5Z2jRn1XioyC4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SiV5Z2jRn1XioyC4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SiV5Z2jRn1XioyC4 .error-icon{fill:#552222;}#mermaid-svg-SiV5Z2jRn1XioyC4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SiV5Z2jRn1XioyC4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SiV5Z2jRn1XioyC4 .marker.cross{stroke:#333333;}#mermaid-svg-SiV5Z2jRn1XioyC4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SiV5Z2jRn1XioyC4 p{margin:0;}#mermaid-svg-SiV5Z2jRn1XioyC4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SiV5Z2jRn1XioyC4 .cluster-label text{fill:#333;}#mermaid-svg-SiV5Z2jRn1XioyC4 .cluster-label span{color:#333;}#mermaid-svg-SiV5Z2jRn1XioyC4 .cluster-label span p{background-color:transparent;}#mermaid-svg-SiV5Z2jRn1XioyC4 .label text,#mermaid-svg-SiV5Z2jRn1XioyC4 span{fill:#333;color:#333;}#mermaid-svg-SiV5Z2jRn1XioyC4 .node rect,#mermaid-svg-SiV5Z2jRn1XioyC4 .node circle,#mermaid-svg-SiV5Z2jRn1XioyC4 .node ellipse,#mermaid-svg-SiV5Z2jRn1XioyC4 .node polygon,#mermaid-svg-SiV5Z2jRn1XioyC4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SiV5Z2jRn1XioyC4 .rough-node .label text,#mermaid-svg-SiV5Z2jRn1XioyC4 .node .label text,#mermaid-svg-SiV5Z2jRn1XioyC4 .image-shape .label,#mermaid-svg-SiV5Z2jRn1XioyC4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-SiV5Z2jRn1XioyC4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SiV5Z2jRn1XioyC4 .rough-node .label,#mermaid-svg-SiV5Z2jRn1XioyC4 .node .label,#mermaid-svg-SiV5Z2jRn1XioyC4 .image-shape .label,#mermaid-svg-SiV5Z2jRn1XioyC4 .icon-shape .label{text-align:center;}#mermaid-svg-SiV5Z2jRn1XioyC4 .node.clickable{cursor:pointer;}#mermaid-svg-SiV5Z2jRn1XioyC4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SiV5Z2jRn1XioyC4 .arrowheadPath{fill:#333333;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SiV5Z2jRn1XioyC4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SiV5Z2jRn1XioyC4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SiV5Z2jRn1XioyC4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SiV5Z2jRn1XioyC4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SiV5Z2jRn1XioyC4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SiV5Z2jRn1XioyC4 .cluster text{fill:#333;}#mermaid-svg-SiV5Z2jRn1XioyC4 .cluster span{color:#333;}#mermaid-svg-SiV5Z2jRn1XioyC4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SiV5Z2jRn1XioyC4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SiV5Z2jRn1XioyC4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-SiV5Z2jRn1XioyC4 .icon-shape,#mermaid-svg-SiV5Z2jRn1XioyC4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SiV5Z2jRn1XioyC4 .icon-shape p,#mermaid-svg-SiV5Z2jRn1XioyC4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SiV5Z2jRn1XioyC4 .icon-shape .label rect,#mermaid-svg-SiV5Z2jRn1XioyC4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SiV5Z2jRn1XioyC4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SiV5Z2jRn1XioyC4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SiV5Z2jRn1XioyC4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读者看到

Current Buffer
交换引用
系统写入

Next Buffer
下一帧读者看到

刚写好的 Buffer

它解决的问题

如果渲染器正在读取一张画面,而游戏逻辑同时往里面写,玩家可能看到半更新的状态。双缓冲通过"读一份、写一份、最后交换"保证外界看到的永远是完整快照。

C# 示例

csharp 复制代码
public sealed class GridBuffer
{
    private readonly char[,] _cells;

    public int Width => _cells.GetLength(0);
    public int Height => _cells.GetLength(1);

    public GridBuffer(int width, int height)
    {
        _cells = new char[width, height];
        Clear('.');
    }

    public char this[int x, int y]
    {
        get => _cells[x, y];
        set => _cells[x, y] = value;
    }

    public void Clear(char value)
    {
        for (var x = 0; x < Width; x++)
        for (var y = 0; y < Height; y++)
        {
            _cells[x, y] = value;
        }
    }
}

public sealed class DoubleBufferedGrid
{
    private GridBuffer _current;
    private GridBuffer _next;

    public DoubleBufferedGrid(int width, int height)
    {
        _current = new GridBuffer(width, height);
        _next = new GridBuffer(width, height);
    }

    public GridBuffer Current => _current;
    public GridBuffer Next => _next;

    public void DrawNextFrame(IEnumerable<(int X, int Y, char Icon)> sprites)
    {
        _next.Clear('.');

        foreach (var sprite in sprites)
        {
            _next[sprite.X, sprite.Y] = sprite.Icon;
        }
    }

    public void Swap()
    {
        (_current, _next) = (_next, _current);
    }
}

什么时候用

  • 渲染、模拟、寻路、可见性数据需要稳定快照。
  • 读者不应看到写入过程中的中间状态。
  • 同一帧内有多个系统写入,最终要一次性提交结果。

使用时的锋利边

双缓冲会让内存翻倍,也会引入"交换时机"的规则。它适合数据块整体替换,不适合每次只改几个字段的小对象。若数据很大,可以只双缓冲索引、命令列表或脏区域。