地图编辑器「墙」全链路增强

07/271 浏览综合

horizontal linehorizontal line

经验 1:子编辑器"连续操作" vs "一次性操作"——提交后别清空激活态

现象:放完一段墙,提示"可继续拖拽放置",但再拖没反应。 根因CommitWall 提交后把 selectedItem=niltool="select",导致 IsActive() 返回 false,下次 HandlePointerDown 第一行就 return。提示语与行为矛盾。 模式:连续工具(墙、擦除)提交后只 ResetDrag()保留 selectedItem/tool;右键 Cancel() 才退出。
lua
-- 墙体拖拽编辑.lua CommitWall 末尾 self:ResetDrag() -- 保持 selectedItem 和 tool 不变,允许连续拖拽放置;右键由 Cancel 退出 self:RequestRebuild(context) return true
对比一次性工具(房间框选)提交后才清空。判断标准:提示语里写"可继续"就必须保持激活
horizontal linehorizontal line

经验 2:onClick 的 event 没有 StopPropagation(二次踩坑,必须入清单)

现象:属性面板"删除该墙"按钮点了无反应(实际是 nil 崩溃,removeSelectedWall 没执行到)。 根因:onClick 回调的 event 不是 PointerEvent,调 event:StopPropagation() 触发 attempt to call a nil value。onPointerDown/Move 才有该方法。 模式:所有 onClick 守卫用方法存在性检查
lua
-- 属性面板.lua 删除按钮 onClick = function(widget, event) if event and event.StopPropagation then -- ✅ 存在性检查 event:StopPropagation() end self.removeSelectedWall(context) -- 守卫之后才能执行 end, -- ❌ 错误写法:self:StopClickPropagation(widget, event) 在 onClick 里会崩
horizontal linehorizontal line

经验 3:拼接可能为 nil 的字段前先兜底

现象:点自由墙报 attempt to concatenate a nil value (field 'roomId')根因:自由墙 roomId=nil"..." .. segment.roomId 在 Lua 里 nil 不能拼接。 模式:用 or 给显示文本兜底,数值字段用 tostring()
lua
-- 地图编辑模式.lua SelectWall local ownerText = segment.roomId or "自由墙" editorState_.status = (segment.isDoor and "已选中门:" or "已选中墙体:") .. ownerText .. " / " .. tostring(segment.side)
horizontal linehorizontal line

经验 4:自由墙删除走"整条 wallId",房间墙不可逐段删

设计:自由墙是 MapData.data.walls[] 的独立对象,按 wallId 整条删;房间墙由 room.wallsEnabled 整体生成,逐段删会破坏闭合,应提示去关"启用墙体"。 数据层(单一职责,删除逻辑放数据模块):
lua
-- 校园主地图.lua function M.RemoveFreeWall(wallId) if not wallId or wallId == "" then return false end local walls = map.walls or {} for i = #walls, 1, -1 do if walls[i].id == wallId then table.remove(walls, i) return true end end return false end
编辑层(区分两类墙):
lua
-- 地图编辑模式.lua RemoveSelectedWall local segment = editorState_.selectedWall if not segment then ... return end if segment.roomId then -- 房间墙:拒绝逐段删 editorState_.status = "该墙体属于房间 " .. segment.roomId .. ",随房间整体生成。如需移除,请关闭房间的“启用墙体”。" return end if MapData.RemoveFreeWall(segment.wallId) then -- 自由墙:整条删 editorState_.selectedKind = nil editorState_.selectedWall = nil context.store.MarkDirty("mapEditorWallDelete") end
horizontal linehorizontal line

经验 5:框选擦除子编辑器——照抄 FloorAreaEditor/WallDragEditor 契约

新增子编辑器必须实现同一组方法,主编辑器才能统一接线。核心是拖拽矩形 + AABB 命中 + 提交删除
lua
-- 墙体框选擦除.lua 关键片段 function WallEraseEditor:IsActive() return self.editorState.tool == "erase_wall" end function WallEraseEditor:HitTestFreeWalls(left, top, right, bottom) local hits = {} for i = 1, #(self.mapData.data.walls or {}) do local w = self.mapData.data.walls[i] if not w.broken then local x0, x1 = math.min(w.ax,w.bx), math.max(w.ax,w.bx) local y0, y1 = math.min(w.ay,w.by), math.max(w.ay,w.by) if not (x1 < left or x0 > right or y1 < top or y0 > bottom) then -- AABB 相交 hits[#hits + 1] = w.id end end end return hits end function WallEraseEditor:Commit(context) local left, top, right, bottom = self:GetRect() if not left then self:ResetDrag(); return false end local hits = self:HitTestFreeWalls(left, top, right, bottom) local count = 0 for i = 1, #hits do if self.mapData.RemoveFreeWall(hits[i]) then count = count + 1 end end self:ResetDrag() -- 保持 erase_wall 激活,可连续框选 self:RequestRebuild(context) return true end
主编辑器接线清单(每新增一个子编辑器都要补齐这 7 处,漏一处就事件不通):
  1. require + 模块级 wallEraseEditor_ = nil
  2. SelectPaletteItemitem.tool == "erase_wall" 路由
  3. CreateCanvasWall 的 HandleClick 加 erase_wall 守卫(避免单段点击抢事件)
  4. mapSurface onPointerDown:右键 Cancel 链 + 左键 HandlePointerDown
  5. onPointerMove / onPointerUp / onPointerCancel 各加一行
  6. mapSurface onClickerase_wall 守卫
  7. Create 实例化 + CreateMapCanvasCreatePreview()
horizontal linehorizontal line

经验 6:编辑器能画 ≠ 运行时能画——两页渲染必须对齐

现象:编辑器保存后,运行时地图(图2)墙不出现,房间/地面却正常。 根因:运行时 CreateMapView 遍历了 rooms/floorAreas/objects/characters,唯独没有遍历 data.walls。房间靠 CreateRoomCard 的边框、地面靠 CreateFloorAreaView 各画各的,墙没有对应渲染代码。 模式:凡是编辑器能创建/修改的实体,运行时页必须有同源的渲染分支;优先复用 MapData 的 getter,配色与编辑器保持一致。
lua
-- 校园地图.lua 自由墙整条渲染(zIndex=2 压在地面上、物件/人物下;pointerEvents=none 不挡点击) local function CreateFreeWallView(wall) if not wall or wall.broken then return nil end local x0, x1 = math.min(wall.ax, wall.bx), math.max(wall.ax, wall.bx) local y0, y1 = math.min(wall.ay, wall.by), math.max(wall.ay, wall.by) local style = wall.style or "普通墙" local color = style == "围墙" and {110,88,60,255} or style == "栅栏" and {120,150,95,230} or style == "破损墙" and {120,95,80,200} or {94,96,89,255} return UI.Panel { position = "absolute", left = x0*5, top = y0*5, width = math.max(5,(x1-x0+1)*5), height = math.max(5,(y1-y0+1)*5), backgroundColor = color, pointerEvents = "none", zIndex = 2, } end -- CreateMapView 内,地面之后、物件之前: for i = 1, #(data.walls or {}) do local v = CreateFreeWallView(data.walls[i]) if v then campusChildren[#campusChildren + 1] = v end end
horizontal linehorizontal line

改动文件清单

文件内容
scripts/页面/地图编辑/墙体拖拽编辑.luaCommitWall 保持激活(连续拖放)
scripts/页面/地图编辑/墙体框选擦除.lua新建框选擦除子编辑器
scripts/页面/地图编辑/编辑器目录.lua调色板 + TOOLS 加「擦除墙」
scripts/页面/地图编辑/属性面板.lua删除按钮 + 修 onClick 崩溃
scripts/页面/地图编辑模式.lua修 SelectWall nil + RemoveSelectedWall + 擦除接线 7 处
scripts/数据/地图/校园主地图.lua新增 RemoveFreeWall
scripts/页面/校园地图.lua新增 CreateFreeWallView 运行时墙渲染
贯穿性教训:① onClick 永远用存在性检查 StopPropagation;② 子编辑器的"提交后是否清空激活态"要和提示语一致;③ 编辑器与运行时的实体渲染要同源对齐,新增实体时两页同步加;④ 新增子编辑器照统一契约写、主编辑器补全 7 处接线。
4