Canvas组件

canvas组件是UI的基础画布,所有的UI元素都必须放在Canvas对象下面,Canvas是可以互相嵌套的

Canvas支持3种绘制方式——Overlay(永远显示在最上层)、Camera和World Space(世界内3D布局),其中用的最多的是Camera(绑定特定的UICamera),可以把正交摄像机投影出来的UI元素绘制在Canvas面板上。注意添加UI Camera时一定要删除它的Audio Listener

自适应屏幕

每一个Canvas都绑定一个Canvas Scaler组件,此组件确定游戏UI的缩放方式,一般UI Scale Mode选择Scall with Screen size,然后Reference Resolution选择1136*640,这是一个比较适中的16:9分辨率,然后Screen Match Mode选择Expand。

这里对应的Screen Match Mode有三种,我们在game窗口设定Free Aspect,然后调整窗口来观察UI的排布变化

Match width or Height 根据屏幕高度或宽度的对应比例来调整UI布局
Expand 水平或垂直扩展画布区域,因此画布的大小永远不会小于Reference Resolution。
Shrink 水平或垂直裁剪画布区域,因此画布的大小永远不会大于Reference Resolution。

游戏中至少需要两个摄像机,一个是主摄像机,另一个就是UI摄像机了。一般UI摄像机的Depth要比主摄像机的大,并且要勾选Orthographic,并且将Near clip plane设为0.01(防止某些UI剔除)。在Canvas中选定UI摄像机后,Camera就和Canvas绑定的,修改camera的Transform不会改变多少外观(World Space可以修改)

UI摄像机的Clear Flags要设为Depth Only,Culling Mask要设为UI,同时主摄像机的Culling Mask要除去UI,这样UI就只显示在UI摄像机里

UICamera

锚点对齐方式

锚点

Unity中的锚点都指的是相对于父级的锚点位置。我们要根据需要选择对应元素的挂靠位置

背景图全屏(Aspect Ratio Fitter)

在一个ImageUI组件上,将Rect Transform的锚点设为stretch

stretch锚定

这样图片的边缘会随着屏幕长宽比例的变化而变化。

如果UI Image勾选了Preserve Aspect的话,在Stretch情况下图片仍然保持本身的长宽比,但是屏幕比例变化到一定程度会出现UI图片该放大时反而缩小的情况,这是因为此时的图片严格按照Rect Transform的数值进行缩放,不会因为长宽变化而动态修改Rect。

此时我们就需要在Image组件上添加Aspect Ratio Fitter组件,这个组件可以驱动Rect Transform,就可以解决上述问题。

我们可以选择Fit in Parent来保证图片完全显示,或者Envelope Parent来保证图片铺满屏幕

Aspect Ratio Fitter

布局组件

首先介绍一下Content Size Fitter组件,通过上面的Aspect Ratio Fitter组件我们可以知道,FItter组件本质是控制当前UI组件的Rect Transform来让UI的内容更好的适配。

Content Size Fitter组件主要用在Text组件上,一个Text组件有自己的Best Fit复选框,这个复选框不会改变Rect大小,而是让文字自己缩放适应。而使用Content Size Fitter组件后,文字本身不会改变,而Rect的大小会改变。

Unity主要的布局组件有Horizontal Layout Group、Vertical Layout Group、Grid Layout Group。这个组件挂载在父级,就会自动排布子级的UI元素

Horizontal Layout Group配合Content size Fitter

新建gameobject,里面添加Horizontal Layout Group、Content Size Fitter、Image组件

注意

新建两个Image和一个Text作为gameObject的子对象,然后在Text中添加Content Size Fitter组件,设置与上图相同

子对象排序

结果

Content Size Fitter可以重新计算子对象的Rect区域,如果子对象很多的话,它的效率并不是很高。

出于效率和正确性的考虑,Content Size Fitter需要等一帧才能算出正确的区域,如果想立即取到正确的区域,使用如下代码

1
LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);

Canvas优化

UGUI会自动合并批次,原理是它会把一个Canvas下的所有元素合并在一个Mesh里。如果Canvas下的元素很多,任意一个元素发生位置、大小的改变,就需要合并所有元素的Mesh。如果元素非常多的话,可能就会造成卡顿。

一个比较好的做法就是每个UI界面都设置成一个Canvas。如果这个界面下的元素比较多,可以考虑多套几个Canvas。尤其是会频繁改变位置和大小的元素,这样就可以降低它合并Mesh的开销。但是每个Canvas都会占用一个DrawCall,所以Canvas也不能过多。

Altas

Unity - Manual: Sprite Atlas (unity3d.com)

前面我们讲了每个canvas会自动合并下面所有元素到一个Mesh里,Mesh虽然可以合并在一起,但是如果贴图是分开的,那么每个贴图依然会多占用一个DrawCall。为了减少DrawCall,我们可以把多张图片合并在一个图片中,这称为Altas(图集)

创建Altas

首先确保已经安装了2D Sprite包,然后在Project Settings——Editor——Sprite Packer选择Always Enabled(这里使用V1),然后在Project窗口中右键——Create——2D——Sprite Atlas。

将工程内使用的Sprite(2D and UI)图片都可以放进Atlas的Objects for Packing处。接着,在Override for PC,Mac,& Linux Standalone处设置图集的大小和贴图压缩格式,最后单击Rack Preview按钮,即可生成图形

图集设置完毕

读取Atlas

Atlas可以把很多Sprite合并在一起。假如有一个ImageUI元素,运行期间需要更换它的图片,可以在代码中加载对应的图集,然后通过名字读取Sprite,这里的名字就是Sprite在Project视图中的文件名。

更换Sprite时,需要先读取图集,首先使用Resources.Load()方法读取Atlas,接着使用m_SpriteAtlas.GetSprite()方法读取改图集上的某张Sprite,并最终将其赋值给Image组件即可。

新建ExchangeSpriteInAtlas脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.U2D;

public class ExchangeSpriteInAtlas : MonoBehaviour
{
public Image image;
private SpriteAtlas m_SpriteAtlas = null;

private void Start()
{
m_SpriteAtlas = Resources.Load<SpriteAtlas>("New Sprite Atlas");
}

private void OnGUI()
{
if (GUILayout.Button("<size=80>更换Sprite</size>"))
{
image.sprite = m_SpriteAtlas.GetSprite("unity");
Debug.Log("Sprite Changed");
}
}
}

Variant

Atlas可以设置Variant,也就是可以引用另外一个Atlas的信息。

新建一个Sprite Atlas,其类型选择Variant,Master Atlas选择之前的Sprite Atlas即可。

另外,还可以重新设置Atlas的缩放,如果将Scale设置为0.5,表示是原图集的一半。而且也可以另外设置图集的压缩格式

重新设置Master Atlas的缩放并应用到不同平台是Variant Atlas的主要用处

Variant Sprite

多图集管理

使用图集就是为了优化效率,避免过多的DrawCall,但是图集也有大小限制。

以移动平台为例,图集尽量不要超过1024。如果图片太多,就要考虑把它们放在多个图集上,如果不同图集下的图片发生叠层的现象,那么DrawCall必然又会上去了。

尽可能地把复用性很强的图片放在一个公共图集下,每个UI系统可以有一个自己的私有图集。由于战斗部分是最容易发生卡顿的地方,所以战斗下的UI尽可能都合并在一个图集上。一定要将UI动静分离,频繁发生改变的UI元素要套上Canvas。

并不是所有图片都适合使用图集。比如游戏中的图标资源,几千个图标都放在一个图集的话,整个图集都会占用巨大的内存,并且没机会释放。