前言

Unity的UIElement已经发布两年多了,最近才开始正式重度与其打交道,此文章主要介绍UIElement和一些常用功能和一些不常见的坑,并且提供一些UIElement的学习资料。

正文

UIElement介绍

UIElement(现更名为UI ToolKit但是程序集名称还是UIElement,雀食没话说)是Unity新推出的一种UI解决方案,目标是一站式解决Editor+Runtime的UI设计需求,使用C# + HTML的形式进行开发,其中HTML用以定义UI样式和内容,C#引用HTML定义内容+绑定数据,并且与GamePlay进行交互。

我们来看看他的底层是怎么样的:

v2-adabfd52b3a005d5ad859b50c50e86f7_1440w

  • UIElement使用了名为UIElements Renderer(UIR)的渲染后台,为UIElement渲染量身定制,尽可能提高性能。
  • 使用Retained Mode GUI (RMGUI)以及按需更新的模式,当UI元素没有发生变化的时候几乎0消耗。
  • 渲染时分配一个大的VB/IB缓冲区,使用特制的Uber Shader,减少渲染状态切换,一个DrawCall即可完成整个UIElements的绘制(即使UI元素发生变化,也会将发生变化的UI元素的VB/IB放入一开始分配的那个大的VB/IB缓冲区)。
  • 性能优化,把更多的工作下放到GPU去做,减少CPU开销,例如UI元素的属性会被存储在GPU内存上,GPU加速Clip,DynamicTransform(GPU加速UI元素的平移,缩放,旋转操作),GPU加速UI视口操作(缩放,平移,更改分辨率)。

此部分更多内容参见:Built for performance: the UIElements Renderer – Unite Copenhagen 2019

  • UXML是UI元素的布局文件,比如一个按钮其中包含背景图片和文字,可通过 VisualTreeAsset.CloneTree(VisualElement);形式实例化出来
  • USS是UI元素的样式文件,比如一个按钮其中包含背景图片和文字具体的内容和样式,可通过 VisualElement.styleSheets.Add(StyleSheet);的形式将其样式应用到VisualElement上,也可以通过 VisualElement.AddToClassList(XXX) 来将其中某一个样式应用给VisualElement
  • USS中的三个特殊字符:.#space(注意这是一个空格)> ,也就是 CSS中的选择器 ,但在USS中有些许不同,其中 . 用于引用和描述UXML中已有的Class样式,而 # 则是在USS中新声明的一个Class,可以直接通过 new VisualElement{ name = "XXX" };的形式实例化一个VisualElement,空格和CSS中的后代选择器作用一致,最后> 表示取得类的某个子元素。三者可以灵活搭配使用。

实用功能

基于VisualElement创建一个拖拽区域

关键字为IDropTarget和IDragTarget,前者为可被放置区域,后者为可以被拖动的区域,下面是个示例

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
39
40
41
42
43
44
45
46
public class DecorateNodeDropDownArea : Box, IDropTarget
{
public static Color DefaultColor = new Color(112 / 255f, 128 / 255f, 144 / 255f, 0.5f);
public static Vector2 DefaultSize = new Vector2(200, 50);
private Label m_TipLabel;
private static string s_FlowGraphBigTitleLabel = "FlowGraphBigTitleLabel";
public DecorateNodeDropDownArea()
{
this.style.backgroundColor = new StyleColor(DefaultColor);
this.style.width = DefaultSize.x;
this.style.height = DefaultSize.y;
m_TipLabel = new Label("拖动一个节点到这里");
this.Add(m_TipLabel);
}
public bool CanAcceptDrop(List<ISelectable> selection)
{
return true;
}
public bool DragUpdated(DragUpdatedEvent evt, IEnumerable<ISelectable> selection, IDropTarget dropTarget,
ISelection dragSource)
{
return true;
}
public bool DragPerform(DragPerformEvent evt, IEnumerable<ISelectable> selection, IDropTarget dropTarget,
ISelection dragSource)
{
this.style.backgroundColor = new StyleColor(DefaultColor);
return true;
}
public bool DragEnter(DragEnterEvent evt, IEnumerable<ISelectable> selection, IDropTarget enteredTarget,
ISelection dragSource)
{
this.style.backgroundColor = new StyleColor(Color.cyan);
return true;
}
public bool DragLeave(DragLeaveEvent evt, IEnumerable<ISelectable> selection, IDropTarget leftTarget,
ISelection dragSource)
{
this.style.backgroundColor = new StyleColor(DefaultColor);
return true;
}
public bool DragExited()
{
return true;
}
}

隐蔽的坑

VisualElement的layout宽高NAN(无限大)

发生这种情况是因为VisualElement的一些信息刷新不是实时的,比如你在当前帧创建了一个VisualElement,并为它设置了位置和大小,但是他可能下一帧,甚至更远的未来才会正式应用你设置的位置,在此之前你获取到的内容都是非法的

解决方式有三个

  • 手动判断宽高(VisualElement.layout)是否为NAN
  • 通过schedule来延迟调用(VisualElement.schedule)
  • 逻辑层自己封装一个Transform

VisualElement selection丢失

焦点丢失就会导致selection丢失

在UI Builder中的布局和在EditorWindow中的布局不一致

因为Unity自作聪明塞得个TemplateContainer导致的一系列问题

解决方法:

1
2
3
4
5
6
// 获取EditorWindow的Root
VisualElement root = rootVisualElement;
// 加载visualTree
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("VarEditWindow.uxml");
// 注意,要直接Clone到Root
visualTree.CloneTree(root);

参考

从零开始学习UIElement(非常适合没有了解过UIElement的人)

Unity UIElement 一篇文章即可学会

Mert Kirimgeri的GraphView教程(偏向代码绘制UIElement)

基于UIElement从零开始制作行为树(偏向UIToolKit制作UIElement)