本文目录
- [📐 第 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 次)
-
- Fix #65:雷数太靠左
- Fix #135:难度标签和重启按钮叠了
- [Fix #147:流式 vs 绝对定位的碰撞](#147:流式 vs 绝对定位的碰撞)
- Fix #210:用户发了视频
- Fix #212:标题压住按钮
- [📐 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 |
⚠️ 最痛苦的 :不是改参数本身,而是让
DrawMenu、DrawGame、DrawStats、DrawCustom、UpdateGame五处的参数完全一致。
🔁 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 帧,清楚地看到 010 和 Beginner 叠在一起。
修复:垂直分离间距加一倍。
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 + 30、y + 50硬编码。测量上一行的高度,再算下一行的位置。
🔗 5. 四屏同步
| 函数 | 用途 | 需同步的参数 |
|---|---|---|
DrawMenu |
渲染菜单按钮 | btnWidth, btnHeight, gap, startY |
UpdateGame (Menu) |
检测菜单点击 | 同一份 btnWidth, btnHeight, gap, startY |
DrawStats |
渲染统计面板 | panelH, rowH, startY |
DrawCustom |
渲染自定义界面 | fieldW, fieldH, gap |
🐛 Fix #105 :
DrawMenu里的 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 + 30、y - 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++ 工程实践系列。
👍 如果你觉得有帮助,点赞 + 收藏 + 关注,三连支持作者继续写下去 🚀