下载 App

修复"地图编辑器刷新数据丢失"

07/27综合
horizontal linehorizontal line

问题诊断过程

用户报告:编辑器建好房间 → 保存 → 刷新页面 → 进编辑器 → 数据全没了。
关键线索:用户发现"校园日"模式刷新后能看到房间,但一进编辑器就没了。
这直接指向了编辑器页面自身的初始化逻辑,而非持久化链路。

根因

地图编辑模式.luaEnsureBlankEditorLayout() 设计意图是"首次打开编辑器提供空白画布",用模块级布尔 editorLayoutCleared_ 防重入。但 WASM 刷新 = Lua 模块重新加载 = 该标志重置为 false
时序:
刷新 → 云端存档正确恢复 rooms/floorAreas → 用户点"地图编辑模式"
→ MapEditorMode.Create() → EnsureBlankEditorLayout()
→ editorLayoutCleared_ == false → 无条件清空 MapData → 数据没了
校园日页面不调用此函数,所以数据正常。
horizontal linehorizontal line

关键代码(3 层修复)

第 1 层:根因修复 — 编辑器初始化守卫(v0.10.92)

scripts/页面/地图编辑模式.lua:563
lua
EnsureBlankEditorLayout = function(context) if editorLayoutCleared_ then return end -- ★ 核心守卫:存档已恢复用户数据时,不清空 local data = MapData.data local hasUserContent = #(data.rooms or {}) > 0 or #(data.floorAreas or {}) > 0 or #(data.objects or {}) > 0 if hasUserContent then editorLayoutCleared_ = true -- 标记已处理 print("[MapEditor] 存档已含用户编辑数据,跳过空白初始化 rooms=" .. tostring(#(data.rooms or {})) .. " floorAreas=" .. tostring(#(data.floorAreas or {})) .. " objects=" .. tostring(#(data.objects or {}))) return end -- 真正的空白地图(首次使用),执行清空 data.rooms = {} data.objects = {} data.characters = {} data.floorAreas = {} data.gameplayMarks = {} editorLayoutCleared_ = true editorState_.status = "空白地图模式:右键拖动画布、滚轮缩放,然后从左侧开始放置房间。" context.store.MarkDirty("mapEditorBlank") end
通用规律:任何"首次初始化重置全局数据"的函数,都不能仅靠模块级一次性标志——WASM 刷新会重置所有模块级变量。必须先检查数据是否已被持久层填充。
horizontal linehorizontal line

第 2 层:云存档时序加固(v0.10.90)

scripts/系统/云存档.lua — 解决 WASM 环境下的异步时序问题。
(a)身份等待 + 超时回退
lua
function CloudSave.Load(Store, onDone) if not HasCloudApi() then (onDone or function() end)(false, "cloud_unavailable") return end loading_ = true loadStore_ = Store loadCallback_ = onDone or function() end if CanUseCloud() then StartLoadRequest() -- userId 已就绪,立即读 else print("[CloudSave] 等待用户身份后读取云端存档") -- Update() 里每帧检测 userId,就绪后调 StartLoadRequest() -- 超过 LOAD_IDENTITY_TIMEOUT(15s) 则回退本地 end end
(b)token 防迟到回调
lua
local function StartLoadRequest() loadToken_ = loadToken_ + 1 local token = loadToken_ clientCloud:Get(CLOUD_KEY, { ok = function(values) if not loading_ or token ~= loadToken_ then return -- ★ 过期回调,丢弃 end -- ... 正常处理 end, }) end
(c)latest-wins 写队列(active/pending 分离)
lua
local Flush Flush = function(now) if saving_ or not pendingPayload_ then return false end local payload = pendingPayload_ pendingPayload_ = nil activePayload_ = payload -- ★ 分离:正在写的 vs 排队等写的 saving_ = true activeWriteToken_ = activeWriteToken_ + 1 local token = activeWriteToken_ clientCloud:Set(CLOUD_KEY, { payload = payload }, { ok = function() if token ~= activeWriteToken_ then return end saving_ = false activePayload_ = nil if pendingPayload_ then Flush(os.time()) -- ★ 写完后如果还有排队的,继续写最新的 else NotifySaveWaiters(true, "cloud") end end, error = function(code, reason) if token ~= activeWriteToken_ then return end saving_ = false if not pendingPayload_ then pendingPayload_ = activePayload_ -- ★ 仅无更新时回填,旧不覆盖新 end activePayload_ = nil nextRetryAt_ = os.time() + SAVE_INTERVAL end, }) end
(d)savedAt/revision 新旧比较
lua
local function ApplyCloudPayload(Store, record) local cloudSavedAt = tonumber(record.updatedAt) or 0 local cloudRevision = tonumber(record.payload.saveMeta and record.payload.saveMeta.revision) or 0 local localSavedAt = Store.GetLoadedSaveTimestamp() local localRevision = Store.GetLoadedSaveRevision() -- ★ 拒绝较旧云端数据覆盖较新本地数据 if localSavedAt > 0 and cloudSavedAt > 0 and (cloudSavedAt < localSavedAt or (cloudSavedAt == localSavedAt and cloudRevision < localRevision)) then return false, "local_newer" end Store.Deserialize(record.payload) return true, "cloud" end
horizontal linehorizontal line

第 3 层:保存链路统一入口(v0.10.90)

scripts/main.lua:125 — 所有保存走同一个 SaveGame
lua
SaveGame = function(onDone) local localSaved = Store.Save() -- 本地 File 快照 if not localSaved then if onDone then onDone(false, "local_save_failed") end return false end CloudSave.Flush(Store, onDone) -- 云端同步,回调通知结果 return true end,
scripts/数据/存储.lua — 每次序列化带版本元数据:
lua
function Store.Serialize() local payload = DeepCopy(state) payload.mapSnapshot = MapData.GetSnapshot() nextSaveRevision_ = nextSaveRevision_ + 1 payload.saveMeta = { savedAt = os.time(), -- 时间戳 revision = nextSaveRevision_, -- 递增版本号 } loadedSaveTimestamp_ = payload.saveMeta.savedAt return payload end
horizontal linehorizontal line

经验提炼(抗体 AB-074 + AB-075)

编号名称核心教训
AB-074WASM 云存档时序本地 File 刷新即丢;保存成功以云端 Set 回调为准;userId 可能延迟就绪需等待+超时;读写都要 token 防迟到;latest-wins 队列分离 active/pending;savedAt/revision 防旧覆新
AB-075编辑器初始化守卫模块级布尔标志在 WASM 刷新后重置;"首次初始化清空全局数据"必须先检查持久层是否已填充数据;不能仅靠一次性标志防重入
诊断方法论:当"页面 A 数据正常、页面 B 数据丢失"时,问题大概率在页面 B 的初始化逻辑,而非持久化链路本身。
地图编辑器地面交互与视觉优化
一、问题链 用户截图反馈了三个递进的问题: 问题 1:点击地面类型(如食堂地砖),弹窗不关闭,挡住画布无法操作 ↓ 修复 v0.10.94 问题 2:地面贴图太大,一块地砖比小人还宽,比例失调 ↓ 修复 v0.10.96(改配置值) 问题 3:改了配置值后刷新仍然太大——旧存档的 tileSize=256 优先级高于新 config ↓ 修复 v0.10.97(反转渲染优先级) 二、关键代码
官方
4 赞
穿透层修复
经验一:CSS transform 穿透层修复模式 问题本质 当容器使用 CSS transform: translateX/Y/scale 平移/缩放子树时,即使父容器有 overflow = "hidden",引擎的命中检测可能不裁剪越界区域。如果 transform 后的子树有更高的 zIndex,它会在视觉范围外拦截其他兄弟节点的指针事件。 受影响的场景 地图编辑器(右键平移/滚轮缩放后)
官方
4 赞
图片加载三层优化设计
目标场景:地图编辑器"物品/墙体/地面/房间/人员"调色板弹窗的图片首帧显示 文档目的:沉淀"图片为什么慢、怎么治、发布后玩家体验如何、未来加资源时 preload_groups 怎么取舍"的完整结论,避免重复踩坑。 一句话结论 WASM 下图片"慢"不是单一问题,而是下载 / 解码 / 上传 GPU 三件事全堆在"玩家点弹窗那一刻"做导致的。优化的本质是把这三件事提前到启动期做(让玩家点的时候图
官方
1 赞
地图编辑器仿真 tick 重建抑制
UI 重建频率 = 事件分发的可靠性上限。 只要有一帧 SetRoot 销毁了正在处理 pointer 事件的控件,跨重建边界的 Down/Up 对就会被吞。高频仿真 tick 必须在 UI 边界按页面过滤。 关键代码(scripts/main.lua HandleUpdate) 四步走:先原地处理人员 → 聚合所有仿真 changed → 地图页强制压制 → 只放行显式/登录态变化。 -- ma
官方
4 赞
右键拖拽平移
一、问题本质 不是平移公式写错,而是 UrhoX UI 库指针事件派发的两个不对称陷阱叠加: 陷阱 现象 根因文件 OnPointerMove 不向父冒泡 右键按下 isPanning_=true,但 move 事件永远到不了 mapView urhox-libs/UI/Core/Widget.lua(OnPointerDown/Up 有 parent 冒泡,OnPointerMove 没有) H
官方
4 赞
地面样式配置归一化
1. 唯一真相源 scripts/数据/地图/地面样式配置.lua local FloorStyleConfig = {} -- ★ 所有地面样式集中定义在这里 FloorStyleConfig.STYLES = { ["木地板"] = { texture = "image/地面/木地板_无缝.png", tileSize = 40 }, ["草地"] = { texture = "imag
官方
4 赞
拾取 / 丢弃物品
拾取的坑是「点击被遮挡」,丢弃的坑是「坐标反解会偏」。两者解法都是绕开复杂机制、用最直接的方式。 问题 根因 解法 走到物品旁点不动、捡不起来 物品命中层和主角标记同层、都无 zIndex,数组顺序靠后的主角标记盖在上面,点击命中的是主角而非物品 给可交互物品显式 zIndex = 20 拖拽丢东西飞到远处 用 ScreenToWorld 把鼠标像素反解成世界坐标当落点,依赖 zoom/offse
官方
4 赞
自动寻路赠送
核心功能 携带模式(点击物品跟鼠标)下: ≤8格:点击小人直接赠送(金色边框) >8格:点击远距离小人 → 自动寻路走向目标 → 到达8格内自动赠送 取消:玩家按方向键/WASD/摇杆手动移动 → 立即取消,恢复物品 关键经验(3条踩坑教训) 经验1:类型标识不是中文,是英文枚举值 地图编辑器放置的角色 type/job 字段是英文标识(teacher_3、doctor、cleaner_2、tea
官方
1 赞
精灵平滑移动、墙碰撞体积
一、 渲染层用连续坐标(scripts/页面/校园地图.lua,三处统一): -- NPC 定位(第408-410行) local contLeft = (character.x or 0) * 5 - 18 - charLeft local contTop = (character.y or 0) * 5 - 36 - charTop -- 主角定位(第504-506行) local con
官方
1 赞
以鼠标为锚点缩放
核心数学原理 缩放前后,鼠标指向的那个世界坐标点必须保持在同一屏幕位置。 已知:旧缩放 oldZoom、新缩放 newZoom、鼠标锚点屏幕坐标 (ax, ay)、当前偏移 (panX, panY) 步骤1:算出旧缩放下鼠标指向的世界坐标 wx = (ax - panX) / oldZoom wy = (ay - panY) / oldZoom 步骤2:新缩放后反解新偏移,让 wx/wy
官方
1 赞