门与墙

核心设计原则

原则说明
格子像素≥精灵尺寸32px格+36px精灵≈1.1格,比例合理
渲染连续+碰撞格级平滑移动不跳格,碰撞按格检测
脚底小盒碰撞半宽<0.5格,站门格不触邻墙
门优先于墙BuildBlockedCells两遍扫描,passable覆盖blocked
后建覆盖先建门墙同格互斥,门替换墙
智能路由门工具按位置自动选择装门/建独立门
horizontal linehorizontal line

错误1 → 正确1:格子比例失衡

lua
-- 错误:5px格子,精灵7.2格宽,碰撞方案全失败 local WORLD_PIXEL_SCALE = 5 width = 5, height = 5, -- 面板硬编码,32px下变圆点 CampusPerimeterRoad.Create(w, h, 5) -- 外围道路与实体scale不匹配 -- 正确:32px对齐环世界,精灵≈1.1格 local WORLD_PIXEL_SCALE = 32 width = WORLD_PIXEL_SCALE, height = WORLD_PIXEL_SCALE CampusPerimeterRoad.Create(w, h, WORLD_PIXEL_SCALE)
horizontal linehorizontal line

错误2 → 正确2:碰撞半宽过大卡门

lua
-- 错误:0.5625格(整精灵),站门格时碰撞矩形跨入垂直相邻墙格→卡死 local COLLISION_HALF = 18 / 32 -- 0.5625 -- 正确:0.4格脚底小盒,站门格[中心±0.4]不跨任何相邻格 local COLLISION_HALF = 0.4
horizontal linehorizontal line

错误3 → 正确3:贴墙滑行坐标互换(穿墙根因)

lua
-- 错误:sxn/syn缩写混淆CanMove返回值顺序,Y轴滑行X/Y互换传送穿墙 local sy, syn, sxn = Navigation.CanMove(x, y, 0, dy, step, ...) if sy then nx, ny = sxn, syn end -- nx=新Y(错!), ny=X(错!) -- 正确:语义化变量名 local canSlideY, slideNX, slideNY = Navigation.CanMove(x, y, 0, dy, step, ...) if canSlideY then nx, ny = slideNX, slideNY end
horizontal linehorizontal line

关键代码4:脚底小盒碰撞检测

lua
local COLLISION_HALF = 0.4 local WALL_OVERLAP_THRESHOLD = 0.1 local function IsRectBlocked(cx, cy, hw, hh, blocked) local minGX = math.floor(cx - hw) local maxGX = math.floor(cx + hw - 1e-6) local minGY = math.floor(cy - hh) local maxGY = math.floor(cy + hh - 1e-6) local rectL, rectR = cx - hw, cx + hw local rectT, rectB = cy - hh, cy + hh for gx = minGX, maxGX do for gy = minGY, maxGY do if IsCellBlocked(gx, gy, blocked) then local overlapX = math.max(0, math.min(rectR, gx+1) - math.max(rectL, gx)) local overlapY = math.max(0, math.min(rectB, gy+1) - math.max(rectT, gy)) if overlapX >= WALL_OVERLAP_THRESHOLD and overlapY >= WALL_OVERLAP_THRESHOLD then return true end end end end return false end
horizontal linehorizontal line

关键代码5:门优先于墙(两遍扫描)

lua
local function BuildBlockedCells(profession) local blocked, passable = {}, {} local segments = MapData.GetAllWallSegments() -- 第一遍:收集所有passable门格 for i = 1, #segments do local seg = segments[i] if seg.isDoor then local door = seg.door local ok = door and door.broken == true or door and door.locked ~= true or profession.canOpenLockedDoors == true if ok then for dx = 0, math.max(1,Round(seg.w or 1))-1 do for dy = 0, math.max(1,Round(seg.h or 1))-1 do passable[CellKey(Round(seg.x)+dx, Round(seg.y)+dy)] = true end end end end end -- 第二遍:收集blocked墙格,跳过passable门格 for i = 1, #segments do local seg = segments[i] local wallPassable = seg.broken == true if seg.isDoor then local door = seg.door wallPassable = door and door.broken == true or door and door.locked ~= true or profession.canOpenLockedDoors == true end if not wallPassable then for dx = 0, math.max(1,Round(seg.w or 1))-1 do for dy = 0, math.max(1,Round(seg.h or 1))-1 do local key = CellKey(Round(seg.x)+dx, Round(seg.y)+dy) if not passable[key] then blocked[key] = true end end end end end return blocked end
horizontal linehorizontal line

关键代码6:自由墙门渲染(运行时读取wall.doors)

lua
local function CreateFreeWallView(wall) -- ... 计算x0,x1,y0,y1,wallColor ... local DOOR_COLOR = { 200, 155, 60, 245 } local doorCells = {} if wall.doors then for di = 1, #wall.doors do local d = wall.doors[di] if not d.broken and not d.locked then for ddx = 0, (d.width or 1) - 1 do doorCells[(d.offset or 0) + ddx] = d end end end end local S = WORLD_PIXEL_SCALE if isHorizontal then for gx = x0, x1 do local door = doorCells[gx - x0] segments[#segments+1] = UI.Panel { left = gx*S, top = y0*S, width = S, height = S, backgroundColor = door and DOOR_COLOR or wallColor, borderRadius = 0, pointerEvents = "none", zIndex = CharZ(y0), } end end -- 垂直同理 end
horizontal linehorizontal line

关键代码7:自由墙段读取wall.doors生成isDoor

lua
local function GetFreeWallSegments(wall) -- ... 计算x0,x1,y0,y1,isHorizontal ... local doorCells = {} if wall.doors then for di = 1, #wall.doors do local d = wall.doors[di] for ddx = 0, (d.width or 1) - 1 do doorCells[(d.offset or 0) + ddx] = d end end end if isHorizontal then for dx = 0, (x1 - x0) do local door = doorCells[dx] result[#result+1] = { -- ... id,wallId,roomId=nil,side,x,y,w,h ... isDoor = door ~= nil, door = door, } end end -- 垂直同理 end
horizontal linehorizontal line

关键代码8:Place()门工具智能路由

lua
if item and item.tool == "place_door" then local gx, gy = math.floor(px + 0.5), math.floor(py + 0.5) -- 优先级1:房间边线 → 自动启用墙体+装门(替换墙格) if self.getRoomAt then local room = self.getRoomAt(gx + 0.5, gy + 0.5) if room then local rx,ry,rw,rh = room.x or 0, room.y or 0, room.w or 0, room.h or 0 local side, offset if gy == ry then side,offset = "top", gx-rx elseif gy == ry+rh-1 then side,offset = "bottom", gx-rx elseif gx == rx then side,offset = "left", gy-ry elseif gx == rx+rw-1 then side,offset = "right", gy-ry end if side then self.installDoorOnSegment({roomId=room.id, side=side, x=gx, y=gy, w=1, h=1, isDoor=false, door=nil}, context) return true end end end -- 优先级2:自由墙 → 自由墙装门 for i,seg in ipairs(self.mapData.GetAllWallSegments()) do if gx==math.floor(seg.x or 0) and gy==math.floor(seg.y or 0) and seg.wallId then self.installDoorOnSegment(seg, context); return true end end -- 优先级3:空地 → 创建独立1格带门自由墙 walls[#walls+1] = { id=wallId, style="普通墙", ax=gx,ay=gy,bx=gx,by=gy, doors={{id=wallId.."_door_0", offset=0, width=1}} } return true end
horizontal linehorizontal line

关键代码9:装门自动启用墙体 + HandleClick优先级

lua
-- InstallDoorOnSegment 房间墙路径:装门自动启用墙体 context.store.Update("mapEditorDoor", function() room.doors = room.doors or {} room.wallsEnabled = true -- 否则GetRoomWallSegments返回空,门不渲染 -- ... 计算offset, 创建door, 加入room.doors ... end) -- ApplyWallStyleToRoom:应用墙样式自动启用墙体 room.wallsEnabled = true -- HandleClick 优先级重排(墙/门互操作优先于空地放置) if canRemoveDoor then removeDoorFromSegment(...) -- 墙工具点门格→移除门 elseif canEditDoor then installDoorOnSegment(...) -- 门工具点墙格→装门 elseif mapPlacementEditor:IsActive() then PlaceAtPointer(...) -- 空地放置 else selectWall(...) end
horizontal linehorizontal line

关键代码10:RebuildUI保留缩放(防跳变)

lua
-- 错误:每次重建重置zoom/pan,放门后地图跳变 canvas_:ResetCanvasView() -- 正确:仅首次创建时重置 if not canvas_ then canvas_ = MapEditorCanvas.Create {...} canvas_:ResetCanvasView() else canvas_:ApplyCanvasTransform() -- 保留当前zoom/pan end
horizontal linehorizontal line

迭代历程(5轮碰撞 + 门系统8个修复)

  1. AABB → 空气墙 → 单点floor → 格中心 → 脚底小盒AABB(5轮碰撞)
  2. 门不显示 → 自由墙渲染读doors → 房间墙wallsEnabled自动启用
  3. 门墙重叠 → BuildBlockedCells门优先两遍扫描
  4. 点房间边装门失败 → Place()智能路由+注入installDoorOnSegment
  5. 放门后跳变 → RebuildUI保留canvasState
  6. 门预览不跟手 → 墙段onPointerMove转发UpdatePreview
  7. 旧门对象残留 → 属性面板加"删除该物件"
  8. 站门格卡住 → 碰撞半宽0.5625→0.4脚底盒
核心心得:当精灵尺寸>格子时,永远不要用整个精灵外框做碰撞——用脚底小盒(半宽<0.5格),既保证墙阻挡准确,又让门格passable不被相邻墙干扰。数据层用passable优先保证门墙重叠时门胜出,编辑层用智能路由+后建覆盖保证门墙互斥。
1