发售周:把 CandyRush 送上 Steam
CandyRush 是一款确定性的俯视角幸存者类 roguelite,于 2026 年 10 月 31 日在 Steam 上线。本文讲的是这个日期背后的 发布工程:构建如何到达 Steam、发售周末的部署是什么样子、我选择不做哪些事,以及 接下来做什么。
这里没有销量、愿望单或玩家数量;我没有可以报告的数字。
发布了什么
以下信息取自撰写当天的 Steam 商店 API:
| 发售日期 | 2026 年 10 月 31 日 |
| 价格 | $7.99,首发 15% 折扣后为 $6.79 |
| 平台 | Windows、macOS、Linux |
| 语言 | 英语、简体中文 |
| Steam 功能 | 成就、统计、完全控制器支持、Steam Input、家庭共享 |
三个平台都出自同一条流水线,我在 之前的一篇文章里写过它。
发售周末的数字
自 8 月流水线重构以来的正式发布(main)部署:8 次全部成功。其中两次在发售
周末。下表时间均为 UTC,取自 GitHub Actions API。
| 运行 | 通道 | 开始时间 | 耗时 | 结果 |
|---|---|---|---|---|
| qa #383 | dev 进入 qa(PR #1087,48 个提交) | 11 月 1 日 01:01 | 26 分钟 | 成功 |
| main #384 | qa 进入 main(PR #1088,630 个提交) | 11 月 1 日 01:31 | 35 分钟 | 成功 |
| qa #387 | 地图修复(PR #1089) | 11 月 2 日 03:48 | 32 分钟 | 成功 |
| main #388 | 地图修复(PR #1090) | 11 月 2 日 03:50 | 56 分钟 | 成功 |
运行 #388 的 56 分钟里,有 31 分钟花在一个名为 "Wait for in-flight CD on the other trunk" 的 job 上。三条通道都在同一组三台 Unity 主机上构建,所以每次运行都会等待 其他分支上更早开始的运行。按运行编号排序可以避免死锁:更早的运行总是先走。这次 main 运行排在 qa #387 之后,随后它自己的构建和 Steam 上传用了约 24 分钟。
构建如何到达 Steam
流水线有三条通道,每个长期存在的 git 分支对应一条:
git dev -> Steam "development" automatic
git qa -> Steam "qualityassurance" automatic; the merge into qa is the gate
git main -> uploads to Steam setting the build live is a human click
推送的提交可以自动到达测试人员手中。但没有人在 Steamworks 里晋级构建,任何东西都 到不了玩家那里。Steam 构建无法删除,所以最后一步刻意保持手动。
这条规则塑造了整个发布过程。9 月,我把 qa 晋级到 main,用于 Valve 的构建审核
(PR #924,9 月 18 日)。审核读取的是 Steam 的默认分支,而我希望那个构建来自
main,而不是某条旁路通道。这是自 2025 年 10 月以来第一次主干晋级。9 月最后一次
晋级(PR #945,9 月 22 日)之后,main 将近六周没有动过。日常工作都走 dev 和
qa。
发售当晚,PR #1088 用一次合并把 main 更新到最新:630 个提交(不含合并提交为
508 个),涵盖 9 月 22 日以来的全部工作。听起来有风险,其实没有,因为被合并的那个
提交已经完整地通过了 qa 通道:Windows、Linux 和 macOS 构建,以及上传到 Steam 的
qualityassurance 分支。main 运行重新构建了同一个提交并上传。晋级里没有任何对
qa 来说是新的东西。
10 月:发售前必须做到的事
10 月的大部分时间都花在让构建值得晋级上。很多代码由 AI 编程代理编写;我确定方向、 撰写 issue,并审查和把关每一处改动。其中最重要的是四项工作。
最坏情况下的性能
发售前一周,第三章最密集的战斗是游戏里最慢的场景。修复(PR #1079,10 月 26 日合并) 前后都用一个新的实时采集测试来测量,测的是该段落在 500 个敌人上限下的表现:
| 之前 | 之后 | |
|---|---|---|
| Arc 3 Horde,深度 5,Burst 开启 | 约 27 FPS | 约 43 FPS |
| 再加上 Box2D 和 animator 的节省(引擎 A/B) | 约 60 FPS | |
| 500 个敌人时的区域道具扫描 | 1.80 ms | 0.57 ms |
这些帧率是在 Unity 编辑器中采集的。根据我的经验,部署后的玩家构建通常比编辑器 快 2-3 倍,但我没有针对这个场景测量过。
难点在于保持确定性。敌人分离变成了一个 Burst job,它从 tick 开始时拍下的快照里读取 每个邻居,而不是读取另一个敌人刚刚改过的位置。快照在任何模拟对象运行之前构建, 所以更新顺序不会改变结果。状态摘要在 1 个和 23 个 job worker、Burst 开启和关闭的 情况下都验证为一致,然后重新认可了回放基线。一次审计表明线上游戏中没有任何地方 通过物理读取敌人,于是敌人也不再由 Box2D 模拟。
关卡设计改不坏的发布闸
10 月中旬,dev 部署通道一直变红,原因与 bug 无关:一些测试断言了随关卡设计几乎每天 都在变的已编写内容,比如道具布局和生成概率。一刀切地忽略这些测试很容易,但是错的。 取而代之的是,测试房间改用内容的冻结副本(10 月 11 日),这样编辑已发布的章节不再 改变确定性基线。一个当天曾作为权宜之计忽略了三个测试的测试夹具,现在完整运行, 并在冻结房间上 4/4 通过。
一周后,PR #1073 把 34 个 PlayMode 测试改成了 32 个 EditMode 测试,原来的每个测试都要 启动一个场景只为检查某个组件是否存在。其中 30 个在约 0.4 秒内跑完。它还删除了 5 个 永远不会失败的测试。
不再宕机的崩溃报告服务器
10 月中旬,自托管的崩溃报告服务器磁盘满了。每个 dev 构建都会上传约 2 GB 的原生调试 符号,而 dev 构建发布得很频繁。修复分两部分:服务器上的一次运维清理,以及一次代码 改动(10 月 13 日)。符号上传现在是按通道(dev、qa、prod)设置的开关,dev 关闭, 玩家和测试人员运行的构建开启。被关闭的通道在 CI 中显示为提示而不是失败,所以这个 设置可见但不吵闹。
缺一台机器时照样发布
macOS 构建依赖一台在线的 Mac。10 月 18 日之前,如果没有 Mac 响应,qa 通道会跳过 整个 Steam 部署。PR #1073 改变了这一点:当 Mac 探测找不到机器时,qa 发布 Windows 和 Linux,macOS depot 保留之前的构建。Mac 构建失败时仍然什么都不发布。通知现在会写明 实际发出了哪些 depot。
发售周的修复
发售后的第一个改动是一个地图 bug,在发售周末发现并修复。run composer 为每局游戏生成 一张分支地图,章节规则规定同类遭遇不能连续出现。composer 的地图连线忽略了这条规则。 后续的一个展示步骤悄悄替换了放置的遭遇,于是小地图显示的是一回事,关卡门显示的是 另一回事。
修复在连线建好之后运行一个确定性的修复步骤。它以最少的移动重排下一行,必要时退回 到重排当前行,最后的手段是删掉一条不会让任何节点孤立的连线。测试在每个已发布章节上 遍历种子 1 到 1000:
before the fix: 34-59% of maps on Arc0001-0005 had a barred repeat edge
after the fix: none
它在 11 月 2 日从 dev 到 qa 再到 main。之后,当构建在 Steamworks 中设为上线时,
它就会到达玩家手中。
我有意没做的事
发售意味着选择不完成什么。值得点名的选择有:
- qa 和 main 通道上的测试闸已关闭。 变量
SKIP_TEST_GATES_QA(10 月 27 日设置) 和SKIP_TEST_GATES_MAIN(10 月 31 日设置)都是true,而SKIP_TEST_GATES_DEV是false。完整的 EditMode 和 PlayMode 套件仍在 dev 通道上运行,每个晋级 PR 都写明了 它所晋级的那次成功运行。跳过避免了在一个已经通过完整套件的提交上重跑一遍。这是 发售周的一个有意取舍,写在每个 PR 的描述里,改一个变量就能撤销。 - dev 通道很吵。 从 10 月 1 日到 11 月 2 日,它完成的运行中 14 次成功、14 次失败、 5 次取消。取消是有意设计的(更新的推送会替换较旧的运行)。失败是让这条通道成为 "坏了也便宜" 的地方的代价,但现在还是太多了。
- Steam 云存档没有开启。 Auto-Cloud 配置及其线上验证是一个等待我审查的未关闭 issue。在端到端验证之前,存档保留在本地。
- 没有新的内容系统。 用于无尽场地的可复用、可旋转道具布局已经立了 issue,但它们 涉及确定性回放路径,所以留到发售之后。
发售之后:排行榜与性能
两个公开页面在发售后覆盖这款游戏:
- 每周排行榜。 提交的分数不被信任。验证服务 在无头浏览器中用已发布的正式构建回放这局游戏记录下的输入,比较状态摘要,然后才写入 它自己推导出的分数。细节见 The Verified Score。
- 性能。 从匿名游戏会话中测得的帧率,按开发版、 候选发布版和正式版分别公布。
接下来做什么
- 等 dev 通道稳定变绿后,重新开启 qa 和 main 的测试闸。
- 降低 dev 通道的失败率。
- 端到端验证 Steam 云存档,然后开启它。
- 通过发售时用的同样三条通道发布内容更新。
可以推广的经验
让不可逆的一步保持手动。 直到 Steam 上传为止的一切都是自动化的。让构建上线是 一次点击,因为 Steam 构建无法删除。
当其中没有任何新东西时,大规模晋级是安全的。 那次 630 个提交的合并平平无奇,
因为完全相同的提交已经在 qa 上构建并上传过。
把没做的事写下来。 跳过的测试闸、吵闹的通道和缺失的云存档都记录在变量、PR 描述 和未关闭的 issue 里,不会被遗忘。