下载 App

以鼠标为锚点缩放

07/291 浏览综合

核心数学原理

缩放前后,鼠标指向的那个世界坐标点必须保持在同一屏幕位置。
已知:旧缩放 oldZoom、新缩放 newZoom、鼠标锚点屏幕坐标 (ax, ay)、当前偏移 (panX, panY)

步骤1:算出旧缩放下鼠标指向的世界坐标
  wx = (ax - panX) / oldZoom
  wy = (ay - panY) / oldZoom

步骤2:新缩放后反解新偏移,让 wx/wy 仍落在 (ax, ay)
  newPanX = ax - wx * newZoom
  newPanY = ay - wy * newZoom

关键代码:缩放函数

lua
-- 校园地图.lua 版本(参考实现,手感好) local function ApplyZoom(delta, anchorX, anchorY) local newZoom = zoomLevel_ + delta newZoom = math.max(ZOOM_MIN, math.min(ZOOM_MAX, newZoom)) if newZoom == zoomLevel_ then return end -- 锚点:鼠标缓存坐标,无则视口中心 local ax = anchorX local ay = anchorY if not ax then local viewRect = UI.GetVisualRect(mapView) ax = viewRect.w * 0.5 ay = viewRect.h * 0.5 end local oldZoom = zoomLevel_ local wx = (ax - worldOffsetX_) / oldZoom local wy = (ay - worldOffsetY_) / oldZoom zoomLevel_ = newZoom worldOffsetX_ = ax - wx * newZoom worldOffsetY_ = ay - wy * newZoom ApplyTransformInPlace() end

鼠标坐标缓存(OnWheel 不带坐标,必须缓存)

lua
-- 在 onPointerMove 中缓存(每帧鼠标移动时更新) onPointerMove = function(event, widget) if event and event.x and event.y then lastMouseX_ = event.x -- 已是 widget 本地坐标 lastMouseY_ = event.y end -- ... 其他逻辑 end, -- OnWheel 回调读缓存 function mapView:OnWheel(dx, dy) if dy == 0 then return end local step = dy > 0 and ZOOM_STEP or -ZOOM_STEP ApplyZoom(step, lastMouseX_, lastMouseY_) end

坐标空间注意事项(地图编辑器踩的坑)

关键:缓存的坐标和 pan 偏移必须在同一坐标空间!
场景event.x 坐标空间panX 坐标空间是否一致
直接在 viewport 上缓存viewport 本地坐标viewport 本地 直接用
在 transform 子节点上缓存子节点本地坐标(已逆变换)viewport 本地 需转换
子节点上的转换公式:
lua
-- mapSurface 有 scale+left/top transform,event.x 是 surface 本地坐标 -- 需要转回 viewport 坐标才能和 panX 同空间 cs.lastMouseX = event.x * cs.zoom + cs.panX cs.lastMouseY = event.y * cs.zoom + cs.panY

偏移应用方式:Yoga left/top 优于 CSS transform

lua
-- ✅ 推荐:Yoga 布局属性,更稳定 local function ApplyTransformInPlace() worldLayerRef_:SetStyle({ scale = zoomLevel_, left = worldOffsetX_, top = worldOffsetY_, }) end -- ❌ 避免:直接写 props.translateX/translateY(CSS transform 属性,在 Yoga 中不稳定) self.mapSurface.props.translateX = cs.panX self.mapSurface.props.translateY = cs.panY
构造时必须声明 transformOrigin:
lua
UI.Panel { position = "absolute", left = 0, top = 0, transformOrigin = "top-left", -- 必须!scale 从左上角扩散,与 left/top 公式一致 -- ... }

线性步进 vs 乘法缩放

lua
-- ✅ 线性加法:全程手感一致,步长均匀 local ZOOM_STEP = 0.03 -- 校园地图 local CANVAS_ZOOM_STEP = 0.05 -- 编辑器(稍大) newZoom = oldZoom + step -- ❌ 乘法指数:zoom 小时步长太小(几乎不动),zoom 大时步长太大(跳变) local zoomFactor = deltaY > 0 and 1.12 or 0.89 newZoom = oldZoom * zoomFactor

完整常量配置参考

lua
-- 校园地图(运行时) local ZOOM_MIN = 0.05 local ZOOM_MAX = 0.6 local ZOOM_STEP = 0.03 local zoomLevel_ = 0.13 -- 默认 -- 地图编辑器 local CANVAS_MIN_ZOOM = 0.05 local CANVAS_MAX_ZOOM = 0.8 local CANVAS_ZOOM_STEP = 0.05 local CANVAS_DEFAULT_ZOOM = 0.18 -- 默认

避坑清单

  1. OnWheel 不带坐标 — 必须在 onPointerMove/onPointerDown 中缓存 event.x/event.y
  2. 坐标空间必须一致 — event.x 是 widget 本地坐标,pan 偏移也必须是同一父容器的本地坐标;在 transform 子节点上缓存要乘 zoom+加 pan 转换
  3. transformOrigin = "top-left" — scale 默认从中心缩放会导致锚点公式完全错误
  4. 用 SetStyle({left, top, scale}) — 不要直接写 props.translateX/Y
  5. 线性步进 — 乘法缩放手感不均匀
  6. 锚点回退视口中心 — +/− 按钮不传锚点时,用 viewRect.w/h × 0.5 作为中心锚点
穿透层修复
经验一:CSS transform 穿透层修复模式 问题本质 当容器使用 CSS transform: translateX/Y/scale 平移/缩放子树时,即使父容器有 overflow = "hidden",引擎的命中检测可能不裁剪越界区域。如果 transform 后的子树有更高的 zIndex,它会在视觉范围外拦截其他兄弟节点的指针事件。 受影响的场景 地图编辑器(右键平移/滚轮缩放后)
官方
4 赞
编辑器实体 zIndex 层级
问题 编辑器中墙段被房间面板覆盖不可见。原因:给房间设 zIndex=1 会创建 stacking context,其内部带背景色/TiledImage 的 floorLayer 作为 paint layer 在合成时盖过了同层级 zIndex=2 的墙段——差距太小不够可靠。 解决:底层不设 zIndex,上层用明显间距 错误做法(zIndex 间距太小 + 给底层也设 zIndex): --
官方
1 赞
地图编辑器仿真 tick 重建抑制
UI 重建频率 = 事件分发的可靠性上限。 只要有一帧 SetRoot 销毁了正在处理 pointer 事件的控件,跨重建边界的 Down/Up 对就会被吞。高频仿真 tick 必须在 UI 边界按页面过滤。 关键代码(scripts/main.lua HandleUpdate) 四步走:先原地处理人员 → 聚合所有仿真 changed → 地图页强制压制 → 只放行显式/登录态变化。 -- ma
官方
4 赞
精灵平滑移动、墙碰撞体积
一、 渲染层用连续坐标(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 赞
槽位变更只走原子方法
经验 下标 = 视觉格子,空槽是合法状态。定长 9 槽,inventory[i]=nil 即第 i 格空着,中间允许空。任何"把物品往前挪/压缩/重排"的操作都会让"你看到的格子"和"数组真实下标"脱节 → 物品消失。渲染直映 inventory[i],数据层绝不重排。 稀疏表禁用 #。带 nil 洞的表 #inv 是未定义的(可能=0 直接读成空包,或=洞前边界)。所有遍历上界一律 Player
官方
4 赞
右键拖拽平移
一、问题本质 不是平移公式写错,而是 UrhoX UI 库指针事件派发的两个不对称陷阱叠加: 陷阱 现象 根因文件 OnPointerMove 不向父冒泡 右键按下 isPanning_=true,但 move 事件永远到不了 mapView urhox-libs/UI/Core/Widget.lua(OnPointerDown/Up 有 parent 冒泡,OnPointerMove 没有) H
官方
4 赞
地图编辑器地面交互与视觉优化
一、问题链 用户截图反馈了三个递进的问题: 问题 1:点击地面类型(如食堂地砖),弹窗不关闭,挡住画布无法操作 ↓ 修复 v0.10.94 问题 2:地面贴图太大,一块地砖比小人还宽,比例失调 ↓ 修复 v0.10.96(改配置值) 问题 3:改了配置值后刷新仍然太大——旧存档的 tileSize=256 优先级高于新 config ↓ 修复 v0.10.97(反转渲染优先级) 二、关键代码
官方
4 赞
UI动画的技巧
悬停动画(缩放+旋转) 技巧:分离X/Y轴缩放时间,配合随机旋转方向,使用EASE_OUT + TRANS_BACK曲线。 关键代码: func hover() -> void: if disabled: return pivot_offset = size / 2.0 var scale_ratio = clampf(width_full_rot / size.x, 0.5, 1.0) var
官方
3 赞
修复"地图编辑器刷新数据丢失"
问题诊断过程 用户报告:编辑器建好房间 → 保存 → 刷新页面 → 进编辑器 → 数据全没了。 关键线索:用户发现"校园日"模式刷新后能看到房间,但一进编辑器就没了。 这直接指向了编辑器页面自身的初始化逻辑,而非持久化链路。 根因 地图编辑模式.lua 的 EnsureBlankEditorLayout() 设计意图是"首次打开编辑器提供空白画布",用模块级布尔 editorLayoutClear
官方
4 赞
碰撞方案
核心教训 1. 格子像素必须匹配精灵尺寸 5px格子+36px精灵=7.2:1比例,任何碰撞方案(AABB/空气墙/单点/格中心)都无法同时满足"不穿墙+四面间距一致+不跳格"。对齐环世界32px后,36px精灵≈1.1格,问题根本解决。 2. 连续渲染 + 格级碰撞的分层设计 渲染用连续坐标 x*S-18(平滑不跳),碰撞用格级检测(不穿墙),二者解耦。 3. 精灵大于格子时,单点检测必然不对称
官方
1 赞