Unity UI Toolkit Manual文档阅读记录

UI Toolkit介绍

UI Toolkit是Unity最新的UI系统,它主要被设计用来优化不同平台的性能,此项技术是基于标准的web技术开发的(standard web technologies),既可以使用UI Toolkit来拓展Unity Editor,也可以在打包出来的游戏和应用里使用Runtime的UI(但需要安装UI Toolkit Package)

UI Toolkit包括以下内容:

  • 一个保留模式的UI系统(A retained-mode UI system),拥有创建UI的核心特性和功能
  • UI资源类型,这些类型启发于标准的web格式 (Inspired by standard web formats),比如HTML、XML和CSS。使用这些资源文件可以构造出整个UI界面
  • 用于学习UI Toolkit的工具和资源,这些工具和资源还可以用于创建和Debug你的interfaces

Unity想推荐UI Toolkit成为新项目的UI系统,但是它跟传统的uGUI和IMGUI相比,还是少了一些功能,后面会再提到。

UI Toolkit是一系列用于创建UI的资源、函数、特性和工具的集合,它可以被用过来创建常规的UI,也可以用来拓展Unity Editor、制作Runtime的Debug工具和创建Runtime的游戏UI。

UI Toolkit受standard web technologies启发得到,很多核心的概念是类似的。

UI Toolkit分为以下三类:

  • UI System: 包含了核心features and functionality
  • UI Assets: 受标准web格式启发得到的文件类型,可以被用来structure and style UI
  • Tools and resources: Create and debug your interfaces, 还可以用于帮助学习UI Toolkit

UI System

UI Toolkit的核心是一个retained-mode UI system based on recognized web technologie。它支持stylesheets,和dynamic and contextual event handling.

UI System有以下内容:

  • Visual tree:定义了所有UI Toolkit创建的UI(Defines every user interface you build with the UI Toolkit),A visual tree即是一个object graph,graph由轻量级node组成,这些node存储了所有在窗口或panel里的UI元素。
  • Controls:提供了标准的UI Control库,比如buttons、popups、list views和color pickers,可以直接原样使用它们、自定义(customize)它们或创建自己的controls。
  • Data binding system:可以把相关的property link到Control上,从而通过UI改变它们的值
  • Layout Engine:一个基于CSS的Flexbox模型的Layout系统,它可以基于layout和styling properties来放置UI元素
  • Event System:事件交互,包括:input、touch and pointer interactions(应该是触碰操作吧?),drag和drop操作等。系统包括了:a dispatcher,a handler,a synthesizer和一大堆event类型
  • UI Renderer:直接在Unity的graphics device layer上创建的渲染系统
  • UI Toolkit Runtime Support(via the UI Toolkit package):包含了用于runtime的相关组件,不过UI Toolkit package is currently in preview.

UI Assets

UI Assets也就是UI Toolkit里用到的资源文件,UI Toolkit提供了两种资源文件来帮助构建UI,与web应用类似:

  • UXML documents,文件后缀是.uxml
  • USS,文件后缀是.uss

UXML全称为Unity eXtensible Markup Lauguage,是受HTML和XML启发得到的一种markup(标记)语言,用于定义UI结构和可复用的UI模板,Unity推荐使用UXML来创建UI,而不是在C#脚本里进行

USS全称为Unity Style Sheets:可以对UI使用可视的style和behaviours,与web的CSS类似,跟上面相同,Unity推荐用USS文件来定义style,而不是直接在C#脚本里对style这个property进行修改


UI Tools and resources

提供了以下工具和资源:

  • UIDebugger:类似web浏览器的debug窗口,可以看到对应的UXML结构和USS对应的style相关的hierarchy的信息,在Window->UI Toolkit -> Debugger下
  • UI Builder(package):帮助用可视化的方式创建UI资源文件,比如uss和hxml documents,需要安装对应package
  • UI Samples:Window->UI Toolkit -> Samples下可看到很多关于UI Control的代码示例


Accessing UI Toolkit

UI Toolkit有两种获取方法,或者说有两个版本:

  • 直接在Unity Editor里获取,也就是Unity提供的引擎编辑器里自带的内置版本
  • 从Unity Package里获取(com.unity.ui)

二者的区别如下:

  1. 目的不同,内置的UI Tooklit旨在加强Unity Editor的编辑,很多Unity Editor的自带功能都是用的内置的UI Toolkit,而Unity Package里的版本添加了很多特性,用于制作runtime下的UI
  2. 二者使用方式是相同的,都是在UnityEditor.Elements和UnityEngine.Elements的命名空间下使用

该选择UI Toolkit两个版本的哪一个
如果相关UI只会在Editor下使用的话,那么使用内置的UI Toolkit,如果该UI需要既能在Editor,也能在Runtime下使用的话,那么使用对应的Package的版本,而且对应的版本也能安装最新的

安装 UI Toolkit package
打开Unity Editor的Package Manager:

  1. Click Add (+)
  2. From the menu, choose Add package from git URL…
  3. In the text field, type com.unity.ui
  4. Click Add


The Visual Tree

UI Toolkit里UI的最基本构建单元被称为Visual Element,这些elements会被排序,形成一个有层次结构的树,称为Visual Tree,下图是一个例子:
Unity UI Toolkit Manual文档阅读记录_第1张图片


Visual elements
VisualElement类是所有出现在Visual Tree里节点的基类,它定义了通用的properties,比如style、layout data和event handles。可以使用
stylesheet来自定义Visual Element的形状
,也可以使用event callback来自定义Visual Element的行为

VisualElement的派生类可以再添加behaviour和功能,比如UI Controls,下面的这些都是基于Visual Element派生出来的:

  • Button
  • Toggles
  • Text Input fields

后面还会介绍更多的内置的Controls

Panels
panel是Visual Tree的父object,对于一个Visual Tree,它需要连接到panel上才能被渲染出来,所有的Panels都从属于Window,比如EditorWindow,Panel除了处理Visual Tree的渲染外,还会处理相关的focus control和event dispatching。

每一个在Visual Tree里的Visual Element都会记录该Panel的引用,VisualElement对象里叫panel的property可以用于检测Element是否与Panel相连,若panel为null说明不相连


Draw Order
Visual Tree里默认是按深度遍历的顺序绘制Element的,如果想要改顺序,可以使用以下函数:

VisualElement e;

// 注意,下面的front和back都是视觉上的绘制关系,front意味着重叠部分不会被遮挡
// 会把该元素移到它原本的parent的children列表的最后面,所以该元素最后画,所以在top
e.BringToFront(); 
// 同上,正好反过来
e.SendToBack();

// 在parent的childrenn列表里,把e放到sbling的前面,即先画e再画sibling,所以e在底层
e.PlaceBehind(UIElements.VisualElement sibling);
// 同上,正好反过来
e.PlaceInFront(UIElements.VisualElement sibling);

Coordinate and position systems
UI Toolkit有一个强大的layout系统,根据每一个Visual Element里名为style的property,就能自动计算出每个Element的位置和size,后面还会详细提到Layout Engine.

UI Toolkit有两种坐标(coordinates):

  • Relative:基于element被计算好的position的相对坐标(Coordinates relative to the element’s calculated position.),也就是说,element的位置等于其parent的位置加上coordinates对应的offset,在这种情况下,子element的位置会影响父element的位置(因为Layout系统需要合理的安排区间,来摆放所有的element)
  • Absolute:基于parent element的绝对坐标(Coordinates relative to the parent element). 这种方式下,element的位置不再由layout系统自动计算,而是直接会被设置position。同一个element下的子elements之间的位置不会受互相的影响,也就是说,element与其parent的位置关系是确定不变的(有点Anchor的意思)

设置一个Element的Coordinates的方法如下所示:

var newElement = new VisualElement();
    newElement.style.position = Position.Relative;
    newElement.style.left = 15;
    newElement.style.top = 35;

在实际计算pos的时候,layout system会为每个element计算位置和size,再把前面的relative或absolute的coordinate offset加进去,最后的结果计算出来,存到element.layout里(类型是Rect)

The layout.position is expressed in points, relative to the coordinate space of its parent.

VisualElement类还有一个继承的Property,叫做ITransform,修改它可以添加额外的Local的position和rotation的变化,相关的变化不会显示在layout属性里,ITransform默认是Identity.

VisualElement.worldBounds代表Element在窗口空间的最终坐标bounds,它既考虑了layout,也考虑了ITransform,This position includes the height of the header of the window.

下面介绍一个例子,使用内置的UI Toolkit来创建Editor下的窗口。首先可以创建一个脚本,脚本内容如下:

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class PositioningTestWindow : EditorWindow
{
     
    [MenuItem("Window/UI Toolkit/Positioning Test Window")]
    public static void ShowExample()
    {
     
        var wnd = GetWindow<PositioningTestWindow>();
        wnd.titleContent = new GUIContent("Positioning Test Window");
    }

    public void CreateGUI()
    {
     
    	// 创建两个数据一模一样的Element, 注意这里没有指定位置,因为位置是Layout系统自己算的
        for (int i = 0; i < 2; i++)
        {
     
            // 创建两个Element, 为一个正方形, 背景是灰色
            var temp = new VisualElement();
            temp.style.width = 70;
            temp.style.height = 70;
            // marginBottom代表当Layout系统计算布局时, 此Element下方会预留20个像素的距离
            temp.style.marginBottom = 20;
            temp.style.backgroundColor = Color.gray;
            rootVisualElement.Add(temp);
        }
	}
}

点击对应的menu操作,就能出现窗口,如下图所示:
Unity UI Toolkit Manual文档阅读记录_第2张图片

继续补充CreateGUI代码,现在画一个Label,而且更改它的style里的pos,代码如下:

public void CreateGUI()
{
     
    // 创建两个数据一模一样的Element, 注意这里没有指定位置,因为位置是Layout系统自己算的
	...//原本的不变

    // 创建一个Label, Label是VisualElement的派生类
    var relative = new Label("Relative\nPos\n25, 0");
    // relative.style.position = Position.Relative;// 默认的就是Relative的方式, 所以不用刻意去写
    relative.style.width = 70;
    relative.style.height = 70;
    relative.style.left = 25;
    relative.style.marginBottom = 20;
    relative.style.backgroundColor = Color.red;
    rootVisualElement.Add(relative);
}

现在的结果变成了下图所示的样子,可以看到,原本Label应该是跟之前的一样,往下20个像素绘制的,但是这里有style.left = 25,所以在原本的基础上,加上offset(25, 0),得到最后右移的位置:
Unity UI Toolkit Manual文档阅读记录_第3张图片
展示完了Relative的方式,下面再看看Absolute的例子,代码也是类似:

public void CreateGUI()
{
     
	...// 画原本三个Element的代码不变

	// 又画两个相同的方块进行对比
	for (int i = 0; i < 2; i++)
    {
     
        var temp = new VisualElement();
        temp.style.width = 70;
        temp.style.height = 70;
        temp.style.marginBottom = 20;
        temp.style.backgroundColor = Color.gray;
        rootVisualElement.Add(temp);
    }

    // 绘制Absolute类型的方块:Absolute Positioning
    var absolutePositionElement = new Label("Absolute\nPos\n25, 25");
    // 类型是Absolute, 基准点是parent element, 其parent element就是窗口里的rootVisualElement
    absolutePositionElement.style.position = Position.Absolute;
    absolutePositionElement.style.top = 25; // 设置上方间距
    absolutePositionElement.style.left = 25; // 设置左边间距
    absolutePositionElement.style.width = 70;
    absolutePositionElement.style.height = 70;
    absolutePositionElement.style.backgroundColor = Color.black;
    rootVisualElement.Add(absolutePositionElement);
}

最后的效果如下图所示,黑色的方块:
Unity UI Toolkit Manual文档阅读记录_第4张图片

注意,在EidtorWindow类里,有一个Property叫做public VisualElement rootVisualElement { get; },可以用于取得窗口的Visual Tree的root visual element。


Transformation between coordinate systems
VisualElement.layout.position和VisualElement.transform两个参数,决定了local coordinate system 和 the parent coordinate system直接的转换,静态类VisualElementExtensions为这些转换提供了一些方法:

  • WorldToLocal:把一个Vector2或Rect,从Panel Space转换到element local space
  • LocalToWorld:同上,方向正好相反
  • ChangeCoordinatesTo:把Vector2或Rect从一个Element的local space转换到另外一个Element的local space


The Layout Engine

Layout Engine可以基于Visual Elements的layout和style属性自动计算UI布局,它是基于Github上的开源项目Yoga开发的(Yoga implements a subset of Flexbox: a HTML/CSS layout system)。

要学习Yoga和Flexbox,还需要到文档上提供的链接里去看,这里就不挂链接了。

Layout System默认有以下特点:

  • 一个container会竖直分布其children(container具体定义是什么?)
  • 一个container rectangle的position会包含其chidren的rectangles,此特点可以被其他的layout属性影响
  • 带有text的Visual Element,会在计算size时使用它字体的size,此特点可以被其他的layout属性影响

使用layout engine的一些方法:

  • 使用width和height来指定element的size
  • 通过flexGrow属性实现flexible size(in USS: flex-grow: ;) ,当element的大小由其兄弟element决定时, flexGrow 属性的值用作权重。
  • 通过将flexDirection属性设置为row,可以把layout从竖直变为水平分布
  • 如果想要在已有的element的位置上做偏移,使用relative positioning
  • 如果想让一个element像一个anchor一样,保持其与parent的位置关系,使用absolute positioning,不会影响其他的element和parent的布局


The UXML format

UXML是一种文本文件,它定义了UI的逻辑结构,本章会介绍UXML的语法、还要如何写入、读取和定义UXML模板等,还包含了一些自定义新的UI Element的方法,以及使用UQuery的方法。

In UXML 可以:

  • 在XML里定义UI的structure
  • 在USS styleshhets里定义UI layout
    而与这些相关的资源加载部分,就留给开发者自己去做了,比如导入资产、压缩数据什么的。

如何理解USS和UXML文件
这里强调一下初次看到这的时候我不理解的问题,UI的structure和UI layout有何区别?

其实Structure代表了节点的组织关系,就是Hierarchy里的父子关系,而UI Layout则代表了每个UI节点的具体的style等参数,如下图所示,HTML文件记录是Structure,CSS文件里记录的是每个节点的绘制信息,这样一看应该就很清楚了:
Unity UI Toolkit Manual文档阅读记录_第5张图片

类比到UI Toolkit里,UXML文件用于描述整体节点之间的Structure,也就是对应的父子连接关系,而每个节点都有自己的USS文件,用于描述那个节点的尺寸等UI信息。


自定义Visual Element
Unity的原文档连接在这里:https://docs.unity3d.com/2020.1/Documentation/Manual/UIE-UXML.html
坦白说,这一段文档官方文档居然没有配合具体的代码展示,感觉官方写的东西就是一坨屎,下面会基于这坨垃圾玩意儿,进行解释,然后加上自己的解释和样例去帮助理解。

  1. 创建类的基本定义
    UI Toolkit是一个可拓展的工具包,可以基于Visual Element自定义UI Element,相关的代码如下:
// 需要继承于VisualElement
class StatusBar : VisualElement
{
     
	// 必须要实现一个默认构造函数
    public StatusBar()
    {
     
    }

    public string status {
      get; set; }
}

然后我试了试,创建了个EditorWindow窗口,代码如下:

public class MyEditorWindow :EditorWindow
{
     
    [MenuItem("Window/Open My Window")]
    public static void OpenWindow()
    {
     
        var window = GetWindow<MyEditorWindow>();

        StatusBar statusBar = new StatusBar();

        statusBar.status = "Hello World";
        statusBar.style.width = 50;
        statusBar.style.height = 50;
        window.rootVisualElement.Add(statusBar);
    }
}

然后打开EditorWindow,发现没有任何显示,但是我打开UIElements Debugger发现是有东西的,只是没有显示String和UI而已,如下图所示:
Unity UI Toolkit Manual文档阅读记录_第6张图片

  1. 创建相关的factory类
    虽然这个类被创建了,但是目前好像new出来,设置width和height之后,并没有在Window中有任何显示。

这是因为,还没有读取对应的UXML,来决定该element的结构。为了读取UXML文件,需要创建一个对应的factory类,这个类可以继承于UxmlFactory,一般推荐在Element类内定义,代码如下:

class StatusBar : VisualElement
{
     
	// 在定义了这个类之后, 就可以在UXML文件里写StatusBar元素了,
	// 不过我还不熟悉这个new class的写法
	public new class UxmlFactory : UxmlFactory<StatusBar> {
      }
	...
};
  1. 创建Element的Attribute
    这个Attribute的概念源自于XML,具体的可以看后面的附录。
    这里需要创建一个UxmlTraits的对象,来实现相关的Attribute的创建:
class StatusBar : VisualElement
{
     
    public new class UxmlFactory : UxmlFactory<StatusBar, UxmlTraits> {
     }

	// 取的类名不变
	public new class UxmlTraits : VisualElement.UxmlTraits
    {
     
    	// 创建一个StringAttribute对象, StatusBar只有一个Attribute, 名字叫status
        UxmlStringAttributeDescription m_Status = new UxmlStringAttributeDescription {
      name = "status" };
        
        // 定义UxmlChildElementDescription函数
        // 函数返回空的IEnumerable,表示StatusBar的没有任何child element, 也不接受任何children
        public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
        {
     
            get {
      yield break; }
        }

		// 会从XML parser里读取到对应的bag, 然后赋值给m_status
        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
        {
     
	        // calls base.Init() to initialize the base class properties.
            base.Init(ve, bag, cc);
            // 把此类定义在StatusBar内部, 可以直接获取私有成员status
            ((StatusBar)ve).status = m_Status.GetValueFromBag(bag, cc);
        }
    }

	public StatusBar()
    {
     
        m_Status = String.Empty;
    }

    string m_Status;
    public string status {
      get; set; }
}

UxmlTraits类有两个作用:

  • 会被Factory对象用于初始化新创建的对象
  • 在schema generation过程中,可以从中获得element的信息,用于转换成XML schema directives

上面的Trait类里定义了UxmlStringAttributeDescription对象代表String的Attribute,一共有以下类型:
Unity UI Toolkit Manual文档阅读记录_第7张图片

前面的uxmlChildElementsDescription函数里,写的代码是不支持任何Children的,如果想支持任何Children,可以这么写:

public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
     
    get
    {
     
        yield return new UxmlChildElementDescription(typeof(VisualElement));
    }
}

UxmlFactory和UxmlTraits实例
这一块内容Unity的文档居然没有给例子,真是辣鸡,这里举个例子。

  • UxmlFactory类, 用于在UXML里识别此类, 并在里面创建此类对应的Tag
  • UxmlTraits类用于在UXML文件里添加自定义的Attributes, 它们都可以在UI Builder里看到

举个例子,在定义这么一个类以后:

class TwoPaneSplitView : VisualElement
{
     
    // 定义UxmlFactory类, 用于在UXML里识别此类, 并在里面创建此类对应的Tag
    public new class UxmlFactory : UxmlFactory<TwoPaneSplitView, UxmlTraits> {
     }

    // UxmlTraits类用于在UXML文件里添加自定义的Attributes, 它们都可以在UI Builder里看到
    public new class UxmlTraits : VisualElement.UxmlTraits{
     }
}

只有在里面加上了UxmlFactory,才可以在Uxml里这么写:

<BuilderAttributesTestElement/>// 目前没有加任何Attribute

Defining a namespace prefix
在完成上面的代码后,就可以在UXML文件里使用对应的Element了,如果是在Namespace里面自定义Element,还需要做额外的处理。

需要定义一个namspace prefix, Namespace prefixes其实就是在UXML的root element上面声明的attributes,它会replace the full namespace name when scoping elements.

写法如下:

// This can be done at the root level (outside any namespace) of any C# file of the assembly.
[assembly: UxmlNamespacePrefix("My.First.Namespace", "first")]
[assembly: UxmlNamespacePrefix("My.Second.Namespace", "second")]

schema generation系统会做这些事情:

  • 检查所有的attributes,使用它们创建schema,也就是XML文件里面的组织结构
  • 为每一个新创建的UXML文件,在里面的这个element上添加namespace prefix的定义
  • includes the schema file location for the namespace in its xsi:schemaLocation attribute.

接下来,需要更新项目里的UXML schema,选择Assets > Update UXML Schema,保证text editor可以辨别出来新的element。

The defined prefix is available in the newly created UXML by selecting Create > UI Toolkit > Editor Window in the Project/Assets/Editor folder.

Advanced usage

Customizing a UXML name
可以通过override继承于UxmlFactory类的Property,代码如下:

public class FactoryWithCustomName : UxmlFactory<..., ...>
{
     
	// 暂时还不知道具体会展示在哪里
    public override string uxmlName
    {
     
        get {
      return "UniqueName"; }
    }

    public override string uxmlQualifiedName
    {
     
        get {
      return uxmlNamespace + "." + uxmlName; }
    }
}

Selecting a factory for an element
默认情况下,IUxmlFactory会创建一个element,然后选择根据它的名字来选择对应的element,主要是为了让它在UXML文件里能够被识别出来


Writing UXML Templates

其实就是用XML语言写的表示UI逻辑结构的uxml文件,举个例子:

<-- 第一行是XML declaration, it is optional, 只可以出现在第一行, 前面不允许有空格-->
<-- version的attribute必须要写, encoding可以不写, 如果写了, 就必须说清楚文件的字符encoding -->

<-- UXML 代表document root, 包含了用于namespace prefix definitions和schema的源文件位置的attributes -->
<UXML
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    <-- 下面这句话有点像是using UnityEngine.UIElements, 表示后面的Label什么的都是这个ns下的, 这里的ns是作为默认的ns -->
    xmlns="UnityEngine.UIElements"
    xsi:noNamespaceSchemaLocation="../UIElementsSchema/UIElements.xsd"
    xsi:schemaLocation="UnityEngine.UIElements ../UIElementsSchema/UnityEngine.UIElements.xsd">

	<-- 这下面的Label、Box、Button等都是Visual Element -->
	<-- 前面的Label代表继承于VisualElement的类名, 而后面的text叫做Element的Attributes--->
    <Label text="Select something to remove from your suitcase:"/>
    <Box>
        <Toggle name="boots" label="Boots" value="false" />
        <Toggle name="helmet" label="Helmet" value="false" />
        <Toggle name="cloak" label="Cloak of invisibility" value="false"/>
    Box>
    <Box>
        <Button name="cancel" text="Cancel" />
        <Button name="ok" text="OK" />
    Box>
UXML>

补充几点:

  • xmlns:engine="UnityEngine.UIElements",这种写法,相当于是typedef,之后可以写,等同于
  • 如果在自己的namespace下自定义了UI Element,那么需要在的tag里包含对应的 namespace definition and schema file location,同时还要包含Unity原本的namespaces

VisualElement通用的Attribute
一共有如下:

  • name: Element的名字,应该是独一无二的
  • picking-mode:Position或者Ignore,用于鼠标事件
  • focus-index: (OBSOLETE) Use tabIndex and focusable.
  • tabindex:一个int,决定当前element的tabbing位置?
  • focusable:a boolean indicating whether the element is focusable.
  • class:a space-separated list of identifiers that characterize the element. Use classes to assign visual styles to elements. You can also use classes to select a set of elements in UQuery.
  • tooltip:一个string
  • view-data-key:一个string,定义了序列化element的key

创建UXML template asset
When you create a new UXML template asset by selecting Asset > Create > UI Toolkit > Editor Window, the Editor automatically defines namespaces for you.


Adding styles to UXML
UXML文件可以引用USS文件,需要在任何element的声明下面使用