Unity对话控制系统

前言

对话系统一直是游戏中重要的一部分,该部分就简单而言,可以只要对话的更新,显示即可。而复杂情况下,可能还需要配合小到给予物品、发生事件,大到结局分支,都与它有关
本文将就对话系统一方面,进行一步步步的开发研究和加强,建立一个比较完善+有扩展性的对话系统
可能用到的设计模式(蛮写、知不知道无所谓):单例、观察者

(有机会的话后面补上视频)

基本模型

对于一个对话框,我们这里设置了对话内容头像人物姓名三个方面,我们先声明对应的变量
并且在Awake()中写下系列代码,帮助我们使用单例
最终结果如下

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI ;

public class DiaLogManager : MonoBehaviour
{
public static DiaLogManager instance ;
[Header("基础部分")]
public GameObject textBackground ;
public Image speakerPic ;
public Text speakerName ;
public Text speakertext ;

private void Awake()
{
if(instance == null ){
instance = this ;
//设置instance
}else{
if(instance != this){
Destroy(gameObject);
}
}

DontDestroyOnLoad(gameObject);

}

private void Start()
{
//这里写的是对应组件的绑定,你也可以写成自己的,或者用拖动的方式绑定
//这一部分不是该文章的重点,如果你想了解这方面,可以参考另外一篇博文:《在Unity中查找Active为false的物体》
textBackground = ((GameObject.Find("UI")).transform.Find("TextBackGround").gameObject) ;
speakerPic = textBackground.transform.Find("speakerPic").GetComponent<Image>() ;
speakerName = textBackground.transform.Find("speakerName").GetComponent<Text>() ;
speakertext = textBackground.transform.Find("speakertext").GetComponent<Text>() ;
}
}

我们的设想是,根据每个有需要对话框的实体(NPC,告示牌等等)来触发这个对话框,也就是说对话的内容是储存在需要对话框的实体那里的
因此我们建立一个string的数组来得到对话内容,并且使用一个text_index来记录对话下标,然后通过一个函数来读取对话内容
于是我们写下

1
2
3
4
5
6
7
8
9
public void LoadDialog(string[] _texts){
texts = new string[_texts.Length] ;
for (int i = 0 ; i < _texts.Length ; i++ )
{
texts[i] = _texts[i] ;
}
Debug.Log("复制完成");

}

NPC类

我们通过一个类来抽象我们的NPC,现在它的功能十分简单,只要先写出NPC需要有声明属性即可

1
2
3
4
5
6
7
8
9
10
11
12
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI ;

public class NPC : MonoBehaviour
{
public Image speakerPic ;
public string speakerName ;
public string[] speakertext ;
}

补充对话框

然后再在DiaLogManager中写在每次加载的函数
这里我们分别写了三个函数来进行加载头像、姓名、内容:

1
2
3
public void LoadImage(Image _image){
speakerPic = _image ;
}
1
2
3
public void LoadName(string _name){
speakerName.text = _name ;
}
1
2
3
4
5
6
7
8
public void LoadDialog(string[] _texts){
texts = new string[_texts.Length] ;
for (int i = 0 ; i < _texts.Length ; i++ )
{
texts[i] = _texts[i] ;
}
Debug.Log("复制完成");
}

我们之前定义了NPC类,于是我们可以整合三个方法为一个方法

1
2
3
4
5
public void LoadSpeaker(NPC _npc){
LoadImage(_npc.speakerPic);
LoadName(_npc.speakerName);
LoadDialog(_npc.speakertext);
}

这样的优势在于:我们只要在对应的NPC的代码中调用该函数即可
而且之后我们有什么特殊的要求,可以对方法进行重载,或者直接单独调用对应的方法

此时,基本的载入功能已经完成,目前代码如下:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI ;

public class DiaLogManager : MonoBehaviour
{
public static DiaLogManager instance ;
[Header("基础部分")]
public GameObject textBackground ;
public Image speakerPic ;
public Text speakerName ;
public Text speakertext ;

private void Awake()
{
if(instance == null ){
instance = this ;
//设置instance
}else{
if(instance != this){
Destroy(gameObject);
}
}

DontDestroyOnLoad(gameObject);

}

private void Start()
{
textBackground = ((GameObject.Find("UI")).transform.Find("TextBackGround").gameObject) ;
speakerPic = textBackground.transform.Find("speakerPic").GetComponent<Image>() ;
speakerName = textBackground.transform.Find("speakerName").GetComponent<Text>() ;
speakertext = textBackground.transform.Find("speakertext").GetComponent<Text>() ;
}


public void LoadSpeaker(NPC _npc){
LoadImage(_npc.speakerPic);
LoadName(_npc.speakerName);
LoadDialog(_npc.speakertext);
}

//public void
public void LoadImage(Image _image){
speakerPic = _image ;
}

public void LoadName(string _name){
speakerName.text = _name ;
}
public void LoadDialog(string[] _texts){
texts = new string[_texts.Length] ;
for (int i = 0 ; i < _texts.Length ; i++ )
{
texts[i] = _texts[i] ;
}
Debug.Log("复制完成");
}

}

补充开关对话已经文本更新

之后我们按需写下两个方法,用于打开和关闭对话框

1
2
3
4
5
6
7
8
9
10
11
12
public void OpenDialog(){
isOpenDialog = true ;
textIndex = 0 ;
speakertext.text = texts[textIndex] ;
textBackground.SetActive(true) ;
}

public void CloseDialog(){
isOpenDialog = false ;
textBackground.SetActive(false);
textIndex = 0 ;
}

这里说明一下,这个textBackground对应的是对话框的UI组件,在Unity的编辑器窗口中,它是:

下面写出对话框内容更新的方法:本质是对对话框数组下标的递增:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void NextString(){
if(!isOpenDialog){
OpenDialog();
}

if(textIndex >= texts.Length){
CloseDialog();
}else{
speakertext.text = texts[textIndex] ;
textIndex++ ;
}

}

此时整体的代码如下,下一步我们会配合NPC的对应脚本,完成基础的对话框功能

使用

我们预想中的对话逻辑是:玩家碰到NPC,玩家选择交互,出对话框
或者NPC如果是告示牌之类的,就不用玩家选择交互,而是直接跳出对话框

此外,我们在OnOnTriggerEnter2D代码中,判断是瞬时的,也就是说玩家按下按键这一部分不应该写在这里
于是对应的,交互逻辑即 玩家碰到NPC->NPC改为可交互状态->此时玩家如果按下F就进入交互状态
这部分代码修改如下:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI ;

public class NPC : MonoBehaviour
{
[Header("基础对话框")]
public Image speakerPic ;
public string speakerName ;
public string[] speakertext ;
public bool isTips ; //如果是告示牌,则采用直接跳出对话框的方式

[Header("交互")]
private bool canTakeAction ; //可以交互
private bool isTakeAction ; //是否已经在交互状态


private void Update()
{
if(canTakeAction && Input.GetKeyDown(KeyCode.F)){
isTakeAction = true ;
}

if(isTakeAction){
//进行交互
}
}

private void EndAction(){
//结束交互
}

private void OnTriggerEnter2D(Collider2D other)
{
if(other.tag == "Player"){
canTakeAction = true ;
}
}

private void OnTriggerExit2D(Collider2D other)
{
canTakeAction = false ;
isTakeAction = false ;
}

}

我们选出交互实现的部分,我们想在进入交互后,玩家按下F则进入对话框
而对话结束后,结束交互

1
2
3
4
5
6
7
8
9
if(isTakeAction){
//进行交互
DiaLogManager.instance.LoadSpeaker(this);
if(Input.GetKeyDown(KeyCode.F)){
DiaLogManager.instance.NextString();
}
}else{
EndAction();
}

其中,个人认为结束和关闭对话框这一部分也要交给NPC管理,于是我们可以修改一下DiaLogMangerNextString()如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public bool NextString(){
if(!isOpenDialog){
OpenDialog();
}

if(textIndex >= texts.Length){
return false ;
}else{
speakertext.text = texts[textIndex] ;
textIndex++ ;
return true ;
}
}

并且将NPC部分的交互改为:

1
2
3
4
5
6
7
8
9
if(isTakeAction){
//进行交互
DiaLogManager.instance.LoadSpeaker(this);
if(Input.GetKeyDown(KeyCode.F)){
isTakeAction = DiaLogManager.instance.NextString();
}
}else{
EndAction();
}

而且补全EndAction()方法如下:

1
2
3
4
5
private void EndAction(){
//结束交互
isTakeAction = false ;
DiaLogManager.instance.CloseDialog();
}

结束

目前最基本的对话框内容就是这样了,进阶的配合请见另外一篇文章
目前用到的两个脚本的完整代码如下:

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
```C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI ;

public class DiaLogManager : MonoBehaviour
{
public static DiaLogManager instance ;
[Header("基础部分")]
public GameObject textBackground ;
public Image speakerPic ;
public Text speakerName ;
public Text speakertext ;
[Header("对话框内容")]
public bool isOpenDialog ;
public string[] texts ;
public int textIndex ;

private void Awake()
{
if(instance == null ){
instance = this ;
//设置instance
}else{
if(instance != this){
Destroy(gameObject);
}
}

DontDestroyOnLoad(gameObject);

}

private void Start()
{
textBackground = ((GameObject.Find("UI")).transform.Find("TextBackGround").gameObject) ;
speakerPic = textBackground.transform.Find("speakerPic").GetComponent<Image>() ;
speakerName = textBackground.transform.Find("speakerName").GetComponent<Text>() ;
speakertext = textBackground.transform.Find("speakertext").GetComponent<Text>() ;
}

public void OpenDialog(){
isOpenDialog = true ;
textIndex = 0 ;
speakertext.text = texts[textIndex] ;
textBackground.SetActive(true) ;
}

public void CloseDialog(){
isOpenDialog = false ;
textBackground.SetActive(false);
textIndex = 0 ;
}

public void LoadSpeaker(NPC _npc){
LoadImage(_npc.speakerPic);
LoadName(_npc.speakerName);
LoadDialog(_npc.speakertext);
}

//public void
public void LoadImage(Sprite _image){
speakerPic.sprite = _image ;
}

public void LoadName(string _name){
speakerName.text = _name ;
}
public void LoadDialog(string[] _texts){
texts = new string[_texts.Length] ;
for (int i = 0 ; i < _texts.Length ; i++ )
{
texts[i] = _texts[i] ;
}
Debug.Log("复制完成");
}



public bool NextString(){
if(!isOpenDialog){
OpenDialog();
}

if(textIndex >= texts.Length){
return false ;
}else{
speakertext.text = texts[textIndex] ;
textIndex++ ;
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
45
46
47
48
49
50
51
52
53
54
55
56
```C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI ;

public class NPC : MonoBehaviour
{
[Header("基础对话框")]
public Sprite speakerPic ;
public string speakerName ;
public string[] speakertext ;
public bool isTips ; //如果是告示牌,则采用直接跳出对话框的方式

[Header("交互")]
[SerializeField]private bool canTakeAction = false ; //可以交互
[SerializeField]private bool isTakeAction = false ; //是否已经在交互状态


private void Update()
{
if(canTakeAction && Input.GetKeyDown(KeyCode.F)){
isTakeAction = true ;
}

if(isTakeAction){
//进行交互
DiaLogManager.instance.LoadSpeaker(this);
if(Input.GetKeyDown(KeyCode.F)){
isTakeAction = DiaLogManager.instance.NextString();
}
}else{
EndAction();
}
}

private void EndAction(){
//结束交互
isTakeAction = false ;
DiaLogManager.instance.CloseDialog();
}

private void OnTriggerEnter2D(Collider2D other)
{
if(other.tag == "Player"){
canTakeAction = true ;
}
}

private void OnTriggerExit2D(Collider2D other)
{
canTakeAction = false ;
isTakeAction = false ;
}

}