穿透层修复

07/27综合

经验一:CSS transform 穿透层修复模式

问题本质

当容器使用 CSS transform: translateX/Y/scale 平移/缩放子树时,即使父容器有 overflow = "hidden",引擎的命中检测可能不裁剪越界区域。如果 transform 后的子树有更高的 zIndex,它会在视觉范围外拦截其他兄弟节点的指针事件。

受影响的场景

地图编辑器(右键平移/滚轮缩放后):
  ┌─ zIndex=50 ────────────────────┐   ← 顶栏按钮
  │                                │
  │  ┌─ zIndex=40 ──┐  ┌─ zIndex=1 ──────────┐
  │  │  左栏工具    │  │  地图工作区          │
  │  │              │  │  (transform 平移后   │
  │  │              │  │   子树越界到顶/底)   │
  │  └──────────────┘  └─────────────────────┘
  │                                │
  └─ zIndex=50 ────────────────────┘   ← 底栏状态栏

修复模式:固定工具条三件套

顶栏 CreateHeaderActions (地图编辑模式.lua:674):
lua
UI.Panel { width = "100%", position = "relative", -- 必须,zIndex 才生效 zIndex = 50, -- 必须高于地图工作区的 zIndex=1 pointerEvents = "auto", -- 必须,确保命中 onPointerDown = StopPointerPropagation, onPointerMove = StopPointerPropagation, onPointerUp = StopPointerPropagation, onPointerCancel = StopPointerPropagation, onWheel = StopPointerPropagation, onClick = StopClickPropagation, backgroundColor = { 28, 30, 30, 245 }, -- ... 内容 }
底栏 CreateBottomStatusBar (地图编辑模式.lua:752):
lua
UI.Panel { width = "100%", position = "relative", zIndex = 50, pointerEvents = "auto", onPointerDown = StopPointerPropagation, onPointerMove = StopPointerPropagation, onPointerUp = StopPointerPropagation, onPointerCancel = StopPointerPropagation, onWheel = StopPointerPropagation, onClick = StopClickPropagation, -- ... 内容 }
左栏 (已有,zIndex = 40):
lua
UI.Panel { width = SIDEBAR_W, flexShrink = 0, position = "relative", zIndex = 40, pointerEvents = "auto", onPointerDown = StopPointerPropagation, -- ... 全套事件 }

关键要点

要素作用缺一不可的原因
position = "relative"建立 stacking context无此属性 zIndex 在 Yoga 中不生效
zIndex 高于地图区命中优先级地图区 zIndex=1,工具条必须 >1
pointerEvents = "auto"显式启用命中Yoga 某些嵌套下默认值可能不同
六种事件 stop 冒泡防止事件穿透到地图pointerDown/Move/Up/Cancel/Wheel/Click
horizontal linehorizontal line

经验二:网关服务模块模式

数据源三层优先级

lua
-- 系统/网关服务.lua -- 1. 公式默认值:永远能算出合理坐标 local function FormulaDefault() local w = MapData.data.width or 240 local h = MapData.data.height or 160 return { id = "main_gate", x = math.floor(w * 0.5 - DEFAULT_GATE_W * 0.5), -- 水平居中 y = h - DEFAULT_GATE_H, -- 贴底部 w = DEFAULT_GATE_W, -- 20格宽 h = DEFAULT_GATE_H, -- 4格深 direction = "up", -- 面朝校园内部 label = "正门", } end -- 2. 读取时 clamp:防止地图尺寸变化后越界 local function ClampZone(zone) local mw = MapData.data.width or 240 local mh = MapData.data.height or 160 local zw = math.min(zone.w or DEFAULT_GATE_W, mw) local zh = math.min(zone.h or DEFAULT_GATE_H, mh) local zx = math.max(0, math.min(zone.x or 0, mw - zw)) local zy = math.max(0, math.min(zone.y or 0, mh - zh)) return { x = zx, y = zy, w = zw, h = zh } end -- 3. 用户数据优先,空时公式兜底 function GateService.GetAllGates() local zones = MapData.data.gateZones if not zones or #zones == 0 then local def = FormulaDefault() local clamped = ClampZone(def) return { { id = def.id, x = clamped.x, y = clamped.y, w = clamped.w, h = clamped.h, direction = def.direction, label = def.label } } end -- ... 遍历用户数据并 clamp end

消费方:随机散布代替单点堆叠

lua
-- 系统/学生位置服务.lua local function ResolveLocation(location) if location == "gate" then -- 在校门区域内随机散布,返回朝向 local gx, gy, gdir = GateService.GetRandomPointInGate() return gx, gy, gdir end -- ... 其他回退 end -- EnsurePosition 用 resolvedDir 作为朝向兜底 local position = ActorSpatialService.Ensure(student.id, { x = x, y = y, direction = student.direction or resolvedDir, })

关键要点

设计决策理由
数组结构从第一天用避免未来从单对象迁移到数组的痛苦
字段精简到 6 个 (id/x/y/w/h/direction/label)不加 isOpen/priority 等死字段——没有消费者就是负担
公式默认 + clamp + 随机散布三重防御:永不 nil、永不越界、永不堆叠
direction 随坐标返回入校即面朝校园内,离校逻辑反过来用
坐标服务与玩法服务分离网关服务.lua(坐标)vs 校门服务.lua(违禁搜查/离校)
horizontal linehorizontal line

经验三:避雷清单(踩坑记录)

1. transform 穿透不能靠 overflow = "hidden" 解决,必须高 zIndex sibling
2. 不能只靠 pointerEvents 和 stopPropagation,transform 越界子树先命中让你收不到事件
3. EmmyLua @return 注解格式是 --- @return number name,不是 --- @return name number
4. 带 nil 洞的稀疏表 cjson.encode 会失败(背包定长槽位要填 false 哨兵)
5. onClick 回调的 event 没有 StopPropagation 方法,必须 if event.StopPropagation then 守卫
6. event.button 守卫用 MOUSEB_RIGHT 不用 MOUSEB_LEFT(后者未暴露为 nil)
7. event.x/event.y 已经是 widget 本地坐标,不要加减 UI.GetVisualRect 的 x/y
4