国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Unity 基于GraphView的對話系統(tǒng)設計(一)對話數(shù)據(jù)與節(jié)點編輯器

這篇具有很好參考價值的文章主要介紹了Unity 基于GraphView的對話系統(tǒng)設計(一)對話數(shù)據(jù)與節(jié)點編輯器。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

對話系統(tǒng)特點

  • 使用節(jié)點編輯器編輯對話,便于策劃等非程序崗參與項目開發(fā)
  • 拓展性強,可自定義節(jié)點,方便根據(jù)項目需求進行拓展
  • 使用邏輯簡單、直觀,無需進行大量配置
  • 對話數(shù)據(jù)持久化儲存,且?guī)г鰟h管理
  • 節(jié)點可進行邏輯控制

系統(tǒng)實現(xiàn)

首先,我們設計一下對話系統(tǒng)進行的結(jié)構(gòu)分層,在該對話系統(tǒng)中,我們將其分為節(jié)點編輯器、對話數(shù)據(jù),對話邏輯處理系統(tǒng)三個部分。我們可以用下圖來表示:

對話數(shù)據(jù)采用 Scriptableobject 來實現(xiàn)編輯模式下的數(shù)據(jù)可變性和運行模式下的數(shù)據(jù)可持久性,節(jié)點編輯器于編輯器下運行,為對話數(shù)據(jù)文件唯一編輯方式。

編輯器部分

對于該節(jié)點式編輯器的制作,我們這里使用的是 Unity 自帶的 GraphView API 來進行開發(fā),GraphView 的相關中文資料較少,在開發(fā)的過程中也是踩到了不少坑。
為了創(chuàng)建一個編輯器窗口,我們打開資源菜單,選擇創(chuàng)建,選擇UI工具箱(UI UIToolkit),選擇編輯器窗口,會出現(xiàn)以下畫面:

graphview unity,Unity基于GraphView的對話系統(tǒng)設計,unity,編輯器,游戲引擎

我們可以將其命名為DialogGraphWindow,點擊創(chuàng)建,Unity會自動為我們創(chuàng)建文件。

graphview unity,Unity基于GraphView的對話系統(tǒng)設計,unity,編輯器,游戲引擎

創(chuàng)建完成后,首先是編輯器的主體部分,雙擊創(chuàng)建好的DialogGraphWindow.uxml文件,Unity會自動打開 UIToolkit 中的 UIBuilder,在該窗口中,我們可以自定義我們的窗口布局,下面是本對話系統(tǒng)的UI布局:

graphview unity,Unity基于GraphView的對話系統(tǒng)設計,unity,編輯器,游戲引擎
布局結(jié)構(gòu)很簡單,值得注意的是,我們需要在面板的Hierarchy窗口中點擊.uxml文件,然后在Inspector窗口中勾選Editor Extension Authoring選項,這樣我們才可以在編輯模式下使用編輯器UI組件,如下圖:

graphview unity,Unity基于GraphView的對話系統(tǒng)設計,unity,編輯器,游戲引擎

其中的DialogGraphView組件,并不是在Stander窗口里創(chuàng)建。我們沒有在 Standard中看到這一 UI 組件,這是因為 GraphView 組件是需要我們自己手動創(chuàng)建的,新建一個腳本,讓他繼承 GraphView,并進行該自定義 UI 組件的配置,代碼如下:

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

namespace DialogueSystem
{
    public class DialogGraphView : GraphView
    {
        public new class UxmlFactory : UxmlFactory<DialogGraphView, GraphView.UxmlTraits>
        {
        }

        public DialogGraphView()
        {
            //增加格子背景
            Insert(0, new GridBackground());

            //增加內(nèi)容縮放,拖動,拖拽,框選控制器
            this.AddManipulator(new ContentZoomer());
            this.AddManipulator(new ContentDragger());
            this.AddManipulator(new SelectionDragger());
            //框選 bug
            //大坑!控制器之間存在優(yōu)先級
            //這就是為什么框選控制器放在選擇拖放節(jié)點控制器之前會導致節(jié)點無法移動
            //因為框選的優(yōu)先級更高
            this.AddManipulator(new RectangleSelector());

            var styleSheet =
                AssetDatabase.LoadAssetAtPath<StyleSheet>(
                    //此處填你項目對應 uss 文件路徑
                    "Assets/DialogueSystem/NodeEditor/EditorWindow/DialogueTreeView.uss");
            styleSheets.Add(styleSheet);

            //初始化 treedata 布局
            if (treeData != null)
            {
                contentViewContainer.transform.position = treeData.GraphViewData.Position;
                contentViewContainer.transform.scale = treeData.GraphViewData.Scale;
            }
        }
}
#endif

打開DialogGraphWindow.cs腳本,修改腳本如下:

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

namespace LFramework.AI.Kit.DialogueSystem
{
    public class DialogGraphWindow : EditorWindow
    {
        [MenuItem("Window/UI Toolkit/DialogueView")]
        public static void ShowExample()
        {
            DialogueView wnd = GetWindow<DialogueView>();
            wnd.titleContent = new GUIContent("DialogueView");
        }

        private DialogGraphView _graphView = null;

        public static void CloseEditorWindow()
        {
            DialogGraphWindow wnd = GetWindow<DialogGraphWindow>();
            wnd.Close();
        }

        public void CreateGUI()
        {
            var visualTree =
                AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
                    "Assets/DialogueSystem/NodeEditor/EditorWindow/DialogGraphWindow.uxml");
            visualTree.CloneTree(rootVisualElement);

            _graphView = rootVisualElement.Q<DialogGraphView>("DialogGraphView");

            var saveButton = rootVisualElement.Q<ToolbarButton>("SaveButton");
            saveButton.clicked += OnSaveButtonClicked;
        }

        //保存資源文件
        private void OnSaveButtonClicked()
        {
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        }

        private void OnDestroy()
        {
            AssetDatabase.SaveAssets();
        }
    }
}
#endif

現(xiàn)在我們回到 Untiy,重新編譯后,點擊 project 按鈕,就可以看到我們剛剛創(chuàng)建好的組件。拖拽組件到窗口中,或許屏幕上并沒有變化。選中 GraphView 組件,修改其 grow 值為 1,現(xiàn)在我們應該就能看到我們的 GraphView 了。完成之后,我們來開始創(chuàng)建我們的對話節(jié)點。

對話節(jié)點

對于每一個對話節(jié)點,我們都可以將其分為 Data 層和 View 層。在View 層中,我們只是對我們的節(jié)點數(shù)據(jù)進行可視化處理,在 View 層中,我們也能對對話節(jié)點進行編輯,編輯的目標就是我們的 Data 層。Data 層才是我們對話數(shù)據(jù)的關鍵部分,保存節(jié)點包含的各種信息。
在該對話系統(tǒng)中,我們采用了 Scriptableobject 來進行數(shù)據(jù)的存儲,使用 Scriptableobject 可以實現(xiàn)在編輯器中的數(shù)據(jù)持久化,而且還能達成數(shù)據(jù)的可復用性。
所以首先,我們需要一個用來管理所有對話節(jié)點數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)類,創(chuàng)建一個 DialogTree 類,代碼如下:

using System;
using System.Collections.Generic;
using LFramework.AI.Kit.DialogueSystem;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace DialogueSystem
{
    [CreateAssetMenu(menuName = "Create new DialogTreeData",fileName = "DialogTreeData")]
    public class DialogTree : ScriptableObject
    {
        public DialogNodeDataBase StartNodeData = null;

        public List<DialogNodeDataBase> ChildNodeDataList = new List<DialogNodeDataBase>();

		// 用來儲存節(jié)點視圖信息
        [Serializable]
        public class ViewData
        {
            public Vector3 Position;
            public Vector3 Scale = new Vector3(1, 1, 1);
        }

        public ViewData GraphViewData = new ViewData();
    }
}

在這里,我們定義一個頭結(jié)點對象和一個子節(jié)點列表,每個對話樹 Scriptableobject 都記錄著他所管理的所有對話節(jié)點的信息,所以接下來,我們來定義節(jié)點數(shù)據(jù)的基類 DialogDataBase,代碼如下:

using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;

namespace DialogueSystem
{
    public abstract class DialogNodeDataBase : ScriptableObject
    {
        /// <summary>
        /// 節(jié)點坐標
        /// </summary>
        [HideInInspector] public Vector2 Position = Vector2.zero;

        [HideInInspector] public string Path;

        /// <summary>
        /// 節(jié)點類型
        /// </summary>
        public abstract NodeType NodeType { get; }

        protected DialogNodeDataBase()
        {
        }

        private void OnValidate()
        {
#if UNITY_EDITOR
            AssetDatabase.SaveAssets();
#endif
        }

        public List<string> OutputItems = new List<string>();

        public List<DialogNodeDataBase> ChildNode = new List<DialogNodeDataBase>();
    }
}

在這個對話節(jié)點基類中,我們定義了所有節(jié)點的通用屬性,其中最重要的是兩個列表,一個是用于儲存節(jié)點輸出信息的 OutputItems 列表,他可以用于儲存對話數(shù)據(jù),也可以根據(jù)不同的需求,儲存節(jié)點所需的字符串信息。另一個ChildNode列表用于儲存對話節(jié)點間的關系,包含了該節(jié)點的所有子節(jié)點對象,這是這套對話系統(tǒng)的關鍵。

在讀取數(shù)據(jù)時,根據(jù)對話樹的頭節(jié)點,按照節(jié)點連線規(guī)則一直往下選擇下一個對話節(jié)點,重復讀取直至對話節(jié)點進行至 End 節(jié)點結(jié)束。這樣的邏輯有點像是鏈表。

除此之外,節(jié)點中的 NodeType 屬性也是對話系統(tǒng)的關鍵部分,在讀取一個節(jié)點時,通過檢測節(jié)點的類型,我們可以使用不同的方式來處理節(jié)點,以實現(xiàn)節(jié)點的邏輯控制,這部分我們將在后面構(gòu)建對話系統(tǒng)的數(shù)據(jù)處理部分詳細介紹。

所以,接下來我們來創(chuàng)建兩個關鍵的對話節(jié)點,StartNode 跟 EndNode。在創(chuàng)建節(jié)點之前,我們先創(chuàng)建一個新的 C#腳本,在腳本中創(chuàng)建一個 NodeType 枚舉類,代碼如下:

namespace DialogueSystem
{
    public enum NodeType
    {
        Start,
        End,
    }
}

接下來是我們的兩個對話節(jié)點,代碼如下;
StartNode:

namespace DialogueSystem
{
    public class StartNodeData : DialogNodeDataBase
    {
        public override NodeType NodeType => NodeType.Start;
    }
}

EndNode:

namespace DialogueSystem
{
    public class EndNodeData : DialogNodeDataBase
    {
        public override NodeType NodeType => NodeType.End;
    }
}

這兩個都繼承了我們的 DialogDataBase 基類,在這里我們只需要指定他們的類型即可。

創(chuàng)建好節(jié)點的 Data 層后,我們來實現(xiàn)對話節(jié)點的 View 層,創(chuàng)建一個 NodeViewBase 基類,代碼如下:

#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

namespace DialogueSystem
{
    public abstract class NodeViewBase : Node
    {
        public Action<NodeViewBase> OnNodeSelected;
        
        public string GUID;

        //對話數(shù)據(jù)
        public DialogNodeDataBase DialogNodeData = null;

        public NodeViewBase(DialogNodeDataBase dialogNodeData) : base()
        {
            GUID = Guid.NewGuid().ToString();
            DialogNodeData = dialogNodeData;
            //大坑,新版本unity不自動在磁盤上應用資源更新,必須先給目標物體打上Dirty標記
            EditorUtility.SetDirty(DialogNodeData);
        }

        public Port GetPortForNode(NodeViewBase node, Direction portDirection,
            Port.Capacity capacity = Port.Capacity.Single)
        {
            return node.InstantiatePort(Orientation.Horizontal, portDirection, capacity, typeof(bool));
        }

        public override void OnSelected()
        {
            base.OnSelected();
            OnNodeSelected?.Invoke(this);
        }
    }
}
#endif

view 層我們主要使用了 GraphView 的 Node 基類,我們的基類繼承了 Node,這樣可以使我們在 GraphView 中創(chuàng)建出自定義的節(jié)點UI,上面的代碼我們重寫了OnSelected方法,將節(jié)點被選中的事件發(fā)送出去。

按照我們的設計,開始節(jié)點只有一個輸出口,且接口只能連接一個子節(jié)點,也就是說,StartNode擁有一個單一輸出口。而EndNode則可以接受多個輸入,但他不能有輸出口,因為EndNode節(jié)點表示的是對話樹的終止處,所以EndNode將有一個支持多輸入的輸入口,我們來實現(xiàn)這兩個節(jié)點,代碼如下:
StartNode:

#if UNITY_EDITOR
using UnityEditor.Experimental.GraphView;
using UnityEngine;

namespace DialogueSystem
{
    public class StartNodeView : NodeViewBase
    {
        public StartNodeView(DialogNodeDataBase dialogNodeData) : base(dialogNodeData)
        {
            title = "Start";
            
            Port output = GetPortForNode(this, Direction.Output, Port.Capacity.Single);
            output.portName = "output";
            output.name = "0";
            
            output.portColor = Color.green;
            outputContainer.Add(output);
            
            if (DialogNodeData.ChildNode.Count < 1)
            {
                DialogNodeData.ChildNode.Add(null);
            }
        }
    }
}
#endif

EndNode:

#if UNITY_EDITOR
using UnityEditor.Experimental.GraphView;
using UnityEngine;

namespace DialogueSystem
{
    public class EndNodeView : NodeViewBase
    {
        public EndNodeView(DialogNodeDataBase dialogNodeData) : base(dialogNodeData)
        {
            title = "End";
            
            Port input = GetPortForNode(this, Direction.Input, Port.Capacity.Multi);
            input.portName = "input";
            input.portColor = Color.gray;
            
            inputContainer.Add(input);
        }
    }
}
#endif

很簡單,我們?yōu)槲覀兊墓?jié)點創(chuàng)建了上面所描述類型的Port,并把他們分別添加進InputContainner和OutputContainner中。接下來,我們回到GraphView腳本,我們來拓展右鍵菜單,讓我們可以創(chuàng)建出我們的節(jié)點。代碼如下:

        /// <summary>
        /// 菜單點擊時鼠標位置
        /// </summary>
        private Vector2 clickPosition;

        /// <summary>
        /// 節(jié)點點擊事件
        /// </summary>
        public Action<NodeViewBase> OnNodeSelected;

        public static DialogTree treeData = null;

        public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
        {
            base.BuildContextualMenu(evt);
            Debug.Log(evt.mousePosition);

            //將鼠標世界坐標轉(zhuǎn)為視圖本地坐標
            clickPosition = contentViewContainer.WorldToLocal(evt.mousePosition);

            evt.menu.AppendAction("Create StartNode", x =>
            {
                var dialogNodeData = ScriptableObject.CreateInstance<StartNodeData>();
                var nodeView = new StartNodeView(dialogNodeData)
                {
                    //設置點擊事件
                    OnNodeSelected = OnNodeSelected
                };
                nodeView.SetPosition(new Rect(clickPosition, nodeView.GetPosition().size));
                this.AddElement(nodeView);
            });

            evt.menu.AppendAction("Create EndNode", x =>
            {
                var dialogNodeData = ScriptableObject.CreateInstance<EndNodeData>();
                var nodeView = new EndNodeView(dialogNodeData)
                {
                    //設置點擊事件
                    OnNodeSelected = OnNodeSelected
                };
                nodeView.SetPosition(new Rect(clickPosition, nodeView.GetPosition().size));
                this.AddElement(nodeView);
            });
        }

如果我們現(xiàn)在回到Unity打開我們節(jié)點編輯器,右鍵創(chuàng)建節(jié)點,我們會看到兩個節(jié)點都成功的創(chuàng)建了出來。可當我們嘗試連接節(jié)點時,我們會發(fā)現(xiàn)所有的節(jié)點端口都變灰了,我們無法連接任何節(jié)點。這是因為我們還沒有配置節(jié)點的連接規(guī)則,重新回到GraphView腳本,Override GetCompatiblePorts函數(shù)。代碼如下:

        //節(jié)點鏈接規(guī)則
        public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
        {
            return ports.ToList().Where(endPort =>
                endPort.direction != startPort.direction &&
                endPort.node != startPort.node &&
                endPort.portType == startPort.portType
            ).ToList();
        }

連接規(guī)則很簡單,即節(jié)點自己不能連接自己,節(jié)點接口方向不能相同,且只有相同類型的端口可以互相連接。在我們這個系統(tǒng)中,節(jié)點間的連線僅表示數(shù)據(jù)流向,不涉及數(shù)據(jù)類型,項目里所有節(jié)點接口的類型都被我統(tǒng)一為bool類型。

現(xiàn)在我們回到Unity,打開節(jié)點編輯器,創(chuàng)建節(jié)點,連接,這次的效果就符合我們的預期了。

graphview unity,Unity基于GraphView的對話系統(tǒng)設計,unity,編輯器,游戲引擎
現(xiàn)在我們完成的是節(jié)點UI的顯示,也就是說,現(xiàn)階段的節(jié)點是沒有任何實際功能的。我們來繼續(xù)完善Start跟End節(jié)點,使他們與他們對應的Data層對象相關聯(lián)。

首先是創(chuàng)建節(jié)點的部分,我們打開GraphView腳本,增加一個CreateNode方法。代碼如下:

        //確保目錄存在
        private void MakeSureTheFolder()
        {
            //TODO:做成可自行設置的對話資源文件部署
            if (!AssetDatabase.IsValidFolder("Assets/DialogueData/NodeData"))
            {
                AssetDatabase.CreateFolder("Assets", "DialogueData");
                AssetDatabase.CreateFolder("Assets/DialogueData", "NodeData");
            }
        }

        /// <summary>
        /// 新建節(jié)點
        /// </summary>
        /// <param name="type"></param>
        /// <param name="position"></param>
        private void CreateNode(NodeType type, Vector2 position = default)
        {
            if (treeData == null)
            {
                return;
            }

            MakeSureTheFolder();
            NodeViewBase nodeView = null;

            //創(chuàng)建節(jié)點的核心,新增的節(jié)點需要在這里進行創(chuàng)建方式的添加
            switch (type)
            {
                case NodeType.Start:
                {
                    var dialogNodeData = ScriptableObject.CreateInstance<StartNodeData>();
                    dialogNodeData.Path = $"Assets/DialogueData/NodeData/StartData[{dialogNodeData.GetInstanceID()}].asset";
                    EditorUtility.SetDirty(dialogNodeData);

                    AssetDatabase.CreateAsset(dialogNodeData,
                        $"Assets/DialogueData/NodeData/StartData[{dialogNodeData.GetInstanceID()}].asset");

                    nodeView = new StartNodeView(dialogNodeData);
                    break;
                }
                case NodeType.End:
                {
                    var dialogNodeData = ScriptableObject.CreateInstance<EndNodeData>();
                    dialogNodeData.Path = $"Assets/DialogueData/NodeData/EndData[{dialogNodeData.GetInstanceID()}].asset";
                    EditorUtility.SetDirty(dialogNodeData);

                    AssetDatabase.CreateAsset(dialogNodeData,
                        $"Assets/DialogueData/NodeData/EndData[{dialogNodeData.GetInstanceID()}].asset");

                    nodeView = new EndNodeView(dialogNodeData);
                    break;
                }
                default:
                {
                    Debug.LogError("未找到該類型的節(jié)點");
                    break;
                }
            }

            //添加節(jié)點被選擇事件
            nodeView.OnNodeSelected = OnNodeSelected;
            nodeView.SetPosition(new Rect(position, nodeView.GetPosition().size));

            this.AddElement(nodeView);
        }

我們在創(chuàng)建節(jié)點UI之前,都會創(chuàng)建一個對應節(jié)點的DialogNodeDataBase對象,并將其保存為asset文件。而在View中,在創(chuàng)建出節(jié)點UI時,將持有剛創(chuàng)建的DialogNodeDataBase對象。
現(xiàn)在我們創(chuàng)建節(jié)點就可以直接使用CreateNode函數(shù)了,我們可以修改右鍵菜單,使用新的創(chuàng)建節(jié)點方法,修改GraphView中代碼如下:

        /// <summary>
        /// 右鍵菜單
        /// </summary>
        /// <param name="evt"></param>
        public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
        {
            base.BuildContextualMenu(evt);
            Debug.Log(evt.mousePosition);

            //將鼠標世界坐標轉(zhuǎn)為視圖本地坐標
            clickPosition = contentViewContainer.WorldToLocal(evt.mousePosition);

            if (treeData.StartNodeData == null)
            {
                evt.menu.AppendAction("Create StartNode", x => { CreateNode(NodeType.Start, clickPosition); });
            }

            evt.menu.AppendAction("Create EndNode", x => { CreateNode(NodeType.End, clickPosition); });
        }

到這里,我們的單個節(jié)點才總算是相對完整了,但是,我們還沒有處理節(jié)點與節(jié)點間的數(shù)據(jù)連接,在該對話系統(tǒng)的設計中,節(jié)點與節(jié)點間的連接表示的是對話節(jié)點數(shù)據(jù)的讀取順序,Unity在GraphView中提供了一個graphViewChanged事件,該事件可以監(jiān)聽GraphView中的各種變化,包括節(jié)點的移動,連線的創(chuàng)建、刪除等,我們打開GraphView腳本,增加以下代碼:

        /// <summary>
        /// 節(jié)點圖變化事件
        /// </summary>
        /// <param name="graphviewchange"></param>
        /// <returns></returns>
        private GraphViewChange OnGraphViewChanged(GraphViewChange graphviewchange)
        {
            if (graphviewchange.elementsToRemove != null)
            {
                graphviewchange.elementsToRemove.ForEach(elem =>
                {
                    //連線刪除
                    else if (elem is Edge edge)
                    {
                        NodeViewBase parentNodeView = edge.output.node as NodeViewBase;
                        NodeViewBase childNodeView = edge.input.node as NodeViewBase;
                        if (parentNodeView != null && childNodeView != null)
                        {
                            if (int.TryParse(edge.output.name, out int index))
                            {
                                parentNodeView.DialogNodeData.ChildNode[index] = null;
                            }
                            else
                            {
                                Debug.LogError("Node.name(string) to int fail");
                            }
                        }
                    }
                });
            }

            if (graphviewchange.edgesToCreate != null)
            {
                //創(chuàng)建連線
                graphviewchange.edgesToCreate.ForEach(edge =>
                {
                    NodeViewBase parentNodeView = edge.output.node as NodeViewBase;
                    NodeViewBase childNodeView = edge.input.node as NodeViewBase;

                    if (parentNodeView != null && childNodeView != null)
                    {
                        if (int.TryParse(edge.output.name, out int index))
                        {
                            parentNodeView.DialogNodeData.ChildNode[index] = childNodeView.DialogNodeData;
                        }
                        else
                        {
                            Debug.LogError("Node.name(string) to int fail");
                        }
                    }
                });
            }

            return graphviewchange;
        }

然后在初始化GraphView的時候往graphViewChanged事件添加我們的OnGraphViewChanged方法,在GraphView腳本中的DialogGraphView()方法添加以下代碼即可。代碼如下:

            //監(jiān)聽graphView變化事件
            graphViewChanged += OnGraphViewChanged;

創(chuàng)建連線時,我們獲取一下該連線的兩端節(jié)點,并將輸入口的節(jié)點對應的DialogNodeDataBase對象添加進輸出口節(jié)點的DialogNodeDataBase的子對象列表中。當然,在連線刪除的時候,我們也應該同步刪除父節(jié)點子對象列表中的對應子節(jié)點。

現(xiàn)在,節(jié)點間已經(jīng)能夠在數(shù)據(jù)層面上進行連接了,但我們還有一件重要的事情沒有做。如果將節(jié)點編輯器關閉后再打開,我們會發(fā)現(xiàn)節(jié)點編輯器回到了初始狀態(tài),我們的節(jié)點數(shù)據(jù)并沒有在節(jié)點圖中展示。所以接下來我們來實現(xiàn)一下節(jié)點圖的保存與解析。

節(jié)點圖的保存與解析

我們在節(jié)點編輯器里編輯的節(jié)點,最終保存為一個個節(jié)點數(shù)據(jù)文件,還記得我們當時創(chuàng)建的DialogTree類嗎?DialogTree就是專門用來管理節(jié)點數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)類,我們修改CreateNode代碼,在創(chuàng)建節(jié)點的時候?qū)⒐?jié)點對象添加進DialogTree中。打開GraphView腳本,在CreateNode函數(shù)末尾加入代碼,代碼如下:

            //對Start節(jié)點做個特判
            if (nodeView.DialogNodeData.NodeType == NodeType.Start)
            {
                treeData.StartNodeData = nodeView.DialogNodeData;
            }
            else
            {
                treeData.ChildNodeDataList.Add(nodeView.DialogNodeData);
            }

在一個DialogTree中,我們只能有一個StartNode,并且StartNode對象是作為一個特別對待的節(jié)點儲存在DialogTree中的StartNodeData變量中?,F(xiàn)在我們可以更改一下編輯器的打開邏輯,不再是通過菜單欄打開節(jié)點編輯器,而是改成打開DialogTree對象的asset文間來打開節(jié)點編輯器,這樣也可以在打開時獲取到打開的DialogTree對象,從而對該DialogTree與GraphView進行關聯(lián)。打開DialogGraphWindow腳本,編輯代碼如下:

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

namespace LFramework.AI.Kit.DialogueSystem
{
    public class DialogGraphWindow : EditorWindow
    {
        // 移除原先的打開方式
        // [MenuItem("Window/UI Toolkit/DialogueView")]
        // public static void ShowExample()
        // {
        //
        //     DialogueView wnd = GetWindow<DialogueView>();
        //     wnd.titleContent = new GUIContent("DialogueView");
        // }

        private DialogGraphView _graphView = null;

        // 打開DialogTree資源時觸發(fā)
        [OnOpenAsset(1)]
        public static bool OnOpenAsssets(int id, int line)
        {
            if (EditorUtility.InstanceIDToObject(id) is DialogTree tree)
            {
                //打開不同DialogTree文件
                if (DialogGraphView.treeData != tree)
                {
                    Debug.Log(DialogGraphView.treeData);
                    DialogGraphView.treeData = tree;

                    //判斷窗口是否打開
                    if (HasOpenInstances<DialogGraphWindow>())
                    {
                        CloseEditorWindow();
                    }

                    //大大大大大坑!新版本unity不自動在磁盤上應用資源更新,必須先給目標物體打上Dirty標記
                    EditorUtility.SetDirty(tree);
                }

                DialogGraphWindow wnd = GetWindow<DialogGraphWindow>();
                wnd.titleContent = new GUIContent("DialogueView");
                
                return true;
            }
            return false;
        }

        public static void CloseEditorWindow()
        {
            DialogGraphWindow wnd = GetWindow<DialogGraphWindow>();
            wnd.Close();
        }

        public void CreateGUI()
        {
            var visualTree =
                AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
                    "Assets/DialogueSystem/NodeEditor/EditorWindow/DialogGraphWindow.uxml");
            visualTree.CloneTree(rootVisualElement);

            _graphView = rootVisualElement.Q<DialogGraphView>("DialogGraphView");

            _inspectorView = rootVisualElement.Q<InspectorView>("InspectorView");

            var saveButton = rootVisualElement.Q<ToolbarButton>("SaveButton");
            saveButton.clicked += OnSaveButtonClicked;
        }

        //保存資源文件
        private void OnSaveButtonClicked()
        {
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            Debug.Log("Save");
        }

        private void OnDestroy()
        {
            //對象銷毀之前記得保存一下,保險
            AssetDatabase.SaveAssets();
        }
    }
}
#endif

打開GraphView腳本,添加一個靜態(tài)變量用于儲存當前DialogTree。代碼如下:

        public static DialogTree treeData = null;

回到Unity,雙擊DialogTree.asset文件,可以看到,節(jié)點編輯器被成功打開。但是,我們還是沒有在編輯器里看到任何節(jié)點,那是因為我們還沒有對DialogTree中的數(shù)據(jù)進行處理。既然在DialogTree里已經(jīng)有了我們整個對話樹的節(jié)點數(shù)據(jù),那我們是不是就可以在編輯器打開的時候,讀取DialogTree中的節(jié)點數(shù)據(jù),并按照所儲存的數(shù)據(jù)對節(jié)點進行復原。
我們打開GraphView腳本,添加以下代碼:

        /// <summary>
        /// 臨時字典,用于初始化節(jié)點圖的,用完記得把內(nèi)存釋放掉
        /// </summary>
        private Dictionary<DialogNodeDataBase, NodeViewBase> NodeDirt;

        /// <summary>
        /// 重置節(jié)點圖
        /// </summary>
        public void ResetNodeView()
        {
            if (treeData != null)
            {
                //初始化字典
                NodeDirt = new Dictionary<DialogNodeDataBase, NodeViewBase>();
                var nodeData = treeData.ChildNodeDataList;

                //檢查StartNode是否存在
                if (treeData.StartNodeData == null)
                {
                    CreateNode(NodeType.Start);
                }
                else
                {
                    RecoveryNode(treeData.StartNodeData);
                }

                //恢復節(jié)點
                foreach (var node in nodeData)
                {
                    RecoveryNode(node);
                }

                //清除字典,后面會講到為什么
                NodeDirt.Clear();
            }
        }

上面的代碼中,我們提供了一個復原節(jié)點圖的函數(shù),我們在這里遍歷DialongTree記錄的所有節(jié)點,并依次還原所有的節(jié)點跟連線。

首先還原節(jié)點,在GraphView增加以下代碼:

        /// <summary>
        /// 恢復節(jié)點
        /// </summary>
        /// <param name="DialogNodeData"></param>
        private void RecoveryNode(DialogNodeDataBase DialogNodeData)
        {
            if (DialogNodeData == null)
            {
                return;
            }

            NodeViewBase nodeView = null;
            //恢復節(jié)點的核心部分,新增的節(jié)點需要在這里進行恢復方式的添加
            switch (DialogNodeData.NodeType)
            {
                case NodeType.Start:
                {
                    nodeView = new StartNodeView(DialogNodeData);
                    break;
                }
                case NodeType.End:
                {
                    nodeView = new EndNodeView(DialogNodeData);
                    break;
                }
                default:
                {
                    Debug.LogError("未找到該類型的節(jié)點");
                    break;
                }
            }

            nodeView.OnNodeSelected = OnNodeSelected;
            nodeView.SetPosition(new Rect(DialogNodeData.Position, nodeView.GetPosition().size));
            this.AddElement(nodeView);
        }

在RecoveryNode方法中,我們根據(jù)所傳的DialogDataBase對象,創(chuàng)建出對應的NodeViewBase。這里跟創(chuàng)建節(jié)點的CreateNode方法不同,我們不需要再創(chuàng)建出節(jié)點的Data層,我們只需要根據(jù)已有的Data,復原出節(jié)點的View層就可以了。

在復原節(jié)點的時候,我們需要讓節(jié)點回到他們原本的位置,所以現(xiàn)在我們需要記錄節(jié)點的位置信息。還記得我們之前用來處理節(jié)點圖變化的OnGraphViewChanged函數(shù)嗎?我們直接在那里監(jiān)聽節(jié)點的位置變化就行了。修改OnGraphViewChanged函數(shù),在末尾加入以下代碼:

            //遍歷節(jié)點,記錄節(jié)點位置信息
            nodes.ForEach(node =>
            {
                NodeViewBase nodeView = node as NodeViewBase;
                if (nodeView != null && nodeView.DialogNodeData != null)
                {
                    nodeView.DialogNodeData.Position = nodeView.GetPosition().position;
                }
            });

既然記錄了節(jié)點位置,我們不妨也記錄一下節(jié)點視圖的位置與縮放,在打開編輯器的時候也進行復原,這樣我們就能保證用戶下一次打開編輯器時節(jié)點圖與上次關閉保持一致。剛好Unity在GraphView中提供了一個viewTransformChanged事件,跟GraphViewChanged一樣,我們在GraphView腳本里添加OnViewTransformChanged函數(shù),代碼如下:

        /// <summary>
        /// graphView的Transform發(fā)生變化時觸發(fā)
        /// </summary>
        /// <param name="graphView"></param>
        private void OnViewTransformChanged(GraphView graphView)
        {
            if (treeData != null)
            {
                //保存視圖Transform信息
                treeData.GraphViewData.Position = contentViewContainer.transform.position;
                treeData.GraphViewData.Scale = contentViewContainer.transform.scale;
            }
        }

在DialogGraphView()中監(jiān)聽事件:

            //監(jiān)聽視圖Transform變化事件
            viewTransformChanged += OnViewTransformChanged;

現(xiàn)在,節(jié)點的復原已經(jīng)完成了,且GraphView的位置跟縮放也將與保存時的狀態(tài)保存一致。

但距離我們解析復原節(jié)點圖完成還剩下最后一個關鍵部分,復原節(jié)點端口間的連線。根據(jù)這套對話系統(tǒng)的設計,我們對話節(jié)點的輸入端口可以與多個端口連接,而輸出端口只能與一個端口連接。在每一個節(jié)點的DialogNodeDataBase中,都有一個ChildNode列表用來記錄該節(jié)點所連的子節(jié)點。因此我們可以先遍歷圖里的所有節(jié)點,再分別遍歷節(jié)點里面的ChildNode,將每一個子節(jié)點的輸入端口與父節(jié)點對應的輸出端口相連就可以了。我們打開GraphView腳本,增加以下代碼:

        /// <summary>
        /// 鏈接兩個點
        /// </summary>
        /// <param name="_outputPort">outputPort</param>
        /// <param name="_inputPort">inputPort</param>
        private void AddEdgeByPorts(Port _outputPort, Port _inputPort)
        {
            //雖然是不可能發(fā)生,但還是保守一點
            if (_outputPort.node == _inputPort.node)
            {
                return;
            }

            Edge tempEdge = new Edge()
            {
                input = _inputPort,
                output = _outputPort
            };
            tempEdge.input.Connect(tempEdge);
            tempEdge.output.Connect(tempEdge);
            Add(tempEdge);
        }

        /// <summary>
        /// 恢復節(jié)點連線
        /// </summary>
        private void RecoveryEdge(DialogNodeDataBase DialogNodeData)
        {
            if (DialogNodeData.ChildNode == null)
            {
                return;
            }

            for (int i = 0; i < DialogNodeData.ChildNode.Count; i++)
            {
                //沒連就跳過
                if (DialogNodeData.ChildNode[i] == null)
                {
                    continue;
                }

                Port _output = NodeDirt[DialogNodeData].outputContainer[i].Q<Port>();
                Port _input = NodeDirt[DialogNodeData.ChildNode[i]].inputContainer[0].Q<Port>();

                AddEdgeByPorts(_output, _input);
            }
        }

上面代碼我們添加了連接兩個Port的函數(shù),用的是Unity提供的API,很簡單。接下來我們添加了一個能根據(jù)DialogNodeDataBase對象,遍歷其子節(jié)點并復原連線的函數(shù)。由于我們的連接操作是在View層的事情,我們需要持有根據(jù)DialogNodeDataBase對象所創(chuàng)建的NodeViewBase對象。為了避免耦合,且我們的復原操作僅在DialogTree打開時進行一次,所以這里我們使用了一個臨時的字典來緩存對象間互相持有的關系。字典的填充在復原節(jié)點的時候進行,我們修改RecoveryNode函數(shù),在函數(shù)末尾加入以下代碼:

		NodeDirt.Add(DialogNodeData, nodeView);

現(xiàn)在,節(jié)點圖的連線部分也完成了,我們來做最后的收尾工作。我們將在GraphView腳本中創(chuàng)建一個靜態(tài)變量,用于實現(xiàn)簡單的單例模式。代碼如下:

        public static DialogGraphView Instance;

        public DialogGraphView()
        {
            //以上代碼省略,在DialogGraphView末尾添加下面代碼即可

            //簡單的單例模式
            Instance = this;
        }

修改ResetNodeView方法,增加復原節(jié)點連線的邏輯,代碼如下:

        /// <summary>
        /// 重置節(jié)點圖
        /// </summary>
        public void ResetNodeView()
        {
            if (treeData != null)
            {
                //初始化字典
                NodeDirt = new Dictionary<DialogNodeDataBase, NodeViewBase>();
                var nodeData = treeData.ChildNodeDataList;

                //檢查StartNode是否存在
                if (treeData.StartNodeData == null)
                {
                    CreateNode(NodeType.Start);
                }
                else
                {
                    RecoveryNode(treeData.StartNodeData);
                }

                //恢復節(jié)點
                foreach (var node in nodeData)
                {
                    RecoveryNode(node);
                }

                //恢復節(jié)點邊
                RecoveryEdge(treeData.StartNodeData);
                foreach (var node in nodeData)
                {
                    RecoveryEdge(node);
                }

                //清除字典
                NodeDirt.Clear();
            }
        }

現(xiàn)在我們打開DialogGraphWindow腳本,在末尾調(diào)用ResetNodeView()方法。代碼如下:

        public void CreateGUI()
        {
            var visualTree =
                AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
                    "Assets/DialogueSystem/NodeEditor/EditorWindow/DialogGraphWindow.uxml");
            visualTree.CloneTree(rootVisualElement);

            _graphView = rootVisualElement.Q<DialogGraphView>("DialogGraphView");

            _inspectorView = rootVisualElement.Q<InspectorView>("InspectorView");

            var saveButton = rootVisualElement.Q<ToolbarButton>("SaveButton");
            saveButton.clicked += OnSaveButtonClicked;

            //初始化節(jié)點圖
            DialogGraphView.Instance.ResetNodeView();
        }

現(xiàn)在我們打開DialogTree,可以看到,節(jié)點圖完美復原。到此,我們完成了一個基本的對話系統(tǒng)節(jié)點編輯器。但現(xiàn)在我們只有Start跟End兩個節(jié)點,這顯然還不能算是一個對話系統(tǒng)。所以接下來,我們要做的就是拓展更多節(jié)點,完善我們的對話系統(tǒng) 。

拓展更多節(jié)點

在前面的代碼實現(xiàn)中,我們已經(jīng)實現(xiàn)了一個節(jié)點View層與Data層的基類,NodeViewBase與DialogNodeDataBase。那么對于拓展節(jié)點,無非就是繼承這兩個類,并實現(xiàn)不同節(jié)點特有的功能而已。

所以現(xiàn)在我們來規(guī)劃一下要實現(xiàn)的節(jié)點,本次我們先拓展兩個基礎的對話系統(tǒng)節(jié)點:

  • 順序?qū)υ捁?jié)點:能夠按照從上到下的順序輸出節(jié)點內(nèi)的對話語句
  • 隨機對話節(jié)點:能從節(jié)點內(nèi)對話數(shù)據(jù)中隨機選出一句進行輸出。

我們打開NodeType枚舉,增加新的節(jié)點類型,代碼如下:

    public enum NodeType
    {
        Start,
        RandomDialogNode,
        SequentialDialogNode,
        End,
    }

首先我們來拓展順序?qū)υ捁?jié)點:
Data層:
新建一個SequentialDialogNodeData腳本,代碼如下:

namespace DialogueSystem
{
    public class SequentialDialogNodeData : DialogNodeDataBase
    {
        public override NodeType NodeType => NodeType.SequentialDialogNode;
    }
}

View層:
新建一個SequentialDialogNodeView腳本,代碼如下:

#if UNITY_EDITOR
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

namespace DialogueSystem
{
    public class SequentialDialogNodeView : NodeViewBase
    {
        private int nextIndex = 0;
        public SequentialDialogNodeView(DialogNodeDataBase dialogNodeData) : base(dialogNodeData)
        {
            title = "SequentialDialogNode";
            
            Port input = GetPortForNode(this, Direction.Input, Port.Capacity.Multi);
            Port output = GetPortForNode(this, Direction.Output, Port.Capacity.Single);
            input.portName = "Input";
            input.portColor = Color.yellow;
            output.portName = "Output";
            output.portColor = Color.yellow;
            output.name = "0";
            
            inputContainer.Add(input);
            outputContainer.Add(output);
            
            //工具條
            Toolbar toolbar = new Toolbar();
            ToolbarButton addButton = new ToolbarButton(AddTextField)
            {
                text = "Add"
            };
            ToolbarButton delButton = new ToolbarButton(DeleteTextField)
            {
                text = "Del"
            };
            toolbar.Add(addButton);
            toolbar.Add(delButton);
            
            toolbar.style.flexDirection = FlexDirection.RowReverse;
            contentContainer.Add(toolbar);
            
            while (nextIndex < DialogNodeData.OutputItems.Count)
            {
                AddTextField();
            }
            
            if (DialogNodeData.ChildNode.Count < 1)
            {
                DialogNodeData.ChildNode.Add(null);
            }
        }
        
        public void AddTextField()
        {
            if (DialogNodeData.OutputItems.Count < nextIndex + 1)
            {
                DialogNodeData.OutputItems.Add(default);
            }

			//拿了個沒有功能的按鍵當背景,這個按鍵并沒有什么實質(zhì)性的功能哈哈
            Button background = new Button();

            TextField textField = new TextField();
            textField.name = nextIndex.ToString();
            textField.style.minWidth = 160;
            //初始化
            textField.SetValueWithoutNotify(DialogNodeData.OutputItems[nextIndex]);

            textField.RegisterValueChangedCallback(evt =>
            {
                if (int.TryParse(textField.name, out int index))
                {
                    DialogNodeData.OutputItems[index] = evt.newValue;
                }
                else
                {
                    Debug.LogError("textField.name(string) to int fail");
                }
            });

            background.Add(textField);
            extensionContainer.Add(background);
            RefreshExpandedState();

            nextIndex++;
        }
        
        public void DeleteTextField()
        {
            if (nextIndex > 0)
            {
                nextIndex--;
                
                DialogNodeData.OutputItems.RemoveAt(DialogNodeData.OutputItems.Count - 1);
                extensionContainer.Remove(extensionContainer[nextIndex]);
            }
        }
    }
}
#endif

接下來是隨機對話節(jié)點:
Data層:
新建一個RandomDialogNodeData腳本,代碼如下:

namespace DialogueSystem
{
    public class RandomDialogNodeData : DialogNodeDataBase
    {
        public override NodeType NodeType => NodeType.RandomDialogNode;
    }
}

View層:
新建一個RandomDialogNodeView腳本,代碼如下:

#if UNITY_EDITOR
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

namespace DialogueSystem
{
    public class RandomDialogNodeView : NodeViewBase
    {
        private int nextIndex = 0;

        public RandomDialogNodeView(DialogNodeDataBase dialogNodeData) : base(dialogNodeData)
        {
            title = "RandomDialogNode";

            Port input = GetPortForNode(this, Direction.Input, Port.Capacity.Multi);
            Port output = GetPortForNode(this, Direction.Output, Port.Capacity.Single);
            input.portName = "Input";
            input.portColor = Color.magenta;
            output.portName = "Output";
            output.portColor = Color.magenta;
            output.name = "0";

            inputContainer.Add(input);
            outputContainer.Add(output);
            
            //工具條
            Toolbar toolbar = new Toolbar();
            ToolbarButton addButton = new ToolbarButton(AddTextField)
            {
                text = "Add"
            };
            ToolbarButton delButton = new ToolbarButton(DeleteTextField)
            {
                text = "Del"
            };
            toolbar.Add(addButton);
            toolbar.Add(delButton);
            
            toolbar.style.flexDirection = FlexDirection.RowReverse;
            contentContainer.Add(toolbar);

            while (nextIndex < DialogNodeData.OutputItems.Count)
            {
                AddTextField();
            }
            
            //加個判斷,不然每開一次創(chuàng)一個
            if (DialogNodeData.ChildNode.Count < 1)
            {
                DialogNodeData.ChildNode.Add(null);
            }
        }
        
        public void AddTextField()
        {
            if (DialogNodeData.OutputItems.Count < nextIndex + 1)
            {
                DialogNodeData.OutputItems.Add(default);
            }

			//拿了個沒有功能的按鍵當背景,這個按鍵并沒有什么實質(zhì)性的功能哈哈
            Button background = new Button();

            TextField textField = new TextField();
            textField.name = nextIndex.ToString();
            textField.style.minWidth = 160;
            //初始化
            textField.SetValueWithoutNotify(DialogNodeData.OutputItems[nextIndex]);

            textField.RegisterValueChangedCallback(evt =>
            {
                if (int.TryParse(textField.name, out int index))
                {
                    DialogNodeData.OutputItems[index] = evt.newValue;
                }
                else
                {
                    Debug.LogError("textField.name(string) to int fail");
                }
            });

            background.Add(textField);
            extensionContainer.Add(background);
            RefreshExpandedState();

            nextIndex++;
        }
        
        public void DeleteTextField()
        {
            if (nextIndex > 0)
            {
                nextIndex--;
                
                DialogNodeData.OutputItems.RemoveAt(DialogNodeData.OutputItems.Count - 1);
                extensionContainer.Remove(extensionContainer[nextIndex]);
            }
        }
    }
}
#endif

完成之后,我們打開GraphView腳本,編輯CreateNode、RecoveryNode方法,使節(jié)點能夠在GraphView中創(chuàng)建,代碼如下:

        /// <summary>
        /// 新建節(jié)點
        /// </summary>
        /// <param name="type"></param>
        /// <param name="position"></param>
        private void CreateNode(NodeType type, Vector2 position = default)
        {
            if (treeData == null)
            {
                return;
            }

            MakeSureTheFolder();
            NodeViewBase nodeView = null;

            //創(chuàng)建節(jié)點的核心,新增的節(jié)點需要在這里進行創(chuàng)建方式的添加
            switch (type)
            {
                case NodeType.Start:
                {
                    var dialogNodeData = ScriptableObject.CreateInstance<StartNodeData>();
                    dialogNodeData.Path =
                        $"Assets/DialogueData/NodeData/StartData[{dialogNodeData.GetInstanceID()}].asset";
                    EditorUtility.SetDirty(dialogNodeData);

                    AssetDatabase.CreateAsset(dialogNodeData,
                        $"Assets/DialogueData/NodeData/StartData[{dialogNodeData.GetInstanceID()}].asset");

                    nodeView = new StartNodeView(dialogNodeData);
                    break;
                }
                case NodeType.RandomDialogNode:
                {
                    var dialogNodeData = ScriptableObject.CreateInstance<RandomDialogNodeData>();
                    dialogNodeData.Path =
                        $"Assets/DialogueData/NodeData/RandomDialogData[{dialogNodeData.GetInstanceID()}].asset";
                    EditorUtility.SetDirty(dialogNodeData);

                    AssetDatabase.CreateAsset(dialogNodeData,
                        $"Assets/DialogueData/NodeData/RandomDialogData[{dialogNodeData.GetInstanceID()}].asset");

                    nodeView = new RandomDialogNodeView(dialogNodeData);
                    break;
                }
                case NodeType.SequentialDialogNode:
                {
                    var dialogNodeData = ScriptableObject.CreateInstance<SequentialDialogNodeData>();
                    dialogNodeData.Path =
                        $"Assets/DialogueData/NodeData/SequentialDialogData[{dialogNodeData.GetInstanceID()}].asset";
                    EditorUtility.SetDirty(dialogNodeData);

                    AssetDatabase.CreateAsset(dialogNodeData,
                        $"Assets/DialogueData/NodeData/SequentialDialogData[{dialogNodeData.GetInstanceID()}].asset");

                    nodeView = new SequentialDialogNodeView(dialogNodeData);
                    break;
                }
                case NodeType.End:
                {
                    var dialogNodeData = ScriptableObject.CreateInstance<EndNodeData>();
                    dialogNodeData.Path =
                        $"Assets/DialogueData/NodeData/EndData[{dialogNodeData.GetInstanceID()}].asset";
                    EditorUtility.SetDirty(dialogNodeData);

                    AssetDatabase.CreateAsset(dialogNodeData,
                        $"Assets/DialogueData/NodeData/EndData[{dialogNodeData.GetInstanceID()}].asset");

                    nodeView = new EndNodeView(dialogNodeData);
                    break;
                }
                default:
                {
                    Debug.LogError("未找到該類型的節(jié)點");
                    break;
                }
            }

            //添加節(jié)點被選擇事件
            nodeView.OnNodeSelected = OnNodeSelected;
            nodeView.SetPosition(new Rect(position, nodeView.GetPosition().size));

            //對Start節(jié)點做個特判
            if (nodeView.DialogNodeData.NodeType == NodeType.Start)
            {
                treeData.StartNodeData = nodeView.DialogNodeData;
            }
            else
            {
                treeData.ChildNodeDataList.Add(nodeView.DialogNodeData);
            }

            this.AddElement(nodeView);
        }

        /// <summary>
        /// 恢復節(jié)點
        /// </summary>
        /// <param name="DialogNodeData"></param>
        private void RecoveryNode(DialogNodeDataBase DialogNodeData)
        {
            if (DialogNodeData == null)
            {
                return;
            }

            NodeViewBase nodeView = null;
            //恢復節(jié)點的核心部分,新增的節(jié)點需要在這里進行恢復方式的添加
            switch (DialogNodeData.NodeType)
            {
                case NodeType.Start:
                {
                    nodeView = new StartNodeView(DialogNodeData);
                    break;
                }
                case NodeType.RandomDialogNode:
                {
                    nodeView = new RandomDialogNodeView(DialogNodeData);
                    break;
                }
                case NodeType.SequentialDialogNode:
                {
                    nodeView = new SequentialDialogNodeView(DialogNodeData);
                    break;
                }
                case NodeType.End:
                {
                    nodeView = new EndNodeView(DialogNodeData);
                    break;
                }
                default:
                {
                    Debug.LogError("未找到該類型的節(jié)點");
                    break;
                }
            }

            nodeView.OnNodeSelected = OnNodeSelected;
            nodeView.SetPosition(new Rect(DialogNodeData.Position, nodeView.GetPosition().size));
            NodeDirt.Add(DialogNodeData, nodeView);

            this.AddElement(nodeView);
        }

最后,修改右鍵菜單,代碼如下:

        /// <summary>
        /// 右鍵菜單
        /// </summary>
        /// <param name="evt"></param>
        public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
        {
            base.BuildContextualMenu(evt);
            Debug.Log(evt.mousePosition);

            //將鼠標世界坐標轉(zhuǎn)為視圖本地坐標
            clickPosition = contentViewContainer.WorldToLocal(evt.mousePosition);

            if (treeData.StartNodeData == null)
            {
                evt.menu.AppendAction("Create StartNode", x => { CreateNode(NodeType.Start, clickPosition); });
            }

            evt.menu.AppendAction("Create RandomDialogNode",
                x => { CreateNode(NodeType.RandomDialogNode, clickPosition); });
            evt.menu.AppendAction("Create SequentialDialogNode",
                x => CreateNode(NodeType.SequentialDialogNode, clickPosition));
            evt.menu.AppendAction("Create EndNode", x => { CreateNode(NodeType.End, clickPosition); });
        }

現(xiàn)在我們回到Unity,隨便打開一個節(jié)點編輯器,創(chuàng)建我們的新節(jié)點試試??梢钥吹?,節(jié)點正常被創(chuàng)建了出來,并且我們在節(jié)點里面編輯數(shù)據(jù),節(jié)點對應的DialogNodeDataBase對象中的數(shù)據(jù)也會同步編輯。

graphview unity,Unity基于GraphView的對話系統(tǒng)設計,unity,編輯器,游戲引擎

到這里,我們的對話系統(tǒng)的對話數(shù)據(jù)跟節(jié)點編輯器部分就已經(jīng)完成了。我們完成了兩個基本的系統(tǒng)流程節(jié)點,還有兩個用于對話控制的對話節(jié)點。根據(jù)我們的系統(tǒng)架構(gòu),我們還可以自由拓展出更多的自定義節(jié)點,比如選擇對話節(jié)點,事件節(jié)點等等。在編輯器中我們實現(xiàn)了利用節(jié)點圖來編輯對話數(shù)據(jù)文件的功能,而在數(shù)據(jù)文件中我們只讓它負責了數(shù)據(jù)處理的相關邏輯。具體的對話邏輯我們并不是在數(shù)據(jù)類中實現(xiàn),這樣的設計不僅實現(xiàn)了解耦,還實現(xiàn)了模塊化,便于維護、拓展新的節(jié)點模塊。下一節(jié),我們將實現(xiàn)對話系統(tǒng)的的邏輯處理部分,以及能掛在Unity GameObject上的Mono對話系統(tǒng)組件。文章來源地址http://www.zghlxwxcb.cn/news/detail-668012.html

到了這里,關于Unity 基于GraphView的對話系統(tǒng)設計(一)對話數(shù)據(jù)與節(jié)點編輯器的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權(quán),不承擔相關法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • [Unity] GraphView 可視化節(jié)點的事件行為樹(二) UI Toolkit介紹,制作事件行為樹的UI

    [Unity] GraphView 可視化節(jié)點的事件行為樹(二) UI Toolkit介紹,制作事件行為樹的UI

    目錄 前文 UI Toolkit 介紹 制作事件行為樹的UI界面 GameObject關聯(lián)編輯器窗口 [Unity] 使用GraphView實現(xiàn)一個可視化節(jié)點的事件行為樹系統(tǒng)(序章/Github下載)_Sugarzo的博客-CSDN博客_unity graphview [Unity] GraphView 可視化節(jié)點的事件行為樹(一) Runtime Node_Sugarzo的博客-CSDN博客 ????????在上一

    2024年02月03日
    瀏覽(20)
  • [Unity] 實現(xiàn)ScriptableObject數(shù)據(jù)同步Excel表格(對話系統(tǒng)數(shù)據(jù)管理,C# ExcelNPOI)

    [Unity] 實現(xiàn)ScriptableObject數(shù)據(jù)同步Excel表格(對話系統(tǒng)數(shù)據(jù)管理,C# ExcelNPOI)

    ????????在制作游戲中需要管理各種各樣的項目資源,其中游戲中的劇情文字也是一種需要管理的資源。自己剛開始接觸游戲開發(fā)的時候,第一次看MStudio里面的對話系統(tǒng)教學,只講了怎么寫腳本同步UI的設置,并沒有講有什么方式去管理這些對話數(shù)據(jù),視頻里拿的是txt來演

    2024年02月10日
    瀏覽(69)
  • Unity—對話系統(tǒng)&&GalGame游戲文字對話制作

    Unity—對話系統(tǒng)&&GalGame游戲文字對話制作

    每日一句:人間總有一兩,填我十萬八千夢 目錄 對話系統(tǒng) 文本逐字打印功能 GalGame游戲(美少女游戲)文字對話 被觸發(fā)物體(掛載腳本)下UI,先不激活 public ? class ? TalkButton ?: MonoBehaviour { ???? public ?GameObject tipshow; //提示UI ???? public ?GameObject talkUI; //對話UI ????

    2023年04月22日
    瀏覽(31)
  • Unity簡單對話系統(tǒng)實現(xiàn)

    Unity簡單對話系統(tǒng)實現(xiàn)

    思路 :將每段對話分為一個單獨的對話模塊,每段對話模塊擁有自己獨有的ID和幾句單獨的對話,每段對話中自己定義是否含有選擇,每個選擇擁有與對話模塊對應的相同ID(自己決定選擇后跳轉(zhuǎn)的對話模塊),點擊后跳轉(zhuǎn)到對應對話模塊,最后有一個用于管理所有對話模塊

    2024年04月28日
    瀏覽(25)
  • Unity下的簡單DialogSystem對話系統(tǒng)實現(xiàn)(支持多個對話樹)

    Unity下的簡單DialogSystem對話系統(tǒng)實現(xiàn)(支持多個對話樹)

    ? GitHub 鏈接 一個簡小但是足夠滿足一定基礎的對話系統(tǒng)。 初始設置步驟: 將package文件夾中的DialogSystem2添加到項目之中。 在Hierarchy窗口面板中創(chuàng)建一個空物體(建議命名成DialogSystem),添加代碼 DialogSystemManager 和 DialogMissionEventHandler ,并將 DialogMissionEventHandler 拖拽到 DialogS

    2024年02月03日
    瀏覽(32)
  • Unity:美觀通用易擴展的對話系統(tǒng)

    Unity:美觀通用易擴展的對話系統(tǒng)

    unity:通用美觀可快進的對話系統(tǒng) 素材下載地址 素材展示: 繪制地形 關于TileMap的使用,這里就不再過多介紹了,關于TileMap的介紹有很多 我們創(chuàng)建一個簡單的地形如下: 繪制對話框,效果如下: 配置人物動畫,主要是奔跑動畫 實現(xiàn)簡單的控制人物移動 新建腳本實現(xiàn)簡單的

    2024年01月25日
    瀏覽(26)
  • Unity通用Buff系統(tǒng),Buff編輯器(1,理論篇)(技能系統(tǒng)之Buff篇)

    Unity通用Buff系統(tǒng),Buff編輯器(1,理論篇)(技能系統(tǒng)之Buff篇)

    @TOC Buff系統(tǒng)是指給玩家或NPC提供臨時的增益效果的一種機制。它可以通過提升玩家角色的能力、增加屬性值、提供額外技能或提供其他有益效果來改變游戲的動態(tài)。以下是一些關鍵的Buff系統(tǒng)設計方面: Buff類型:Buff的類型可以包括增加攻擊力、防御力、生命恢復速度、移動速

    2024年04月17日
    瀏覽(19)
  • Unity Fungus插件的對話系統(tǒng)簡單使用

    Unity Fungus插件的對話系統(tǒng)簡單使用

    Fungus是Unity免費的一款開源的插件,它可以無代碼的實現(xiàn)玩家與NPC之間的對話,對于Fungus這個插件,我今天說一說我對它的看法以及一些簡單的運用和簡單的代碼功能的實現(xiàn)。 這里需要導入Fungus插件,插件導完之后會出現(xiàn)Tools/Fungus ? SayDialog模板使用,Menu菜單選項,Character玩

    2024年02月09日
    瀏覽(28)
  • 【機組組合】基于數(shù)據(jù)驅(qū)動的模型預測控制電力系統(tǒng)機組組合優(yōu)化【IEEE24節(jié)點】(Matlab代碼實現(xiàn))

    【機組組合】基于數(shù)據(jù)驅(qū)動的模型預測控制電力系統(tǒng)機組組合優(yōu)化【IEEE24節(jié)點】(Matlab代碼實現(xiàn))

    ???????? 歡迎來到本博客 ???????? ??博主優(yōu)勢: ?????? 博客內(nèi)容盡量做到思維縝密,邏輯清晰,為了方便讀者。 ?? 座右銘: 行百里者,半于九十。 ?????? 本文目錄如下: ?????? 目錄 ??1 概述 ??2 運行結(jié)果 2.1 UC_original ?2.2 UC_compact 2.3 SCi結(jié)果? ??

    2024年02月07日
    瀏覽(95)
  • Unity編輯擴展:功能篇之Json數(shù)據(jù)編輯器

    Unity編輯擴展:功能篇之Json數(shù)據(jù)編輯器

    前言 編輯器擴展算是比較純粹的功能開發(fā),基本沒有什么理論知識,都是一些 Unity 相關接口的使用與數(shù)據(jù)類型的設計操作等。在本篇文章主要的文字描述基本都是在做代碼解釋,為了使內(nèi)容接受度更高,我會盡量描述到代碼結(jié)構(gòu)中的每個細節(jié)。如果有對此不太了解又很感興

    2024年02月06日
    瀏覽(25)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包