07 | 全局缩放重构实录

本文目录

  • [📐 第 7 篇:960×640 → 1280×720 ------ 一次全局缩放重构实录](#📐 第 7 篇:960×640 → 1280×720 —— 一次全局缩放重构实录)
    • [📏 1. CELL_SIZE 的三次迭代](#📏 1. CELL_SIZE 的三次迭代)
      • [1.1 v1:30px(够用,但不够好用)](#1.1 v1:30px(够用,但不够好用))
      • [1.2 v2:36px(好一点,但不够)](#1.2 v2:36px(好一点,但不够))
      • [1.3 v3:40px(最终的答案)](#1.3 v3:40px(最终的答案))
    • [📊 2. 每次升级的影响范围](#📊 2. 每次升级的影响范围)
    • [🔁 3. 同一个 HUD 重叠,修了 5 次](#🔁 3. 同一个 HUD 重叠,修了 5 次)
    • [📐 4. 居中公式的四代演进](#📐 4. 居中公式的四代演进)
      • [4.1 第一代:硬编码](#4.1 第一代:硬编码)
      • [4.2 第二代:MeasureTextEx + roundf 🎯](#4.2 第二代:MeasureTextEx + roundf 🎯)
      • [4.3 第三代:分组居中(Win/Lose Overlay)](#4.3 第三代:分组居中(Win/Lose Overlay))
      • [4.4 第四代:层叠居中(Stats 面板)](#4.4 第四代:层叠居中(Stats 面板))
    • [🔗 5. 四屏同步](#🔗 5. 四屏同步)
    • [🗺️ 6. 最终布局](#🗺️ 6. 最终布局)
    • [💡 7. 五个核心教训](#💡 7. 五个核心教训)
    • [🏁 写在最后](#🏁 写在最后)
    • [📚 全系列目录](#📚 全系列目录)

🎯 系列最终篇 · CELL_SIZE 从 30 到 36 到 40,中间隔了 50 多次 Bug 修复。

📖 上一篇200 个单元测试:C 项目也能 TDD


📐 第 7 篇:960×640 → 1280×720 ------ 一次全局缩放重构实录


"一个格子 30px 够了。"

"......不对,太小了。"

"那 36px。"

"......还是小。"

"40px。"

"这次对了。"

一个格子的大小,牵动了全部布局参数。本文记录三次全局重构的全过程。


📏 1. CELL_SIZE 的三次迭代

1.1 v1:30px(够用,但不够好用)

复制代码
画布:960 × 640(3:2 比例)
格子:30px
Expert 棋盘:900 × 480  → 画布占 94% × 75%
Beginner 棋盘:270 × 270 → 画布占 28% × 42%

问题

  • 🟡 640px 高度下,HUD 70px + 棋盘 480px + 间距 = 只剩 40px
  • 🟡 30px 格子太小,点击需要瞄准
  • 🔴 3:2 画布在 16:9 显示器上左右黑边严重

1.2 v2:36px(好一点,但不够)

复制代码
画布:1280 × 720(16:9!适配现代显示器)
格子:36px
Expert 棋盘:1080 × 576  → 画布占 84% × 80%

动机:用户反馈"棋盘太小,鼠标易误触"。而且画布从 3:2 改为 16:9 ------ 这是个正确的决定。

代价:布局参数全部重调。HUD 70→80px,PADDING 20→30px。

1.3 v3:40px(最终的答案)

复制代码
画布:1280 × 720(不变)
格子:40px
Expert 棋盘:1200 × 640  → 画布占 94% × 89%
Beginner 棋盘:360 × 360 → 画布占 28% × 50%

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

Expert: 1200×640

画布占比: 94%×89%
v2: 36px 格子
画布: 1280×720

Expert: 1080×576

画布占比: 84%×80%
v1: 30px 格子
画布: 960×640

Expert: 900×480

画布占比: 94%×75%

🎯 一个教训 :一步到位比逐步升级省 5 倍工作量。 每次 CELL_SIZE 变化,都要改菜单、统计、自定义、HUD 四屏。


📊 2. 每次升级的影响范围

参数 v1 (30px) v2 (36px) v3 (40px)
CANVAS_WIDTH 960 1280 🆕 1280
CANVAS_HEIGHT 640 720 🆕 720
CELL_SIZE 30 36 40
PADDING 20 30 40
菜单标题字号 52 52 60
菜单按钮字号 18 18 22
菜单按钮宽度 280 280 340
菜单按钮高度 48 50 68
统计面板高度 70 70 98
统计行间距 80 80 114
自定义字段宽度 200 200 260
自定义字段间距 60 60 76

⚠️ 最痛苦的 :不是改参数本身,而是让 DrawMenuDrawGameDrawStatsDrawCustomUpdateGame 五处的参数完全一致


🔁 3. 同一个 HUD 重叠,修了 5 次

UI 面板顶部只有 80px,要塞进:雷数、难度标签、笑脸按钮、计时器。五个组件挤一块。
#mermaid-svg-u6C6CJilPZhiB2cW{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-u6C6CJilPZhiB2cW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-u6C6CJilPZhiB2cW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-u6C6CJilPZhiB2cW .error-icon{fill:#552222;}#mermaid-svg-u6C6CJilPZhiB2cW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-u6C6CJilPZhiB2cW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-u6C6CJilPZhiB2cW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-u6C6CJilPZhiB2cW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-u6C6CJilPZhiB2cW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-u6C6CJilPZhiB2cW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-u6C6CJilPZhiB2cW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-u6C6CJilPZhiB2cW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-u6C6CJilPZhiB2cW .marker.cross{stroke:#333333;}#mermaid-svg-u6C6CJilPZhiB2cW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-u6C6CJilPZhiB2cW p{margin:0;}#mermaid-svg-u6C6CJilPZhiB2cW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-u6C6CJilPZhiB2cW .cluster-label text{fill:#333;}#mermaid-svg-u6C6CJilPZhiB2cW .cluster-label span{color:#333;}#mermaid-svg-u6C6CJilPZhiB2cW .cluster-label span p{background-color:transparent;}#mermaid-svg-u6C6CJilPZhiB2cW .label text,#mermaid-svg-u6C6CJilPZhiB2cW span{fill:#333;color:#333;}#mermaid-svg-u6C6CJilPZhiB2cW .node rect,#mermaid-svg-u6C6CJilPZhiB2cW .node circle,#mermaid-svg-u6C6CJilPZhiB2cW .node ellipse,#mermaid-svg-u6C6CJilPZhiB2cW .node polygon,#mermaid-svg-u6C6CJilPZhiB2cW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-u6C6CJilPZhiB2cW .rough-node .label text,#mermaid-svg-u6C6CJilPZhiB2cW .node .label text,#mermaid-svg-u6C6CJilPZhiB2cW .image-shape .label,#mermaid-svg-u6C6CJilPZhiB2cW .icon-shape .label{text-anchor:middle;}#mermaid-svg-u6C6CJilPZhiB2cW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-u6C6CJilPZhiB2cW .rough-node .label,#mermaid-svg-u6C6CJilPZhiB2cW .node .label,#mermaid-svg-u6C6CJilPZhiB2cW .image-shape .label,#mermaid-svg-u6C6CJilPZhiB2cW .icon-shape .label{text-align:center;}#mermaid-svg-u6C6CJilPZhiB2cW .node.clickable{cursor:pointer;}#mermaid-svg-u6C6CJilPZhiB2cW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-u6C6CJilPZhiB2cW .arrowheadPath{fill:#333333;}#mermaid-svg-u6C6CJilPZhiB2cW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-u6C6CJilPZhiB2cW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-u6C6CJilPZhiB2cW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-u6C6CJilPZhiB2cW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-u6C6CJilPZhiB2cW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-u6C6CJilPZhiB2cW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-u6C6CJilPZhiB2cW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-u6C6CJilPZhiB2cW .cluster text{fill:#333;}#mermaid-svg-u6C6CJilPZhiB2cW .cluster span{color:#333;}#mermaid-svg-u6C6CJilPZhiB2cW 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-u6C6CJilPZhiB2cW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-u6C6CJilPZhiB2cW rect.text{fill:none;stroke-width:0;}#mermaid-svg-u6C6CJilPZhiB2cW .icon-shape,#mermaid-svg-u6C6CJilPZhiB2cW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-u6C6CJilPZhiB2cW .icon-shape p,#mermaid-svg-u6C6CJilPZhiB2cW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-u6C6CJilPZhiB2cW .icon-shape .label rect,#mermaid-svg-u6C6CJilPZhiB2cW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-u6C6CJilPZhiB2cW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-u6C6CJilPZhiB2cW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-u6C6CJilPZhiB2cW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HUD 面板 (80px)
雷数

位置: x=40, y=4

尺寸: 动态
难度标签

位置: x=动态, y=panelH-diffSize.y-4

尺寸: 动态
笑脸按钮

位置: 居中, y=4

尺寸: 40×40
计时器

位置: x=右对齐, y=4

尺寸: 动态

Fix #65:雷数太靠左

修复:PADDING 10 → 40。

Fix #135:难度标签和重启按钮叠了

难度标签在雷数右侧流式布局(PADDING + mineSize.x + 20),重启按钮居中。随着 "010" 变成 "-03" 宽度变化,向右挤到按钮。

修复:难度标签左移、重启按钮 160→200px。

Fix #147:流式 vs 绝对定位的碰撞

Fix #135 后,难度标签与雷数水平排列。中文字符变长时仍会延伸到重启按钮区域。

修复 :改为垂直堆叠 。雷数上(y=4),难度标签下(y = panelH - diffSize.y - 4)。三区完全解耦。

Fix #210:用户发了视频

用户录了屏幕录像发过来。第 015-060 帧,清楚地看到 010Beginner 叠在一起。

修复:垂直分离间距加一倍。

Fix #212:标题压住按钮

菜单标题在 y=108,第一个按钮起始 y=86 ------ 标题和按钮的视觉中心重叠了。

修复 :标题固定 y=40,按钮起始 y=170。彻底断开。


📐 4. 居中公式的四代演进

4.1 第一代:硬编码

c 复制代码
y + (48 - 14) / 2   // 硬编码 14px,字号一变就歪

4.2 第二代:MeasureTextEx + roundf 🎯

c 复制代码
Vector2 size = MeasureTextEx(font, text, fontSize, spacing);
float posY = btn.y + (btn.height - size.y) / 2.0f;
DrawTextEx(font, text, (Vector2){roundf(posX), roundf(posY)}, ...);

💡 roundf 消除 .5 像素的亚像素模糊。如果不加,奇宽按钮的居中坐标有小数,DrawTextEx 在亚像素位置采样,小字号边缘发虚。

4.3 第三代:分组居中(Win/Lose Overlay)

两行文字作为一个整体居中:

c 复制代码
Vector2 titleSize = MeasureTextEx(font, title, 56, 2);
Vector2 msgSize   = MeasureTextEx(font, message, 24, 1);

float totalH = titleSize.y + 40 + msgSize.y;
float blockY = (CANVAS_HEIGHT - totalH) / 2.0f;

DrawTextEx(font, title,  (Vector2){cx, roundf(blockY)},                56, ...);
DrawTextEx(font, message, (Vector2){cx, roundf(blockY + titleSize.y + 40)}, 24, ...);

4.4 第四代:层叠居中(Stats 面板)

三行文字用累积 Y 偏移:

c 复制代码
float textY = panel.y + 8;
// 第 1 行
DrawTextEx(font, name, (Vector2){textX, roundf(textY)}, 20, ...);
textY += nameSize.y + 6;
// 第 2 行
DrawTextEx(font, line1, (Vector2){textX, roundf(textY)}, 16, ...);
textY += l1Size.y + 6;
// 第 3 行
DrawTextEx(font, line2, (Vector2){textX, roundf(textY)}, 16, ...);

🔑 黄金法则 :永远不用 y + 30y + 50 硬编码。测量上一行的高度,再算下一行的位置。


🔗 5. 四屏同步

函数 用途 需同步的参数
DrawMenu 渲染菜单按钮 btnWidth, btnHeight, gap, startY
UpdateGame (Menu) 检测菜单点击 同一份 btnWidth, btnHeight, gap, startY
DrawStats 渲染统计面板 panelH, rowH, startY
DrawCustom 渲染自定义界面 fieldW, fieldH, gap

🐛 Fix #105DrawMenu 里的 btnWidth=320,但 UpdateGame 里是 280。玩家看到了按钮,但点不中右半部分。一个表格把参数抄出来对比,发现 3 处不一致。


🗺️ 6. 最终布局

复制代码
┌─────────────────────────────────────────────────────────────┐ 
│  010             ┌──────────────┐                00:42      │  ← HUD (80px)
│  Beginner        │    😊        │                           │
│                  └──────────────┘                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│    ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐      │
│    │  1 │  2 │    │  F │    │  3 │  1 │    │    │    │      │
│    ├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤      │
│    │    │    │    │    │  ? │    │    │    │    │    │      │
│    └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

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

字段间距: 76px
HUD 面板 (80px)
雷数: x=40, y=4
难度标签: x=动态, y=动态
笑脸按钮: 居中, 40×40
计时器: 右对齐, y=4
棋盘区域
Expert 棋盘: 1200×640

格子: 40×40

间距: 2px
统计面板 (98px)
三行文字: 层叠居中


💡 7. 五个核心教训

① 一步到位

CELL_SIZE 30→36→40 三次迭代,每次都是全局大改。直接上 40 省去一次重构。
② 集中配置

所有布局参数放在一个地方,而不是散落在五个绘制函数里。搜 PADDING 看引用次数就知道耦合度。
③ 同一份数据

碰撞检测和绘制用同一组变量。各自算各自的 → 点击区域和绘制位置错位。
④ 不要硬编码

y + 30y - 6(height - 14) / 2 是布局 Bug 的温床。MeasureTextEx 是你最好的朋友。
⑤ 截图对比

每次布局改动前后截一张全屏图。肉眼比代码更容易发现"标题偏了 3px"这种差异。


🏁 写在最后

这个项目从第一行代码到最终版本,经历了:

复制代码
📦 222 个修复
🧪 200 个测试
🎨 18+ 个功能
📐 7 轮全局重构
🌏 2 种语言

如果你把代码看作一个产品,那么 90% 的时间花在了"让它看起来正确"。扫雷很简单,但做一个好的扫雷很难。
#mermaid-svg-qRAqyfY3inIq31aG{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-qRAqyfY3inIq31aG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qRAqyfY3inIq31aG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qRAqyfY3inIq31aG .error-icon{fill:#552222;}#mermaid-svg-qRAqyfY3inIq31aG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qRAqyfY3inIq31aG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qRAqyfY3inIq31aG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qRAqyfY3inIq31aG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qRAqyfY3inIq31aG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qRAqyfY3inIq31aG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qRAqyfY3inIq31aG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qRAqyfY3inIq31aG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qRAqyfY3inIq31aG .marker.cross{stroke:#333333;}#mermaid-svg-qRAqyfY3inIq31aG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qRAqyfY3inIq31aG p{margin:0;}#mermaid-svg-qRAqyfY3inIq31aG .mermaid-main-font{font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-qRAqyfY3inIq31aG .exclude-range{fill:#eeeeee;}#mermaid-svg-qRAqyfY3inIq31aG .section{stroke:none;opacity:0.2;}#mermaid-svg-qRAqyfY3inIq31aG .section0{fill:rgba(102, 102, 255, 0.49);}#mermaid-svg-qRAqyfY3inIq31aG .section2{fill:#fff400;}#mermaid-svg-qRAqyfY3inIq31aG .section1,#mermaid-svg-qRAqyfY3inIq31aG .section3{fill:white;opacity:0.2;}#mermaid-svg-qRAqyfY3inIq31aG .sectionTitle0{fill:#333;}#mermaid-svg-qRAqyfY3inIq31aG .sectionTitle1{fill:#333;}#mermaid-svg-qRAqyfY3inIq31aG .sectionTitle2{fill:#333;}#mermaid-svg-qRAqyfY3inIq31aG .sectionTitle3{fill:#333;}#mermaid-svg-qRAqyfY3inIq31aG .sectionTitle{text-anchor:start;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-qRAqyfY3inIq31aG .grid .tick{stroke:lightgrey;opacity:0.8;shape-rendering:crispEdges;}#mermaid-svg-qRAqyfY3inIq31aG .grid .tick text{font-family:"trebuchet ms",verdana,arial,sans-serif;fill:#333;}#mermaid-svg-qRAqyfY3inIq31aG .grid path{stroke-width:0;}#mermaid-svg-qRAqyfY3inIq31aG .today{fill:none;stroke:red;stroke-width:2px;}#mermaid-svg-qRAqyfY3inIq31aG .task{stroke-width:2;}#mermaid-svg-qRAqyfY3inIq31aG .taskText{text-anchor:middle;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-qRAqyfY3inIq31aG .taskTextOutsideRight{fill:black;text-anchor:start;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-qRAqyfY3inIq31aG .taskTextOutsideLeft{fill:black;text-anchor:end;}#mermaid-svg-qRAqyfY3inIq31aG .task.clickable{cursor:pointer;}#mermaid-svg-qRAqyfY3inIq31aG .taskText.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-svg-qRAqyfY3inIq31aG .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-svg-qRAqyfY3inIq31aG .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-svg-qRAqyfY3inIq31aG .taskText0,#mermaid-svg-qRAqyfY3inIq31aG .taskText1,#mermaid-svg-qRAqyfY3inIq31aG .taskText2,#mermaid-svg-qRAqyfY3inIq31aG .taskText3{fill:white;}#mermaid-svg-qRAqyfY3inIq31aG .task0,#mermaid-svg-qRAqyfY3inIq31aG .task1,#mermaid-svg-qRAqyfY3inIq31aG .task2,#mermaid-svg-qRAqyfY3inIq31aG .task3{fill:#8a90dd;stroke:#534fbc;}#mermaid-svg-qRAqyfY3inIq31aG .taskTextOutside0,#mermaid-svg-qRAqyfY3inIq31aG .taskTextOutside2{fill:black;}#mermaid-svg-qRAqyfY3inIq31aG .taskTextOutside1,#mermaid-svg-qRAqyfY3inIq31aG .taskTextOutside3{fill:black;}#mermaid-svg-qRAqyfY3inIq31aG .active0,#mermaid-svg-qRAqyfY3inIq31aG .active1,#mermaid-svg-qRAqyfY3inIq31aG .active2,#mermaid-svg-qRAqyfY3inIq31aG .active3{fill:#bfc7ff;stroke:#534fbc;}#mermaid-svg-qRAqyfY3inIq31aG .activeText0,#mermaid-svg-qRAqyfY3inIq31aG .activeText1,#mermaid-svg-qRAqyfY3inIq31aG .activeText2,#mermaid-svg-qRAqyfY3inIq31aG .activeText3{fill:black!important;}#mermaid-svg-qRAqyfY3inIq31aG .done0,#mermaid-svg-qRAqyfY3inIq31aG .done1,#mermaid-svg-qRAqyfY3inIq31aG .done2,#mermaid-svg-qRAqyfY3inIq31aG .done3{stroke:grey;fill:lightgrey;stroke-width:2;}#mermaid-svg-qRAqyfY3inIq31aG .doneText0,#mermaid-svg-qRAqyfY3inIq31aG .doneText1,#mermaid-svg-qRAqyfY3inIq31aG .doneText2,#mermaid-svg-qRAqyfY3inIq31aG .doneText3{fill:black!important;}#mermaid-svg-qRAqyfY3inIq31aG .doneText0.taskTextOutsideLeft,#mermaid-svg-qRAqyfY3inIq31aG .doneText0.taskTextOutsideRight,#mermaid-svg-qRAqyfY3inIq31aG .doneText1.taskTextOutsideLeft,#mermaid-svg-qRAqyfY3inIq31aG .doneText1.taskTextOutsideRight,#mermaid-svg-qRAqyfY3inIq31aG .doneText2.taskTextOutsideLeft,#mermaid-svg-qRAqyfY3inIq31aG .doneText2.taskTextOutsideRight,#mermaid-svg-qRAqyfY3inIq31aG .doneText3.taskTextOutsideLeft,#mermaid-svg-qRAqyfY3inIq31aG .doneText3.taskTextOutsideRight{fill:black!important;}#mermaid-svg-qRAqyfY3inIq31aG .crit0,#mermaid-svg-qRAqyfY3inIq31aG .crit1,#mermaid-svg-qRAqyfY3inIq31aG .crit2,#mermaid-svg-qRAqyfY3inIq31aG .crit3{stroke:#ff8888;fill:red;stroke-width:2;}#mermaid-svg-qRAqyfY3inIq31aG .activeCrit0,#mermaid-svg-qRAqyfY3inIq31aG .activeCrit1,#mermaid-svg-qRAqyfY3inIq31aG .activeCrit2,#mermaid-svg-qRAqyfY3inIq31aG .activeCrit3{stroke:#ff8888;fill:#bfc7ff;stroke-width:2;}#mermaid-svg-qRAqyfY3inIq31aG .doneCrit0,#mermaid-svg-qRAqyfY3inIq31aG .doneCrit1,#mermaid-svg-qRAqyfY3inIq31aG .doneCrit2,#mermaid-svg-qRAqyfY3inIq31aG .doneCrit3{stroke:#ff8888;fill:lightgrey;stroke-width:2;cursor:pointer;shape-rendering:crispEdges;}#mermaid-svg-qRAqyfY3inIq31aG .milestone{transform:rotate(45deg) scale(0.8,0.8);}#mermaid-svg-qRAqyfY3inIq31aG .milestoneText{font-style:italic;}#mermaid-svg-qRAqyfY3inIq31aG .doneCritText0,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText1,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText2,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText3{fill:black!important;}#mermaid-svg-qRAqyfY3inIq31aG .doneCritText0.taskTextOutsideLeft,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText0.taskTextOutsideRight,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText1.taskTextOutsideLeft,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText1.taskTextOutsideRight,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText2.taskTextOutsideLeft,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText2.taskTextOutsideRight,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText3.taskTextOutsideLeft,#mermaid-svg-qRAqyfY3inIq31aG .doneCritText3.taskTextOutsideRight{fill:black!important;}#mermaid-svg-qRAqyfY3inIq31aG .vert{stroke:navy;}#mermaid-svg-qRAqyfY3inIq31aG .vertText{font-size:15px;text-anchor:middle;fill:navy!important;}#mermaid-svg-qRAqyfY3inIq31aG .activeCritText0,#mermaid-svg-qRAqyfY3inIq31aG .activeCritText1,#mermaid-svg-qRAqyfY3inIq31aG .activeCritText2,#mermaid-svg-qRAqyfY3inIq31aG .activeCritText3{fill:black!important;}#mermaid-svg-qRAqyfY3inIq31aG .titleText{text-anchor:middle;font-size:18px;fill:#333;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-qRAqyfY3inIq31aG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 2026-06-16 2026-06-16 2026-06-17 2026-06-17 2026-06-18 2026-06-18 2026-06-19 2026-06-19 2026-06-20 项目开始 核心算法实现 Raylib 渲染架构 222 个 Bug 修复 中英文双语支持 持久化/撤销/提示 200 个单元测试 CELL_SIZE 30→36 CELL_SIZE 36→40 四屏同步优化 项目发布 核心功能 Bug 修复 功能完善 质量保证 全局重构 完成 项目迭代里程碑


📚 全系列目录

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

🚀 全系列完 · 感谢你的阅读

欢迎 Star / Fork / 提 Issue。如果你觉得这个系列对你有帮助,点赞 + 收藏 + 关注,后续会继续写更多 C / C++ 工程实践系列。


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

相关推荐
牧以南歌〆3 小时前
数据结构<八>链式队列
c语言·数据结构·算法
无相求码4 小时前
数组越界为什么有时候不崩溃?VS2013 下栈上变量的幽灵布局解密
c语言·后端
m0_466525296 小时前
四大维度深层重构,解锁软件产业智能化变革新路径
重构
天空'之城7 小时前
C 语言工业级通用组件 02:通用内存池
c语言·嵌入式·内存管理·内存池
观远数据8 小时前
BI选型评估维度重构:为什么‘让业务用起来‘应该占权重的40%
重构
十月的皮皮8 小时前
C语言学习笔记20260717-预处理机制
c语言·笔记·学习
GeekArch10 小时前
第8讲:裸机单片机项目——Vibe快速迭代的最优边界
c语言·stm32·单片机·嵌入式硬件·mcu·物联网·ai编程
慧都小项10 小时前
C/C++ 静态分析为什么要前移?Parasoft C/C++test 在 IDE 与 CI/CD 中的应用
c语言·c++·ide·功能测试·ai
qq_337763201911 小时前
国际期货资管系统开发方案|内外盘交易平台源码授权与二次开发支持
java·c语言·开发语言·c++·c#
openKylin1 天前
与全球技术演进同频,openKylin 3.0从C迈向Rust
c语言·开发语言·rust·开源·开放原子·openkylin