简介
通过对话树构造对话系统,支持选项分支、自动对话和对话历史。
演示
原理
通过树结构保存对话节点,没有选项的时候进入索引为 0 的下一节点,有选项的时候根据选项的索引进入对应的下一节点。
DialogueTree1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| using System.Collections.Generic;
public class DialogueTree { public int id { get; set; } public int d_id { get; set; } public string target { get; set; } public string content { get; set; } public List<string> selection { get; set; } public List<DialogueTree> next { get; set; } public string effect { get; set; } public float autoSpeed { get; set; }
public DialogueTree() { next = new List<DialogueTree>(); selection = new List<string>(); } }
|
自动对话利用协程进行,在遇到选项的时候停止,在选项选择后重新开启自动对话协程;在手动点击下一个对话的情况下停止协程并重新开启一个自动对话的协程。
对话历史则是把每一条对话和选择的选项记录到一个对话列表中,展示的时候只要遍历即可。
UI 逻辑
UI 展示
UI 展示封装为ShowDialogue
,每次调用更新对话人,对话内容,如果有选项则根据选项数量显示选项按钮。同时,在该方法内调用DialogueManager.Instance().RecordDialogue(_content.text);
记录对话内容用于回放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
private void ShowDialogue() { _content.text = DialogueManager.Instance().GetContent(); DialogueManager.Instance().RecordDialogue(_content.text); _target.text = DialogueManager.Instance().GetTarget();
List<string> selections = DialogueManager.Instance().GetSelection(); if (selections.Count > 0) { for (int i = 0, len = selections.Count; i < len; i++) { string selection = selections[i]; Button button = _buttons[i]; button.gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = selection; button.gameObject.SetActive(true); } } }
|
开始对话
调用DialogueManager.Instance().StartDialogue();
方法开始对话,该方法接受一个对话 id 参数。
开始对话之后,立刻调用ShowDialogue
展示对话内容,并且判断是否自动对话,是的情况调用自动对话协程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
private void OnStartClick() { _start.gameObject.SetActive(false); _dialoguePanel.SetActive(true);
DialogueManager.Instance().StartDialogue(0); ShowDialogue();
if (_isAuto) { _dialogCo = StartCoroutine(AutoDialogue()); } }
|
自动对话
利用协程根据每一段对话的速度来决定自动进入下一对话的时间。
点击按钮开启自动对话,再次点击关闭。
自动对话协程等待时间到了之后,先判断是否有下一对话。
- 是的情况判断下一对话是否有选项,没有选项的情况下进入下一对话,有的话就停止协程。
- 否的情况,结束对话。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
private void OnAutoClick() { _isAuto = !_isAuto; if (_isAuto) { _dialogCo = StartCoroutine(AutoDialogue()); _auto.gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = "Auto On"; } else { StopCoroutine(_dialogCo); _auto.gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = "Auto"; } }
IEnumerator AutoDialogue() { float speed = DialogueManager.Instance().GetAutoSpeed(); yield return new WaitForSeconds(speed); bool next = DialogueManager.Instance().Next(); if (next) { bool nextSelection = DialogueManager.Instance().GetSelection().Count == 0; if (nextSelection) { StartCoroutine(AutoDialogue()); } ShowDialogue(); } else { _start.gameObject.SetActive(true); _dialoguePanel.SetActive(false); } }
|
下一对话
首先判断选项按钮是否显示,显示的情况下禁止进入下一对话。否则调用DialogueManager.Instance().Next()
进入下一对话,并且调用ShowDialogue()
更新 UI。如果没有下一对话就结束对话。
在开启自动对话的情况下,先停止上一个自动对话的协程防止冲突,然后另起一个自动对话协程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
private void OnNextClick() { if (!_buttons[0].gameObject.activeInHierarchy) { bool next = DialogueManager.Instance().Next(); if (next) { ShowDialogue(); } else { _start.gameObject.SetActive(true); _dialoguePanel.SetActive(false); }
if (_isAuto) { StopCoroutine(_dialogCo); _dialogCo = StartCoroutine(AutoDialogue()); } } }
|
选项
点击选项的时候调用DialogueManager.Instance().SetSelect(index)
设置当前选项,调用DialogueManager.Instance().RecordDialogue(DialogueManager.Instance().GetSelection()[index])
保存已选选项的内容到对话历史中。然后就是和点击下一对话的逻辑一样。另外在完成选择后隐藏所有选项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
private void OnSelectClick(int index) { DialogueManager.Instance().SetSelect(index); DialogueManager.Instance().RecordDialogue(DialogueManager.Instance().GetSelection()[index]); DialogueManager.Instance().Next(); for (int i = 0; i < _buttons.Count; i++) { Button button = _buttons[i]; button.gameObject.SetActive(false); } ShowDialogue(); if (_isAuto) { _dialogCo = StartCoroutine(AutoDialogue()); } }
|
对话历史
对话历史只要调用DialogueManager.Instance().GetRecordedDialogue()
获得历史对话列表,然后根据需要输出即可。
1 2 3 4 5 6 7 8 9 10 11
|
private void OnRecordClick() { List<string> record = DialogueManager.Instance().GetRecordedDialogue(); for(int i = 0,len = record.Count; i < len; i++) { string rec = record[i]; Debug.Log(rec); } }
|
对话管理器
对话管理器负责简单的数据存储和获取功能。最复杂的部分就在于选择下一对话。
下一对话先判断是否有选项
- 有选项的情况,下一对话直接根据当前选择的索引去
_curDialogue.next
中取对应索引的节点。
- 没有选项的情况,如果
_curDialogue.next
的计数大于 0,即有下一对话,则置当前对话节点_curDialogue
为_curDialogue.next[0]
(保存的下一对话只有一个,索引为 0)。否则置下一对话为null
。
下一对话选择完成后,返回成功或失败的结果,用于 UI 展示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
public bool Next() { if (_curDialogue.selection.Count == 0) { if (_curDialogue.next.Count > 0) {
_curDialogue = _curDialogue.next[0]; return true; } else { _curDialogue = null; return false; } } else { _curDialogue = _curDialogue.next[_select]; return true; } }
|
代码
对话树1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| using System.Collections.Generic;
public class DialogueTree { public int id { get; set; } public int d_id { get; set; } public string target { get; set; } public string content { get; set; } public List<string> selection { get; set; } public List<DialogueTree> next { get; set; } public string effect { get; set; } public float autoSpeed { get; set; }
public DialogueTree() { next = new List<DialogueTree>(); selection = new List<string>(); } }
|
UI脚本1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
| using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI;
public class RootScript : MonoBehaviour { public TextMeshProUGUI _content;
public List<Button> _buttons;
public Button _auto;
public Button _start;
public Button _panel;
public Button _record;
public TextMeshProUGUI _target;
public GameObject _dialoguePanel;
private bool _isAuto = false; private Coroutine _dialogCo;
void Start() { DialogueManager.Instance().Init();
_auto.onClick.AddListener(OnAutoClick); _start.onClick.AddListener(OnStartClick); _panel.onClick.AddListener(OnNextClick); _record.onClick.AddListener(OnRecordClick);
for (int i = 0, len = _buttons.Count; i < len; i++) { Button button = _buttons[i]; int index = i; button.onClick.AddListener(() => { OnSelectClick(index); }); } } private void ShowDialogue() { _content.text = DialogueManager.Instance().GetContent(); DialogueManager.Instance().RecordDialogue(_content.text); _target.text = DialogueManager.Instance().GetTarget();
List<string> selections = DialogueManager.Instance().GetSelection(); if (selections.Count > 0) { for (int i = 0, len = selections.Count; i < len; i++) { string selection = selections[i]; Button button = _buttons[i]; button.gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = selection; button.gameObject.SetActive(true); } } } private void OnStartClick() { _start.gameObject.SetActive(false); _dialoguePanel.SetActive(true);
DialogueManager.Instance().StartDialogue(0); ShowDialogue();
if (_isAuto) { _dialogCo = StartCoroutine(AutoDialogue()); } } private void OnAutoClick() { _isAuto = !_isAuto; if (_isAuto) { _dialogCo = StartCoroutine(AutoDialogue()); _auto.gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = "Auto On"; } else { StopCoroutine(_dialogCo); _auto.gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = "Auto"; } } private void OnNextClick() { if (!_buttons[0].gameObject.activeInHierarchy) { bool next = DialogueManager.Instance().Next(); if (next) { ShowDialogue(); } else { _start.gameObject.SetActive(true); _dialoguePanel.SetActive(false); }
if (_isAuto) { StopCoroutine(_dialogCo); _dialogCo = StartCoroutine(AutoDialogue()); } } } private void OnSelectClick(int index) { DialogueManager.Instance().SetSelect(index); DialogueManager.Instance().RecordDialogue(DialogueManager.Instance().GetSelection()[index]); DialogueManager.Instance().Next(); for (int i = 0; i < _buttons.Count; i++) { Button button = _buttons[i]; button.gameObject.SetActive(false); } ShowDialogue(); if (_isAuto) { _dialogCo = StartCoroutine(AutoDialogue()); } } private void OnRecordClick() { List<string> record = DialogueManager.Instance().GetRecordedDialogue(); for(int i = 0,len = record.Count; i < len; i++) { string rec = record[i]; Debug.Log(rec); } } IEnumerator AutoDialogue() { float speed = DialogueManager.Instance().GetAutoSpeed(); yield return new WaitForSeconds(speed); bool next = DialogueManager.Instance().Next(); if (next) { bool nextSelection = DialogueManager.Instance().GetSelection().Count == 0; if (nextSelection) { StartCoroutine(AutoDialogue()); } ShowDialogue(); } else { _start.gameObject.SetActive(true); _dialoguePanel.SetActive(false); } } }
|
对话管理器1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
| using System.Collections.Generic;
public class DialogueManager { private static DialogueManager _instance = null;
private List<DialogueTree> _dialogues = new List<DialogueTree>();
private DialogueTree _curDialogue = null;
private int _select;
private List<string> _record = new List<string>(); public static DialogueManager Instance() { if (_instance == null) { _instance = new DialogueManager(); } return _instance; } private DialogueManager() { }
public void Init() { DialogueTree d1 = new DialogueTree(); d1.id = 0; d1.d_id = 0; d1.content = "第一条对话"; d1.target = "NPC"; d1.autoSpeed = 2;
DialogueTree d2 = new DialogueTree(); d2.id = 1; d2.d_id = 0; d2.content = "第二条对话"; d2.target = "NPC"; d2.autoSpeed = 2; d2.selection.Add("选项1"); d2.selection.Add("选项2");
DialogueTree d3 = new DialogueTree(); d3.id = 2; d3.d_id = 0; d3.content = "第三条对话"; d3.target = "NPC"; d3.autoSpeed = 2;
DialogueTree d4 = new DialogueTree(); d4.id = 3; d4.d_id = 0; d4.content = "第四条对话"; d4.target = "Player"; d4.autoSpeed = 2;
d1.next.Add(d2); d2.next.Add(d3); d2.next.Add(d4);
_dialogues.Add(d1); }
public void StartDialogue(int id) { _curDialogue = _dialogues[id]; }
public string GetContent() { return _curDialogue?.content; }
public string GetTarget() { return _curDialogue?.target; }
public List<string> GetSelection() { return _curDialogue?.selection; }
public float GetAutoSpeed() { return _curDialogue.autoSpeed; }
public void SetSelect(int select) { _select = select; }
public bool Next() { if (_curDialogue.selection.Count == 0) { if (_curDialogue.next.Count > 0) {
_curDialogue = _curDialogue.next[0]; return true; } else { _curDialogue = null; return false; } } else { _curDialogue = _curDialogue.next[_select]; return true; } }
public void RecordDialogue(string content) { _record.Add(content); }
public List<string> GetRecordedDialogue() { return _record; } public void ClearRecordedDialogue() { _record.Clear(); } }
|