1.前言
隨著Unity開發(fā)的深入,基本的Unity編輯器界面并不能滿足大部分玩家高階開發(fā)的要求。為了提高開發(fā)的效率,有針對性的定制化擴展編輯器界面是提高開發(fā)效率的不錯選擇。
今天就給大家?guī)鞺nity官方提高的編輯器擴展工具UIToolkit(集成了UIBuilder和UI Debugger等插件)的使用教程。本次的案例會以游戲中最常用的對話系統(tǒng)作為編輯器管理的內(nèi)容-制作一個對話系統(tǒng)的編輯器界面。
如果覺得圖文教程不夠詳細(xì)的便宜已經(jīng)直接觀看視頻教程,更加直觀詳細(xì)哦!
合集·Unity官方編輯器擴展工具UI ToolKit】UI Builder 制作簡易對話系統(tǒng)編輯器
下圖為使用UIToolkit制作的對話系統(tǒng)編輯器界面(使用節(jié)點樹管理界面)
2.UIToolkit安裝
UI Toolkit 的歷史可以追溯到 Unity 2018 年發(fā)布的 UIElement,起初主要用于 Editor 編輯面板中的 UI 開發(fā),自 Unity 2019 起,它開始支持運行時 UI,并更名為 UIToolkit,它以 Package 包(com.unity.ui)的形式存在,并在 Unity 2021.2 版本后被官方內(nèi)置在Unity編輯器中。
因此Unity2021.2之前的版本要使用UIToolkit的話需要在Package Manager中引入UIToolkit包 (舊名UIBuilder)
1.在Unity編輯器頂部欄點擊 Window > Package Manager 來打開 Package Manager 窗口
2.然后左上角點擊+號,在下拉選項中選擇 Add package from git URL…,
3.分別通過輸入 com.unity.ui 和 com.unity.ui.builder 來獲取 UI Toolkit 包和 UI Builder 包。
而在Unity2021.2之后的版本則可以直接在編輯器頂部欄點擊Window>UI Toolkit>選擇對應(yīng)的工具使用
3.編寫運行時對話腳本
在日常游戲?qū)υ捪到y(tǒng)中,并不是每次對話都是一模一樣的 在玩家進(jìn)行不同的選擇時 輸出的對話都是不相同的,因此對話系統(tǒng)一般都是以節(jié)點樹的形式來編寫。
因此我們運行時腳本需要以下的類構(gòu)成
1.對話節(jié)點(每一句對話內(nèi)容存儲的載體)
2.對話節(jié)點樹(每次對話中都包含了許多句對話內(nèi)容 所有對話內(nèi)容都以樹的形式存儲下來)
3.對話節(jié)點樹運行器(對話發(fā)生的觸發(fā)器)
3-1.對話內(nèi)容節(jié)點
基類節(jié)點-此節(jié)點為所有節(jié)點的父類包含了所有節(jié)點的基礎(chǔ)屬性與方法
using UnityEngine;
public abstract class Node : ScriptableObject
{
// 對話節(jié)點狀態(tài)枚舉值為運行和等待兩種狀態(tài)
public enum State{ Running , Waiting }
// 對話節(jié)點當(dāng)前狀態(tài)
public State state = State.Waiting;
// 是否已經(jīng)開始當(dāng)前對話節(jié)點判斷指標(biāo)
public bool started = false;
// 每個對話節(jié)點的描述
[TextArea] public string description;
public Node OnUpdate(){
// 判斷該節(jié)點首次調(diào)用OnUpdate時調(diào)用一次OnStart方法
if(!started){
OnStart();
started =true;
}
Node currentNode = LogicUpdate();
// 判斷該節(jié)點結(jié)束時調(diào)用一次OnStop方法
if(state != State.Running){
OnStop();
started =false;
}
return currentNode;
}
public abstract Node LogicUpdate();
protected abstract void OnStart();
protected abstract void OnStop();
}
單向節(jié)點-此類節(jié)點只能單對單的進(jìn)行內(nèi)容關(guān)聯(lián)
public abstract class SingleNode : Node
{
// 只有一個子類
public Node child;
}
復(fù)合節(jié)點-此類節(jié)點可以多對多的進(jìn)行內(nèi)容關(guān)聯(lián)
using System.Collections.Generic;
public abstract class CompositeNode : Node
{
// 有多個子節(jié)點構(gòu)成的列表
public List<Node> children = new List<Node>();
}
上述都為抽象類節(jié)點 因此還需要編寫繼承了對應(yīng)抽象節(jié)點的實體對話類來才可以使用
普通對話節(jié)點
using UnityEngine;
// 普通對話節(jié)點 后續(xù)只會返回一種情況的對話內(nèi)容
public class NormalDialogue : SingleNode
{
[TextArea] public string dialogueContent;
public override Node LogicUpdate()
{
// 判斷進(jìn)入下一節(jié)點條件成功時 需將節(jié)點狀態(tài)改為非運行中 且 返回對應(yīng)子節(jié)點
if(Input.GetKeyDown(KeyCode.Space)){
state = State.Waiting;
if(child != null){
child.state = State.Running;
return child;
}
}
return this;
}
//首次進(jìn)入該節(jié)點時打印對話內(nèi)容
protected override void OnStart()
{
Debug.Log(dialogueContent);
}
// 結(jié)束時打印OnStop
protected override void OnStop()
{
Debug.Log("OnStop");
}
}
分支對話節(jié)點
using System.Collections.Generic;
using UnityEngine;
public class BranchDialogue : CompositeNode
{
[TextArea] public string dialogueContent;
public int nextDialogueIndex = 0;
public override Node LogicUpdate()
{
// 判斷進(jìn)入哪個對話節(jié)點
if(Input.GetKeyDown(KeyCode.A)){
nextDialogueIndex = 0;
}
if(Input.GetKeyDown(KeyCode.B)){
nextDialogueIndex = 1;
}
// 判斷進(jìn)入下一節(jié)點條件成功時 需將節(jié)點狀態(tài)改為非運行中 且 返回對應(yīng)子節(jié)點
if(Input.GetKeyDown(KeyCode.Space)){
state = State.Waiting;
if(children.Count > nextDialogueIndex){
children[nextDialogueIndex].state = State.Running;
return children[nextDialogueIndex];
}
}
return this;
}
//首次進(jìn)入該節(jié)點時打印對話內(nèi)容
protected override void OnStart()
{
Debug.Log(dialogueContent);
}
// 結(jié)束時打印OnStop
protected override void OnStop()
{
Debug.Log("OnStop");
}
}
3-2.對話樹
基類節(jié)點樹
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
/* 繼承腳本數(shù)據(jù)化結(jié)構(gòu)對象 ScriptableObject */
public class NodeTree : ScriptableObject
{
// 當(dāng)前正在播放的對話
public RootNode rootNode;
// 當(dāng)前正在播放的對話
public Node runningNode;
// 對話樹當(dāng)前狀態(tài) 用于判斷是否要開始這段對話
public Node.State treeState = Node.State.Waiting;
// 所有對話內(nèi)容的存儲列表
public List<Node> nodes = new List<Node>();
// 判斷當(dāng)前對話樹和對話內(nèi)容都是運行中狀態(tài)則進(jìn)行OnUpdate()方法更新
public virtual void Update() {
if(treeState == Node.State.Running && runningNode.state == Node.State.Running){
runningNode = runningNode.OnUpdate();
}
}
// 對話樹開始的觸發(fā)方法
public virtual void OnTreeStart(){
treeState = Node.State.Running;
}
// 對話樹結(jié)束的觸發(fā)方法
public virtual void OnTreeEnd(){
treeState = Node.State.Waiting;
}
}
實體類對話節(jié)點樹
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();
}
}
}
以上就是所有的運行時腳本了
此時在項目內(nèi)點擊右鍵,便可以看到我們剛剛編寫的可創(chuàng)建資產(chǎn)化的對話節(jié)點樹和對話節(jié)點了
4.啟動運行時對話腳本
4-1.創(chuàng)建實例話腳本對象
右鍵創(chuàng)建5個實例話腳本對象,分別為:
1個對話節(jié)點樹
3個普通對話節(jié)點
1個分支對話節(jié)點
并且按照父子節(jié)點順序關(guān)系命名
4-2.管理對話節(jié)點樹對應(yīng)屬性
1.選擇對對話節(jié)點樹并在屬性面板中創(chuàng)建4個子對話節(jié)點
2.選擇對應(yīng)初始運行節(jié)點
3.將對應(yīng)實例化對話節(jié)點按照對話順序拖動到對應(yīng)位置
4-3.管理各個對話節(jié)點對應(yīng)屬性
普通對話實例
分支對話實例
后續(xù)的對話實例也以此類推管理其對應(yīng)的屬性
1.填寫對話內(nèi)容
2.將對應(yīng)的子對話內(nèi)容關(guān)聯(lián)到子節(jié)點當(dāng)中(PS : 如果是最后的節(jié)點則無需關(guān)聯(lián))
4-4.創(chuàng)建對話啟動器
1.在場景中創(chuàng)建一個對象
2.將對話啟動器腳本掛載到該對象上
3.將創(chuàng)建好的對話樹掛載到該啟動器腳本的對話樹屬性上
此時我們點擊運行啟動腳本便可以,按照啟動器Update()與對話節(jié)點LogicUpdate()中所寫好的操作方法觸發(fā)播放對應(yīng)的對話內(nèi)容了。
5.UIToolkit創(chuàng)建對話系統(tǒng)編輯器
5-1.補充完善Runtime腳本
在上一章節(jié)當(dāng)中我們編寫的Runtime腳本僅僅從運行時的角度出發(fā),并沒有考慮到可視化編輯相關(guān)的邏輯,因此我們需要在之前的腳本當(dāng)中補充對應(yīng)代碼邏輯
1.需要在Node抽象類中補充一個guid和position屬性
[HideInInspector]public string guid;
[HideInInspector]public Vector2 position;
2.需要在NodeTree類中補充添加節(jié)點和刪除節(jié)點的方法
#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.創(chuàng)建NodeEditor窗口
1.我們需要在項目中右鍵 Create => UI Toolkit => Editor Window
2.輸入對應(yīng)的編輯器窗口名稱
3.點擊Confirm成功創(chuàng)建出NodeEditor界面
4.此時我需要把默認(rèn)生成的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() 為了保證行為樹的單例防止重復(fù)實例化,以及需要將此root作為傳參實時更新編輯器狀態(tài)
nodeTree.CloneTree(root);
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/NodeEditor/Editor/UI/NodeEditor.uss");
root.styleSheets.Add(styleSheet);
// 將節(jié)點樹視圖添加到節(jié)點編輯器中
nodeTreeViewer = root.Q<NodeTreeViewer>();
// 將節(jié)屬性面板視圖添加到節(jié)點編輯器中
inspectorViewer = root.Q<InspectorViewer>();
}
private void OnSelectionChange() {
// 檢測該選中對象中是否存在節(jié)點樹
NodeTree tree = Selection.activeObject as NodeTree;
// 判斷如果選中對象不為節(jié)點樹,則獲取該對象下的節(jié)點樹運行器中的節(jié)點樹
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里面默認(rèn)生成的一些元素刪除,我們就可以得到一個嶄新干凈的編輯器界面了
6.我們通過一些前端的技術(shù)手法將該NodeEditor分為左右兩邊的區(qū)域(左邊為Inspector右邊NodeTreeViewer
(圖文難以說明,詳細(xì)內(nèi)容可以觀看下面視頻教程 )。
合集·Unity官方編輯器擴展工具UI ToolKit】UI Builder 制作簡易對話系統(tǒng)編輯器
5-3.創(chuàng)建NodeTreeViewer視圖
1.在項目中右鍵創(chuàng)建一個名為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視圖中添加右鍵節(jié)點創(chuàng)建欄
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
// 添加Node抽象類下的所有子類到右鍵創(chuàng)建欄中
{
var types = TypeCache.GetTypesDerivedFrom<Node>();
foreach(var type in types){
evt.menu.AppendAction($"{type.Name}", (a) => CreateNode(type));
}
}
}
void CreateNode(System.Type type){
// 創(chuàng)建運行時節(jié)點樹上的對應(yīng)類型節(jié)點
Node node = tree.CreateNode(type);
CreateNodeView(node);
}
void CreateNodeView(Node node){
// 創(chuàng)建節(jié)點UI
NodeView nodeView = new NodeView(node);
// 節(jié)點創(chuàng)建成功后 讓nodeView.OnNodeSelected與當(dāng)前節(jié)點樹上的OnNodeSelected關(guān)聯(lián) 讓該節(jié)點屬性顯示在InspectorViewer上
nodeView.OnNodeSelected = OnNodeSelected;
// 將對應(yīng)節(jié)點UI添加到節(jié)點樹視圖上
AddElement(nodeView);
}
// 只要節(jié)點樹視圖發(fā)生改變就會觸發(fā)OnGraphViewChanged方法
private GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
{
// 對所有刪除進(jìn)行遍歷記錄 只要視圖內(nèi)有元素刪除進(jìn)行判斷
if(graphViewChange.elementsToRemove != null){
graphViewChange.elementsToRemove.ForEach(elem =>{
// 找到節(jié)點樹視圖中刪除的NodeView
NodeView nodeView = elem as NodeView;
if(nodeView != null){
// 并將該NodeView所關(guān)聯(lián)的運行時節(jié)點刪除
tree.DeleteeNode(nodeView.node);
}
});
}
return graphViewChange;
}
internal void PopulateView(NodeTree tree){
this.tree = tree;
// 在節(jié)點樹視圖重新繪制之前需要取消視圖變更方法OnGraphViewChanged的訂閱
// 以防止視圖變更記錄方法中的信息是上一個節(jié)點樹的變更信息
graphViewChanged -= OnGraphViewChanged;
// 清除之前渲染的graphElements圖層元素
DeleteElements(graphElements);
// 在清除節(jié)點樹視圖所有的元素之后重新訂閱視圖變更方法OnGraphViewChanged
graphViewChanged += OnGraphViewChanged;
}
}
3.創(chuàng)建一個與NodeTreeViewer同名的USS文件
4.且將下列背景樣式Copy到USS文件當(dāng)
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腳本當(dāng)中添加了
“ public new class UxmlFactory : UxmlFactory<NodeTreeViewer,GraphView.UxmlTraits>{} ” 腳本
使用我們可以在組件庫的Custom Controls當(dāng)中找到我們剛剛寫好的NodeTreeViewer視圖,我們直接將該視圖拖拽到uxml工程當(dāng)中即可
6.在調(diào)整好了UXML每個元素的樣式之后這(這里圖文難以講解,具體的看視頻為主)就得到了一個可拖拽 可縮放的NodeTreeViewer網(wǎng)格視圖了。
5-4.創(chuàng)建Node節(jié)點視圖
1.創(chuàng)建一個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關(guān)聯(lián)進(jìn)行后續(xù)的視圖層管理
this.viewDataKey = node.guid;
style.left = node.position.x;
style.top = node.position.y;
CreateInputPorts();
CreateOutputPorts();
}
private void CreateInputPorts()
{
/*將節(jié)點入口設(shè)置為
接口鏈接方向 橫向Orientation.Vertical 豎向Orientation.Horizontal
接口可鏈接數(shù)量 Port.Capacity.Single
接口類型 typeof(bool)
*/
// 默認(rèn)所有節(jié)點為多入口類型
input = InstantiatePort(Orientation.Vertical, Direction.Input, Port.Capacity.Multi, typeof(bool));
if(input != null){
// 將端口名設(shè)置為空
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);
}
}
// 設(shè)置節(jié)點在節(jié)點樹視圖中的位置
public override void SetPosition(Rect newPos)
{
// 將視圖中節(jié)點位置設(shè)置為最新位置newPos
base.SetPosition(newPos);
// 將最新位置記錄到運行時節(jié)點樹中持久化存儲
node.position.x = newPos.xMin;
node.position.y = newPos.yMin;
EditorUtility.SetDirty(node);
}
// 復(fù)寫Node類中的選中方法OnSelected
public override void OnSelected()
{
base.OnSelected();
// 如果當(dāng)前OnNodeSelected選中部位空則將該節(jié)點視圖傳遞到OnNodeSelected方法中視為選中
if(OnNodeSelected != null){
OnNodeSelected.Invoke(this);
}
}
}
5-5.創(chuàng)建InspectorViewer面板視圖
1.創(chuàng)建一個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視窗中可視化創(chuàng)建節(jié)點
在完成了上述的所有工作之后,來嘗試一下在我們自己制作的NodeEditor視窗中可視化創(chuàng)建節(jié)點把
1.我們在項目中重新創(chuàng)建一個NodeTree(一定要重新創(chuàng)建)
2.在頂部欄點擊Window => UI Toolkit => NodeEditor打開編輯窗口
3.此時需要在選中NodeTree腳本對象的情況下(一定要雙擊選中否則會報空指針異常)在NodeEditor編輯界面中右鍵選中我們需要創(chuàng)建的節(jié)點即可
這樣我們就成功的在NodeEditor視窗中可視化創(chuàng)建了一個節(jié)點
6.引用文獻(xiàn)
【Unity UIBuilder】官方使用手冊
【Unity UIToolkit】官方使用手冊
【Unity3D】UI Toolkit簡介 - 作者 : little_fat_sheep文章來源:http://www.zghlxwxcb.cn/news/detail-761589.html
以上就是本文章全部內(nèi)容了,如果覺得實用可以點個收藏和關(guān)注。博主空間還有更多和Unity相關(guān)的實用技巧歡迎大家來一起相互學(xué)習(xí)。文章來源地址http://www.zghlxwxcb.cn/news/detail-761589.html
到了這里,關(guān)于【Unity UIToolkit】UIBuilder基礎(chǔ)教程-制作簡易的對話系統(tǒng)編輯器 3步教你玩轉(zhuǎn)Unity編輯器擴展工具的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!