Unity 适配 iPhone X 的一种实现方法
我一边骂着苹果是专利流氓,对开发者还极其不友好,一边又得靠 iOS 这个平台养活自己。于是当他们出了个带刘海儿的鬼东西之后,我还是得想想怎么适配 GUI。在 UWA 上提问之后,有同行提供了一些思路(参见这里),再加上和朋友讨论,决定用本文描述的这种实现方法。
注意:
- 以下基于横屏的情况描述,但竖屏的情况也是类似的。
- 以下实现基于 NGUI 插件,但我估计 UGUI、FairyGUI 等均可以类似的方式实现。
基本原理
苹果建议的安全区是这样的:
其实就是需要比屏幕的外接矩形小一号的矩形(上图中浅绿色矩形),一些控件需要锚定到这个小一号的矩形上。
在这个基础上,我需要做几件事:
- 实现一个安全区控制器组件,配合
UIWidget
使用。运行时判断机型是否为 iPhone X 横屏,如果是,调整UIWidget
的尺寸。 - 基于这个组件,给拼 UI 的人(不论是策划、美术还是程序)设计一个操作流程,来修改已经做好的 UI prefab 和创建新的 UI prefab。
- 在编辑器里能够快速的「假装」自己是 iPhone X 横屏,以便拼 UI 的人可以在某种程度上「所见即所得」的看到这些 UI 在 iPhone X 上的样子。
运行时
运行时的部分主要由两个组件(MonoBehaviour
)组成:
安全区管理器(Manager)
- 既然是个管理器,Manager 这个类就只能有一个。不妨实现为 Unity 风格的单例(即在
Awake
时设置静态Instance
字段的值为自身,在OnDestroy
时设置Instance
为null
)。这样,之后要说的 Controller 就可以从 Manager 这里获得其唯一实例了。 - 这个类的主要工作,是根据当前平台、设备型号(
SystemInfo.deviceModel
)等信息来获得当前是否为 iPhone X 横屏(需要判断横屏的话,UnityEngine.Screen
类能胜任),或者是否是在「假装」iPhone X 横屏。这样便可以去配置文件里获得相应的安全区的尺寸和位置。 - 这个部分的实现,可以是策略模式。其好处将在于,以后如果下一代 iPhone 或者什么别的设备又有了新的安全区设定,这个功能将很容易扩展。
安全区控制器(Controller)
- 这个类将要直接配合一个
UIRect
的子类对象来使用(如果是 UGUI 的话应该是配合一个RectTransform
)。 - 其实现要在合适的时候(对 NGUI 来说就是
Start
时)将安全区的位置、尺寸信息从 Manager 处获取到,并设置到这个UIRect
的锚点信息中(这个UIRect
锚的对象是该界面的任何一个全屏UIPanel
对象),从而改变其位置和尺寸。NGUI 中的UIRect.SetAnchor
方法组有多个重载,可以根据需要调用。 - 把需要配合安全区改变位置的控件,锚定在这个 Controller 上附带的
UIRect
上,在运行时它们就会随着UIRect
的变化而变化。 - 将上述内容做在一个 Prefab 里,供拼 UI 的人使用。拼 UI 的人只需要将其拖到正在编辑的 UI Prefab 中,指定一个
UIPanel
就可以让它生效。(当然直接做个菜单项就更好了)
编辑器
很讽刺,反倒是编辑器当中需要做的事情比较多。
简单的部分是,将上述内容做在一个 Prefab 里,供拼 UI 的人使用。拼 UI 的人只需要将其拖到正在编辑的 UI Prefab 中,指定一个 UIPanel
就可以让它生效。(当然直接做个菜单项就更好了)
实现起来麻烦一点的,就是在编辑器里假装自己是 iPhone X。
-
准备一个开关,切换正常屏显模式(A)和 iPhone X 横屏模拟模式(B)。如果以后有其他安全区形式还可以扩展更多的模式。
-
准备一个场景,用于 B 模式下显示一个 iPhone X 形状的遮罩(我是在这里得到的图)。因为它将只在编辑器中使用,因此我直接用 UGUI。注意适当设置其中的 Canvas 以便显示在最前。
其中的Mask节点
-
在切换到 B 模式的时候,首先强迫 Unity 的 Game 视图展示为一个和 iPhone X 等比例的范围,即 812:375。实现方法参见这里。
-
接下来利用
UnityEditor
的 API 以增量方式加载这个新场景,然后它就会显示在现有场景的前面,在其中编辑 UI Prefab 的时候,就在一定程度上所见即所得了。(EditorSceneManager
中的
方法OpenScene(scenePath, OpenSceneMode.Additive)
可以胜任这个工作)。
-
恢复 A 模式的时候,将增量加载出来的场景关闭就是了。(用
EditorSceneManager
中的方法
GetSceneByName
和CloseScene
即可)
陷阱和问题
- 支持
UIAnchor
:UIAnchor
我理解是个比较过时的东西,能不用就尽量别用。如果一定要支持它,那么 Controller 的脚本执行顺序(Script execution order)需要调整到UIAnchor
之前,即需要在UIAnchor
起作用之前,设置好其锚定目标的位置和尺寸。注意,在导入 NGUI 插件时,UIAnchor
的执行顺序本来就不是 0。 - Manager 的实例在哪儿:为了在编辑模式下能所见即所得地调整 UI,需要让 Manager 和 Controller 都在编辑模式下可执行,并且在制作 UI 的场景(比如游戏的启动场景)中预先保存挂上 Manager 脚本的节点。但这时候如果触发了编译,编译之后 Manager 的
Instance
就获取不到了,需要重新载入场景才能获取到。这个是不是有什么更优美的方式来解决? - 由于有上面这个问题,我觉得需要研究研究这类既能在编辑模式执行,又能在运行模式执行的脚本,如何写比较好了。