下载 App
10/1022 浏览BUG反馈

Tap制造踩坑实录:persistent_world 常驻服多玩家冷启动分裂,以及我们被 RestartGuard 逼出来的无奈解法


引擎:TapTap Maker(UrhoX WebGL)
模式:多人游戏 + 常驻服(persistent_world)+ 自定义大厅(lobby_runtime)
日期:2026-10-11
horizontal linehorizontal line

一、问题是什么

我们的游戏用了 TapTap Maker 的常驻服模式(persistent_world)——也就是说,玩家启动游戏后,引擎会自动查询一个"中厅"(middle room),所有玩家都应该进到同一个常驻服实例里对战。
正常情况下流程是这样的:
玩家冷启动 → 引擎 QUICK_QUERY_MIDDLE_ROOM → 找到中厅 → JOIN → 连服 → 开局
但我们发现一个致命问题:
当两个玩家几乎同时启动游戏、而此时中厅还不存在时,两个人会各自建房,变成两个互不相通的"孤房",永远无法汇合。
玩家A 冷启动 → QUICK_QUERY 失败(中厅还没建好)→ 建房 #111
玩家B 冷启动 → QUICK_QUERY 失败(中厅还没建好)→ 建房 #222
→ A 在 #111,B 在 #222,谁也看不到谁
线上如果有 10 个玩家同时打开游戏、而中厅恰好处于无人状态(比如服务器刚重启、或所有人都退了),就会分裂成 10 个孤房,每个房 1 个人。
这在实时对战游戏里是不可接受的。
horizontal linehorizontal line

二、我们怎么定位的

先上结论:这个问题的根因不在客户端逻辑,在引擎的调度黑盒里。

2.1 服务端选举:谁该退房?

我们先做了一个客户端+服务端配合的"选举"机制:
  • 每个孤房里的玩家,每 2 秒把自己的 uid 写到服务端的 serverCloud 键里(lone_claim)
  • 服务端每 1 秒聚合所有孤房的声明,找出 uid 最小的那个(它是"锚点房",应该留守)
  • 其他所有 uid 更大的玩家,通过 lobby_state 广播收到一个 E=1(eject)标记
  • 收到 E=1 的客户端执行退房,试图回流到锚点房
这部分是好做的,服务端选举判定也正确——真机测试里,小 uid 玩家收到 E=2(留守),大 uid 玩家收到 E=1(退房),完全符合预期。

2.2 问题出在:退房之后呢?

大 uid 玩家收到 E=1,执行 ReturnToLobby() 退房回大厅。然后呢?
我们以为退房回大厅后,引擎会像冷启动一样自动 QUICK_QUERY_MIDDLE_ROOM 重新查中厅。
并没有。
翻文档(后来用云端 AI 帮我们翻了引擎内部文档)才确认:
ReturnToLobby 是主动离开语义。引擎设计上就不会自动重连、不会自动查房,只恢复大厅 UI 等玩家手动操作。
冷启动会自动查房,是因为它走的是 persistent_world 的连接流程;ReturnToLobby 走的是"主动返回",两条路径的引擎契约本就不同。
这不是 bug,是产品设计语义。普通模式下玩家退房后手动点"开始游戏"就行了,但我们是常驻服——根本没有"开始游戏"按钮,玩家是被引擎自动拉进中厅的。
更坑的是,我们客户端调 client:GetRoomList() 想自己找中厅,返回永远是空数组——中厅不在普通房间列表里。
horizontal linehorizontal line

三、我们试过的所有方法(全部失败)

方法 1:服务端下发锚点 roomId,客户端回流后 JoinRoom

服务端在 eject 时把锚点房的 roomId 通过 A 字段下发,客户端回流大厅后直接 client:JoinRoom(锚点roomId)。
结果:A=nil。服务端写的锚点 roomId 没下发成功——GetRankList 聚合时读不到我们写的第二个 iscores 键,或者 PERSISTENT_WORLD_KEY 根本不是普通意义上的 roomId。
这条路我们加了诊断日志想查服务端到底写了什么,但服务端日志的 watcher 只跟单实例,拉不到我们要的东西。

方法 2:RequestRestartGame(薄壳加载时立即发)

既然退房后引擎不查房,那我们就重启整个游戏——重启后 Lua 状态重建、Network/Lobby 保留,重走冷启动连接流程,自然就会 QUICK_QUERY。
引擎确实有这个本地事件:SendEvent("RequestRestartGame", VariantMap())。官方 i18n 切语言就是这么做的(i18n.lua:1648)。
结果:真机日志直接报 [RestartGuard] blocked RequestRestartGame。
引擎在某个相位会吞掉这个事件。文档里对 RestartGuard 零命中——它是 C++ 内部实现,没有任何公开文档。

方法 3:PersistentApp 壳延迟 5s 再发

我们猜 RestartGuard 拦的是"薄壳加载瞬间",于是改成:退房后落盘一个标志,PersistentApp 壳连服稳定后(大厅相位)延迟 5 秒再发。
结果:倒计时还没跑完(5 秒内),新的 lobby_state E=1 又来了,触发 eject 退房,脚本卸载,倒计时被打断。
而且更要命的是——这个逻辑放在 PersistentApp 壳里,正常冷启动建房的玩家也会被误武装倒计时(因为 pending 标志上次没清干净)。

方法 4:薄壳里延迟 10s 再发

又猜:那等 10 秒够了吧?RestartGuard 冷却总该过了。
结果:10 秒到点发,日志还是 [RestartGuard] blocked RequestRestartGame。
不是冷却问题,是整个大厅相位都被 guard 拦。

方法 5:游戏脚本相位连发 ReturnToLobby + RequestRestartGame

文档说 i18n 能发是因为它在游戏脚本稳定运行中发(不是大厅相位)。那我们就在 PersistentApp 壳里(游戏脚本环境)同一帧连发两个事件:
lua
SendEvent("ReturnToLobby", VariantMap()) -- 先退房 SendEvent("RequestRestartGame", VariantMap()) -- 紧接着重启
引擎 C++ 层排队处理:先退房再重启。重启时已经退房了(Left,不是断线),服务端孤房已销毁,Lobby 重连走冷启动 QUICK_QUERY。
结果:日志还是 [RestartGuard] blocked RequestRestartGame。
不管在哪个相位发,只要和大厅/lobby 状态沾边,全拦。

方法 6:给个"开始游戏"按钮,让玩家手动点了再进中厅

这是我们最期望的方案——不自动连中厅,大厅界面放一个按钮,玩家点了才连。这样 eject 分裂后退回大厅,玩家再点一次按钮就行,不用退出游戏。
结果:走不通。
引擎文档写得很明确:
客户端连接流程(Lobby 自动处理,游戏脚本无需关心):
Lobby 加载 → 发现 persistent_world 已启用 → 查询可加入的房间 → 找到就加入/没找到就建房 → 连服
这个流程是 Lobby C++ 层自动跑的,薄壳 Lua 既拦不住、也触发不了:
  • 冷启动时引擎自动查房连服,薄壳代码里 grep 不到任何 QUICK_QUERY 相关字符串——全是引擎内部行为
  • ReturnToLobby 回大厅后引擎不自动查房(设计如此),但薄壳里也没有"手动触发查房"的 API
  • client:StartMatch 是普通房间匹配,在 persistent_world 模式下走的不是中厅查房(实测 GetRoomList 永远返回空,中厅不在普通房间列表里)
中厅必须自动进,没有手动入口。
horizontal linehorizontal line

四、被堵死之后的无奈解法

自动重启这条路彻底走不通了。客户端又没有 JS 桥(不能 location.reload()),engine:Exit() 是直接退游戏进程不是刷新页面。给按钮手动进中厅也走不通——引擎不支持。
那只能让玩家自己动手退出游戏再重进。

最终方案:强制弹窗,红色按钮退出游戏

既然 E=1(被 eject)的客户端就是"该退的那个",那我们直接弹一个关不掉的全屏弹窗:
  • 红色标题:"大厅环境异常"
  • 白色提示:"请退出游戏后重新进入"
  • 一个红色按钮:"退出游戏" → 点击调 engine:Exit()
没有关闭按钮,没有遮罩点击穿透,玩家只能点退出。
然后玩家重新打开游戏 = 真正的冷启动 → 引擎自动 QUICK_QUERY → 这时候锚点房(小 uid 玩家建的那个)已经存在了,直接 JOIN 成功,两人进同一个房。

判据

lua
-- onLobbyState 收到 (R, S, G, E, A) 时 if R == 1 and E == 1 then -- 自己在孤房,且被服务端标记为 eject 方(大 uid) -- 弹强制退出弹窗 ShowSplitAlert() end
  • E=1(大 uid,被 eject)→ 弹窗
  • E=2(小 uid,锚点留守)→ 正常待着,不弹窗
horizontal linehorizontal line

五、踩坑总结(给后来者)

方法为什么失败
服务端下发锚点 roomId 客户端 JoinRoomrankList 聚合读不到第二键,A=nil
薄壳加载即发 RequestRestartGameRestartGuard 拦
PersistentApp 壳延迟 5s 发倒计时被新 eject 打断;误武装正常冷启动
薄壳延迟 10s 发RestartGuard 拦整个大厅相位,不是冷却
游戏脚本相位连发 ReturnToLobby+Restart还是被拦
给"开始游戏"按钮手动触发中厅连接引擎不支持——Lobby 自动查房,Lua 拦不住也触发不了
client:GetRoomList 找中厅中厅不在普通房间列表,永远空数组
Lua JS 桥刷新页面引擎没有 EvalJS / location.reload 接口
核心教训:
  1. TapTap Maker 的 persistent_world 模式假设"中厅只有一个、且由服务端管理"。如果出现客户端同时冷启动各自建房的分裂,客户端层几乎没有干净的解法。
  2. RequestRestartGame 不是你想发就能发的——RestartGuard 在 C++ 内部,文档零命中,我们试了所有相位全被拦。只有 i18n 那种"对局中切语言"的场景能成功。
  3. ReturnToLobby 是主动离开语义,引擎不自动重连不自动查房——别指望退房后引擎帮你重新匹配。
  4. GetRoomList 返回的是普通房间,不含 persistent_world 中厅。别想着客户端自己找中厅。
  5. 中厅连接是 Lobby 自动跑的,Lua 拦不住也触发不了——没有"手动触发查房"的 API,给不了玩家一个"点了才进中厅"的按钮。
  6. 最后能用的,就是让玩家手动退出重进。丑,但可靠。。。。。。
horizontal linehorizontal line

六、还没解决的问题

  • RestartGuard 到底在什么条件下放行?冷却窗口多长?文档里完全没有,只能靠真机逐相位试。
  • 如果线上真的同时来了 100 个玩家,小 uid 那个建房后,其他 99 个人都要弹"请退出重进"——体验很糟,但这是目前客户端能做的极限。
  • 真正的解法应该在服务端:冷启动建房时加全局锁,或者由服务端主动创建中厅而不是让客户端建。但我们管不到服务端。
如果你也在做 TapTap Maker 常驻服游戏,希望这篇能帮你少踩几个坑。
别再烧积分了!从每次 2000 到每次 50 的血泪经验截图
别再烧积分了!从每次 2000 到每次 50 的血泪经验
适用场景:所有在用 TapTap Code 开发游戏的小白 难度:★☆☆☆☆ 背景最近群里总有人"炫耀"一次操作花了几千积分,说实话这不是值得高兴的事——这是在告诉所有人你的开发方式有根本性问题。我也走过这条弯路,所以来说说怎么改。 积分消耗的本质是什么积分 = AI 处理的 token 数量 = 你发给 AI 的内容 + AI 回给你的内容所以烧积分的本质只有两种: 你让 AI 做了大量无效/重
精华
74 赞
23 回复
熬夜2个月游戏从0做到了1,又从1回到了0截图
熬夜2个月游戏从0做到了1,又从1回到了0
8月26日,我不懂编程,不懂设计,从0开始手搓一款回合制挂机游戏。 10月14日,快两个月了,功能全部实现。总算实现了从0-1的过程。 阶段手测还算正常,今天信心满满全栈手测,惊喜地发现,实现过程从1回到了0[表情_豹哭] 因为之前用AI配的底层数值写死了,所有装备和技能要推倒重做。 熬夜1-2个月,实现了从0-1-0的闭环。 看着屏幕,不知道是坚持还是放弃。 大家遇到这种努力全白费的情况,是怎么
1 赞
03:06
为了不被斩杀,我只能做到这些了截图
为了不被斩杀,我只能做到这些了
之前一直申请不到计划3,之前说太花哨,后面说体量完成度不足,在我一番小作文下,编辑终于给了我解答,什么原生质感,玩家开局体验啥的。 本来计划2也够用,准备以后再优化了,结果计划2赠送的积分要砍半了,学生党实在消费不起,于是只能再冲一波计划3了 从周六中午到现在一天半时间,优化了一遍角色卡,战斗卡,还有抽卡界面,优化了新手教程,该说不说确实比之前要舒服些。 前半部分为新版,后半部分为旧版。 明天再次
44 赞
9 回复
作品自荐-放置军团截图
作品自荐-放置军团
真标题:独立开发者手记:用 AI 评测倒逼自己迭代,从 5.8 分到 7.68 分 写给所有在 TapTap 上做游戏的独立开发者。 --- 先说一句题外话 最近 TapTap 在流量机制上做了不少调整,咕咕觉得整体是好的。平台愿意把流量分给认真做内容的人,这件事本身就值得鼓励。就是运营下班放假了,游戏可一直在发布更新;总之还是希望这个方向能持续下去,咕咕相信好的游戏都会被看到的。 --- 正文:
11 赞
4 回复
游戏自荐《少女终焉契约》截图
游戏自荐《少女终焉契约》
之前玩放置类型的rpg游戏的时候我就在想,现在游戏的操作有的太过于繁琐了,有没有一款休闲一点的,想上线就上线,离线的时候角色能自动行动的游戏呢?感谢TapTap制造,我按照我的想法开发出了现在这款游戏。 一、故事背景和UI 游戏的故事背景是传统的异世界穿越故事,玩家被女神召唤到异世界,随后组建工会,招募美少女,打怪升级探索世界。所以我选择了更加二次元萌系画风的UI风格,主页为各个主要的功能,点开来
8 赞
3 回复
01:29
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,截图
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,
视频内容只是一部分, 本游戏耗时3个月,目前完成进度百分之80, 是生存游戏,进入修仙界,走剧情找到宗门加入, 这时游戏才是真正开始, 提升实力境界后就可以变强,获取功法法术法宝, @视频内是游戏里挑战妖族入侵一种主流玩法,游戏就是围绕着这个来进行, 还有很多地方可以战斗,获得资源修炼变强,比如去藏书阁学习功法, 去法宝楼抽法宝,每一个宗门数十个法宝抽取 去修仙界探索,获得各自的资源,可能遇见妖兽
1 赞
3 回复
【游戏上新】——宅男打丧尸截图
【游戏上新】——宅男打丧尸
这是一款横向俯视角射击生存游戏,主打"轻松上手、越玩越爽"的爽快体验。游戏目前已包含: 四种核心模式:挑战、防守、生存、无尽,满足不同玩家口味 多种枪械,从手枪到重型火力,打出自己的风格 地雷,手雷,枪架多种不同军火自由选择,给敌人造成最大伤害 完整的成长体系:角色等级、军火系统、被动技能,越肝越强 丰富的敌人设计:普通丧尸到多种 Boss,战斗层次感明显 未来会制作更多不同的角色,天赋,军火,玩
4 赞
12 回复
【游戏自荐】一款“黑客级”的心理博弈截图
【游戏自荐】一款“黑客级”的心理博弈
喜欢策略游戏的朋友集合啦!我希望找到一些群体,虽然我被我的一些其他游戏玩家说游戏小众,但我还是想把这个游戏做好,为什么呢?可能我觉得这是我最满意的一个游戏吧。我更希望大家测试玩法,欢迎大家提出问题。可能游戏策略会有点难度,非常考验玩家的策略思路。 这是一款什么游戏? 简单来说,你将扮演一名背负天价债务的“地下走私客”,在无底的深渊口岸,试图把违禁资产从对手审查官的眼皮底下转移出去。 市面上有很多“
4 赞
3 回复
【UI教程】如何将已有的效果图应用为实际的UI界面截图
【UI教程】如何将已有的效果图应用为实际的UI界面
在经过了一些折腾和比较折磨的尝试后,我总算是摸清楚了嗒啦啦怎么把UI效果图转换为实际游戏中的UI组件,今天就给大家带来这部分的经验分享。 tips:1.本篇教程需要各位掌握最基本的ps使用,具体的操作部分我会大致讲解,但仍需各位自行学习ps的基本操作,部分可以由ai代劳的环节我会注明。 2.本篇教程需要在以我前两篇教程作为前置环节,请在阅读这两篇前置教程后再进行本篇教程内容的学习 在按照前
12 赞
5 回复
【游戏自荐】异海回响截图
【游戏自荐】异海回响
一、游戏简介 《异海回响》是一款赛博朋克风格的深海净化点击放置类游戏。 在这个世界中,海洋被未知污染侵蚀,万物异变为危险的污染体。你将化身净化者,点击驱散一个个污染生物,收集生态碎片强化自身,召唤伙伴协力作战,逐层深入八大海域,挑战精英变异体与深海巨兽Boss,最终揭开深渊核心的真相。 ·游戏中所有美术资源和形象动画全部由程序化矢量绘图生成,不使用任何图片素材。 ·游戏有动感的15首独一无二的音
7 赞
2 回复