战争迷雾系统
05/165 浏览综合
我找到了根本原因。让我仔细比对代码:
位置保存(正常工作):
lua
复制
-- Server.lua: loadAndSendPosition
serverCloud:Get(userId, S.POSITION_KEY, {
ok = function(values)
local pos = values[S.POSITION_KEY] or {}
-- ...发送给客户端
end
})
迷雾保存 — 关键问题在 serverCloud:Set 的数据格式:
lua
复制
-- Server.lua: HandleWallSaveFog
serverCloud:Set(userId, S.FOG_KEY, fogData, {...})
迷雾的 fogData 结构是 { w=N, h=N, rows={{r, {c1,c2,...}}, ...} }。其中 rows 是一个嵌套数组,每个元素是 {行号, {列数组}}。
根据 serverCloud API 文档:serverCloud:Set 写入 → scores,Get 的 ok 回调是 function(scores, iscores, sscores)。
但真正的问题在于:values[S.FOG_KEY] 中的 rows 字段是一个 数组中嵌套了混合类型的数组({数字, 表})。在 JSON 序列化/反序列化过程中,cjson.encode({1, {2,3,5}}) → [1,[2,3,5]],然后 cjson.decode('[1,[2,3,5]]') → {1, {2,3,5}}。这本身没问题。
让我再检查一下数据量大小——这可能是真正的瓶颈。
好,地图尺寸从 mapData.width/height 获取。现在让我重点审视一个核心差异——发布版和 PC 测试版的 userId 行为。
关键的一点:在 HandleWallClientReady 中,只有 userId > 0 才会加载迷雾:
lua
复制
if userId > 0 then
...
loadAndSendFog(connection, userId)
end
在 HandleWallSaveFog 中:
lua
复制
local userId = S.resolveUserId(connection)
if userId == 0 then
print("[Server] SAVE_FOG: userId=0,忽略")
return
end
如果发布版中 userId 为 0,保存和加载都会被跳过。但如果 userId 为 0,位置保存也同样会被跳过(代码逻辑一致),那位置也不应该工作才对…
让我再仔细看一下——用户说的是"PC端测试,迷雾可以持久化",但"发布之后没有持久化"。位置持久化在发布后是否正常工作呢?
用户没有明确说位置在发布后正常,可能两者都有问题。但让我关注一个更可能的根本原因:serverCloud:Get 回调中 values 参数的结构问题。
看 loadAndSendFog 的回调:
lua
复制
ok = function(values)
local fogData = values[S.FOG_KEY] or {}
根据文档,serverCloud:Get 的 ok 回调参数是 (scores, iscores, sscores)。所以 values 实际上是 scores 表。serverCloud:Set 写入的数据存在 scores 中,所以 scores[S.FOG_KEY] 应该能取到数据。
但是 —— 让我注意 serverCloud:Set 的签名:serverCloud:Set(uid, key, value, events)。value 是 fog 数据表。这会写入 scores[key] = value。
对比位置保存:
serverCloud:Set(userId, S.POSITION_KEY, position, {...})
位置 position 是 { worldX=N, worldY=N }——一个简单的扁平表。
而迷雾 fogData 是 { w=N, h=N, rows={{r, {c1,c2,...}}, ...} }——嵌套数组。
我怀疑问题在于 serverCloud 对嵌套复杂数据的序列化/存储有限制或丢失数据。让我验证一下——让我检查是否有大小限制。
文档中提到:数据量限制是 48 MB/分钟,单次操作没有明确的大小限制。但 scores 的值类型限制可能是问题所在。
让我换个思路——再审查一下 HandleWallSaveFog 中的数据传递链路:
客户端 Client.SaveFog(fogData) → sendToServer(EVENTS.SAVE_FOG, { fog = fogData })
数据经过 Shared.SendChunked → cjson.encode({ fog = fogData }) → 如果超过 50KB 则分片
服务端收到后 → HandleWallSaveFog → cjson.decode(payload) → req.fog
serverCloud:Set(userId, S.FOG_KEY, fogData, {...})
等一下——让我检查分片路由。在 HandleWallChunkedDataServer 中:
lua
复制
[EVENTS.SAVE_FOG] = function(fullJson, chunkEventData)
chunkEventData["Payload"] = Variant(fullJson)
HandleWallSaveFog("", chunkEventData)
end,
这里用 chunkEventData(即分片事件的 eventData),覆盖了 Payload。但关键是 —— chunkEventData 中有没有 Connection 字段?
在分片事件 HandleWallChunkedDataServer 中:
lua
复制
function HandleWallChunkedDataServer(eventType, eventData)
Shared.HandleChunk(eventData, { ... })
end
Shared.HandleChunk 最后调用 handler(fullJson, eventData) —— 注意这里的 eventData 是最后一个分片的 eventData。
而 HandleWallSaveFog 需要从 eventData 中取 Connection:
local connection = eventData["Connection"]:GetPtr("Connection")
问题是:分片事件 WallChunkedData 的 eventData 是否包含 Connection 字段?
RemoteEvent 从客户端发到服务端时,引擎会自动在 eventData 中填充 Connection 字段。所以直接的 WallSaveFog 事件和分片事件 WallChunkedData 都应该有 Connection。
这对于分片场景是正确的——因为分片也是通过 RemoteEvent 发送,最后一个分片的 eventData 自然包含 Connection。
让我重新思考——PC 测试和发布版最大的区别是什么?
PC 测试时通常是本地服务器 + 客户端,数据直接保存在本地 serverCloud 中。发布后是远端服务器,数据通过网络保存到云端。
一个关键可能性:serverCloud:Set 在保存复杂嵌套表时,云端服务可能对值做了 JSON 序列化,而 rows 中的 {1, {2,3,5}} 这种混合数组可能被解析为 JSON object 而不是 array。因为 cjson 对于 {1, {2,3,5}} 会编码为 [1,[2,3,5]](纯数组),但云端存储后再次取出时,如果后端系统对 JSON 做了处理,可能会丢失数据结构。
但这也应该影响位置数据… 不过位置数据是扁平的 {worldX=N, worldY=N},不包含数组。
让我重新审视一个更简单的可能原因:saveDirty 标志的行为。
在 saveAllToCloud 中:
lua
复制
if WallClient and MainGame.gs.fogOfWar and MainGame.gs.fogOfWar.saveDirty then
...
MainGame.gs.fogOfWar.saveDirty = false
而在 FogOfWar.Deserialize 中:
fog.saveDirty = false -- 反序列化后重置
问题可能是这样的:
玩家探索 → saveDirty = true
移动停止 → saveAllToCloud() → 保存成功 → saveDirty = false
继续探索 → saveDirty = true
自动保存定时器 → saveAllToCloud() → 保存成功 → saveDirty = false
玩家退出游戏
在 PC 测试中,退出游戏时可能还有时间触发最后一次保存。但在发布版(移动端),退出时可能更突然(切后台立即断连),最后的 saveDirty = true 没来得及保存。
但这只是部分问题——用户说的是"再重新进入游戏就还是漆黑一片",意味着之前保存的数据也完全丢失,不只是最后一次探索。
让我再回头检查一个非常关键的细节——serverCloud:Set 的 value 是否必须是字符串或数字,不能是表?
看文档:
serverCloud:Set(uid, "game_config", { difficulty = "hard" })
文档明确显示 Set 可以接受 table 作为 value。所以这不是问题。
让我检查另一种可能:发布版是否使用了不同的 serverCloud 实例或配置? 或者 发布版和 PC 版是否指向同一个云端存储空间?
实际上这正是根本原因——PC 调试和发布后很可能是不同的 serverCloud 存储空间。每次发布产生新的 project/app,serverCloud 绑定的是 app 级别的存储。PC 测试时保存的数据在发布后的 app 中不存在。
不过用户说的是"发布之后测试发现并没有持久化",意味着用户在发布版中先探索了地图,然后退出,再进入时迷雾重置。不是说 PC 的数据在发布后找不到。
所以这不是存储空间不同的问题。
让我再仔细看一下——迷雾加载的时机和 fog 实例创建的顺序
现在让我检查 fogOfWar 初始化和加载的时序关系——fogOfWar 是在 MainGameSetup 的 _setupGameUI 中创建的(line 311),而 OnFogLoaded 回调注册在 line 882。
关键问题是:什么时候 serverCloud:Get 的回调返回? 在服务端 HandleWallClientReady 中,loadAndSendFog 是异步调用的。在 PC 上本地服务器可能很快返回。在发布版上,网络延迟可能导致数据在回调注册前就到达了。
等等,代码已经有了"主动合并"逻辑(line 893-900)来处理这个竞态。
让我再检查一下 Client.lua 中的 FOG_LOADED 事件接收,特别是 HandleWallFogLoaded。我需要确认是否在分片路由中正确处理了 FOG_LOADED:
在 HandleWallChunkedDataClient 中,FOG_LOADED 已经注册:
lua
复制
[EVENTS.FOG_LOADED] = function(fullJson, chunkEventData)
chunkEventData["Payload"] = Variant(fullJson)
HandleWallFogLoaded("", chunkEventData)
end,
但同时 FOG_LOADED 也被直接订阅了:
SubscribeToEvent(EVENTS.FOG_LOADED, "HandleWallFogLoaded")
如果数据通过分片发送,那么:
第一个分片 → 触发 HandleWallChunkedDataClient → 缓存分片
最后一个分片 → 重组完成 → 调用 HandleWallFogLoaded
如果数据不需要分片(小于 50KB),则直接通过 WallFogLoaded 事件发送,HandleWallFogLoaded 被直接调用。
这没问题。
让我现在追踪另一个角度——检查服务端发送迷雾数据时使用的 S.sendToClient:
lua
复制
-- loadAndSendFog
S.sendToClient(connection, EVENTS.FOG_LOADED, { fog = fogData })
S.sendToClient 调用 Shared.SendChunked:
lua
复制
function ServerState.sendToClient(connection, eventName, payload)
Shared.SendChunked(function(evName, jsonStr)
local vm = VariantMap()
vm["Payload"] = Variant(jsonStr)
connection:SendRemoteEvent(evName, true, vm)
end, eventName, payload)
end
而直接发送时:connection:SendRemoteEvent(evName, true, vm) 中的 true 表示 in-order delivery。
对于位置,使用直接发送(不走 sendToClient):
lua
复制
local vm = VariantMap()
vm["Payload"] = Variant(cjson.encode({ position = pos }))
connection:SendRemoteEvent(EVENTS.POSITION_LOADED, true, vm)
位置数据很小,不需要分片。迷雾数据可能也不大(32x20 的地图),但它走了 sendToClient → SendChunked 路径。
等等,我发现了一个关键差异:
位置加载:直接 cjson.encode + SendRemoteEvent
迷雾加载:通过 S.sendToClient → Shared.SendChunked → cjson.encode
Shared.SendChunked 内部:
lua
复制
function Shared.SendChunked(sendFn, eventName, payload)
Shared.SendChunkedRaw(sendFn, eventName, cjson.encode(payload))
end
所以 payload { fog = fogData } 被 cjson.encode,然后如果小于 50KB,直接通过 sendFn 发送。但这里 payload 已经被 encode 一次了!
等等不对,让我再看——sendFn 是:
lua
复制
function(evName, jsonStr)
local vm = VariantMap()
vm["Payload"] = Variant(jsonStr)
connection:SendRemoteEvent(evName, true, vm)
end
jsonStr 是已编码的 JSON 字符串。客户端收到后在 HandleWallFogLoaded 中 cjson.decode 一次。这是正确的。
让我回到最根本的问题——让我检查发布版中 serverCloud:Set 的回调是否打印了成功信息。如果 ok 回调没有触发,说明 Set 操作失败了。
我没法看到发布版的服务端日志,但我可以加更多诊断日志来帮助定位问题。
不过等等,让我再仔细检查一遍——serverCloud:Get 的 ok 回调中 values 的实际结构。文档说:
serverCloud:Get(uid, key, events) → ok = function(scores, iscores, sscores)
第一个参数是 scores,它是一个表,包含通过 Set 写入的键值对。所以 scores["fog_explored"] 应该就是之前 Set 保存的 fogData 表。
现在我想到一种非常可能的情况: 在 loadAndSendFog 中如果 serverCloud:Get 的 ok 回调里 values[S.FOG_KEY] 为 nil(因为从未成功保存),那 fogData 就是 {}(空表)。发送给客户端后,客户端的 HandleWallFogLoaded 收到 fog = {},这时 fog.w 为 nil,所以 fogData.w ~= nil 检查失败,迷雾不会反序列化——这就是用户看到的"漆黑一片"。
所以根本问题是 serverCloud:Set 保存是否成功。
让我找一个可能的原因——在发布版中,如果客户端在断连前发送了 SAVE_FOG,但服务端还没来得及执行 serverCloud:Set 就关闭了。
但更可能的问题是——我刚才忽略了一个细节。让我再查 saveAllToCloud:
if WallClient and MainGame.gs.fogOfWar and MainGame.gs.fogOfWar.saveDirty then
saveDirty 在什么时候被设为 true? 在 FogOfWar.Reveal 中,当有新格子被揭示时设为 true。但如果 Deserialize 被调用了(从云端加载数据),saveDirty 被重置为 false。


