下载 App
背景音乐持久化方案
问题演进
| 轮次 | 方案 | 失败原因 |
|---|---|---|
| 1 | File("save.pref", FILE_WRITE) | WASM MEMFS 刷新即丢失 |
| 2 | clientCloud:SetInt("music_enabled", 1/0) | 写 0 被后端吞掉;HTTP 请求刷新时被浏览器取消 |
| 3 | clientCloud:Set("music_enabled", "off") | 同上,HTTP 不可靠 |
| 4 | serverCloud via WebSocket 远程事件 | 可靠但 2-3 秒延迟导致 Toggle 闪烁 |
| 5 | ... 占位符等 serverCloud | 占位符卡死不消失 |
| 6 | 服务端内存缓存 + 播放守卫 + 超时兜底 | 最终方案 |
核心经验
- WASM 多人模式唯一可靠持久化路径:serverCloud via WebSocket(非 HTTP)
- 服务端进程在 WASM 刷新时不重启:内存缓存跨刷新持久,可实现重连零延迟
- 播放守卫比占位符更好:Toggle 始终可见(允许短暂闪变),但音乐不能先响再停
- ApplyMusicFromServer 必须无条件调用 UpdateLobbyMusic():守卫刚解除时值可能没变,但仍需触发播放
关键代码
1. Shared.lua — 事件定义
lua-- 客户端 → 服务端 SAVE_MUSIC_PREF = "BullBear_SaveMusicPref", -- 服务端 → 客户端 MUSIC_PREF = "BullBear_MusicPref", -- serverCloud key MUSIC_PREF_KEY = "friendship_boat_music_pref_v1",
2. Server.lua — 内存缓存 + 两层读取
luaS.userIdMusicPrefCache_ = {} -- userId → tag(服务端进程级,WASM 刷新不丢失) function Server.LoadMusicPref(slot) local p = S.players_[slot] if not p or not p.connection or not p.userId then return end local userId = p.userId -- 第1层:内存缓存命中 → 同步发送(零延迟) local cached = S.userIdMusicPrefCache_[userId] if cached ~= nil then Server.SendMusicPrefToConnection(p.connection, cached, "缓存命中") end -- 第2层:异步 serverCloud 读取(首次连接或缓存刷新) if not serverCloud then if cached == nil then Server.SendMusicPrefToConnection(p.connection, "", "服务端云不可用") end return end serverCloud:Get(userId, Shared.CONFIG.MUSIC_PREF_KEY, { ok = function(scores) local current = S.players_[slot] if not current or current.userId ~= userId or not current.connection then return end local tag = tostring(scores and scores[Shared.CONFIG.MUSIC_PREF_KEY] or "") S.userIdMusicPrefCache_[userId] = tag -- 更新缓存 if cached == nil then -- 缓存未命中时才需要发(命中已在上面同步发了) Server.SendMusicPrefToConnection(current.connection, tag, tag ~= "" and "已从云端读取" or "云端无记录") end end, error = function(code, reason) local current = S.players_[slot] if not current or current.userId ~= userId or not current.connection then return end if cached == nil then Server.SendMusicPrefToConnection(current.connection, "", "云端读取失败") end end, }) end function Server.SaveMusicPref(slot, tag) local p = S.players_[slot] if not p or not p.connection or not p.userId then return end local userId = p.userId tag = tostring(tag or "") S.userIdMusicPrefCache_[userId] = tag -- 立即更新缓存(下次连接零延迟) if not serverCloud then Server.SendMusicPrefToConnection(p.connection, tag, "服务端云不可用") return end serverCloud:Set(userId, Shared.CONFIG.MUSIC_PREF_KEY, tag, { ok = function() local current = S.players_[slot] if not current or current.userId ~= userId or not current.connection then return end Server.SendMusicPrefToConnection(current.connection, tag, "已保存到云端") end, error = function(code, reason) local current = S.players_[slot] if not current or current.userId ~= userId or not current.connection then return end Server.SendMusicPrefToConnection(current.connection, tag, "云端保存失败") end, }) end
3. Client.lua — 状态变量
lualobbyMusicEnabled_ = true, -- 当前开关状态 serverMusicPrefLoaded_ = false, -- 服务端偏好是否已到达 musicPrefLoadTimer_ = 0, -- 超时计时器 lobbyMusicSaveStatus_ = "未保存", -- 保存状态文案
4. ClientCore.lua — 播放守卫 + 超时兜底
luafunction Client.UpdateLobbyMusic() -- 关键守卫:服务端偏好未到达前不播放也不停止 if not serverMusicPrefLoaded_ then return end if not lobbyMusicEnabled_ or not IsLobbyMusicPhase() then Client.StopLobbyMusic() return end if lobbyMusicSource_ and lobbyMusicSource_:IsPlaying() then return end -- ... 创建 SoundSource 播放 ... end -- Update 循环中的超时兜底 if not serverMusicPrefLoaded_ then musicPrefLoadTimer_ = (musicPrefLoadTimer_ or 0) + dt if musicPrefLoadTimer_ >= 5.0 then serverMusicPrefLoaded_ = true Client.UpdateLobbyMusic() if phase_ == Shared.PHASE.WAITING then Client.BuildWaitingUI() end end end
5. ClientStorage.lua — 接收 + 保存
luafunction Client.ApplyMusicFromServer(tag, status) serverMusicPrefLoaded_ = true -- 解除守卫 lobbyMusicSaveStatus_ = status or lobbyMusicSaveStatus_ if type(tag) == "string" and tag ~= "" then local serverEnabled = (tag ~= "off") if serverEnabled ~= lobbyMusicEnabled_ then lobbyMusicEnabled_ = serverEnabled -- 写回本地 File 缓存(同会话内有效) local file = File(MUSIC_PREF_PATH, FILE_WRITE) if file and file:IsOpen() then file:WriteString(lobbyMusicEnabled_ and "on" or "off") file:Close() end end end -- 关键:无论值是否变化都必须调用(守卫刚解除,需要立即播放/停止) Client.UpdateLobbyMusic() if phase_ == Shared.PHASE.WAITING then Client.BuildWaitingUI() end end function Client.SaveMusicPreference() -- 本地 File 缓存(同会话即时生效) local file = File(MUSIC_PREF_PATH, FILE_WRITE) if file and file:IsOpen() then file:WriteString(lobbyMusicEnabled_ and "on" or "off") file:Close() end -- WebSocket 远程事件 → 服务端 serverCloud(WASM 可靠) if serverConnection_ then lobbyMusicSaveStatus_ = "服务器保存中" local data = VariantMap() data["Tag"] = Variant(lobbyMusicEnabled_ and "on" or "off") serverConnection_:SendRemoteEvent(Shared.EVENTS.SAVE_MUSIC_PREF, true, data) else lobbyMusicSaveStatus_ = "等待服务器连接" end end
6. ClientCore.lua — 事件订阅 + _G 桥接
lua-- Start() 中 SubscribeToEvent(Shared.EVENTS.MUSIC_PREF, "Client_HandleMusicPref") -- 处理函数 function Client_HandleMusicPref(eventType, eventData) local tag = eventData["Tag"]:GetString() local status = eventData["Status"]:GetString() if Client.ApplyMusicFromServer then Client.ApplyMusicFromServer(tag, status) end end -- 文件末尾 _G.Client_HandleMusicPref = Client_HandleMusicPref
数据流时序
首次连接:
客户端启动 → 默认ON,守卫阻止播放 → Toggle显示ON
WebSocket连接 → ClientIdentity → 服务端serverCloud异步读取(~0.05s)
→ MUSIC_PREF事件 → ApplyMusicFromServer → 守卫解除 → 播放/停止 → Toggle更新
WASM刷新(第2次+):
客户端启动 → 默认ON,守卫阻止播放 → Toggle显示ON
WebSocket连接 → ClientIdentity → 服务端内存缓存命中 → 同步发送(~0s)
→ MUSIC_PREF事件 → ApplyMusicFromServer → 守卫解除 → 立即正确播放/停止
最坏情况(事件丢失):
5秒超时 → 守卫解除 → 用默认值播放排查口诀(借鉴用户提供的方法论)
介质对不对?→ 写到没?→ ACK 收到没?→ Ready 了没?→ 白名单有没?
↓ ↓ ↓ ↓ ↓
serverCloud 日志确认 回调触发 事件订阅+_G桥接 本引擎无白名单
via WebSocket "已保存" "已发送" SubscribeToEvent 但需RegisterRemoteEvent
