對話系統(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)以下畫面:
我們可以將其命名為DialogGraphWindow,點擊創(chuàng)建,Unity會自動為我們創(chuàng)建文件。
創(chuàng)建完成后,首先是編輯器的主體部分,雙擊創(chuàng)建好的DialogGraphWindow.uxml文件,Unity會自動打開 UIToolkit 中的 UIBuilder,在該窗口中,我們可以自定義我們的窗口布局,下面是本對話系統(tǒng)的UI布局:
布局結(jié)構(gòu)很簡單,值得注意的是,我們需要在面板的Hierarchy窗口中點擊.uxml文件,然后在Inspector窗口中勾選Editor Extension Authoring選項,這樣我們才可以在編輯模式下使用編輯器UI組件,如下圖:
其中的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é)點,連接,這次的效果就符合我們的預期了。
現(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ù)也會同步編輯。
文章來源:http://www.zghlxwxcb.cn/news/detail-668012.html
到這里,我們的對話系統(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)!