抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

通过对话树构造对话系统,支持选项分支、自动对话和对话历史。

演示

原理

通过树结构保存对话节点,没有选项的时候进入索引为 0 的下一节点,有选项的时候根据选项的索引进入对应的下一节点。

DialogueTree
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
{
/// <summary>
/// id,用来判断对话顺序
/// </summary>
public int id { get; set; }
/// <summary>
/// 对话id,用来判断所属对话树
/// </summary>
public int d_id { get; set; }
/// <summary>
/// 对话人
/// </summary>
public string target { get; set; }
/// <summary>
/// 内容
/// </summary>
public string content { get; set; }
/// <summary>
/// 选项
/// </summary>
public List<string> selection { get; set; }
/// <summary>
/// 下一对话
/// </summary>
public List<DialogueTree> next { get; set; }
/// <summary>
/// 特效 暂无用处
/// </summary>
public string effect { get; set; }
/// <summary>
/// 自动播放速度
/// </summary>
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
/// <summary>
/// UI展示
/// </summary>
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
/// <summary>
/// 开始对话点击事件
/// </summary>
private void OnStartClick()
{
_start.gameObject.SetActive(false);
_dialoguePanel.SetActive(true);

DialogueManager.Instance().StartDialogue(0);
ShowDialogue();

if (_isAuto)
{
_dialogCo = StartCoroutine(AutoDialogue());
}
}

自动对话

利用协程根据每一段对话的速度来决定自动进入下一对话的时间。

点击按钮开启自动对话,再次点击关闭。

自动对话协程等待时间到了之后,先判断是否有下一对话。

  1. 是的情况判断下一对话是否有选项,没有选项的情况下进入下一对话,有的话就停止协程。
  2. 否的情况,结束对话。
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
/// <summary>
/// 自动对话点击事件
/// </summary>
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";
}
}

/// <summary>
/// 自动对话协程
/// </summary>
/// <returns></returns>
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
/// <summary>
/// 下一对话点击事件
/// </summary>
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
/// <summary>
/// 选项点击事件
/// </summary>
/// <param name="index"></param>
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
/// <summary>
/// 对话回放点击事件
/// </summary>
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);
}
}

对话管理器

对话管理器负责简单的数据存储和获取功能。最复杂的部分就在于选择下一对话。

下一对话先判断是否有选项

  1. 有选项的情况,下一对话直接根据当前选择的索引去_curDialogue.next中取对应索引的节点。
  2. 没有选项的情况,如果_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
/// <summary>
/// 下一对话
/// </summary>
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
{
/// <summary>
/// id,用来判断对话顺序
/// </summary>
public int id { get; set; }
/// <summary>
/// 对话id,用来判断所属对话树
/// </summary>
public int d_id { get; set; }
/// <summary>
/// 对话人
/// </summary>
public string target { get; set; }
/// <summary>
/// 内容
/// </summary>
public string content { get; set; }
/// <summary>
/// 选项
/// </summary>
public List<string> selection { get; set; }
/// <summary>
/// 下一对话
/// </summary>
public List<DialogueTree> next { get; set; }
/// <summary>
/// 特效 暂无用处
/// </summary>
public string effect { get; set; }
/// <summary>
/// 自动播放速度
/// </summary>
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
{
//UI控件 -----start
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;
//UI控件 -----end

/// <summary>
/// 是否自动对话
/// </summary>
private bool _isAuto = false;
/// <summary>
/// 自动对话协程
/// </summary>
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);
});
}
}
/// <summary>
/// UI展示
/// </summary>
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);
}
}
}
/// <summary>
/// 开始对话点击事件
/// </summary>
private void OnStartClick()
{
_start.gameObject.SetActive(false);
_dialoguePanel.SetActive(true);

DialogueManager.Instance().StartDialogue(0);
ShowDialogue();

if (_isAuto)
{
_dialogCo = StartCoroutine(AutoDialogue());
}
}
/// <summary>
/// 自动对话点击事件
/// </summary>
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";
}
}
/// <summary>
/// 下一对话点击事件
/// </summary>
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());
}
}
}
/// <summary>
/// 选项点击事件
/// </summary>
/// <param name="index"></param>
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());
}
}
/// <summary>
/// 对话回放点击事件
/// </summary>
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);
}
}
/// <summary>
/// 自动对话协程
/// </summary>
/// <returns></returns>
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);
}

/// <summary>
/// 开始对话
/// </summary>
/// <param name="id"></param>
public void StartDialogue(int id)
{
_curDialogue = _dialogues[id];
}

/// <summary>
/// 获取对话内容
/// </summary>
/// <returns></returns>
public string GetContent()
{
return _curDialogue?.content;
}

/// <summary>
/// 获取对话人
/// </summary>
/// <returns></returns>
public string GetTarget()
{
return _curDialogue?.target;
}

/// <summary>
/// 获得选项
/// </summary>
/// <returns></returns>
public List<string> GetSelection()
{
return _curDialogue?.selection;
}

/// <summary>
/// 获得自动对话速度
/// </summary>
/// <returns></returns>
public float GetAutoSpeed()
{
return _curDialogue.autoSpeed;
}

/// <summary>
/// 设置选项
/// </summary>
/// <param name="select"></param>
public void SetSelect(int select)
{
_select = select;
}

/// <summary>
/// 下一对话
/// </summary>
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;
}
}

/// <summary>
/// 记录对话
/// </summary>
/// <param name="content"></param>
public void RecordDialogue(string content)
{
_record.Add(content);
}

/// <summary>
/// 获得对话记录
/// </summary>
/// <returns></returns>
public List<string> GetRecordedDialogue()
{
return _record;
}
/// <summary>
/// 清除对话记录
/// </summary>
public void ClearRecordedDialogue()
{
_record.Clear();
}
}

评论