我一边骂着苹果是专利流氓,对开发者还极其不友好,一边又得靠 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 以便显示在最前。
在切换到 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
就获取不到了,需要重新载入场景才能获取到。这个是不是有什么更优美的方式来解决? - 由于有上面这个问题,我觉得需要研究研究这类既能在编辑模式执行,又能在运行模式执行的脚本,如何写比较好了。