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 ; }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>() ; } }
我们的设想是,根据每个有需要对话框的实体(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 ; }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 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管理,于是我们可以修改一下DiaLogManger
的NextString()
如下:
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 ; } }