手机端点击位置和特效位置对不上?一个坐标系不匹配的坑
05/1026 浏览开发心得
现象
在做一个点击类游戏时,点击金币会在点击位置弹出 “+100” 的飘字和金币粒子特效。
在 PC 上一切正常,点哪儿特效就出现在哪儿。
但到了手机上,飘字和金币图标总是偏移一段距离——你点左边,特效跑到更左边去了。
排查过程
先理清数据流。
点击事件从 UI 组件的 onPointerDown 回调拿到 event.x, event.y,然后一路传递:
UI onPointerDown(event.x, event.y)
→ GameManager.OnCoinClick(x, y)
→ CoinParticle.Spawn(x, y) -- NanoVG 绘制金币粒子
→ FloatingText.Show(text, x, y) -- NanoVG 绘制飘字
中间没有任何坐标转换,直接把 UI 事件的坐标原样传给了 NanoVG 渲染。
看起来很直觉,但问题就出在这里——这两套系统用的不是同一个坐标系。
根因:两套坐标系的换算不一致
UI 系统的事件坐标使用的是基准像素(Base Pixels)。这个值等于:UI 坐标 = 物理像素 / UI.scale
而 NanoVG 渲染使用的是系统逻辑像素,在 nvgBeginFrame 中定义:
local dpr = graphics:GetDPR()
local logW = graphics:GetWidth() / dpr
local logH = graphics:GetHeight() / dpr
nvgBeginFrame(vg_, logW, logH, dpr)
所以 NanoVG 的坐标等于:NanoVG 坐标 = 物理像素 / dpr
关键在于 UI.scale 和 dpr 不一定相等。
项目使用了 UI.Scale.DEFAULT(即 DPR_DENSITY_ADAPTIVE 模式),它的计算逻辑是:
UI.scale = dpr * densityFactor
其中 densityFactor 是一个根据屏幕短边动态调节的系数,在大屏(PC)上等于 1.0,在小屏(手机)上会缩小到 0.625~0.9 左右,目的是让小屏幕上的 UI 元素不要占满整个屏幕。
为什么 PC 上没问题
PC 上 dpr 通常是 1.0,densityFactor 也是 1.0,所以 UI.scale = 1.0 * 1.0 = 1.0。
两个坐标系的除数都是 1.0,结果完全一致。
为什么手机上出问题
假设一台手机 dpr = 3.0,densityFactor = 0.8,那么 UI.scale = 3.0 * 0.8 = 2.4。
你点击了屏幕上物理坐标 (720, 1200) 的位置:
• UI 事件给出的坐标:720 / 2.4 = 300
• NanoVG 期望的坐标:720 / 3.0 = 240
你把 300 传给了期望 240 的渲染系统,特效自然往右下方偏移了。
偏移量就是 densityFactor 带来的差异。
修复
知道了原因,修复就很简单——在把坐标传给 NanoVG 之前,做一次坐标转换:
NanoVG坐标 = UI坐标 × (UI.scale / dpr)
代入上面的例子验证:300 × (2.4 / 3.0) = 300 × 0.8 = 240,完全正确。
实际代码改动只有几行,加在 OnCoinClick 函数开头:
function
GM.OnCoinClick(x, y)
-- UI 基准像素 → NanoVG 逻辑像素
local UI = require("urhox-libs/UI")
local uiScale = UI.GetScale() or 1
local dpr = graphics:GetDPR()
local scaleFactor = uiScale / dpr
if x then x = x * scaleFactor end
if y then y = y * scaleFactor end
-- 后续逻辑不变...
end
总结
这个 bug 的本质是:UI 事件系统和 NanoVG 渲染系统各自有一套从物理像素到逻辑像素的换算规则,当两者的缩放因子不同时,直接传递坐标就会产生偏移。
在只有一个 dpr 参与的简单场景(PC、或 scale 恰好等于 dpr)下,问题被掩盖了。
只有当引入了额外的密度适配因子(densityFactor),两个缩放因子产生差异时,偏移才会暴露出来。
记住一个原则:跨系统传递坐标时,永远要确认两边用的是同一个坐标系。
如果不是,就先转换,再传递。



