UIToolkit基础教程

  • 1.前言
  • 2.UIToolkit安装
  • 3.编写运行时对话脚本
  • 3-1.对话内容节点
  • 3-2.对话树
  • 3-3.对话树启动器
  • 4.启动运行时对话脚本
  • 4-1.创建实例话脚本对象
  • 4-2.管理对话节点树对应属性
  • 4-3.管理各个对话节点对应属性
  • 4-4.创建对话启动器
  • 5.UIToolkit创建对话系统编辑器
  • 5-1.补充完善Runtime脚本
  • 5-2.创建NodeEditor窗口
  • 5-3.创建NodeTreeViewer视图
  • 5-4.创建Node节点视图
  • 5-5.创建InspectorViewer面板视图
  • 5-6.在NodeEditor视窗中可视化创建节点
  • 6.引用文献


1.前言

随着Unity开发的深入,基本的Unity编辑器界面并不能满足大部分玩家高阶开发的要求。为了提高开发的效率,有针对性的定制化扩展编辑器界面是提高开发效率的不错选择。
今天就给大家带来Unity官方提高的编辑器扩展工具UIToolkit(集成了UIBuilder和UI Debugger等插件)的使用教程。本次的案例会以游戏中最常用的对话系统作为编辑器管理的内容-制作一个对话系统的编辑器界面。

如果觉得图文教程不够详细的便宜已经直接观看视频教程,更加直观详细哦!
合集·Unity官方编辑器扩展工具UI ToolKit】UI Builder 制作简易对话系统编辑器

下图为使用UIToolkit制作的对话系统编辑器界面(使用节点树管理界面)

unity 语音对讲_unity 语音对讲

2.UIToolkit安装

UI Toolkit 的历史可以追溯到 Unity 2018 年发布的 UIElement,起初主要用于 Editor 编辑面板中的 UI 开发,自 Unity 2019 起,它开始支持运行时 UI,并更名为 UIToolkit,它以 Package 包(com.unity.ui)的形式存在,并在 Unity 2021.2 版本后被官方内置在Unity编辑器中。

因此Unity2021.2之前的版本要使用UIToolkit的话需要在Package Manager中引入UIToolkit包 (旧名UIBuilder)

1.在Unity编辑器顶部栏点击 Window > Package Manager 来打开 Package Manager 窗口

2.然后左上角点击+号,在下拉选项中选择 Add package from git URL…,

unity 语音对讲_unity 语音对讲_02


3.分别通过输入 com.unity.ui 和 com.unity.ui.builder 来获取 UI Toolkit 包和 UI Builder 包。

unity 语音对讲_编辑器_03


而在Unity2021.2之后的版本则可以直接在编辑器顶部栏点击Window>UI Toolkit>选择对应的工具使用

unity 语音对讲_编辑器_04

3.编写运行时对话脚本

在日常游戏对话系统中,并不是每次对话都是一模一样的 在玩家进行不同的选择时 输出的对话都是不相同的,因此对话系统一般都是以节点树的形式来编写。
因此我们运行时脚本需要以下的类构成

1.对话节点(每一句对话内容存储的载体)
2.对话节点树(每次对话中都包含了许多句对话内容 所有对话内容都以树的形式存储下来)
3.对话节点树运行器(对话发生的触发器)

3-1.对话内容节点

基类节点-此节点为所有节点的父类包含了所有节点的基础属性与方法

using UnityEngine;

public abstract class Node : ScriptableObject
{
    // 对话节点状态枚举值为运行和等待两种状态
    public enum State{ Running , Waiting }
    // 对话节点当前状态
    public State state = State.Waiting;
    // 是否已经开始当前对话节点判断指标
    public bool started = false;
    // 每个对话节点的描述
    [TextArea] public string description;
    public Node OnUpdate(){
        // 判断该节点首次调用OnUpdate时调用一次OnStart方法
        if(!started){
            OnStart();
            started =true;
        }
        Node currentNode = LogicUpdate();
        // 判断该节点结束时调用一次OnStop方法
        if(state != State.Running){
            OnStop();
            started =false;
        }
        return currentNode;
    } 
    public abstract Node LogicUpdate();
    protected abstract void OnStart();
    protected abstract void OnStop();
}

单向节点-此类节点只能单对单的进行内容关联

public abstract class SingleNode : Node
{
	// 只有一个子类
    public Node child;
}

复合节点-此类节点可以多对多的进行内容关联

using System.Collections.Generic;
public abstract class CompositeNode : Node
{
	// 有多个子节点构成的列表
    public List<Node> children = new List<Node>();
}

上述都为抽象类节点 因此还需要编写继承了对应抽象节点的实体对话类来才可以使用

普通对话节点

using UnityEngine;

// 普通对话节点 后续只会返回一种情况的对话内容
public class NormalDialogue : SingleNode
{
    [TextArea] public string dialogueContent;
    public override Node LogicUpdate()
    {
        // 判断进入下一节点条件成功时 需将节点状态改为非运行中 且 返回对应子节点
        if(Input.GetKeyDown(KeyCode.Space)){
            state = State.Waiting;
            if(child != null){
                child.state = State.Running;
                return child;
            }
        }
        return this;
    }
    //首次进入该节点时打印对话内容
    protected override void OnStart()
    {
        Debug.Log(dialogueContent);
    }
	// 结束时打印OnStop
    protected override void OnStop()
    {
        Debug.Log("OnStop");
    }
}

分支对话节点

using System.Collections.Generic;
using UnityEngine;

public class BranchDialogue : CompositeNode
{
    [TextArea] public string dialogueContent;
    public int nextDialogueIndex = 0;
    public override Node LogicUpdate()
    {
    	// 判断进入哪个对话节点
        if(Input.GetKeyDown(KeyCode.A)){
            nextDialogueIndex = 0;
        }
        if(Input.GetKeyDown(KeyCode.B)){
            nextDialogueIndex = 1;
        }
        // 判断进入下一节点条件成功时 需将节点状态改为非运行中 且 返回对应子节点
        if(Input.GetKeyDown(KeyCode.Space)){
            state = State.Waiting;
            if(children.Count > nextDialogueIndex){
                children[nextDialogueIndex].state = State.Running;
                return children[nextDialogueIndex];
            }
        }
        return this;
    }
    //首次进入该节点时打印对话内容
    protected override void OnStart()
    {
        Debug.Log(dialogueContent);
    }
	// 结束时打印OnStop
    protected override void OnStop()
    {
        Debug.Log("OnStop");
    }
}

3-2.对话树

基类节点树

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

/* 继承脚本数据化结构对象 ScriptableObject */
public class NodeTree : ScriptableObject
{
    // 当前正在播放的对话
    public RootNode rootNode;
    // 当前正在播放的对话
    public Node runningNode;
    // 对话树当前状态 用于判断是否要开始这段对话
    public Node.State treeState = Node.State.Waiting;
    // 所有对话内容的存储列表
    public List<Node> nodes = new List<Node>();

    // 判断当前对话树和对话内容都是运行中状态则进行OnUpdate()方法更新
    public virtual void Update() {
        if(treeState == Node.State.Running && runningNode.state == Node.State.Running){
            runningNode = runningNode.OnUpdate();
        }
    }
    // 对话树开始的触发方法
    public virtual void OnTreeStart(){
        treeState = Node.State.Running;
    }
    // 对话树结束的触发方法
    public virtual void OnTreeEnd(){
        treeState = Node.State.Waiting;
    }
}

实体类对话节点树

using UnityEngine;

[CreateAssetMenu()]
public class DialogueTree : NodeTree{
    public override void OnTreeStart(){
        base.OnTreeStart();
        runningNode.state = Node.State.Running;
    }
}

3-3.对话树启动器

using UnityEngine;

public class DialogueRunner : MonoBehaviour
{
    public DialogueTree tree;

    private void Start() {
    
    }
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.P)){
            tree.OnTreeStart();
        }
        if(tree != null){
            tree.Update();
        }
        if(Input.GetKeyDown(KeyCode.D)){
            tree.OnTreeEnd();
        }
    }
}

以上就是所有的运行时脚本了

unity 语音对讲_游戏引擎_05


此时在项目内点击右键,便可以看到我们刚刚编写的可创建资产化的对话节点树和对话节点了

unity 语音对讲_游戏引擎_06

4.启动运行时对话脚本

4-1.创建实例话脚本对象

右键创建5个实例话脚本对象,分别为:
1个对话节点树
3个普通对话节点
1个分支对话节点

并且按照父子节点顺序关系命名

unity 语音对讲_unity_07

4-2.管理对话节点树对应属性

1.选择对对话节点树并在属性面板中创建4个子对话节点

unity 语音对讲_unity 语音对讲_08


2.选择对应初始运行节点

unity 语音对讲_编辑器_09

3.将对应实例化对话节点按照对话顺序拖动到对应位置

unity 语音对讲_游戏引擎_10

4-3.管理各个对话节点对应属性

普通对话实例

unity 语音对讲_游戏引擎_11


分支对话实例

unity 语音对讲_1024程序员节_12


后续的对话实例也以此类推管理其对应的属性

1.填写对话内容

2.将对应的子对话内容关联到子节点当中(PS : 如果是最后的节点则无需关联)

4-4.创建对话启动器

1.在场景中创建一个对象

2.将对话启动器脚本挂载到该对象上

3.将创建好的对话树挂载到该启动器脚本的对话树属性上

unity 语音对讲_unity 语音对讲_13

此时我们点击运行启动脚本便可以,按照启动器Update()与对话节点LogicUpdate()中所写好的操作方法触发播放对应的对话内容了。

unity 语音对讲_unity 语音对讲_14

5.UIToolkit创建对话系统编辑器

5-1.补充完善Runtime脚本

在上一章节当中我们编写的Runtime脚本仅仅从运行时的角度出发,并没有考虑到可视化编辑相关的逻辑,因此我们需要在之前的脚本当中补充对应代码逻辑
1.需要在Node抽象类中补充一个guid和position属性

[HideInInspector]public string guid;
    [HideInInspector]public Vector2 position;

2.需要在NodeTree类中补充添加节点和删除节点的方法

#if UNITY_EDITOR
        public Node CreateNode(System.Type type){
            Node node = ScriptableObject.CreateInstance(type) as Node;
            node.name =type.Name;
            node.guid = GUID.Generate().ToString();
         
            nodes.Add(node);
            if(!Application.isPlaying){
                AssetDatabase.AddObjectToAsset(node,this);
            }
            AssetDatabase.SaveAssets();
            return node;
        }
        public Node DeleteeNode(Node node){
            nodes.Remove(node);
            AssetDatabase.RemoveObjectFromAsset(node);
            // Undo.DestroyObjectImmediate(node);
            AssetDatabase.SaveAssets();
            return node;
        }
#endif

5-2.创建NodeEditor窗口

1.我们需要在项目中右键 Create => UI Toolkit => Editor Window

2.输入对应的编辑器窗口名称

unity 语音对讲_编辑器_15


3.点击Confirm成功创建出NodeEditor界面

unity 语音对讲_unity 语音对讲_16


4.此时我需要把默认生成的NodeEditor脚本里的代码修改一下

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;


public class NodeEditor : EditorWindow
{
    NodeTreeViewer nodeTreeViewer;
    InspectorViewer inspectorViewer;
    [MenuItem("Window/UI Toolkit/NodeEditor")]
    public static void ShowExample()
    {
        NodeEditor wnd = GetWindow<NodeEditor>();
        wnd.titleContent = new GUIContent("NodeEditor");
    }

    public void CreateGUI()
    {
        VisualElement root = rootVisualElement;
        
        var nodeTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/NodeEditor/Editor/UI/NodeEditor.uxml");
        // 此处不使用visualTree.Instantiate() 为了保证行为树的单例防止重复实例化,以及需要将此root作为传参实时更新编辑器状态
        nodeTree.CloneTree(root);

        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/NodeEditor/Editor/UI/NodeEditor.uss");
        root.styleSheets.Add(styleSheet);
        
        // 将节点树视图添加到节点编辑器中
        nodeTreeViewer = root.Q<NodeTreeViewer>();
        // 将节属性面板视图添加到节点编辑器中
        inspectorViewer = root.Q<InspectorViewer>();
    }
    private void OnSelectionChange() {
        // 检测该选中对象中是否存在节点树
        NodeTree tree = Selection.activeObject as NodeTree;
        // 判断如果选中对象不为节点树,则获取该对象下的节点树运行器中的节点树
        if(!tree){
            if(Selection.activeGameObject){
                NodeTreeRunner runner = Selection.activeGameObject.GetComponent<NodeTreeRunner>();
                if(runner){
                    tree = runner.tree;
                }
            }
        }
        if(Application.isPlaying){
            if(tree){
                if(nodeTreeViewer != null){
                    nodeTreeViewer.PopulateView(tree);
                }
            }
        }else{
            if(tree && AssetDatabase.CanOpenAssetInEditor(tree.GetInstanceID())){
                if(nodeTreeViewer != null){
                    nodeTreeViewer.PopulateView(tree);
                }
            }
        }
    }
}

5.此时我们需要把NodeEditor.uxml里面默认生成的一些元素删除,我们就可以得到一个崭新干净的编辑器界面了

unity 语音对讲_unity 语音对讲_17


6.我们通过一些前端的技术手法将该NodeEditor分为左右两边的区域(左边为Inspector右边NodeTreeViewer

(图文难以说明,详细内容可以观看下面视频教程 )。

合集·Unity官方编辑器扩展工具UI ToolKit】UI Builder 制作简易对话系统编辑器

unity 语音对讲_游戏引擎_18

5-3.创建NodeTreeViewer视图

1.在项目中右键创建一个名为NodeTreeViewer脚本
2.该脚本需要继承GraphView,并添加一些GraphView功能代码

using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
using System;

public class NodeTreeViewer : GraphView
{
    public Action<NodeView> OnNodeSelected;
    public new class UxmlFactory : UxmlFactory<NodeTreeViewer,GraphView.UxmlTraits>{}
    NodeTree tree;
    public NodeTreeViewer(){
        Insert(0, new GridBackground());
        // 添加视图缩放
        this.AddManipulator(new ContentZoomer());
        // 添加视图拖拽
        this.AddManipulator(new ContentDragger());
        // 添加选中对象拖拽
        this.AddManipulator(new SelectionDragger());
        // 添加框选
        this.AddManipulator(new RectangleSelector());
        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/NodeEditor/Editor/UI/NodeTreeViewer.uss");
        styleSheets.Add(styleSheet);
    }
    
    // NodeTreeViewer视图中添加右键节点创建栏
    public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
    {
        // 添加Node抽象类下的所有子类到右键创建栏中
        {
            var types = TypeCache.GetTypesDerivedFrom<Node>();
            foreach(var type in types){
                evt.menu.AppendAction($"{type.Name}", (a) => CreateNode(type));
            }
        }
    }

    void CreateNode(System.Type type){
        // 创建运行时节点树上的对应类型节点
        Node node = tree.CreateNode(type);
        CreateNodeView(node);
    }

    void CreateNodeView(Node node){
        // 创建节点UI
        NodeView nodeView = new NodeView(node);
        // 节点创建成功后 让nodeView.OnNodeSelected与当前节点树上的OnNodeSelected关联 让该节点属性显示在InspectorViewer上
        nodeView.OnNodeSelected = OnNodeSelected;
        // 将对应节点UI添加到节点树视图上
        AddElement(nodeView);
    }
    
    // 只要节点树视图发生改变就会触发OnGraphViewChanged方法
    private GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
    {
        // 对所有删除进行遍历记录 只要视图内有元素删除进行判断
        if(graphViewChange.elementsToRemove != null){
            graphViewChange.elementsToRemove.ForEach(elem =>{
                // 找到节点树视图中删除的NodeView
                NodeView nodeView = elem as NodeView;
                if(nodeView != null){
                    // 并将该NodeView所关联的运行时节点删除
                    tree.DeleteeNode(nodeView.node);
                }
            });
        }
        return graphViewChange;
    }
internal void PopulateView(NodeTree tree){
        this.tree = tree;
        // 在节点树视图重新绘制之前需要取消视图变更方法OnGraphViewChanged的订阅
        // 以防止视图变更记录方法中的信息是上一个节点树的变更信息
        graphViewChanged -= OnGraphViewChanged;
        // 清除之前渲染的graphElements图层元素
        DeleteElements(graphElements);
        // 在清除节点树视图所有的元素之后重新订阅视图变更方法OnGraphViewChanged
        graphViewChanged += OnGraphViewChanged;
    }
}

3.创建一个与NodeTreeViewer同名的USS文件

unity 语音对讲_游戏引擎_19


4.且将下列背景样式Copy到USS文件当

GridBackground{
    --grid-background-color: rgb(40,40,40);
    --line-color: rgba(193,196,192,0.1);
    --thick-line-color: rgba(193,196,192,0.1);
    --spacing: 15;
}

5.回到UI Builder的NodeEditor工程中,由于我们在NodeTreeViewer脚本当中添加了

“ public new class UxmlFactory : UxmlFactory<NodeTreeViewer,GraphView.UxmlTraits>{} ” 脚本

使用我们可以在组件库的Custom Controls当中找到我们刚刚写好的NodeTreeViewer视图,我们直接将该视图拖拽到uxml工程当中即可

unity 语音对讲_unity_20


6.在调整好了UXML每个元素的样式之后这(这里图文难以讲解,具体的看视频为主)就得到了一个可拖拽 可缩放的NodeTreeViewer网格视图了。

5-4.创建Node节点视图

1.创建一个NodeView脚本,且需要继承GraphView.Node

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using UnityEditor;

public class NodeView : UnityEditor.Experimental.GraphView.Node
{
    public Action<NodeView> OnNodeSelected;
    public Node node;
    public Port input;
    public Port output;
    public NodeView(Node node){
        this.node = node;
        this.title = node.name;
        // 将guid作为Node类中的viewDataKey关联进行后续的视图层管理
        this.viewDataKey = node.guid;
        style.left = node.position.x;
        style.top = node.position.y;

        CreateInputPorts();
        CreateOutputPorts();
    }

    private void CreateInputPorts()
    {
        /*将节点入口设置为 
            接口链接方向 横向Orientation.Vertical  竖向Orientation.Horizontal
            接口可链接数量 Port.Capacity.Single
            接口类型 typeof(bool)
        */
        // 默认所有节点为多入口类型
        input = InstantiatePort(Orientation.Vertical, Direction.Input, Port.Capacity.Multi, typeof(bool));
        
        if(input != null){
            // 将端口名设置为空
            input.portName = "";
            inputContainer.Add(input);
        }
    }

    private void CreateOutputPorts()
    {
       	output = InstantiatePort(Orientation.Vertical, Direction.Output, Port.Capacity.Multi, typeof(bool));
        if(output != null){
            output.portName = "";
            outputContainer.Add(output);
        }
    }
    // 设置节点在节点树视图中的位置
    public override void SetPosition(Rect newPos)
    {
        // 将视图中节点位置设置为最新位置newPos
        base.SetPosition(newPos);
        // 将最新位置记录到运行时节点树中持久化存储
        node.position.x = newPos.xMin;
        node.position.y = newPos.yMin;
        EditorUtility.SetDirty(node);
    }

    // 复写Node类中的选中方法OnSelected
    public override void OnSelected()
    {
        base.OnSelected();
        // 如果当前OnNodeSelected选中部位空则将该节点视图传递到OnNodeSelected方法中视为选中
        if(OnNodeSelected != null){
            OnNodeSelected.Invoke(this);
        }
    }
}

5-5.创建InspectorViewer面板视图

1.创建一个InspectorViewer脚本,且需要继承VisualElement

using UnityEngine.UIElements;
using UnityEditor;
using UnityEngine;

public class InspectorViewer : VisualElement
{
    public new class UxmlFactory : UxmlFactory<InspectorViewer,VisualElement.UxmlTraits>{}
    Editor editor;
    public InspectorViewer(){

    }
    internal void UpdateSelection(NodeView nodeView ){
        Clear();
        UnityEngine.Object.DestroyImmediate(editor);
        editor = Editor.CreateEditor(nodeView.node);
        IMGUIContainer container = new IMGUIContainer(() => { 
            if(editor.target){
            editor.OnInspectorGUI();
            }
        });
        Add(container);
    }   
}

5-6.在NodeEditor视窗中可视化创建节点

在完成了上述的所有工作之后,来尝试一下在我们自己制作的NodeEditor视窗中可视化创建节点把

1.我们在项目中重新创建一个NodeTree(一定要重新创建)

2.在顶部栏点击Window => UI Toolkit => NodeEditor打开编辑窗口

unity 语音对讲_编辑器_21


3.此时需要在选中NodeTree脚本对象的情况下(一定要双击选中否则会报空指针异常)在NodeEditor编辑界面中右键选中我们需要创建的节点即可

unity 语音对讲_游戏引擎_22


这样我们就成功的在NodeEditor视窗中可视化创建了一个节点

6.引用文献

【Unity UIBuilder】官方使用手册

【Unity UIToolkit】官方使用手册

【Unity3D】UI Toolkit简介 - 作者 : little_fat_sheep

以上就是本文章全部内容了,如果觉得实用可以点个收藏和关注。博主空间还有更多和Unity相关的实用技巧欢迎大家来一起相互学习。