用户说「重做」,不是「修改」
前面几篇技术笔记讲的都是逻辑层的坑。这篇换个方向,讲 UI。
Outland 的 UI 被整个推翻重做过一次。不是渐进式的小修小补,是用户直接下了”重做”的指令:
「ui 我要的不只是修改,而是重新做,现在这个版本完全达不到能玩。我需要一个现代美观好用的 ui,其次是这些最基础的游戏功能我希望全部做全做好,最后你放大了窗口,这很好,但是放大窗口是为了能显示更多内容,而不是单纯把文字和画面放大。」
这句话里有三个信号,每个都对应一条后来固化下来的规矩。
一、先分清:这是修改还是重做
重做发起之前,项目其实已经在按 P0、C、A、B、D……一路推小提交,每个 commit 只动一个系统的 UI。用户对这个节奏的评价是碎片化——一次动一个系统,感觉永远做不完。
所以这次重做切分方式变了:不按系统切,按交付目标切。一个 sprint 把三栏重设计、弹窗统一、主菜单、背包、交互、状态、法术页面一次性翻新。
这条教训后来写成了开工前必须问自己的一句话:这是修改还是重做?当前版本能不能玩? 如果答案是”不能玩”,就不要拿”修补一下”的思路进场。
二、五条硬性规则
重做之前,用户把规矩一条条钉死了。这几条后面全部变成了验收标准:
- HUD 必须完全不透明。HUD 是 UI,地图是地图,不能混淆。早期用
alpha=235的半透明面板,视觉上”能看到下面的格子”,玩家以为 HUD 浮在地图上。修法:box 一律alpha=255,配色比游戏背景明显更亮(UI 背景{35,42,55}vs 游戏背景{6,8,14}),让玩家一眼分清哪个是界面哪个是地图。 - 字号绝对固定,不随窗口缩放。字号 18 像素,是屏幕上的绝对像素,窗口从 720p 拉到 4K,字的大小一模一样。
- 更大的分辨率显示更多内容,不是更大的内容。窗口大 = 面板多、行多、信息密度高,字不放大图不放大。
- 窗口只锁 16:9,不允许任意比例。
- 色板固定、扁平、实色 + 1px 描边。参考 Tales of Maj’Eyal、Dwarf Fortress Steam 版、CDDA lto。不要渐变、不要半透明。
其中第 2、3 条组合起来,就是整个 UI 的核心哲学:窗口是信息密度的旋钮,不是缩放的旋钮。
三、坑:让字号跟着窗口缩的那个 SDL 调用
实现”窗口可以随便拉、但不崩坏”的时候,最初用了 SDL_RenderSetLogicalSize(1280, 720):逻辑分辨率固定,窗口任意缩放,SDL 自动 letterbox。
它解决了”不崩坏”,但带来一个隐蔽的副作用:文字也参与了 logical scale。4K 窗口里的字比 720p 的大一圈。这直接违反第 2 条,但只有把两个分辨率的截图并排看才能发现。
修法是删掉 SDL_RenderSetLogicalSize 和 SDL_RenderSetIntegerScale,把窗口宽高从编译期常量改成运行时变量,由 main 在窗口事件里实时从 SDL_GetRendererOutputSize 注入。布局按真实窗口尺寸算比例,字号不参与。
配套的验证手段也顺手加了:--win-w / --win-h 参数强制初始窗口尺寸。截 1280×720、1920×1080、3840×2160 三张图,字大小必须完全一致——这是”字号固定”唯一可验证的证据,光靠”代码里没写缩放”不算数。
四、坑:只回答「渲染了没有」的截图自检
UI 工作每改一截,出截图自检。前几版的自检方式是问视觉模型”看到了什么”,回答是”看到地图、角色、装备、三栏”——然后放行。
问题全藏在”没被提到”的地方:
- 右上角的 hover 面板和右栏撞在一起
- 底部行动栏压在日志上,日志最末行被截断
- 主菜单”继续游戏”在没有存档时仍可点
“渲染了”和”布局对”是两回事。修正后的自检要求:视觉模型必须逐区域列出位置关系——左栏起止、右栏起止、hover 面板位置、是否有任何重叠。“看到了”三个字不算答案。
五、坑:全屏页的三条叠字规律
游戏里有一批全屏页(状态、制作、建造、帮助、日志、键位……),它们共用同一个绘制起点。这批页面最初全是叠字的,翻出来是三条系统性规律:
1. 内容起点必须让开顶部标题带。 标题带是 0~26px 的实色条,内容若从 y=24 起画,每个页面的第一行都会和标题叠字。修法:内容起点统一 y=34。
2. 长列表必须有可视窗口。 建造列表几百条、配方列表三百多条,直接全画会铺满整屏压住页脚。修法:算出可视行数,跟随光标滚动,只画窗口内的行。
3. 行上限要算行高,不能只判 y。 页脚画在窗口底部往上 30px 处,只判断 y > view_h - 40 会让最后一行的下沿落进页脚。修法:一律 y + 行高 > view_h - 32 就停;滚动可用高度也要扣掉一行行高,否则最后一条永远画不出来。
这三条的共同点是:固定 y 起点 + 列表无上限 = 必然叠字。 修的时候 720p 是最紧的一档(可视行数最少),逐页截图看”顶部两条是否分开、底部末行是否与页脚分开、有没有裁半行”。
六、响应式分级:T0 到 T5
第 3 条规矩(大窗口 = 更多内容)落地成一个六级分级,按窗口宽度判:
| 宽度 | 级别 | 显示内容 |
|---|---|---|
| < 1024 | T0/T1 | 核心状态 |
| 1024–1279 | T1 | + 生存 |
| 1280–1919(1080p) | T2 | + 角色装备 |
| 1920–2559 | T3 | + 部位技能 |
| 2560–3839 | T4 | + 物品法术 |
| 3840+(4K) | T5 | + 缩略图任务 |
注意判据是宽度数字,不是”4K""1080p”这种分辨率名称——多显示器切换时窗口宽度会变但分辨率名不变。1280 只显示核心,4K 才出缩略图,中间每一档加一组面板。
七、还有两条工作方式上的纠正
1. 修复循环型 bug,先读作用域。 加 --win-w 参数时,一个 std::vector<int> force_w_h 的声明位置错了(C2065 未声明),当时没读完整作用域就反复 patch 文本,错了五次才停下来把 main 的参数解析循环读一遍——声明必须在循环之前。这类 bug 的正确动作是停手读代码,不是继续 patch。
2. 没有改动就不跑验证。 UI 工作有一条铁律:截图先于收尾——每一刀先出截图给用户看,说 OK 再进下一刀。反过来,工作区没有改动的时候,直接说”没有改动所以不需要验证”,不跑脚本凑 PASS。空跑的绿数字只会制造虚假的安心。
八、这套规矩现在长什么样
重做完成的部分:三栏 HUD、全部全屏页、弹窗尺寸自适应(视口比例 + 上下限钳制)、主菜单、存档浏览器、建人物向导。还在推进的:地图缩放(tile 独立于字号缩放)、窗口化 UI(可拖拽/最小化/隐藏的面板窗口,消息日志是重点)。
回头看,这次重做最大的收获不是那套扁平 UI 本身,而是开工前把”修改还是重做”问清楚了、把五条规矩钉死了。后面所有争议——某个面板该不该透明、某个窗口该不该放大字——都拿那五条对,不用反复商量。
下一篇讲另一条长跑线:世界快照抽取——八个世界切换时怎么把整个世界的状态拍下来再还原,以及一条跑了将近两小时的全量套件是怎么被 GPU 驱动坑掉的。