02 | Raylib渲染架构

本文目录

  • [🎨 2 | Raylib 渲染架构 ------ 从 Camera2D 踩坑到 RenderTexture2D](#🎨 2 | Raylib 渲染架构 —— 从 Camera2D 踩坑到 RenderTexture2D)
    • [📌 1. 问题:窗口该多大?](#📌 1. 问题:窗口该多大?)
    • [❌ 2. Phase 1:固定窗口 + SetWindowSize](#❌ 2. Phase 1:固定窗口 + SetWindowSize)
    • [❌ 3. Phase 2:Camera2D ------ 理想很丰满](#❌ 3. Phase 2:Camera2D —— 理想很丰满)
      • [🕳️ 坑 1:黑边](#🕳️ 坑 1:黑边)
      • [🕳️ 坑 2:Camera2D + Texture 管线冲突](#🕳️ 坑 2:Camera2D + Texture 管线冲突)
      • [🕳️ 坑 3:缩放锯齿](#🕳️ 坑 3:缩放锯齿)
    • [✅ 4. Phase 3:RenderTexture2D ------ 最终方案](#✅ 4. Phase 3:RenderTexture2D —— 最终方案)
      • [4.1 核心架构](#4.1 核心架构)
      • [4.2 鼠标坐标转换](#4.2 鼠标坐标转换)
      • [4.3 背景图的处理](#4.3 背景图的处理)
    • [🚀 5. Phase 4:全屏高清( canvasMult )](#🚀 5. Phase 4:全屏高清( canvasMult ))
      • [5.1 问题](#5.1 问题)
      • [5.2 解决方案](#5.2 解决方案)
    • [📊 6. 架构演进总结](#📊 6. 架构演进总结)
    • [🔜 下篇预告](#🔜 下篇预告)
    • [📚 系列目录](#📚 系列目录)

🎯 系列第 2 篇 · 前面我们讲了扫雷的核心算法,现在来看这些内容是怎么渲染到屏幕上的。

📖 上一篇项目概览与扫雷核心算法


🎨 2 | Raylib 渲染架构 ------ 从 Camera2D 踩坑到 RenderTexture2D


📌 1. 问题:窗口该多大?

扫雷的棋盘尺寸是动态的:

难度 棋盘像素(40px 格子) 需要窗口
Beginner 9×9 360 × 360 很小就够了
Expert 16×30 1200 × 640 需要大窗口

如果每次切难度都 SetWindowSize() ------ Windows DPI 缩放会干预,窗口忽大忽小,全屏时布局全乱。

解决方案迭代了 4 个阶段。前两个是坑,后两个是答案。


❌ 2. Phase 1:固定窗口 + SetWindowSize

c 复制代码
int winW = game.cols * CELL_SIZE + 2 * PADDING;
int winH = game.rows * CELL_SIZE + UI_PANEL_HEIGHT + PADDING;
SetWindowSize(winW, winH);

📋 Phase 1 的问题SetWindowSize() 每次切难度窗口会跳变。下面是当时记录的典型问题:

  • ❌ Beginner → Expert:窗口从 400×500 跳到 1300×680,肉眼可见闪烁
  • ❌ Windows DPI 缩放抢夺控制权,窗口有时仅 200×200
  • ❌ 全屏/窗口切换时布局完全错位

问题清单

问题 严重性
Windows DPI 缩放使窗口只有 200×200 ❌ 致命
切难度窗口跳变 ⚠️ 体验差
全屏/窗口切换布局重算 ⚠️ 不可靠
用户不能自由拖拽缩放 ❌ 不友好

💡 教训 :永远不要让窗口尺寸跟着游戏内容变。用固定画布 + 自动缩放代替。


❌ 3. Phase 2:Camera2D ------ 理想很丰满

改用固定内部画布(960×640),用 Raylib 的 Camera2D 把画布坐标自动映射到窗口:

c 复制代码
Camera2D camera = {0};
camera.zoom = fminf((float)screenW / CANVAS_WIDTH,
                    (float)screenH / CANVAS_HEIGHT);
BeginMode2D(camera);
// 所有绘制用 960×640 坐标
EndMode2D();

鼠标坐标用 GetScreenToWorld2D() 转换回画布空间。

🕳️ 坑 1:黑边

Camera2D 只渲染画布区域,宽高比不一致时左右/上下全是黑边。背景图填不满。

🕳️ 坑 2:Camera2D + Texture 管线冲突

c 复制代码
BeginTextureMode(canvasTarget);
BeginMode2D(camera);  // 嵌套使用!渲染管线冲突

Raylib 的这两个模式嵌套时缩放行为不可预测。高 DPI 窗口下文字画错位置、点击区域完全偏移。

🕳️ 坑 3:缩放锯齿

Camera2D 用 GL_NEAREST,对棋盘像素风格是好事,但文字全是锯齿。

🐛 代价6 个 Fix(#77-#82)实现 Camera2D 方案,又用 6 个 Fix(#108-#112)全部推翻。


✅ 4. Phase 3:RenderTexture2D ------ 最终方案

4.1 核心架构

c 复制代码
// 1️⃣ 创建固定画布纹理
canvasTarget = LoadRenderTexture(CANVAS_WIDTH, CANVAS_HEIGHT);

// 2️⃣ 每帧先画到纹理
BeginTextureMode(canvasTarget);
ClearBackground(BLANK);
// ... 所有游戏内容(用 1280×720 坐标)
DrawToast();
EndTextureMode();

// 3️⃣ 等比缩放 + 居中,blit 到窗口
float canvasScale = fminf((float)screenW / CANVAS_WIDTH,
                          (float)screenH / CANVAS_HEIGHT);
float offsetX = (screenW - CANVAS_WIDTH * canvasScale) / 2.0f;
float offsetY = (screenH - CANVAS_HEIGHT * canvasScale) / 2.0f;

// 🖼️ blit 时用 bilinear 过滤,避免马赛克
SetTextureFilter(canvasTarget.texture, TEXTURE_FILTER_BILINEAR);

Rectangle src = {0, 0, CANVAS_WIDTH, -CANVAS_HEIGHT};
Rectangle dst = {offsetX, offsetY,
                 CANVAS_WIDTH * canvasScale,
                 CANVAS_HEIGHT * canvasScale};
DrawTexturePro(canvasTarget.texture, src, dst, (Vector2){0,0}, 0.0f, WHITE);

🖼️ RenderTexture2D 渲染管线
#mermaid-svg-8ahyBQbLPxpelst0{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-8ahyBQbLPxpelst0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8ahyBQbLPxpelst0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8ahyBQbLPxpelst0 .error-icon{fill:#552222;}#mermaid-svg-8ahyBQbLPxpelst0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8ahyBQbLPxpelst0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8ahyBQbLPxpelst0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8ahyBQbLPxpelst0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8ahyBQbLPxpelst0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8ahyBQbLPxpelst0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8ahyBQbLPxpelst0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8ahyBQbLPxpelst0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8ahyBQbLPxpelst0 .marker.cross{stroke:#333333;}#mermaid-svg-8ahyBQbLPxpelst0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8ahyBQbLPxpelst0 p{margin:0;}#mermaid-svg-8ahyBQbLPxpelst0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8ahyBQbLPxpelst0 .cluster-label text{fill:#333;}#mermaid-svg-8ahyBQbLPxpelst0 .cluster-label span{color:#333;}#mermaid-svg-8ahyBQbLPxpelst0 .cluster-label span p{background-color:transparent;}#mermaid-svg-8ahyBQbLPxpelst0 .label text,#mermaid-svg-8ahyBQbLPxpelst0 span{fill:#333;color:#333;}#mermaid-svg-8ahyBQbLPxpelst0 .node rect,#mermaid-svg-8ahyBQbLPxpelst0 .node circle,#mermaid-svg-8ahyBQbLPxpelst0 .node ellipse,#mermaid-svg-8ahyBQbLPxpelst0 .node polygon,#mermaid-svg-8ahyBQbLPxpelst0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8ahyBQbLPxpelst0 .rough-node .label text,#mermaid-svg-8ahyBQbLPxpelst0 .node .label text,#mermaid-svg-8ahyBQbLPxpelst0 .image-shape .label,#mermaid-svg-8ahyBQbLPxpelst0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-8ahyBQbLPxpelst0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8ahyBQbLPxpelst0 .rough-node .label,#mermaid-svg-8ahyBQbLPxpelst0 .node .label,#mermaid-svg-8ahyBQbLPxpelst0 .image-shape .label,#mermaid-svg-8ahyBQbLPxpelst0 .icon-shape .label{text-align:center;}#mermaid-svg-8ahyBQbLPxpelst0 .node.clickable{cursor:pointer;}#mermaid-svg-8ahyBQbLPxpelst0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8ahyBQbLPxpelst0 .arrowheadPath{fill:#333333;}#mermaid-svg-8ahyBQbLPxpelst0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8ahyBQbLPxpelst0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8ahyBQbLPxpelst0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8ahyBQbLPxpelst0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8ahyBQbLPxpelst0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8ahyBQbLPxpelst0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8ahyBQbLPxpelst0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8ahyBQbLPxpelst0 .cluster text{fill:#333;}#mermaid-svg-8ahyBQbLPxpelst0 .cluster span{color:#333;}#mermaid-svg-8ahyBQbLPxpelst0 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-8ahyBQbLPxpelst0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8ahyBQbLPxpelst0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-8ahyBQbLPxpelst0 .icon-shape,#mermaid-svg-8ahyBQbLPxpelst0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8ahyBQbLPxpelst0 .icon-shape p,#mermaid-svg-8ahyBQbLPxpelst0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8ahyBQbLPxpelst0 .icon-shape .label rect,#mermaid-svg-8ahyBQbLPxpelst0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8ahyBQbLPxpelst0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8ahyBQbLPxpelst0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8ahyBQbLPxpelst0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 每帧渲染循环
🎮 游戏逻辑

UpdateGame()
🖌️ 画到画布

BeginTextureMode()

1280×720
📐 缩放计算

canvasScale = min(sW/1280, sH/720)
🖼️ Blit 到窗口

DrawTexturePro()
🖥️ 屏幕显示
🎯 鼠标输入

ScreenToCanvas()

4.2 鼠标坐标转换

c 复制代码
static Vector2 ScreenToCanvas(Vector2 screenPos) {
    return (Vector2){
        (screenPos.x - canvasOffsetX) / canvasScale,
        (screenPos.y - canvasOffsetY) / canvasScale
    };
}

⚠️ 对照 Fix #88 :如果不转换,按钮画在画布 (640, 200) 但碰撞检测以为在窗口 (640, 200)。窗口一缩放,点击完全错位

4.3 背景图的处理

背景图(泼墨风格 background.png)直接画到全屏窗口,不在画布内:

c 复制代码
BeginDrawing();
ClearBackground(BLACK);
// 画背景到全屏(窗口坐标系)
DrawTexturePro(bgTexture, src, (Rectangle){0,0, screenW, screenH}, ...);
// 画遮罩到全屏
DrawRectangle(0, 0, screenW, screenH, Fade(BLACK, 0.7f));
// 画游戏到画布(画布坐标系)
BeginTextureMode(canvasTarget);
// ...

这样窗口宽高比任意变化,背景都填满,画布永远居中。


🚀 5. Phase 4:全屏高清( canvasMult )

5.1 问题

1280×720 画布在全屏(2560×1600)被放大 2 倍以上 。即使 bilinear 过滤,文字边缘仍有"柔化感"。中文笔画密集,放大后笔画之间模糊粘连(Fix #221)。

📋 canvasMult 要解决的问题:全屏时 1280×720 画布被放大到 2~3 倍。

  • 修复前:中文笔画密集处在放大后互相粘连模糊(英文几乎无影响)
  • 修复后:canvasMult 将渲染目标重建为 2560×1440+,Camera2D 保持 1280×720 布局,下采样后中文清晰锐利

5.2 解决方案

c 复制代码
static void UpdateCanvasScale(void) {
    // ... 计算 canvasScale ...

    if (canvasScale > 1.0f) {
        int mult = (int)ceilf(canvasScale);
        if (mult < 1) mult = 1;
        if (mult > 4) mult = 4;
        targetW = CANVAS_WIDTH * mult;   // 1280 → 2560
        targetH = CANVAS_HEIGHT * mult;  // 720 → 1440
        canvasMult = mult;
    }

    // 重建高分辨率画布
    if (canvasTarget.texture.width != targetW) {
        UnloadRenderTexture(canvasTarget);
        canvasTarget = LoadRenderTexture(targetW, targetH);
        SetTextureFilter(..., TEXTURE_FILTER_BILINEAR);
    }
}

绘制时用 Camera2D.zoom = canvasMult 映射:

c 复制代码
BeginTextureMode(canvasTarget);
Camera2D cam = {0};
cam.zoom = (float)canvasMult;
BeginMode2D(cam);
// ... 所有 1280×720 布局代码零改动 ...
EndMode2D();
EndTextureMode();
显示器 canvasScale mult 实际 RT 分辨率
1280×720 1.0 1 1280×720
1920×1080 1.5 2 2560×1440
2560×1600 2.0 2 2560×1440
3840×2160 (4K) 3.0 3 3840×2160

🔑 关键 :所有现有布局代码零改动。Camera2D 自动把 1280×720 的绘制指令映射到高分辨率画布上,下采样回窗口时清晰度远超纯 1280 放大。


📊 6. 架构演进总结

🖼️ 架构演进路线
#mermaid-svg-vCghOdKMkM3RkXob{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-vCghOdKMkM3RkXob .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vCghOdKMkM3RkXob .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vCghOdKMkM3RkXob .error-icon{fill:#552222;}#mermaid-svg-vCghOdKMkM3RkXob .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vCghOdKMkM3RkXob .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vCghOdKMkM3RkXob .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vCghOdKMkM3RkXob .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vCghOdKMkM3RkXob .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vCghOdKMkM3RkXob .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vCghOdKMkM3RkXob .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vCghOdKMkM3RkXob .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vCghOdKMkM3RkXob .marker.cross{stroke:#333333;}#mermaid-svg-vCghOdKMkM3RkXob svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vCghOdKMkM3RkXob p{margin:0;}#mermaid-svg-vCghOdKMkM3RkXob .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vCghOdKMkM3RkXob .cluster-label text{fill:#333;}#mermaid-svg-vCghOdKMkM3RkXob .cluster-label span{color:#333;}#mermaid-svg-vCghOdKMkM3RkXob .cluster-label span p{background-color:transparent;}#mermaid-svg-vCghOdKMkM3RkXob .label text,#mermaid-svg-vCghOdKMkM3RkXob span{fill:#333;color:#333;}#mermaid-svg-vCghOdKMkM3RkXob .node rect,#mermaid-svg-vCghOdKMkM3RkXob .node circle,#mermaid-svg-vCghOdKMkM3RkXob .node ellipse,#mermaid-svg-vCghOdKMkM3RkXob .node polygon,#mermaid-svg-vCghOdKMkM3RkXob .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vCghOdKMkM3RkXob .rough-node .label text,#mermaid-svg-vCghOdKMkM3RkXob .node .label text,#mermaid-svg-vCghOdKMkM3RkXob .image-shape .label,#mermaid-svg-vCghOdKMkM3RkXob .icon-shape .label{text-anchor:middle;}#mermaid-svg-vCghOdKMkM3RkXob .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vCghOdKMkM3RkXob .rough-node .label,#mermaid-svg-vCghOdKMkM3RkXob .node .label,#mermaid-svg-vCghOdKMkM3RkXob .image-shape .label,#mermaid-svg-vCghOdKMkM3RkXob .icon-shape .label{text-align:center;}#mermaid-svg-vCghOdKMkM3RkXob .node.clickable{cursor:pointer;}#mermaid-svg-vCghOdKMkM3RkXob .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vCghOdKMkM3RkXob .arrowheadPath{fill:#333333;}#mermaid-svg-vCghOdKMkM3RkXob .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vCghOdKMkM3RkXob .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vCghOdKMkM3RkXob .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vCghOdKMkM3RkXob .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vCghOdKMkM3RkXob .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vCghOdKMkM3RkXob .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vCghOdKMkM3RkXob .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vCghOdKMkM3RkXob .cluster text{fill:#333;}#mermaid-svg-vCghOdKMkM3RkXob .cluster span{color:#333;}#mermaid-svg-vCghOdKMkM3RkXob 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-vCghOdKMkM3RkXob .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vCghOdKMkM3RkXob rect.text{fill:none;stroke-width:0;}#mermaid-svg-vCghOdKMkM3RkXob .icon-shape,#mermaid-svg-vCghOdKMkM3RkXob .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vCghOdKMkM3RkXob .icon-shape p,#mermaid-svg-vCghOdKMkM3RkXob .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vCghOdKMkM3RkXob .icon-shape .label rect,#mermaid-svg-vCghOdKMkM3RkXob .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vCghOdKMkM3RkXob .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vCghOdKMkM3RkXob .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vCghOdKMkM3RkXob :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ❌ Phase 1

固定窗口

SetWindowSize
❌ Phase 2

Camera2D

黑边+锯齿
✅ Phase 3

RT + BILINEAR

1280×720
🚀 Phase 4

canvasMult

高清全屏

阶段 画布 自适应 文字清晰度 代码复杂度
❌ Phase 1 固定窗口 随棋盘变 一般 简单
❌ Phase 2 Camera2D 960×640 ❌ 锯齿多 复杂(坑多)
✅ Phase 3 RT+BILINEAR 1280×720 ✅ 英文清晰 适中
🚀 Phase 4 canvasMult 自适应高分辨率 ✅ 中文也清晰 稍复杂

💡 最终选型RenderTexture2D + 固定 1280×720 逻辑画布 + TEXTURE_FILTER_BILINEAR + 全屏 canvasMult 高分辨率重建 + Camera2D zoom 自动映射。


🔜 下篇预告

第 3 篇:222 个 Bug 修复教会我的事

栈溢出、浮点 UB、字体缺失、异步保存、初始化顺序错误...... 精选 15 个让你看完直呼"我也踩过"的经典修复故事。(可能是全系列最有趣的一篇)


📚 系列目录

# 标题 状态
1 项目概览与扫雷核心算法 ✅ 已发布
2 Raylib 渲染架构 ← 本文 ✅ 已发布
3 222 个 Bug 修复教会我的事 ✅ 已发布
4 中英文双语的工程实现 📝 待发布
5 持久化、撤销、提示:非核心功能 📝 待发布
6 200 个单元测试:C 项目也能 TDD 📝 待发布
7 960×640 → 1280×720:全局缩放重构实录 📝 待发布

👍 如果你觉得有帮助,点赞 + 收藏 + 关注,三连支持作者继续写下去 🚀