在製作 RPG 甚至 AVG 的時候,我們要涉及大量的文字編輯,這個時候不視覺化的介面會大大提升專案的維護成本(非常好指令碼,使我 AVG 專案崩潰),所以我們需要自己建立一個文字編輯介面。
而在涉及對話的時候,這個叫做對話樹的結構具有不錯的性質,對話樹是一個簡單的多叉樹結構,但節點被分為 CP 對話和 PL 對話,這樣我們就能比較方便的依靠節點來推出 UI,下面我們就來細說一下如何實現視覺化編輯對話樹。
首先對於一個對話樹,我們採用兩個指令碼來實現,一個是節點指令碼,存放了每個對話節點的資訊,比如:
· 文字:此時對話中顯示的文字
· 對話者:這個節點中對話的發起者
· 出度:這個節點連線的下一個節點
而為了實現視覺化編輯,我們還另外儲存這些資訊:
· 節點的 Rect :記錄節點在編輯介面中的位置和長寬
· 節點的 Texture2D :記錄節點的背景樣式,這樣對眼睛比較友好,而且易於維護節點中的對話者
· isChange :節點資訊是否被改變,這樣我們只在節點大小改變的時候重新繪製一次 Texture2D
那麼節點的程式碼如下
namespace Dia.Dialogue
{
public enum Speaker
{
Player,
Npc
}
public class DialogueNode : ScriptableObject
{
[TextArea(5,50)]
public string text;
public Texture2D texture;
public Speaker speaker = Speaker.Npc;
[HideInInspector] public bool isChange = false;
[HideInInspector] public Rect pos = new Rect(0, 0, 180, 90);
[HideInInspector] public List<string> nxt = new List<string>();
private void OnValidate() => isChange = true;
}
}
另一個則是文字庫,用來返回任一文字節點的資訊,以及維護文字節點的建立與刪除,在文字庫中我們需要實現以下功能:
· 查詢:利用文字節點 .name 來獲取文字節點
· 獲得根節點:便於後續對話的運作
· 獲得所有節點:獲取文字庫中的所有節點
· 獲取所有後代:獲取一個節點的所有後代,方便對話選項的建立和 CP 隨機對話
· 建立與刪除節點:在視覺化編輯介面中建立和刪除節點,在建立或刪除之後要過載查詢字典和處理節點的後代
那麼程式碼如下
namespace Dia.Dialogue
{
[CreateAssetMenu(fileName = "New Dialogue", menuName = "New Dialogue", order = 0)]
public class Dialogue : ScriptableObject
{
[SerializeField] private List<DialogueNode> nodes = new List<DialogueNode>();
private Dictionary<string, DialogueNode> NodeLookUp =new Dictionary<string, DialogueNode>();
#if UNITY_EDITOR
private void Awake()
{
Initialize();
}
#endif
public void Initialize()
{
if (nodes.Count != 0) return;
CreateNode(null);
}
private void OnValidate()
{
NodeLookUp.Clear();
foreach(DialogueNode node in GetNodes())
NodeLookUp[node.name] = node;
}
public IEnumerable<DialogueNode> GetNodes()
{
return nodes;
}
public DialogueNode GetRootNode()
{
return nodes[0];
}
public IEnumerable<DialogueNode> GetAllChildren(DialogueNode parentNode)
{
foreach(string childID in parentNode.nxt)
{
if (!NodeLookUp.ContainsKey(childID)) continue;
yield return NodeLookUp[childID];
}
}
public IEnumerable<DialogueNode> GetPlayerChildren(DialogueNode currentNode)
{
foreach(DialogueNode node in GetAllChildren(currentNode))
{
if (node.speaker == Speaker.Player) yield return node;
}
}
public IEnumerable<DialogueNode> GetAIChildren(DialogueNode currentNode)
{
foreach (DialogueNode node in GetAllChildren(currentNode))
{
if (node.speaker == Speaker.Npc) yield return node;
}
}
public void CreateNode(DialogueNode parent)
{
DialogueNode newNode = CreateInstance<DialogueNode>();
if (parent != null) newNode.name = System.Guid.NewGuid().ToString();
else newNode.name = "Root";
//Undo.RecordObject(this, "A");
//Undo.RegisterCreatedObjectUndo(newNode, "Creating Dialogue Node");
if (parent != null)
{
parent.nxt.Add(newNode.name);
newNode.pos.x = parent.pos.x + 10f;
newNode.pos.y = parent.pos.y + 10f;
if (parent.speaker == Speaker.Npc) newNode.speaker = Speaker.Player;
if (parent.speaker == Speaker.Player) newNode.speaker = Speaker.Npc;
}
nodes.Add(newNode);
AssetDatabase.AddObjectToAsset(newNode, this);
OnValidate();
}
public void DeleteNode(DialogueNode nodeToRemove)
{
Undo.RecordObject(this, "Delete Dialogue Node");
nodes.Remove(nodeToRemove);
OnValidate();
CleanChildren(nodeToRemove);
Undo.DestroyObjectImmediate(nodeToRemove);
}
private void CleanChildren(DialogueNode nodeToRemove)
{
foreach (DialogueNode node in GetNodes())
node.nxt.Remove(nodeToRemove.name);
}
}
}
接下來就是視覺化介面的建立, GetWindow(typeof(this.name),false,"editor name")
函式可以建立一個自定義編輯器視窗,這個函式必須在一個擁有 [MenuItem()]
屬性的函式中執行,程式碼如下。
[MenuItem("Window/Dialogue Editor")] //Create a option in tools
public static void ShowEditorWindow()
{
GetWindow(typeof(DialogueEditor), false, "Dialogue System");
}
而我們需要實現選中一個文字庫時,自動彈出編輯視窗,這個時候我們就要使用 Unity 提供的一個回撥屬性 OnOpenAsset(x)
,其中 x
是函式被呼叫的次序,加入了這個屬性的靜態函式將會在一個資產被選中的時候被呼叫,這個靜態函式必須擁有以下兩種特徵之一。(這是官網對該屬性的介紹)
· static bool OnOpenAsset(int instanceID, int line)
· static bool OnOpenAsset(int instanceID, int line, int column)
其中, instanceID 是選中資產在 Unity 中的編號,利用 EditorUtility.InstanceIDToObject
函式就能獲取這個資產,接著我們使用投射 as
來將這個資產轉換為 dialogue ,如果轉換成功的話,我們就選中這個 dialogue 作為當前編輯的文字庫,並且開啟編輯器視窗。以下是程式碼。
[OnOpenAsset(1)] //Open the window
public static bool OpenDialogue(int instanceID,int line)
{
Dialogue tmp = EditorUtility.InstanceIDToObject(instanceID) as Dialogue;
if (tmp == null) return false;
ShowEditorWindow();
return true;
}