防止因搞混分辨率而导致位置计算偏差
简单记录下遇到的一个因为搞混物理分辨率和显示分辨率而导致的交互问题
防止因搞混分辨率而导致位置计算偏差
一、物理分辨率与显示分辨率
- 对于手机等显示设备,其物理分辨率和所谓显示分辨率可能是不同的(关于分辨率相关概念,可参考这篇博客)
- 物理分辨率:固定不可变,即屏幕物理意义上拥有的像素个数的长宽积表示形式,也即显示器的最佳推荐分辨率
- 显示分辨率:可动态变化,例如对于物理分辨率为3200x2700的一块显示屏,若其显示分辨率为1600x900,则指的是屏幕被划分为了$1600$行$900$列的$1600\times900=1440000$个像素点,每个这样的像素点由物理意义上的$2\times3=6$个像素组成
二、遇到的交互问题及其解决
- 假设当手机游戏运行时,我们可以获知以下信息
- 玩家设备的物理分辨率(直接通过Unity的
Screen.width
和Screen.height
获取) - 手指点在屏幕上的物理像素坐标
clickedPosition
,其坐标原点是屏幕左下角
- 玩家设备的物理分辨率(直接通过Unity的
- 我们的目是实现物体的拖动,即让某个游戏对象
draggedGO
跟随玩家点击的位置- 假设该对象处于UGUI的Canvas下某个UI父对象
parentGO
上,且draggedGO
的包括parentGO
在内的所有祖先的RectTransform均在横竖方向上都采用Stretch的适应性布局设置,使得parentGO
会拉伸到适应显示游戏的整个视口的程度 - 上述显示游戏的视口尺寸体现为
parentGO
的RectTransform组件的.rect.width
与.rect.height
,被拖动的draggedGO
的锚定坐标的坐标原点为parentGO
的中心点,该坐标可通过该游戏对象本身的RectTransform组件的.anchoredPosition
以读取或进行改写(关于UGUI中的锚点等概念,可参考这篇博客)
- 假设该对象处于UGUI的Canvas下某个UI父对象
- 在此基础上,不难想到可将点击位置的坐标通过上述两个坐标系原点间的关系(一个在屏幕左下角一个在正中心,那二者即相差$(\frac{Screen.width}{2},\frac{Screen.height}{2})$偏移向量)转换为被拖动游戏对象的锚定位置的坐标,然后直接对被拖动对象的
.anchoredPosition
赋值
1
2
3
4
5
6
-- local posX = clickedPosition.x - parentGO:GetComponent("RectTransform").rect.width / 2
-- local posY = clickedPosition.y - parentGO:GetComponent("RectTransform").rect.height / 2
local posX = clickedPosition.x - Screen.width / 2
local posY = clickedPosition.y - Screen.height / 2
-- 应用转换后的坐标(其实上述两种写法其实都是错的,因为clickedPosition与Screen属物理分辨率坐标系,而anchoredPosition则属显示分辨率坐标系)
draggedGO:GetComponent("RectTransform").anchoredPosition = Vector3(posX, posY, 0)
- 如果我们应用这样思路的代码,则会发现在部分手机机型上会出现被拖动的物品偏移(具体体现为拖动物体在屏幕正中间不偏移,越往周边拖动则物体越偏向屏幕外侧)的现象,说明坐标计算有问题
- 上述转换方式错误地认为
Screen.width/height
和parentGO:GetComponent("RectTransform").rect.width/height
是尺寸相等的,实际上前者代表物理分辨率,后者代表显示分辨率 - 而前文提到了物理分辨率和显示分辨率不是一回事,实际上我们在游戏中将二者打印出来就会发现,出现偏移问题的机型必然是显示分辨率$\leq$物理分辨率的,而没出问题的机型上二者总是相等的
- 所以我们赋值给
draggedGO:GetComponent("RectTransform").anchoredPosition
的坐标,应当是处于显示分辨率坐标系下的坐标,如下才是正确的写法
- 上述转换方式错误地认为
1
2
3
4
5
6
7
-- 按比例转换坐标
local zoomRatioX = parentGO:GetComponent("RectTransform").rect.width / Screen.width
local posX = (clickedPosition.x - Screen.width / 2) * zoomRatioX
local zoomRatioY = parentGO:GetComponent("RectTransform").rect.height / Screen.height
local posY = (clickedPosition.y - Screen.height / 2) * zoomRatioY
-- 此时才是正确的
draggedGO:GetComponent("RectTransform").anchoredPosition = Vector3(posX, posY, 0)
- 总结:当显示分辨率小于物理分辨率时,例如某些手机有一些大脑门或者圆角边就会压缩视口,此时每个显示分辨率的像素点就包含多个物理分辨率像素点,导致两个分辨率下同样长度的一条移动轨迹中,包含的自己坐标系下的像素点个数不同,因此原先的错误写法会导致
posX
与posY
值在UI内是偏大的,而由于原点在正中,越往外侧则该误差积累越明显,从而导致了离心偏移现象
本文由作者按照 CC BY-NC-SA 4.0 进行授权