碰撞方案
07/29
核心教训
1. 格子像素必须匹配精灵尺寸 5px格子+36px精灵=7.2:1比例,任何碰撞方案(AABB/空气墙/单点/格中心)都无法同时满足"不穿墙+四面间距一致+不跳格"。对齐环世界32px后,36px精灵≈1.1格,问题根本解决。
2. 连续渲染 + 格级碰撞的分层设计 渲染用连续坐标 x*S-18(平滑不跳),碰撞用格级检测(不穿墙),二者解耦。
3. 精灵大于格子时,单点检测必然不对称 36px精灵半宽18px=0.5625格,超出半格0.0625格(2px)。单点检测中心点时:
- 下/右墙:中心在墙前0.5格,精灵边缘埋入墙2px(看似正常)
- 上/左墙:中心在墙前0.5格,精灵边缘伸出墙外2px(看似穿墙)
解法:小矩形AABB覆盖精灵实际占位。
4. 变量缩写陷阱 sxn/syn 接收 CanMove 返回的 (bool, resolvedX, resolvedY),赋值时 nx,ny=sxn,syn 在Y轴分支把X/Y写反,导致对角线贴墙传送穿墙。语义化命名 slideNX/slideNY 从根源避免。


关键代码
碰撞核心 scripts/小人/导航.lua:
lua-- 碰撞半宽:36px精灵 / 32px格子 = 0.5625格 local COLLISION_HALF = 18 / 32 -- 小矩形碰撞:检测中心点±半宽范围内所有格 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) for gx = minGX, maxGX do for gy = minGY, maxGY do if IsCellBlocked(gx, gy, blocked) then return true end end end return false end -- CanMove:目标位置矩形内有墙则保持原坐标(不跳格) function Navigation.CanMove(x, y, dx, dy, step, profession) local blocked = BuildBlockedCells(profession or {}) local nx = x + dx * step local ny = y + dy * step if IsRectBlocked(nx, ny, COLLISION_HALF, COLLISION_HALF, blocked) then return false, x, y end return true, nx, ny end
贴墙滑行 scripts/系统/主角操控服务.lua:
luaif not canMove then if math.abs(dx) > 0.01 then local canSlideX, slideNX, slideNY = Navigation.CanMove( spatial.x, spatial.y, dx, 0, step, PLAYER_MOVEMENT_PROFILE) if canSlideX then nx, ny = slideNX, slideNY; canMove = true end end if not canMove and math.abs(dy) > 0.01 then local canSlideY, slideNX, slideNY = Navigation.CanMove( spatial.x, spatial.y, 0, dy, step, PLAYER_MOVEMENT_PROFILE) if canSlideY then nx, ny = slideNX, slideNY; canMove = true end end if not canMove then return false end end
连续渲染 scripts/页面/校园地图.lua(4处统一模式):
lualocal S = WORLD_PIXEL_SCALE -- 32 local cx = character.x or 0 -- 连续坐标,不floor local cy = character.y or 0 local contLeft = cx * S - 18 -- 18=精灵半宽36/2 local contTop = cy * S - 18
围墙渲染 scripts/页面/校园地图.lua CreateFreeWallView:
lualocal S = WORLD_PIXEL_SCALE for gx = x0, x1 do segments[#segments + 1] = UI.Panel { left = gx * S, top = y0 * S, width = S, height = S, -- 每格填满32px,连续墙 backgroundColor = color, borderRadius = 0, -- 必须!否则小方块变圆点 zIndex = CharZ(y0), -- Y-sorting前后遮挡 } end
像素常量统一:
- 运行时 WORLD_PIXEL_SCALE = 32(校园地图.lua)
- 编辑器 GRID_SCALE = 32(地图编辑模式.lua 注入地图画布.lua)
- 外围道路 CampusPerimeterRoad.Create(w, h, WORLD_PIXEL_SCALE) 不再硬编码5

