Question:
When developing the guidance module, we will always encounter the character dialogue process. In most cases, the product sends the document of the dialogue process to the program, and then the program is processed into its own readable text data. In a sense, the program and product cause a waste of time. For each change in the later stage, the development time of the team will be superimposed.
expect
In order to simplify the development process, we hope to develop a visualization tool for the product. The data file saved after the product is written can be read seamlessly by the program, and then each iterative dialogue process uses this tool to generate new files to replace the old files.
Tool screenshot
First, if we develop this tool, we first need to define a data type to store the information we need,
Then, start with the smallest unit and push back the definition. We need a statement information. This statement needs to include the id, speaker type, speaking content, speaking duration, etc. of this sentence in the whole speaking process, as follows:
/// <summary> ///Speaking information /// </summary> [Serializable] public class SpeakInfo { public int id;// id in the conversation flow to which it belongs public SpeakerType type;//Speaker type public string info;//Speech content public float time=3; }
Define speaker enumeration. In order to facilitate product configuration, I also made a tool to generate this enumeration file. The effect is shown in the following figure:
public enum SpeakerType { Teacher, Wang, Li, Zhang, }
Then we know that each conversation is connected by multiple statements. Then, in order to facilitate indexing the conversation flow later, we define a unique flag for the conversation flow, and the data definition of the conversation flow is as follows:
/// <summary> ///Dialogue flow /// </summary> [Serializable] public class DialogFlow:ISerializationCallbackReceiver { /// <summary> ///Dialog stream flag (unique tag) /// </summary> public string flag; /// <summary> ///All words /// </summary> public List<SpeakInfo> speakInfoList =new List<SpeakInfo>(); public void OnBeforeSerialize() { } public void OnAfterDeserialize() { if (speakInfoList==null||speakInfoList.Count==0)return; for (int i = 0; i < speakInfoList.Count; i++)speakInfoList[i].id = i; } }
Since the stored data has been clearly defined, we need an object to control the reading and saving of data. I name it DialogFlowBook:
/// <summary> ///Dialogue flow textbook (including all dialogue flows, and the target flow can be retrieved) /// </summary> public class DialogFlowBook { private static DialogFlowBook _instance; public static DialogFlowBook instance { get { if (_instance==null)_instance=new DialogFlowBook(); return _instance; } } public static string filePath = Application.dataPath+"/Dialog/DialogInfo.txt"; [Serializable] private class DialogFlowGroup:ISerializationCallbackReceiver { public List<DialogFlow> list = new List<DialogFlow>(); private Dictionary<string,DialogFlow> dict= new Dictionary<string, DialogFlow>(); public DialogFlow GetDialogFlow(string flag) { if (dict == null || dict.Count == 0) return null; if (dict.ContainsKey(flag)) return dict[flag]; return null; } private void OnInitDict() { if (list == null || list.Count == 0) return; for (int i = 0; i < list.Count; i++) { DialogFlow df = list[i]; if (dict.ContainsKey(df.flag)) { Debug.LogErrorFormat("[Error: There are in the dialogue process Flag: [{0}][repeat]",df.flag); return; } dict.Add(df.flag,df); } } public void OnBeforeSerialize() { } public void OnAfterDeserialize() { OnInitDict(); } } private static DialogFlowGroup dialogFlowGroup; private DialogFlowBook() { dialogFlowGroup = LoadDialogFlowGroup(); } private DialogFlowGroup LoadDialogFlowGroup() { DialogFlowGroup group = null; string json = ReadLocal(filePath); if (string.IsNullOrEmpty(json)) { group=new DialogFlowGroup(); Debug.Log("The local read dialog process information is empty"); return group; } group=JsonUtility.FromJson<DialogFlowGroup>(json); return group; } /// <summary> ///Get conversation flow /// </summary> ///< param name = "flag" > unique identification < / param > /// <returns></returns> public DialogFlow GetDialogFlow(string flag) { return dialogFlowGroup.GetDialogFlow(flag); } #if UNITY_EDITOR public static List<DialogFlow> Load() { DialogFlowGroup group = null; string json = ReadLocal(filePath); if (string.IsNullOrEmpty(json)) { group=new DialogFlowGroup(); Debug.Log("The local read dialog process information is empty"); return group.list; } group=JsonUtility.FromJson<DialogFlowGroup>(json); return group?.list; } public static void Save(List<DialogFlow> list) { if (dialogFlowGroup==null)dialogFlowGroup=new DialogFlowGroup(); dialogFlowGroup.list = list; string json=JsonUtility.ToJson(dialogFlowGroup); WriteToLocal(filePath,json); } #endif public static void WriteToLocal(string path,string info) { if (File.Exists(path))File.Delete(path); string dirPath = Path.GetDirectoryName(path); if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath); using (FileStream fs= new FileStream(path,FileMode.CreateNew)) { StreamWriter sw = new StreamWriter(fs); sw.Write(info); sw.Close(); sw.Dispose(); } } public static string ReadLocal(string path) { if (!File.Exists(path))return null; string info = ""; using (FileStream fs = new FileStream(path,FileMode.Open,FileAccess.Read)) { StreamReader sr = new StreamReader(fs); info = sr.ReadToEnd(); sr.Close(); sr.Dispose(); } return info; } }
Let's start developing the editor. The source code is as follows:
using System; using System.Collections.Generic; using System.Text; using UnityEditor; using UnityEngine; public class SayTypeTools : EditorWindow { private List<string> speakerTypeList; private Vector2 scrollPos; private string filePath; [MenuItem("Tools/dialogue/Type configuration")] static void OpenWindow() { SayTypeTools window = GetWindow<SayTypeTools>("Type configuration"); window.OnInit(); window.Show(); } void OnInit() { filePath=Application.dataPath + "/Dialog/SpeakerType.cs"; speakerTypeList=new List<string>(); speakerTypeList.AddRange(Enum.GetNames(typeof(SpeakerType))); } private void OnGUI() { DrawSayTypes(); } void DrawSayTypes() { if (GUILayout.Button("Add conversation type(Name with English characters)"))speakerTypeList.Add(""); GUILayout.Space(10); scrollPos=EditorGUILayout.BeginScrollView(scrollPos); for (int i = 0; i < speakerTypeList.Count; i++) { EditorGUILayout.BeginHorizontal(); speakerTypeList[i] = EditorGUILayout.TextField(speakerTypeList[i]); if (GUILayout.Button("delete",GUILayout.Width(50))) { speakerTypeList.RemoveAt(i); i--; } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); if (GUILayout.Button("preservation"))SaveSayTypeFile(); } void SaveSayTypeFile() { StringBuilder sayTypeStr=new StringBuilder(); sayTypeStr.AppendLine("public enum SpeakerType"); sayTypeStr.AppendLine("{"); for (int i = 0; i < speakerTypeList.Count; i++) { sayTypeStr.AppendLine("\t"+speakerTypeList[i]+","); } sayTypeStr.AppendLine("}"); DialogFlowBook.WriteToLocal(filePath,sayTypeStr.ToString()); AssetDatabase.Refresh(); } } public class DialogTools : EditorWindow { private List<DialogFlow> dialogFlowList; private List<bool> bigSwitchList; private GUIStyle smallPageStyle; private Vector2 scrollPos; [MenuItem("Tools/dialogue/Content editing")] static void OpenWindow() { DialogTools window = GetWindowWithRect<DialogTools>(new Rect(0,0,800,900),false,"Content editing"); window.OnInit(); window.Show(); } void OnInit() { smallPageStyle=new GUIStyle(); smallPageStyle.normal.textColor = Color.green; smallPageStyle.fontSize = 50; smallPageStyle.alignment = TextAnchor.MiddleCenter; bigSwitchList=new List<bool>(); dialogFlowList = DialogFlowBook.Load(); if (dialogFlowList!=null&&dialogFlowList.Count>0)for (int i = 0; i < dialogFlowList.Count; i++)bigSwitchList.Add(false); } private void OnGUI() { if(GUILayout.Button("Add a dialogue")) { GUI.FocusControl(null); DialogFlow big = new DialogFlow(); dialogFlowList.Add(big); bigSwitchList.Add(true); } EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("open")) { GUI.FocusControl(null); for (int i = 0; i < bigSwitchList.Count; i++) bigSwitchList[i] = true; } if (GUILayout.Button("shrink")) { GUI.FocusControl(null); for (int i = 0; i < bigSwitchList.Count; i++) bigSwitchList[i] = false; } EditorGUILayout.EndHorizontal(); scrollPos = EditorGUILayout.BeginScrollView(scrollPos); DrawMain(); EditorGUILayout.EndScrollView(); if (GUILayout.Button("preservation")) { GUI.FocusControl(null); Save(); } } void DrawMain() { void DrawBigTitle(int id,out bool isDelete) { isDelete = false; EditorGUILayout.BeginHorizontal(); bigSwitchList[id]=EditorGUILayout.Foldout(bigSwitchList[id],"paragraph"+(id+1)+","); EditorGUILayout.LabelField("label",GUILayout.Width(30)); DialogFlow flow = dialogFlowList[id]; flow.flag=EditorGUILayout.TextField(flow.flag); if (GUILayout.Button("Increase dialogue")) { GUI.FocusControl(null); flow.speakInfoList.Add(new SpeakInfo()); bigSwitchList[id] = true; } if (GUILayout.Button("delete")) { GUI.FocusControl(null); dialogFlowList.RemoveAt(id); bigSwitchList.RemoveAt(id); isDelete = true; } EditorGUILayout.EndHorizontal(); } if (dialogFlowList == null || dialogFlowList.Count == 0) return; for (int i = dialogFlowList.Count-1; i >=0 ; i--) { bool isDelete; DrawBigTitle(i,out isDelete); if (isDelete)continue; if (bigSwitchList[i])DrawDialogFlow(dialogFlowList[i]); EditorGUILayout.LabelField("---------------------------------------------------------------------------------------------------------------------"); } } void DrawDialogFlow(DialogFlow dialogFlow) { if (dialogFlow == null) return; if (dialogFlow.speakInfoList == null || dialogFlow.speakInfoList.Count == 0) return; for (int i = dialogFlow.speakInfoList.Count-1; i >=0 ; i--) { SpeakInfo speakInfo = dialogFlow.speakInfoList[i]; bool isDelete; DrawSayInfo(speakInfo,i,out isDelete); if (isDelete)dialogFlow.speakInfoList.RemoveAt(i); } } void DrawSayInfo(SpeakInfo speakInfo,int id,out bool isDelete) { isDelete = false; if (speakInfo == null) return; EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginVertical(GUILayout.Width(20)); speakInfo.type = (SpeakerType)EditorGUILayout.EnumPopup(speakInfo.type,GUILayout.Width(100)); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("duration",GUILayout.Width(40)); speakInfo.time = EditorGUILayout.FloatField(speakInfo.time,GUILayout.Width(60)); EditorGUILayout.EndHorizontal(); GUILayout.Space(14); EditorGUILayout.LabelField((id+1).ToString(),smallPageStyle,GUILayout.Width(100)); EditorGUILayout.EndVertical(); speakInfo.info = EditorGUILayout.TextArea(speakInfo.info,GUILayout.Height(EditorGUIUtility.singleLineHeight*5)); if (GUILayout.Button("delete",GUILayout.Width(35),GUILayout.Height(EditorGUIUtility.singleLineHeight*5))) { GUI.FocusControl(null); isDelete = true; } EditorGUILayout.EndHorizontal(); } void Save() { int bigCount = dialogFlowList == null ? 0 : dialogFlowList.Count; if (dialogFlowList.Count==0) { if (!EditorUtility.DisplayDialog("Tips:", "\n The data is empty. Are you sure you want to replace the local data?", "yes", "no")) { AssetDatabase.Refresh(); return; } } for (int i = 0; i < bigCount; i++) { if (string.IsNullOrEmpty(dialogFlowList[i].flag)) { if (EditorUtility.DisplayDialog("Tips:", "\n Segment tag is empty and cannot be saved", "yes")) { AssetDatabase.Refresh(); } return; } } DialogFlowBook.Save(dialogFlowList); AssetDatabase.Refresh(); Debug.Log("Data saved successfully!"); } }
ok tool development completed.
The following is a simple dialogue process framework I wrote to adapt to this tool, including playing dialogue, stopping dialogue, callback at the beginning and end of each dialogue, callback at the end of process dialogue, etc. These commonly used functional modules have been developed. If you are interested in children's shoes, you can download the source code and have a look.