终于看到 UI 了。
Text
修剪字体
因为一般 .ttf 的字体文件里面会有很多无用字符,所以可以用 FontCreator 处理一下,下面将演示在幼圆字体中删去几个字符,并且将黑体的数字和幼圆的汉字做拼接。
打开 FontCreator ,然后点击 File->Open Installed Font, 选择幼圆字体和黑体,再将幼圆字体中不需要的字删掉(快捷键 Delete),然后在黑体中将 0-9,A-Z,a-z 复制到幼圆对应的位置。效果如图所示:
然后将处理好的字体导出为 .ttf,并放到 Unity 中,再将字体赋给 text 组件的 Font 属性中,对比如下:
Raycast Target
如果 UI 组件不需要点击事件,那么就不要勾选上 Raycast Target 复选框,因为 UGUI 的事件系统会遍历出所有带 Raycast Target 的组件,会带来额外的开销。
Material
UGUI 的默认材质是我们无法更改的,但是我们可以复写它,只要将自定义的材质拖到 Materail 中就好了。
描边和阴影
在 Text 游戏对象上添加 Outline 和 Shadow 组件的话,文本可以获得描边和阴影的效果。可以设置颜色,描边距离等参数。描边的底层实现是在原有 Text 的基础上在上、下、左、右又多画了一遍,所以效率很低,但是阴影就只需要多画一遍,所以能用阴影的尽量用阴影,少用描边。
动态字体
Unity3D中支持动态字体和静态字体两种格式字体,动态字体即使用TTF格式字体库,静态字体则需要自己打包字体图集。
动态字体和静态字体区别在于,动态字体如果出现字体库中不存在的字体,会使用系统字体,而静态字体则不会,而且静态字体是图片,字体大小通过缩放来改变。Unity3D也有自带的字体,Windows下自带字体为Arial。如果使用Unity3D的自带字体Arial字体,在某些机型上可能显示不全。因为Arial是英文字体,并不包含中文字体,在程序运行过程中如果遇到字库中没有的字,程序就会从系统的默认字体库中查找对应的文字,如果系统默认的字库中也没有这个字,那么就会造成字体不显示问题。
动态字体的原理还有一个是如果同样的文字但是是不同的大小,那同样的文字会在纹理中生成三份。
- 实验一:缺失字体会用默认字体补全
在上文修剪字体中已经去掉了几个常用字,然后在 Unity 的 text 组件中输入被删去的字符,发现那几个字会变成 Arial 字体。应该是 Unity 选择了当前系统的默认字体。 - 实验二:动态字体会因为大小不同而出现冗余
设置两个含有相同内容的同一个字体的 Text,然后点击相应的字体,看它的 Font Material,可以看到 “喵星人(号字体)” 等几个字符都是有两份的。
也是基于以上原因,不建议在游戏中使用动态字体。
游戏中除了聊天和起名字等必须由用户自己主动输入的文字外,其实大量的文字是不需要使用到动态字体的。在游戏中强烈推荐使用 TextMeshPro,这原本是一个第三方软件,后来被 Unity 官方收购,现在可以免费使用。只需要在Window > Package Manager
中安装 TextMesh Pro 即可。
TextMeshPro 最基础的用法就是使用它的 Text 组件来显示文本。有两种使用方法,一种是通过网格 MeshRenderer 渲染,一种是通过 UI 系统渲染。
- Mesh 方式创建方法:Hierarchy 的 Create 菜单(右键菜单)> 3D > TextMesh Pro - Text
- UI 方式创建方法:Hierarchy 的 Create 菜单(右键菜单)> UI > TextMesh Pro - Text
对比一下 UGUI 的 Text 和 TextMesh Pro 的 Text:
可以很明显的看到 UGUI 的 Text 在放大之后会模糊,但 TextMesh Pro 依然很清晰。
这是因为 TextMesh Pro 的 Text 使用了不同的技术来渲染文字,叫做Signed Distance Field(后面简称 SDF,原理是用位图保存矢量信息,记录到边的最短距离,最后用 Shader 还原回来)。使用 SDF 技术字符在渲染时不会因为缩放而造成字符模糊的情况,总是能够准确的渲染字符的边缘。
TextMesh Pro 没有动态字体,所以对中文环境的游戏建议是:
- 对于游戏中显示的文字可以使用TextMesh Pro的SDF字体,提高显示效果和特效处理。
- 对于游戏中的输入框建议使用UGUI自带输入框,使用动态字体。
创建字体过程如下:
- 菜单栏打开Window -> TextMeshPro -> Font Asset Creator
- Font Source -> Character Set -> Generate Font Atlas -> Save
Font Source 选择要创建SDF字体的源字体
面板参数解释如下:
Sampling Point Size
创建字体的字号的大小。Auto Sizing
建议使用该选项。根据给定 Atlas Resolution(图集分辨率)来计算并使用最大字号。Custom Size
自定义字号。
Padding
图集中每个字符之间的间隔,以便在渲染时能却分他们的边缘。此外 padding 也用于文字的特效,所以不宜过小;但是也不宜过大,过大会造成更大的atlas分辨率或者atlas上能承载的字符更少。对于512*512的图集,padding为5通常比较合适。Packing Method
打包方式Fast
可能计算出不是最大的字号,但是计算速度会快一些Optimum
可以计算出图集上能承载的最大的字号- 通常在测试设置时使用 Fast,在最终打包时使用 Optimum
Atlas Resolution
使用 SFD 字体时,更高的分辨率会产生更精细的渐变,从而产生更高质量的字体。对于大多数字体,仅包含所有 ASCII 字符时,512x512 纹理分辨率足够用。- 当需要支持数千个字符时,不得不使用大纹理。但即使在最高分辨率下,也可能无法满足所有要求。在这种情况下,可以通过创建多个字体资源来拆分字符。将最常用的字符放在主字体资源中,将其他字符放在后备(fallback)字体资源中。
Character Set
字体文件中的字符不会自动包含在字体资源中。你必须指定你需要的那些。你可以选择一些预定义的字符集,也可以自己提供字符列表。ASCII
大小写字母 + 数字 + 常见符号Extended ASCII
包含所有的 ASCII 字符ASCII lowercase
小写字母 + 数字 + 常见符号ASCII uppercase
大写字母 + 数字 + 常见符号Numbers + Symbols
数字 + 常见符号Custom Range
使用十进制来制定字符的编码范围,可以使用减号和英文逗号来指定范围,如32-126,161-255
。使用自定义字符范围时,可以直接引用一个SDF字体,使用这个字体中的字符集Unicode Range(Hex)
使用 16 进制来制定字符的编码范围,可以使用减号和英文逗号来指定范围,如 20-7E,A1-FFCustom Characters
自定义字符,直接输入对应的字符Characters from file
从外部文件中导入字符
Render Mode
距离场模式创建SDF纹理需要与 SDF shader 一起使用。字符是在高分辨率下采样来创建良好的渐变。16x 是默认值并且足以用于大多数情况。32x 生成较慢但可以让复杂或小字符产生更好的质量。
其他模式直接将字符渲染为位图,以便与位图字体一起使用。Raster 模式不使用抗锯齿,Smooth 模式使用抗锯齿。两者都有一个Hinted模式,它将字符像素与纹理像素对齐以获得更清晰的结果。Get Kerning Pairs
你可以选择从字体中提取字距数据。此数据用于调整特定字符对之间的间距,以产生更好的视觉效果。但是请注意很多字体没有字距数据。
TextMesh Pro 有文档和例子,需要使用时可以详细了解一下。字体花屏
UGUI 的动态字体每出现一个新的字(或者同样的字但是大小不一样),材质上也都会多一个字,一开始是 256*256(像素)的材质,但扩大到 4096*4096(像素)的时候,就会触达 UGUI 内部的重建字体贴图的命令,就会导致字体花屏,为了解决这一问题,我们需要监听 Font.textureRebuilt,在贴图重建的时候,在下一帧找到场景中所有的该字体,并且调用 FontTextureChanged() 方法刷新。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// 文件名: Script_05_01.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
// 当字体贴图发生改变的时候,为了防止字体花屏,刷新场景中的每一个 Text。
public class Script_05_1 : MonoBehaviour
{
private Font m_changedFont = null;
void Start()
{
// textureRebuilt 的传参是贴图重建的字体
Font.textureRebuilt += delegate (Font font)
{
m_changedFont = font;
};
}
void Update()
{
if(m_changedFont != null)
{
Text[] text = GameObject.FindObjectsOfType<Text>();
if(text != null)
{
foreach(Text t in text)
{
if(t == m_changedFont)
{
t.FontTextureChanged();
}
}
}
// 将贴图重建的字体还原回 null;
m_changedFont = null;
}
}
}
这里的代码感觉可能是有问题的,因为如果在同一帧中有多个字体贴图发生了重建的话,这里只能检测到一个,所以或许用 List 来存储可能会好一点,但是如果是 List 的话,就需要查找两个集合之间的交集(贴图改变的字体集合和场景中所有 text 的字体)。
Image 组件
Image 用来显示图片。
- Image Type
Simple
直接显示图片Sliced
通过九宫格的形式显示图片,可用 SpriteEditor 来编辑九宫格的区域。Tiled
平铺图片。Filled
像技能 CD 冷却一样,旋转图片。
- 9 切片
官方文档上的例子挺好的,也按照上面实现了一下,要注意的就是在修改 Sprite Type 为 Sprite(2D and UI)时,要注意把 Mesh Type 也改成 Full Rect,再使用 Sprite Editor。然后可以新建一个 Sprite,将 Source Images 选成刚刚处理好的图片,将 Image Type 改成 Filled 或者 Sliced 试试,感觉很有用。Raw Image 组件
Image 组件只能显示 Texture Type 为 Sprite(2D and UI) 的图片,但是 Raw Image 既可以显示任意 Texture 也可以使用 Sprite(虽然还是以 Texture 的形式显示的),Unity 一般会在为场景中的每一个 Texture 调用一次 draw call,所以如果一个场景中有大量的 Texture,那么大量的 draw call 调用将有可能导致资源密集型的效率问题。可以使用 Sprite Atlas 来处理这个问题,Sprite Atlas 可以将多个 Texture 合并为 一个 Texture,这样就可以只调用一次 draw call 而不是很多次 draw call。
在大量 UI 系统中不建议使用 Raw Image,但是有时不得不用它,比如 Render Texture ,需要将摄像机渲染到纹理中,就必须要使用它。Button 组件
Button 组件依赖于 Image 组件。可以监听点击事件,下面演示在代码中实现的方式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 文件米:Script_05_02.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Script_05_02 : MonoBehaviour
{
public Button btn;
// Start is called before the first frame update
void Start()
{
btn.onClick.AddListener(delegate ()
{
Debug.Log("按钮被按下了");
});
}
}
当点击 Button 的时候控制台会显示 按钮被按下了
。另一种注册事件的方法时在按钮的 Inspector 面板上点击 OnClick,将需要注册的方法所在的物体拖进 Object 里,随后便可以选择相应组件上相应的方法。但不建议这么使用,因为在代码中对按钮进行监听更加灵活。
Toggle 组件
在一个物体上添加 Toggle Group 组件,里面只有一个属性:
Allow Switch Off
如果不勾选上的话,那么不能将同一组的 Toggle 同时同时设置为取消勾选状态;反之,可以不勾选所有的 Toggle。
将想要放在同一组的 Toggle 的 Group 选项引用挂有 Toggle Group 的物体,那么这一组 Toggle 就可以实现互斥选择,即最多只选择其中一个 Toggle。可以监听 Toggle 的选择/取消选择时间。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// 文件名:Script_05_03.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Script_05_03 : MonoBehaviour
{
public Toggle[] toggles;
// Start is called before the first frame update
void Start()
{
foreach(Toggle toggle in toggles)
{
// 委托参数为 bool 类型的 Toggle 当前状态
toggle.onValueChanged.AddListener(delegate (bool isOn)
{
Debug.LogFormat("{0}的值发生了改变,当前状态为:{1}", toggle.name, isOn);
});
}
}
// 测试通过 Toggle.isOn 来设置 Toggle 状态的效果是否和手动点击一样(一样的)
private void OnGUI()
{
if(GUILayout.Button("改变 Toggle1 的状态"))
{
if(toggles != null && toggles[0].name == "Toggle1")
{
toggles[0].isOn = !toggles[0].isOn;
}
}
}
}
注意 onValueChanged 的委托的参数是改变后的值,也就是当前的值,而且在注册了之后只要是 Toggle 的状态发生了改变方法都会被调用,不管是因为手动改变了状态还是在代码中调用 Toggle.isOn, 还是因为选择了其他的 Toggle 导致的当前 Toggle 的状态发生改变。
Slider 组件
Slider 组件是一个在进度条上拖动,游戏中经常用作人物血条,光照强度等。同样的,可以监听 Slider.onValueChanged()
方法来取到滑动条的进度。不过要注意这里的 onValueChanged()
方法和上面 Toggle 中传递的 UnityEvent 是不同多的,回调的参数也是不同的,上面的参数是 bool 值,这里的是 float。
1 | // 文件名:Script_05_04.cs |
Slider 的 minValue 和 maxValue 类似于 Mathf.Clamp
一样,比如 maxValue 的值为 100 的话,当加得超过一百的时候,取值就是 100,minValue 同理。
Scrollbar & ScrollView 组件
新建一个 Scroll View 组件,默认内容是这样的:
虽然根物体的名字是 Scroll View,但其实最有用的是根物体上挂着的 Scroll Rect 组件,他有 Scrollbar 的引用,还有 Content 的引用,如果将 Viewport 下的 Content 给移除,然后新建一个 Text,并将引用赋值给 Scroll Rect 的 Content,再改变 Text 的大小,让其超过 Mask 的覆盖范围,那么就可以实现用滑动条来拖动文字的效果。
Viewport 上有 Mask 组件,如果不知道 Text 需要拖到多大,可以将 Mask 先禁用。
使用 ScrollRect 组件制作游戏摇杆
书中没有明确两个 Sprite 的关系,但是经过试验,个人觉得下面的方式比较好:
- 摇杆背景(yaogan)
- 摇杆手柄(Image2)
然后将 Script_05_05 脚本挂在 yaogan 上面,Image2 赋给 脚本中的 content,Image1 和 Image2 的位置关系都设置为不延伸的中间,这样是可行的,但是缺点在于 yaogan 不能更改位置关系,因为一旦改变了位置关系,那么 sizeDelta
算出来的就不是背景的大小了(这里没有一个 size 属性吗?),一个解决方案就是在 yaogan 外层再加一个 EmptyGameobject ,但紧接着带来的坏处就是不能通过 EmptyGameobject 来改变整个摇杆装置的大小。而且这里也默认了背景是正方形的,因为只是取了 sizeDelta.x
来作为半径。
1 | // 文件名:Script_05_05.cs |
参考
Unity3D中的动态字体和静态字体
TextMeshPro插件