随着 unity3d 4.6 ~ 5.x + 新 UI 系统最终与大家见面了。这篇文章将不会介绍怎样使用button、滚动栏之类的UI控件。这些内容能够參考Unity Manual;这篇文章的重点是。怎样理解 UI 系统的设计,以便更好的在实际中使用它。
RectTransform
Unity UI 系统使用 RectTransform 实现基本的布局和层次控制。
RectTransform 继承于 Transform。所以 Transform 的全部特征 RectTransform 同样拥有。
在 Transform 基础上,RectTransform 添加了 轴心(pivot)、锚点(实际上是用 anchorMin、anchorMax 两个点定义的矩形区域)、和 尺寸变化量(sizeDelta)。
轴心:表示UI元素的中心,使用相对于自身矩形范围的百分比表示的点位置。这会影响定位、缩放和旋转。
锚点:相对于父级矩形的子矩形区域。这个矩形各个边界值使用百分比表示。
尺寸变化量:相对锚点定义的子矩形的大小变化量,与锚点定义的子矩形合并后的区域才是最终的UI矩形。
在 Inspector 界面上,为了更方便的调节 RectTransform 的属性,锚点的两个点重合时会显示位置和宽高(直接调节位置和sizeDelta)。否则显示相对锚点矩形边界的偏移量(通过计算后再赋值给位置和sizeDelta)。在程序中。RectTransform 加入了 anchoredPosition 和 rect 属性来更方便的编程。
RectTransform 组件同样负责组织 GameObject 的层级关系。
在 UI 系统中,子级 UI 对象总是覆盖显示在父级 UI 对象上。层级同样的 UI 对象,下方的 UI 对象总是覆盖显示在上方的 UI 对象上。这种设计避免了繁琐的深度设置。在程序中。Transform 加入了 SetSiblingIndex、GetSiblingIndex、SetAsFirstSibling、SetAsLastSibling 这些方法来方便的改动物体的层级顺序。
EventSystem
假设你使用 UI 系统,那么 EventSystem 对象会自己主动创建。这个对象负责监听用户输入。默认情况下,在电脑上能够使用键盘和鼠标输入,在移动设备上能够使用触摸输入。可是假设你要为surface这种设备开发,你也能够同一时候启用两种输入。
当须要屏蔽用户输入时,将此对象关闭就可以。UnityEngine.EventSystems.EventSystem.current 保存了当前活动的 EventSystem 对象。
Canvas
Canvas 是其它全部 UI 对象的根。在一个场景里 Canvas 数量和层级都没有限制。
子 Canvas 使用与父 Canvas 同样的渲染模式。
一个 Canvas 有三种渲染模式:
Screen Space - Overlay:UI元素相对于屏幕空间,以2D方式显示在不论什么相机画面的上面。这是很标准的 UI 风格。典型样例:大量窗体、文本和button的策略游戏。
Screen Space - Camera:UI元素相对于屏幕空间,由指定的相机负责显示,相机的參数影响显示的结果。你能够把 Canvas 理解为相机的子物体。
典型样例:射击游戏屏幕上的 3D HUD。
World Space:UI元素相对于世界空间。和其它场景里的物体一样有世界位置、遮挡关系。通经常使用来做很创新的 UI 设计。
样例:游戏内的手机屏幕、与场景绑定的游戏指导等。
CanvasScaler
这个组件负责屏幕适配。UI 系统使用 RectTransform 来计算 UI 的位置和大小。但这还不够。怎样让设计的 UI 能够适配不同的分辨率、宽高比和 DPI?这个组件给出了以下3种适配方法。注意不论什么一种适配方法都不会改变UI的宽高比和相对定位。
Constant Pixel Size:通过调节 Canvas 像素大小来维持缩放不变。它的意思是在不论什么屏幕上不改变 Canvas 的缩放系数(Scale Factor),而是调节 Canvas 的像素大小与屏幕保持一致。你能够手动或通过代码调节 Canvas 的缩放系数。这是 UI 系统默认的适配方案。例如以下图两种分辨率下同样的UI显示的不同之处,尽管不同屏幕下UI元素定位、大小没有发生变化(图中两个白色元素定位分别为屏幕左上角和右下角)。可是较小的屏幕上UI元素占用了大部分屏幕空间。显得更拥挤。
这就是这种适配方式的缺点。小屏幕太拥挤、大屏幕太空旷,没有考虑到屏幕的分辨率和DPI。可是这种模式的优点是UI元素能够保持设计时的细节(由于没有缩放)。这种模式可能适用于:你希望UI在一定范围内按原始大小显示。这样既能够让UI显示的尽可能清晰、又能够让屏幕较大的玩家拥有更广阔的视野,可是在太小或太大的屏幕上,你能够通过程序来调节缩放系数。不至于小屏幕被UI占满、大屏幕找不到UI。
Scale With Screen Size:依据屏幕分辨率缩放。这可能是大部分游戏最方便的适配方法。
在这种模式下。你须要指定一种设计分辨率,然后指定缩放的算法。不管哪种缩放算法,假设实际宽高比与设计宽高比同样。UI 都会被等比缩放。实际上。Canvas 仅仅是保持自己的大小和设计分辨率一致。假设实际宽高比与设计宽高比不同,这时缩放算法才会影响显示结果。缩放算法有三种:扩展、收缩 和 匹配宽高。
扩展算法的逻辑是,扩大 Canvas (在宽高比上)较短的一边。使得 Canvas 宽高比与屏幕一致。
例如以下左图。设计分辨率宽高比为1:1(红色线框)。实际屏幕更宽所以 Canvas 的 width 添加以匹配屏幕。
这种算法在宽高比不同的屏幕上将始终导致UI更“开阔”。收缩算法的逻辑是,收缩 Canvas (在宽高比上)较长的一边,使得 Canvas 宽高比与屏幕一致。例如以下中图,设计分辨率宽高比为1:1(红色线框),实际屏幕更窄所以 Canvas 的 height 减小以匹配屏幕。这种算法在宽高比不同的屏幕上将始终导致UI更“紧凑”。
匹配宽高的算法逻辑是,依据指定的权重,同一时候调节 Canvas 的宽和高。使得 Canvas 宽高比与屏幕一致。例如以下右图。设计分辨率为红色线框。设定宽度和高度的权重相等(0.5)。实际屏幕上 Canvas 的宽和高都被调整以匹配屏幕。这种算法目的是,通过可调节的宽高权重,尽可能的保持UI的原始设计。
Constant Physical Size:通过调节 Canvas 物理大小来维持缩放不变。它的意思是在不论什么屏幕上不改变 Canvas 的 DPI,而是调节 Canvas 的物理大小总是与屏幕保持一致。
这种说法可能比 Constant Pixel Size 更难以理解,实际上他们本质是一样的,仅仅只是 Constant Pixel Size 通过逻辑像素大小调节来维持缩放。而 Constant Physical Size 通过物理大小调节来维持缩放。使用这种模式必须指定一个像素转换物理大小的因数(填写96方便在windows上进行开发)。执行时通过详细设备报告的dpi计算 Canvas 像素大小和缩放系数。
这种模式从设计的意图来看,是为了在开发时使用物理单位而非像素单位。这仅仅会让程序和美术的工作变得复杂,实际使用价值并不高。由于开发者更关心设计的像素分辨率。他们须要绘制明白的像素大小的图片。假设未来开发者和玩家都使用了超高DPI的显示器。那时也许会更注重物理尺寸。
Selectable
可交互UI组件的基类。它负责响应用户的输入,产生视觉变化、切换导航目标 以及 处理通用的UI事件。
Transition:可交互组件有4种视觉状态:正常(normal), 高亮(highlighted), 按下(pressed)和 禁用(disabled)。Selectable 依据用户的输入和自己当前的状态执行状态切换,切换状态的视觉效果有4种类型:none、color tint、sprite swap 和 animation。使用 animation 效果必须再加入一个 animator 组件,此动画控制器含有上述4种状态。
能够通过Auto Generate Animation button自己主动加入组件并创建动画控制器。
Navigation:能够使用键盘和游戏控制器切换导航目标,假设你要开发一个仅使用游戏控制器就能够玩的游戏(主机游戏)。那么这个功能很重要。由于玩家没有鼠标也无法使用触摸屏,仅仅能通过button来切换导航目标。
这个功能被设计的很完好,以至于你差点儿什么都不用做就能够处理的很好。一共同拥有5种导航选项:不使用(None)、水平(Horizontal)、垂直(Vertical)、自己主动(Automatic)、显式指定(Explicit)。在非显式指定的情况下。导航系统依据每一个UI元素的矩形位置和大小。自己主动查找4个方向上是否存在最合适的切换目标。
假设选择显式指定,须要为4个方向指定切换目标(Selectable)。在Inspector 界面点击 Visualize button能够查看导航路径(下图中的黄色线条),在 EventSystems 中能够设置默认选中的对象。
通用事件:OnSelect, OnDeselect, OnPointEnter, OnPointExit, OnPointDown, OnPointUp, ... 。重写这些方法来定义自己的可交互组件。文末将通过一个样例来说明怎样实现自己定义控件。
Auto Layout
自己主动布局用于简化UI的布局工作。自己主动布局基于 RectTransform 的布局系统。包括 布局元素(Layout Elements) 和 布局控制器(Layout Controllers)两个概念。
布局元素含有 最小尺寸、首选尺寸 和 可选尺寸 这些參数,布局控制器依据这些參数来调整布局元素的大小和位置。
布局控制器调整的基本原则是:首先分配最小尺寸,然后假设还有足够空间就分配首选尺寸,最后假设还有空间则分配可选尺寸。一个含有 RectTransform 组件的游戏对象就是一个布局元素。
加入某些组件会改动布局元素的參数。LayoutElement 是一个用来改动默认布局參数的组件。
布局控制器以多种组件的形式存在,它们控制自身或子级的布局元素的大小和位置。关于各种布局控制器组件的功能和用法请參考 Unity 文档。
Rich Text
默认情况下一个 Text 组件以单一样式显示全部文本,使用富文本能够让显示样式更丰富,比方高亮部分文本。
实际上,富文本功能不仅能够用在 UI 系统,还能够用在 Legacy GUI 系统 和 Debug 中。
富文本用法相似 html 标签,比方 "<b>Hello</b>" 将显示为加粗的 "Hello"。这些标签还能够嵌套使用。可用的标签有 b(加粗)、i(倾斜)、size(大小)和 color (颜色)。当中 size 和 color 必须指定属性值,如:"<size=50>Hello</size>" "<color=#FF0080FF>Hello</color>"。size 属性的单位是像素,color 属性使用RGBA格式的16进制表示颜色或直接填写经常使用颜色名称。下图为使用演示样例。
假设使用 TextMesh,还能够使用 material 和 quad 标签。material 须要指定 material 数组中的 material 下标,如 "<material=1>Cool</material>";quad 标签没有结束标签,通经常使用来在文本中显示一个图片,如 "This is me: <quad material=2 size=24 x=0 y=0 width=1 height=1 />"。
UnityEvent
一个可序列化的、可显示在 Inspector 上的事件类型。典型的用途就是 Button 的 OnClick 事件。拖拽一个对象或组件到方框中,就能够选择事件触发时调用的方法。可选方法必须是公开的、无返回值的、含有0个或1个可序列化的參数。也能够调用 set 类型的属性。
你也能够在自己的脚本里使用 UnityEvent。仅仅要定义一个序列化的字段。就能够和 OnClick 看起来一样了!
UnityEvent 能够通过代码加入、移除或调用方法,是对 C# 的 delegate 的包装。
另外还有泛型版本号的 UnityEvent,最多支持 4 个參数。只是由于是泛型抽象类。须要先继承再使用。
[Serializable]
public class MyEvent : UnityEvent<GameObject> { } public MyEvent myEvent;
UnityEvent 是从程序里通过 Invoke 方法调用的,Invoke 须要的參数类型和数量与泛型參数一致。可是,Inspector上依旧仅仅能填写0个或1个參数,在 Inspector 上加入的方法是无法直接获得 Invoke 时传递的參数的。
自己定义控件
通过此自己定义控件的样例,来说明怎样灵活运用 UI 系统各种功能实现各种奇葩需求。
需求:实现一种角色点数分配的控件,角色有一定数量的点数,能够分配给多种属性,每种属性都能够分配一定范围的点数,各属性点数之和不能超过角色拥有的点数。一般的实现方法是,为每一个角色显示剩余点数和多条属性滑块。拖动每一个滑块会改变剩余点数。这里我们要求把全部滑块放到一个大滑动条中,能够直接拖动每一个滑块。这种优点是能够直观的看到每一个角色总能力对照以及剩余可分配点数。
例如以下为设计图。
我们先分析这个需求:整个滑动条代表点数总量。全部滑块是左对齐拼接的。每一个滑块具有自身最大值、最小值限制。全部滑块总长度不超过整个滑动条。每一个滑块是可交互的。能够得到初步的设想是,每一个滑块拥有一个继承 Selectable 的组件,重写按下和弹起的事件,在这个过程中,控制滑块的长度。可是怎样保持全部滑块始终左对齐呢?一种直接的想法是使用水平自己主动布局。可是这里我们不这么做(你能够试试这样来实现),而是充分利用 RectTransform 锚点的功能,让每一个滑块对其到前一个滑块的右端。这样要求每一个滑块是前一个滑块的子级。
解决方式有了,再考虑一下让这件事情更简单、更通用一些。全部初始參数填写在一个根部的组件里。这个组件依据这些參数来自己主动创建全部滑块。那么一个滑块须要的參数大概就是这种吧:
// 属性
[Serializable]
class Attribute
{
public string name;
public Sprite image;
public Color color;
public int min;
public int max;
public int value; [NonSerialized]
public ValueSlider valueSlider;
}
min、max、value 分别为 最小点数、最大点数 和 初始点数。最后一个 ValueSlider 就是我们自己定义的 Selectable 组件。在属性里保存相应的组件引用方便对其进行改动。
ValueSlider 定义例如以下:
// 滑块
class ValueSlider : Selectable
{
MultiAttributesSlider _multiAttributesSlider;
Attribute _attribute; public void Init(MultiAttributesSlider multiAttributesSlider, Attribute attribute)
{
_multiAttributesSlider = multiAttributesSlider;
_attribute = attribute;
} public override void OnPointerDown(PointerEventData eventData)
{
base.OnPointerDown(eventData);
_multiAttributesSlider.BeginSlide(_attribute, eventData);
} public override void OnPointerUp(PointerEventData eventData)
{
base.OnPointerUp(eventData);
_multiAttributesSlider.EndSlide();
}
}
初始化的时候保存根部组件(起个名字叫“多属性滑动条”)和相应的属性引用,然后按下和弹起时分别调用根部组件的開始滑动、结束滑动方法。
没有什么实际的内容,基本的操作都在根部组件上。根部组件定义例如以下:
class MultiAttributesSlider : MonoBehaviour
{
// 总点数
[SerializeField]
int _totalValue; // 属性数组
[SerializeField]
Attribute[] _attributes; //剩余点数
int _restValue; // 一个点数相应的像素大小
float pixelsPerPoint; // 保存滑块按下时的信息
Attribute _currentAttribute = null;
PointerEventData _eventData;
int _beginValue;
int _beginRestValue; // 当鼠标按下不论什么一个滑块时调用
public void BeginSlide(Attribute currentAttribute, PointerEventData eventData)
{
_currentAttribute = currentAttribute;
_eventData = eventData;
_beginValue = currentAttribute.value;
_beginRestValue = _restValue;
} // 当鼠标从不论什么一个滑块释放时调用
public void EndSlide()
{
_currentAttribute = null;
} // 初始化
void Awake()
{
// 须要通过自己定义编辑器来保证 Inspector 填写的參数全然合理。 这个样例忽略这一步。 // 统计已使用的点数
int valueCount = 0;
for(int i=0; i<_attributes.Length; i++)
{
valueCount += _attributes[i].value;
} // 计算剩余点数
_restValue = _totalValue - valueCount; RectTransform lastParent = transform as RectTransform; // 计算一个点数相应的像素大小
pixelsPerPoint = lastParent.sizeDelta.x / _totalValue; // 创建每一个滑块。更好的做法是,在自己定义编辑器中使用一个button来生成全部滑块
for(int i=0; i<_attributes.Length; i++)
{
GameObject slider = new GameObject(_attributes[i].name); // 初始化 RectTransform
RectTransform rect = slider.AddComponent<RectTransform>();
rect.SetParent(lastParent, false);
rect.localScale = Vector3.one;
rect.localRotation = Quaternion.identity;
rect.pivot = new Vector2(0, 0.5f);
rect.anchoredPosition = Vector2.zero;
if (i == 0)
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = new Vector2(0, 1);
}
else
{
rect.anchorMin = new Vector2(1, 0);
rect.anchorMax = Vector2.one;
}
rect.sizeDelta = new Vector2(pixelsPerPoint * _attributes[i].value, 0); // 初始化 Image
Image image = slider.AddComponent<Image>();
image.sprite = _attributes[i].image;
image.color = _attributes[i].color;
image.type = Image.Type.Sliced;
image.fillCenter = true; // 初始化 ValueSlider
_attributes[i].valueSlider = slider.AddComponent<ValueSlider>();
_attributes[i].valueSlider.Init(this, _attributes[i]); // 将当前 RectTransform 作为下一个滑块的父级
lastParent = rect;
}
} // 更新滑块的值
void Update()
{
if(_currentAttribute != null)
{
// 计算滑动距离相应的点数变化
int deltaValue = Mathf.RoundToInt((_eventData.position.x - _eventData.pressPosition.x) / pixelsPerPoint); // 受最小、最大值限制的点数变化
deltaValue = Mathf.Clamp(_beginValue + deltaValue, _currentAttribute.min, _currentAttribute.max) - _beginValue; // 更新剩余点数
_restValue = _beginRestValue - deltaValue; // 假设剩余点数用完,须要降低点数变化
if(_restValue < 0)
{
deltaValue += _restValue;
_restValue = 0;
} // 更新当前点数
_currentAttribute.value = _beginValue + deltaValue; // 更新滑块大小
(_currentAttribute.valueSlider.transform as RectTransform).sizeDelta
= new Vector2(pixelsPerPoint * _currentAttribute.value, 0);
}
}
}
代码的含义在上面的分析和凝视里写的很清晰了。不再赘述。这个组件最好再配合一个自己定义的编辑器,可是这里就不想写了,假设你感兴趣能够试试。
以下就是測试。创建一个滑动条背景。加入此脚本,填写參数。最后看起来这种:
然后执行起来吧。图就不截了,就是上面的设计图......最后再来张合影。
加入 CanvasGroup 组件能够控制整个子级的可交互性、不透明度。对制作非交互 UI 对象或总体控制一个窗体的不透明度很方便。