下载 App
《公交公司》开发者日志6
修改于10/2224 浏览综合
《公交公司》开发者日志 06 · 上线冲刺:地图、排行榜、存档与教学收敛
这一篇记录上线前的最后一轮开发。做完这四件事,游戏就正式发布了。
这一期聊四件事:地图选择、排行榜、自动存档、教学优化。它们看起来是四个独立功能,其实都在解决同一个问题——让玩家能真正「开始玩」,并且玩得下去。
这四件事做完,《公交公司》才算是准备好上线了。

一、地图选择:先把「地图」这个概念从代码里拆出来
起因:一个按钮决定的命运
上线前的首页只有一个「开始游戏」按钮,点下去固定进入 18 站成长局。玩家不能选战场,也没法为不同拓扑的城市做准备。
但真正的问题在数据结构,而不是 UI:
「地图」在这款游戏里,当时根本不是一个独立概念。
站点表、边表、桥梁、初始开放站,全部硬编码在「模式」的分支里。growth/weekly 走 18 站扩展图,short/middle 走 12 站基础图,tutorial 走 6 站教学图。
也就是说:「换地图」等价于「换模式」。
想做「选地图」,必须先把地图从模式里拆出来。
拆法:让 mapId 成为独立参数
新建一张地图注册表,把站点和道路数据变成声明式定义:
Maps.DEFS.riverside = {
id = "riverside",
name = "滨江",
stations = { { name = "松林社区", kind = "home", x = 190, y = 250 }, … },
edges = { … },
bridges = { 8, 14 },
lineCount = 5,
}
然后让 mapId 沿着 mode/seed 的透传链一路走下去——建世界、重建、存档全部带上它。
这里有个必须说清楚的边界:这一期 mapId 只对成长局生效。教学、中局、短局、周历各自回到固定图。为什么?因为周历的事件表是写死站号的(「第 3 周开放 17、18 号站」),换一张站数不同的图,它会去取一个不存在的站点,运行中直接崩。
这个坑是加第二张图之后才暴露的:玩家在棋盘城用成长局玩,中途切「周历」模式,程序沿用 mapId="starter" 去建 16 站图,而周历剧本要开 17、18 号站 —— 取到 nil,崩。
修复只有一行:进世界时,非成长局强制把 mapId 清空,回默认图。
第二张图:棋盘城
有了选图功能,自然要有一张**「第一眼就懂」**的入门图。于是新增「棋盘城」:
- 4×4 规整方格,16 站,24 条边,无桥、无对角线;
- 从中心三站的 L 形向外一圈圈生长,每波新站必与已开站相邻;
- 开局极小:1 辆车、1 条线、3 个站(对比滨江的 4 车 / 3 线 / 5 站)。
原来的 18 站图降为「滨江 · 进阶」,保留双桥瓶颈、5 条线、3 种车型。
这里有个设计上的选择值得说:入门的「易」,不靠砍时长。
两张图的局长都是 600 模拟秒,都是每 40 秒开一个新站,成长弧线完全对齐。棋盘城的「易」来自:
- 开局极小(1 车 1 线,玩家不会被资源淹没);
- 无桥(没有瓶颈,怎么改线都通,容错高);
- 线路最多 4 条,不引入快速公交(只引入大容量)。
难度是靠机制的减法降下来的,不是靠稀客流或短时长。这样从入门图毕业到滨江,玩家面对的是同一套规则、更复杂的拓扑。
二、自动存档:为什么必须走云
一个把本地文件判了死刑的场景
需求很简单:刷新页面后还能接着玩。
但 Web(WASM)上,本地 File 写在内存文件系统里,页面一刷新就没了。而「刷新后还能继续」恰恰是核心场景——本地文件存档在这款游戏里没有意义。
所以存储走 clientCloud,按 TapTap 账号在服务端持久化。
存档最难的不是「存」,是「恢复后世界还得是活的」
朴素地逐表转 JSON 会复制对象、打断仿真。有三处对象同一性必须重建:
- 乘客:站台候车列表、车内货舱、世界乘客表,必须是同一张表;
- 服务:车指向的服务对象,必须就是服务注册表里的那一张;
- 道路队列:不存队列本身,加载时由车辆位置重建。
第 2 条踩过一次:服务表的键是 bus.service.id,不是 bus.lineId。两者在正常情况下相等,但换线失败的分支会把 lineId 写成 service.id,并不总是相等。存错键,读档后车就挂到了错误的服务上。
cjson 的两个陷阱
- 稀疏整数键:world.trips 在滤掉已送达乘客后会变得稀疏,而 cjson 对「键不连续的数字表」往返不可靠。一律序列化成记录数组,加载时重建键表。
- math.huge:有些站点标记是「永不到来」,值是 math.huge,JSON 无法编码。读取侧本来就是「取不到就当无穷大」,所以编码前统一过滤掉非有限值即可。
只存「还在路上的人」
world.trips 只增不减(每个乘客一条,永不清),跑长局会撑爆存档体积。
裁剪依据是:已送达的乘客不再被任何逻辑读取。所以只存「候车 ∪ 在途」,体积收敛到「站点容量 + 车队容量」。
两个真实踩的坑
坑 1:把云判成不存在。
守卫最初写的是 if type(clientCloud) ~= "table" then return nil end。但 clientCloud 是引擎绑定对象,Lua 侧是 userdata 不是 table。于是明明有云,却被判成「没有云」,白回落到内存后端——表现就是「返回首页后没有继续游戏」。
正确的存在性判断只有一种:
if not clientCloud then ... end -- ✅
坑 2:一个跨场景存活的标志。
修完上面这个,返回首页仍然显示「开始游戏」。查下来是:点「开始游戏」时记录了一个「当时没存档」的探测标志,返回首页时正确渲染成「继续游戏」,但下一帧主循环看到那个陈旧标志,又把按钮打回了「开始游戏」。
教训:凡是「回调只写标志、由每帧 Update 消费」的模式,标志的生命周期必须和它所属的那块界面同生共死,否则跨场景会串味。
存档节奏
两条触发:真实秒计时器(12 秒)是主保证;有结构变更(改线、投车、开站)时提前落盘。
为什么不能只靠「有变更才存」?因为时钟推进、车辆移动、上下车、需求生成都不算结构变更——纯靠变更触发会漏掉大部分进度。
三、排行榜
排行榜看着简单,实际上大部分工作量在「网络不可靠时怎么办」:
- 空榜返回的是一个特定错误码,要当成「暂无数据」而不是报错;
- 回调可能根本不触发 → 5 秒看门狗强制降级 + 0.5 秒延迟显示(防闪烁);
- 昵称接口没有超时 → 昵称非阻塞,先占位显示「玩家」,拿到后刷新列表;
- 切换地图 Tab 时如果不清数据,会显示上一张图的榜单 → 请求带上代数标记防竞态。
上线前抓到的一个严重 bug
测试时发现失败局的分数有时不进排行榜。
一开始怀疑是排行榜读取问题,或者是结算没计分。查下来都不是——是提交前的「云端基线」没就绪。
提交流程需要先知道「云端纪录是多少」才敢写(否则无法判断是否破纪录,只能暂存)。而基线的加载,当时只在选图页里触发一次:
玩家路径 经过选图页吗 结果
首页「开始游戏」→ 选图页 → 进局 ✅ 基线就绪,分数正常写云
首页「继续游戏」读档进局 ❌ 基线未就绪,分数只进暂存,不写云
教学完成后开局 ❌ 同上
所以这跟胜负无关——走读档路径的胜利局同样会丢,只是恰好在失败局上被注意到。
修复是在入口层保证:所有注册地图都加载过基线才放行,在「首次进游戏 / 新局 / 读档续玩」三个入口都调用;同时让提交自身也能在基线缺失时自愈补一次。
这个 bug 值得一提的地方是:它只在特定入口路径下出现。如果测试永远从「开始游戏」进去,永远发现不了。
四、教学:从 10 步收敛到 9 步
上线前又重构了一次
上一篇日志里,教学是 10 步。上线前我们把它收敛成了 9 步:
连接 → 延长 → 成环 → 加车 → 加站 → 第二条线 → 断环 → 缩短 → 金线
主要变化是把「环线」提前到了第 3 步。
原来的顺序是先教一堆基础操作、最后才碰环线。但实际试玩下来发现:成环和断环才是这款游戏最有特色的操作,也是最能体现「同一套规则在更大网络里产生复杂问题」的地方。把它埋在后面,玩家可能还没玩到就退出了。
现在的第 3 步就是:把一条线首尾连起来,看它变成环线;第 7 步再把它断开。一开一合,玩家就理解了环线和往返线的区别。
引导动画也跟着做细了
每一步的引导不再是「一句文字 + 一个高亮圈」,而是按操作类型播放对应的手势动画:
操作 引导动画
建线 / 延长 从起点拖向终点的手指轨迹
加站 拖动线路段到新站
成环 / 断环 拖住 T 型柄
缩短 端点柄拖到空白(循环播放)
同时调整了数值:第 4 步的候客人数减半、开局只放 2 个人——教学期的目的不是考验玩家,是让他看懂规则。
教学完成后的引导
教学完成会写一个云变量标记。回到首页后:
- 教学按钮的排序会变(已完成的玩家,主按钮优先);
- 文案会调整,引导玩家去「开始游戏」而不是反复重玩教学。
一个必须记住的 Modal 坑
教学完成后的结算界面有三个按钮:再次教学 / 开始游戏 / 返回首页。
第一次实现时,点「开始游戏」完全没反应。原因是:结算界面本身是一个打开的 Modal,它吞掉了新界面的全部点击。
修复方式是:按钮回调里先关闭 Modal 摘掉遮罩层,再切换场景。
这个坑记在这里是因为它很典型:残留的打开态弹窗会吞掉整个新界面的事件。任何「弹窗里换场景」的地方都要注意。
五、回到「涌现」
这四件事放在一起看,其实回答了同一个问题:
玩家怎么才能真的开始玩,并且愿意再玩一局?
- 地图选择 → 让不同水平的玩家都能找到合适的起点(棋盘城 vs 滨江);
- 自动存档 → 让「一局 10 分钟」这件事变得可以被打断,玩家不必一次玩完;
- 排行榜 → 让「再玩一局」有了外部理由,也让规划水平变得可见;
- 教学收敛 → 让新玩家在前三分钟就摸到这款游戏最有特色的操作。
而它们共享同一条底线:每一个数字都必须是真的。
最高分是真的、排行榜是真的、存档恢复出来的世界是真的(跑 600 帧逐步一致)、教学里那个「环线」也是真的在闭合。
六、下一步
- 第三张图:环状卫星城 / 双岸桥城这类「专属机制」地图;
- 周历适配选图:需要先把周历事件表里写死的站号解耦;
- 成就系统:目前还没有,有了之后可以扩展成就榜;
- 继续打磨真机体感:触摸热区、手指遮挡。
这四件事收尾之后,《公交公司》正式上线了。欢迎来玩:https://www.taptap.cn/app/965953
如果你玩过之后想聊聊:棋盘城和滨江你更喜欢哪张?你觉得入门图还该再简单一点,还是可以更早引入桥梁和快速公交?
#聚光灯gamejam开发者日志 #公交公司












