下载 App

多人同步与持久化模块

07/033 浏览
本模块记录多人模式下必须优先考虑的即时反馈、持久化、重连和事件时序设计。
核心原则 TAGIRON 类推理游戏有隐藏信息,必须由服务端作为权威裁判。
服务端掌握:
完整牌堆。 所有玩家手牌。 中央隐藏牌。 问题卡牌堆。 当前回合。 已公开回答。 胜负判定。 客户端只掌握:
自己手牌。 公开问题卡。 公开回答。 可见玩家状态。 自己可执行的动作。 房间状态结构建议 lua
复制 RoomState = { roomId = "room_001", mode = "2p", phase = "asking", version = 1, players = {}, hands = {}, centerTiles = {}, questionDeck = {}, openQuestions = {}, usedQuestions = {}, currentTurnUid = nil, eliminated = {}, publicLog = {}, createdAt = 0, updatedAt = 0, } 注意:hands 和 centerTiles 是服务端私有状态,不能完整广播给所有客户端。
客户端动作命令 客户端只发送“我要做什么”,不发送“结果是什么”。
text
复制 CLIENT_READY CREATE_ROOM JOIN_ROOM LEAVE_ROOM PLAYER_READY ASK_QUESTION MAKE_GUESS REQUEST_FULL_SYNC REQUEST_RECONNECT 服务端广播事件 text
复制 ROOM_SYNC_FULL ROOM_PLAYER_JOINED ROOM_PLAYER_LEFT ROOM_STARTED TURN_CHANGED QUESTION_ASKED QUESTION_ANSWERED GUESS_RESULT PLAYER_ELIMINATED GAME_OVER ERROR_MESSAGE 所有事件建议带:
lua
复制 { roomId = roomId, version = room.version, serverTime = now, payload = data } 完整快照同步 必须支持 ROOM_SYNC_FULL。
用于:
首次进入房间。 刷新重连。 事件丢失后修复。 版本号跳变时恢复。 不同玩家收到的快照不同。
二人模式快照 玩家只能看到:
自己完整手牌。 对手手牌数量。 公开问题卡。 公共日志。 当前回合。 游戏阶段。 不能看到:
对手手牌内容。 三四人模式快照 玩家只能看到:
自己完整手牌。 其他玩家手牌数量。 中央牌数量。 公开问题卡。 公共日志。 出局状态。 不能看到:
其他玩家手牌内容。 中央牌内容。 异步时序:pendingClients 模式 常见问题:
text
复制 客户端 CLIENT_READY 已到 服务端 RoomState 还没从云端加载完成 如果直接 return 客户端永远收不到 ROOM_SYNC_FULL 正确模式:
lua
复制 if not room.loaded then table.insert(room.pendingClients, connection) return end
SendFullSync(connection) 加载完成后:
lua
复制 for _, conn in ipairs(room.pendingClients) do SendFullSync(conn) end room.pendingClients = {} 初始化保护期 危险流程:
text
复制 Start initDefaultRoom saveRoom loadCloudRoom 这会用默认状态覆盖真实云端状态。
正确流程:
lua
复制 initializing = true LoadRoomFromCloud() ApplyCloudRoom() initializing = false SaveRoomIfNeeded() 保存函数必须检查:
lua
复制 if initializing then return end 版本号机制 每次服务端改变房间状态:
room.version = room.version + 1 客户端保存本地版本号:
收到旧版本:忽略。 收到连续新版本:应用。 发现跳号:请求 REQUEST_FULL_SYNC。 持久化分类 房间短期状态 用于断线重连和刷新恢复。
建议 key:
tagiron_room_{roomId}_state 保存时机:
房间创建。 发牌完成。 每次提问后。 每次推测后。 玩家出局后。 游戏结束后。 玩家断线时。 玩家长期状态 建议 key:
text
复制 tagiron_player_{uid}profile tagiron_player{uid}_stats 内容:
昵称。 胜场。 败场。 平局。 总局数。 教程进度。 偏好设置。 排行榜整数 排行榜字段必须单独保存为整数。
示例:
text
复制 tagiron_rank_score tagiron_rank_wins 写入策略 采用:
脏标记 + 定时保存 + 关键节点即时保存 + 断线 flush 建议:
普通状态 10 秒自动保存。 胜负、发牌、出局等关键节点立即保存。 断线时主动 flush 当前快照。 防止隐藏信息泄漏 任何发送给客户端的数据都要经过过滤。
禁止广播:
所有玩家完整手牌。 中央牌内容。 未公开的问题牌堆顺序。 推测判定前的真实答案。 允许广播:
自己手牌。 其他玩家手牌数量。 中央牌数量。 已公开回答。 已使用问题。 胜负结果。 日志建议 第一版必须保留详细日志:
text
复制 [Room] Create roomId=... [Room] Load start roomId=... [Room] Load done roomId=... version=... [Net] CLIENT_READY uid=... roomLoaded=... [Net] pending client added uid=... [Net] Send ROOM_SYNC_FULL uid=... version=... [Action] ASK_QUESTION uid=... qid=... [Action] MAKE_GUESS uid=... target=... [Persist] Save roomId=... version=... [Persist] Load roomId=... version=... [Error] Reject action reason=... 排查清单 遇到刷新后状态错误或便签式数据不显示的问题,按顺序检查:
事件是否注册。 RemoteEvent 是否被引擎丢弃。 云端 key 是否一致。 回调触发时消费者是否就绪。 初始化是否覆盖云端真实状态。 是否有 pending 请求被直接 return 丢弃。 是否有完整快照兜底。 是否有断线 flush。 是否存在并行回调写同一个变量。 是否所有 error 分支都避免盲写。 结论 多人模式的重点不是 UI,而是:
服务端权威。 隐藏信息安全。 刷新重连恢复。 异步请求不丢。 初始化不覆盖。 关键节点持久化。 完整快照兜底。 玩家昵称持久化与异步覆盖保护 公共房间大厅中,玩家昵称属于“玩家长期状态 + UI 身份展示”数据。它不参与对局裁判,但会影响房间列表、等待页、日志和测试可识别性。
昵称优先级 推荐优先级:
text
复制 云端自定义昵称 user_nickname
本地昵称缓存 bagua_player_pref.txt TapTap 平台昵称 默认“玩家” 为什么不能直接用 TapTap 昵称覆盖 危险时序:
text
复制 玩家手动改昵称为 A ↓ 刷新重进 ↓ 客户端先显示默认值 ↓ 服务端 GetUserNickname 异步返回 TapTap 昵称 B ↓ 如果无条件应用 B,就会覆盖 A 因此 TapTap 昵称只能作为“没有自定义昵称时的默认值”,不能作为最高优先级。
本地 + 云端双层持久化 本地文件用于快速恢复当前设备昵称:
bagua_player_pref.txt 云端变量用于账号级恢复:
user_nickname 手动修改昵称时必须同时执行:
text
复制 更新内存 写本地文件 写 clientCloud user_nickname 向服务端发送 CustomName=true 刷新 UI 云端读取的版本号保护 云端读取是异步的,必须防止旧回包覆盖玩家刚刚输入的新昵称。
建议模式:
lua
复制 local requestVersion = playerNameVersion_ clientCloud:Get("user_nickname", { ok = function(values) if playerNameVersion_ ~= requestVersion then return end -- 只有期间玩家没有再次修改昵称,才应用云端值 end }) 服务端 TapTap 昵称回包保护 客户端如果使用自定义昵称,应在房间协议中携带:
CustomName = true 服务端记录:
customNameKeys_[connectionKey] = true GetUserNickname 回调回来后:
lua
复制 if not customNameKeys_[key] then UpdateConnectionName(connection, tapTapNick) end 这样 TapTap 昵称不会覆盖用户自定义昵称。
推荐测试流程 [ ] 新用户首次进入时,默认显示 TapTap 昵称。 [ ] 修改昵称后,房间卡片和等待页立即显示新昵称。 [ ] 刷新/重进后仍显示自定义昵称。 [ ] 云端旧昵称晚到不会覆盖刚修改的新昵称。 [ ] TapTap 昵称查询晚到不会覆盖自定义昵称。 [ ] 换设备登录同一账号时,能恢复云端 user_nickname。
弹窗内做"搜索 + 分类 + 网格"
经验 1:弹窗内做"搜索 + 分类 + 网格",用局部刷新保住输入焦点 为什么有效:搜索框是 UI.TextField,如果每次输入都 RebuildUI 重建整棵树,焦点会丢、光标乱跳。正确做法是只把"网格容器"这一棵子树的子节点 ClearChildren + AddChild,搜索框和分类标签不动。模块级变量 objectCategory_ 跨弹窗开关不重置,天然实现"记住上次分类"。 关键
官方
穿透层修复
经验一:CSS transform 穿透层修复模式 问题本质 当容器使用 CSS transform: translateX/Y/scale 平移/缩放子树时,即使父容器有 overflow = "hidden",引擎的命中检测可能不裁剪越界区域。如果 transform 后的子树有更高的 zIndex,它会在视觉范围外拦截其他兄弟节点的指针事件。 受影响的场景 地图编辑器(右键平移/滚轮缩放后)
官方
4 赞
地图编辑器「墙」全链路增强
经验 1:子编辑器"连续操作" vs "一次性操作"——提交后别清空激活态 现象:放完一段墙,提示"可继续拖拽放置",但再拖没反应。 根因:CommitWall 提交后把 selectedItem=nil、tool="select",导致 IsActive() 返回 false,下次 HandlePointerDown 第一行就 return。提示语与行为矛盾。 模式:连续工具(墙、擦除)提交后只
官方
4 赞
右键拖拽平移
一、问题本质 不是平移公式写错,而是 UrhoX UI 库指针事件派发的两个不对称陷阱叠加: 陷阱 现象 根因文件 OnPointerMove 不向父冒泡 右键按下 isPanning_=true,但 move 事件永远到不了 mapView urhox-libs/UI/Core/Widget.lua(OnPointerDown/Up 有 parent 冒泡,OnPointerMove 没有) H
官方
4 赞
【日常更新】支持2人、3人、4人对战截图
【日常更新】支持2人、3人、4人对战
优化了排行榜显示玩家昵称
3 赞
以鼠标为锚点缩放
核心数学原理 缩放前后,鼠标指向的那个世界坐标点必须保持在同一屏幕位置。 已知:旧缩放 oldZoom、新缩放 newZoom、鼠标锚点屏幕坐标 (ax, ay)、当前偏移 (panX, panY) 步骤1:算出旧缩放下鼠标指向的世界坐标 wx = (ax - panX) / oldZoom wy = (ay - panY) / oldZoom 步骤2:新缩放后反解新偏移,让 wx/wy
官方
1 赞
地图编辑器仿真 tick 重建抑制
UI 重建频率 = 事件分发的可靠性上限。 只要有一帧 SetRoot 销毁了正在处理 pointer 事件的控件,跨重建边界的 Down/Up 对就会被吞。高频仿真 tick 必须在 UI 边界按页面过滤。 关键代码(scripts/main.lua HandleUpdate) 四步走:先原地处理人员 → 聚合所有仿真 changed → 地图页强制压制 → 只放行显式/登录态变化。 -- ma
官方
4 赞
图片加载三层优化设计
目标场景:地图编辑器"物品/墙体/地面/房间/人员"调色板弹窗的图片首帧显示 文档目的:沉淀"图片为什么慢、怎么治、发布后玩家体验如何、未来加资源时 preload_groups 怎么取舍"的完整结论,避免重复踩坑。 一句话结论 WASM 下图片"慢"不是单一问题,而是下载 / 解码 / 上传 GPU 三件事全堆在"玩家点弹窗那一刻"做导致的。优化的本质是把这三件事提前到启动期做(让玩家点的时候图
官方
1 赞
修复"地图编辑器刷新数据丢失"
问题诊断过程 用户报告:编辑器建好房间 → 保存 → 刷新页面 → 进编辑器 → 数据全没了。 关键线索:用户发现"校园日"模式刷新后能看到房间,但一进编辑器就没了。 这直接指向了编辑器页面自身的初始化逻辑,而非持久化链路。 根因 地图编辑模式.lua 的 EnsureBlankEditorLayout() 设计意图是"首次打开编辑器提供空白画布",用模块级布尔 editorLayoutClear
官方
4 赞
地图编辑器地面交互与视觉优化
一、问题链 用户截图反馈了三个递进的问题: 问题 1:点击地面类型(如食堂地砖),弹窗不关闭,挡住画布无法操作 ↓ 修复 v0.10.94 问题 2:地面贴图太大,一块地砖比小人还宽,比例失调 ↓ 修复 v0.10.96(改配置值) 问题 3:改了配置值后刷新仍然太大——旧存档的 tileSize=256 优先级高于新 config ↓ 修复 v0.10.97(反转渲染优先级) 二、关键代码
官方
4 赞