让TD塔释放的小人是DIY造型
2 小时前综合
根本问题:缓存键名不匹配
LoadNpcSprites 时:
mapping = {enemyType → npcName} (如 "bread_loaf" → "淀粉酶NPC")
cacheNpcPixels("bread_loaf", ...) ← 以 enemyType 为 key 缓存
DrawEnzymeNpc 时:
Render.GetSpriteData(npc.typeName) ← npc.typeName = "唾液淀粉酶"(Config 里的 npc.name)
npcSpriteCache_["唾液淀粉酶"] ← 找不到!→ 绿色圆球
修复方案:在 cacheNpcPixels 中同时也用 npcName 作为第二个 key 缓存,这样 "唾液淀粉酶" 也能命中缓存。另外还需要在 LoadNpcSprites 里额外加载所有酶塔精灵(因为当前 mapping 只有 enemy 类型)。
关键问题找到了:NPC 编辑器保存完整帧数据(frames 字段完整)到本地文件,但 syncAllToCloud() 保存到云端时只有元数据(无 frames)。而 Render.LoadNpcSprites 使用 "npc_" + npcName 格式的 key 从 clientCloud 加载,但 NPC 编辑器用 "npc_editor_data" 这个单一 key。
解决方案:在 NpcEditor/Persistence.lua 的 syncAllToCloud() 中,额外将每个 NPC 的完整帧数据以 "npc_" + npcName 格式单独保存到 clientCloud。
根本原因:两处问题组合导致酶塔一直显示绿色圆球:
缓存 key 不匹配(已在上次修复)
npcSpriteCache_ 用 enemyType(如 "bread_loaf")作 key,但酶塔 NPC 的 typeName 是 "唾液淀粉酶",查找时 miss。
→ cacheNpcPixels 现在同时以 npcName 双重索引。
酶塔精灵从未被加载(已在上次修复)
LoadNpcSprites 只遍历敌人绑定,没有遍历 Config.EnzymeTowers。
→ 增加了 enzymeCloudPending 循环专门加载酶塔精灵。
云端无精灵数据(本次修复)
syncAllToCloud() 只保存元数据到 "npc_editor_data",而 Render.LoadNpcSprites 从 clientCloud:BatchGet("npc_") 查找——key 格式完全不同,且云端从未保存帧像素。
→ 修改 NpcEditor/Persistence.lua:syncAllToCloud() 在保存元数据之后,额外对每个 NPC 以 "npc_" key 单独保存 idle 首帧稀疏数据。
使用流程:在 NPC 编辑器里画好酶的造型后保存,游戏刷新后酶塔即可显示对应精灵(通过 cloud:Set("npc_唾液淀粉酶", ...) 持久化,游戏启动时 BatchGet 恢复)。
WebSocket 超限的修复
原因:128×128 像素帧有数万个像素点,序列化后 ~121KB,超过 65535 字节限制。
修复:保存前降采样到 32×32(等比缩放,每 4×4 块取第一个非透明像素),降到约 ~8KB,远低于限制。游戏里塔的投射物本来也很小,32×32 精度完全够用。
好的,日志已加好。但等一下——上次报错 message size 121524 exceeds maximum 65535,而 CLOUD_KEY 元数据只有 NPC 名称和属性、无像素,不可能那么大。 真正的问题很可能是:frames[1] 格式是稀疏格式 {"row,col": [r,g,b,a]},但稀疏 key 是字符串 "0,0"、"0,1" 这种。JSON 里每个像素大概占 20-30 字节,128×128=16384 个非透明像素 × 25 字节 ≈ 409KB! downsampleSparse 的 key:match("^(%d+),(%d+)$") 应该能匹配这个格式。再检查一遍…… 不对! 看 pixelsToSparse: sparse[r .. "," .. c] = { px[1], px[2], px[3], px[4] } 这里 r 和 c 是来自 for r, row in pairs(pixels) 的表的键,而 pixels 是二维数组,键是数字,所以 r 是数字,r .. "," 会自动转字符串。 downsampleSparse 里 key:match("^(%d+),(%d+)$") 应该能匹配 "0,0" 这种格式。但等等……看看索引范围: 如果 pixels 是 1-based(Lua 数组),那么 pixelsToSparse 生成的 key 从 "1,1" 开始(不是 "0,0")。而 downsampleSparse 里 math.floor(1 / 4) = 0,这没问题,但所有输出的 key 范围是 0..31,共 32×32 个格子,正常。 问题不在 key 格式。那为什么还是 121KB? 让我确认一个可能性:frames[1] 在文件里是什么结构? buildSaveData 调用 pixelsToSparse(frame) 把二维数组转成稀疏表存文件。JSON 里应该是 {"1,1":[255,0,0,255], ...} 这种格式。 这是正确的稀疏格式,downsampleSparse 可以处理…… 等一下!我注意到一个关键问题。看 downsampleSparse 里: if sr and sc and px and (px[4] or 255) > 0 then 这里 px 是从 JSON 解析回来的数组 [r,g,b,a],在 Lua 中是 {1, 2, 3, 4} — 数字索引,所以 px[4] 是 alpha 值。这应该没问题。 真正的可疑点:上次报的两个错误 121555 和 121524 相差只有 31 字节!这说明第二次根本没有用到降采样代码,或者降采样代码根本没被执行! 最可能的原因:#data .animations.idle.frames 的 # 操作符在 JSON 解析的数组上可能返回 0!
因为 JSON 数组 ["key": {...}] 解析回来可能不是 Lua 序列,特别是如果 pixelsToSparse 的输出(稀疏表)被放进 frames 数组里,它在 JSON 里是 frames: [{...稀疏像素...}],但稀疏像素本身就是一个表,frames 数组只有一个元素,所以 #frames = 1 应该正确……
等一等!121524 这个数字非常接近 121555,差值才 31 字节。这说明降采样代码在第二次还是被执行了(因为有少许改变),但它没有真正压缩数据!
我怀疑这是因为:降采样后的 cloudFrame 实际上还是原来大小,因为 srcSize = data.gridSize or 32 — 如果 data.gridSize 是 128(128×128 画布),那 SPRITE_CLOUD_GRID = 32,应该 4:1 压缩。但如果 data.gridSize 是 32(默认),那 downsampleSparse 直接返回原值不压缩!
这就是 bug!如果用户用的是 32×32 的网格编辑的 NPC,srcSize = 32 = SPRITE_CLOUD_GRID = 32,downsampleSparse 会直接返回原稀疏数据,32×32 像素可能有最多 1024 个像素点,每个约 25 字节,还是只有 ~25KB,不可能到 121KB。
那 121KB 一定来自 CLOUD_KEY 的元数据!
但 CLOUD_KEY 只存元数据……除非 allData.npcs[name] 里混入了像素帧!
让我仔细再看 syncAllToCloud 里的元数据提取:
lua
复制
local meta = {
...
animations = {},
}
if data.animations then
for animKey, animData in pairs(data.animations) do
meta.animations[animKey] = {
fps = animData.fps,
loop = animData.loop,
frameCount = ...,
-- frames 不同步,体积太大
}
end
end
allData.npcs[name] = meta
这里只拷贝了 fps, loop, frameCount,没有复制 frames。这应该是小的。
那 121KB 到底从哪来的?让我先构建一下,用日志数据来确认,然后再针对性修复。
因为 JSON 数组 ["key": {...}] 解析回来可能不是 Lua 序列,特别是如果 pixelsToSparse 的输出(稀疏表)被放进 frames 数组里,它在 JSON 里是 frames: [{...稀疏像素...}],但稀疏像素本身就是一个表,frames 数组只有一个元素,所以 #frames = 1 应该正确……
等一等!121524 这个数字非常接近 121555,差值才 31 字节。这说明降采样代码在第二次还是被执行了(因为有少许改变),但它没有真正压缩数据!
我怀疑这是因为:降采样后的 cloudFrame 实际上还是原来大小,因为 srcSize = data.gridSize or 32 — 如果 data.gridSize 是 128(128×128 画布),那 SPRITE_CLOUD_GRID = 32,应该 4:1 压缩。但如果 data.gridSize 是 32(默认),那 downsampleSparse 直接返回原值不压缩!
这就是 bug!如果用户用的是 32×32 的网格编辑的 NPC,srcSize = 32 = SPRITE_CLOUD_GRID = 32,downsampleSparse 会直接返回原稀疏数据,32×32 像素可能有最多 1024 个像素点,每个约 25 字节,还是只有 ~25KB,不可能到 121KB。
那 121KB 一定来自 CLOUD_KEY 的元数据!
但 CLOUD_KEY 只存元数据……除非 allData.npcs[name] 里混入了像素帧!
让我仔细再看 syncAllToCloud 里的元数据提取:
lua
复制
local meta = {
...
animations = {},
}
if data.animations then
for animKey, animData in pairs(data.animations) do
meta.animations[animKey] = {
fps = animData.fps,
loop = animData.loop,
frameCount = ...,
-- frames 不同步,体积太大
}
end
end
allData.npcs[name] = meta
这里只拷贝了 fps, loop, frameCount,没有复制 frames。这应该是小的。
那 121KB 到底从哪来的?让我先构建一下,用日志数据来确认,然后再针对性修复。
现在请你在 NPC 编辑器里保存一次(点保存按钮),然后把日志发给我,特别是包含 [NpcEditor][SIZE] 的行。
另外,我分析了两次报错 121555 vs 121524(差 31 字节)——这个规律说明降采样代码确实执行了,但压缩效果几乎为零。这让我高度怀疑是 CLOUD_KEY 元数据包(allData)本身就有 121KB,而不是 sprite key 里的帧数据。
如果日志显示 CLOUD_KEY payload bytes: 121xxx,那说明 meta.animations 虽然我们以为不包含帧数据,但实际上通过 Lua 引用共享,直接引用了原始 data.animations[animKey] 的 frames 字段。
meta.animations[animKey] 里只有 fps, loop, frameCount — 代码本身是正确的,没有 Lua 引用泄漏问题(因为都是值类型,不是表引用)。 所以 CLOUD_KEY 元数据不应该有 121KB,除非有大量 NPC(几百个)。 这意味着 121KB 大概率来自 sprite key 的 Set。但 #data .animations.idle.frames 的 # 运算符……
我突然想到一个关键问题:稀疏表不是 Lua 序列!
frames[1] 是一个稀疏 pixel 表,它的键是 "0,1", "1,0" 等字符串,不是整数。frames 数组本身是 {sparse_table} — 一个只有一个元素的 Lua 序列,#frames = 1,没问题。
但是——frames[1] 是稀疏表,它的键是字符串 "r,c",这没问题,downsampleSparse 用 key:match("^(%d+),(%d+)$") 来解析,应该能匹配……
等等!我看到了!pixelsToSparse 里:
lua
复制
for r, row in pairs(pixels) do
for c, px in pairs(row) do
sparse[r .. "," .. c] = ...
r 是数字(Lua 数组的整数 key),r .. "," 里 Lua 会把整数 r 转成字符串,比如 "1", "12" 这种——没有前导零,纯数字字符串,%d+ 完全匹配!
那问题到底在哪?我先让你保存一次看日志,但同时我想检查一下这个极可能的 bug:
#data .animations.idle.frames 可能在 cjson 解析后返回 0!
cjson.decode 对于 JSON array [{...}] 返回的是 Lua table {[1]={...}},这是标准 Lua 序列,# 应该返回 1。但 cjson 有个已知问题:如果 JSON array 里的元素被识别为 sparse(比如 JSON object 作为 array 元素时),有时会出问题。
不过这个问题应该不会发生,因为 frames 里就是一个 JSON 对象(稀疏像素表)作为单一数组元素。
更可能的问题:CloudProxy 在多人模式下,cloud:Set 走 WebSocket,但 CLOUD_KEY 的那次 Set 先执行,cjson.encode(allData) 后的 allData 对象包含了对 data.gameStats 的引用,如果 gameStats 非常大……
不,gameStats 应该很小。
我突然想到一个关键问题:稀疏表不是 Lua 序列!
frames[1] 是一个稀疏 pixel 表,它的键是 "0,1", "1,0" 等字符串,不是整数。frames 数组本身是 {sparse_table} — 一个只有一个元素的 Lua 序列,#frames = 1,没问题。
但是——frames[1] 是稀疏表,它的键是字符串 "r,c",这没问题,downsampleSparse 用 key:match("^(%d+),(%d+)$") 来解析,应该能匹配……
等等!我看到了!pixelsToSparse 里:
lua
复制
for r, row in pairs(pixels) do
for c, px in pairs(row) do
sparse[r .. "," .. c] = ...
r 是数字(Lua 数组的整数 key),r .. "," 里 Lua 会把整数 r 转成字符串,比如 "1", "12" 这种——没有前导零,纯数字字符串,%d+ 完全匹配!
那问题到底在哪?我先让你保存一次看日志,但同时我想检查一下这个极可能的 bug:
#data .animations.idle.frames 可能在 cjson 解析后返回 0!
cjson.decode 对于 JSON array [{...}] 返回的是 Lua table {[1]={...}},这是标准 Lua 序列,# 应该返回 1。但 cjson 有个已知问题:如果 JSON array 里的元素被识别为 sparse(比如 JSON object 作为 array 元素时),有时会出问题。
不过这个问题应该不会发生,因为 frames 里就是一个 JSON 对象(稀疏像素表)作为单一数组元素。
更可能的问题:CloudProxy 在多人模式下,cloud:Set 走 WebSocket,但 CLOUD_KEY 的那次 Set 先执行,cjson.encode(allData) 后的 allData 对象包含了对 data.gameStats 的引用,如果 gameStats 非常大……
不,gameStats 应该很小。
找到了问题的关键!Client.CloudSave 里:
data["Payload"] = Variant(cjson.encode(value))
整个 value(allData 或 spriteData)被 JSON 编码后放入 Payload,然后通过 WebSocket 发出去。WebSocket 消息大小 = 整个 VariantMap 的序列化大小,不只是 Payload 字符串的长度。
现在看 Client.SendToServer 的第 285-286 行,那个 fragment… 这是分片发送!
第 286 行是 ReportBodyState,不是分片发送。整个 CloudSave 是单包发送,没有分片机制。
所以根本解决方案是:在 Persistence.lua 里根本不通过 WebSocket 保存 sprite 像素数据。在多人模式下,cloud:Set 走 WebSocket,65535 是硬限制。
正确的解决方案:在单人模式下 clientCloud:Set 直接可以存任意大小数据;在多人模式下应该跳过 sprite key 的云保存,或者用更激进的压缩(把像素进一步缩小到 16×16 甚至 8×8)。
但更重要的是先知道 121KB 到底来自哪里。让我在代码里加一个多人/单人模式判断,在多人模式下不通过 WebSocket 保存大尺寸 sprite
根因分析
syncAllToCloud() 里的 cloud:Set(spriteKey, spriteData) 经过 CloudProxy,在多人游戏模式下路由到 Client.CloudSave,后者会把整个 spriteData(含像素帧)JSON 编码成一个 WebSocket 消息。WebSocket 硬限制 65535 字节,而一帧 128×128 像素的稀疏格式 JSON 约 120KB+,因此溢出。
两次错误 121555 vs 121524 相差仅 31 字节是因为:降采样条件 srcSize = data.gridSize or 32 — 当 NPC 用的是 32×32 网格时 srcSize == SPRITE_CLOUD_GRID,downsampleSparse 直接返回原数据不压缩,所以大小几乎没变。
解决方法
Sprite 像素数据是本玩家的个人客户端数据,不需要经服务端 WebSocket 中转。改为直接调用 clientCloud:Set(),绕过 WebSocket 路由,无大小限制。Render.lua 读取时本来就用 clientCloud:BatchGet(),一致性保持不变。



