← 全部文章

用户说「重做」,不是「修改」

前面几篇技术笔记讲的都是逻辑层的坑。这篇换个方向,讲 UI。

Outland 的 UI 被整个推翻重做过一次。不是渐进式的小修小补,是用户直接下了”重做”的指令:

「ui 我要的不只是修改,而是重新做,现在这个版本完全达不到能玩。我需要一个现代美观好用的 ui,其次是这些最基础的游戏功能我希望全部做全做好,最后你放大了窗口,这很好,但是放大窗口是为了能显示更多内容,而不是单纯把文字和画面放大。」

这句话里有三个信号,每个都对应一条后来固化下来的规矩。

一、先分清:这是修改还是重做

重做发起之前,项目其实已经在按 P0、C、A、B、D……一路推小提交,每个 commit 只动一个系统的 UI。用户对这个节奏的评价是碎片化——一次动一个系统,感觉永远做不完。

所以这次重做切分方式变了:不按系统切,按交付目标切。一个 sprint 把三栏重设计、弹窗统一、主菜单、背包、交互、状态、法术页面一次性翻新。

这条教训后来写成了开工前必须问自己的一句话:这是修改还是重做?当前版本能不能玩? 如果答案是”不能玩”,就不要拿”修补一下”的思路进场。

二、五条硬性规则

重做之前,用户把规矩一条条钉死了。这几条后面全部变成了验收标准:

  1. HUD 必须完全不透明。HUD 是 UI,地图是地图,不能混淆。早期用 alpha=235 的半透明面板,视觉上”能看到下面的格子”,玩家以为 HUD 浮在地图上。修法:box 一律 alpha=255,配色比游戏背景明显更亮(UI 背景 {35,42,55} vs 游戏背景 {6,8,14}),让玩家一眼分清哪个是界面哪个是地图。
  2. 字号绝对固定,不随窗口缩放。字号 18 像素,是屏幕上的绝对像素,窗口从 720p 拉到 4K,字的大小一模一样。
  3. 更大的分辨率显示更多内容,不是更大的内容。窗口大 = 面板多、行多、信息密度高,字不放大图不放大。
  4. 窗口只锁 16:9,不允许任意比例。
  5. 色板固定、扁平、实色 + 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 条规矩(大窗口 = 更多内容)落地成一个六级分级,按窗口宽度判:

宽度级别显示内容
< 1024T0/T1核心状态
1024–1279T1+ 生存
1280–1919(1080p)T2+ 角色装备
1920–2559T3+ 部位技能
2560–3839T4+ 物品法术
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 驱动坑掉的。