Unity 简单RPG对话系统——龙之谷2的NPC对话系统

news/2024/10/17 4:51:35/

Unity 简单RPG对话系统——龙之谷2的NPC对话系统

龙之谷2手游正式上线后不久,试玩了十几分钟(包括捏脸的5分钟),之后就再也没有打开过了。
本文章将对龙之谷2的NPC对话系统进行高仿,同时考虑到 策划可能不断修改对话内容 工具的重复利用,将部分通过编辑器模式来进行制作。

写在前边:

1.参考文章

知乎: Yumir——用128行代码实现一个文字冒险游戏
站内: 虚拟喵——Unity 编辑器扩展总结 五:数组或list集合的显示方式

2.素材使用
unity AssetStore——Unity-Chan! Model

3.根据如下游戏内截图,拆分实现步骤

龙之谷NPC对话系统gif
对话系统

正式开始

一、新建Unity工程,并设置UIPanel

  1. 这里为了 偷懒 方便演示,使用Unity娘到场景中,并复制出两个作为NPC,每个人物模型的Perfab作为空物体**Handle的子物体

图1

  1. NPC1Handle和NPC2Handle分别创建子物体SphereCollider,调整至合适大小,勾选Is Trigger,并重命名为DialogTrigger,Tag选择为NPCDialog,作为主角经过时触发对话界面的触发器

图2

  1. 新建Canvas–>Panel,重命名为DialogPanel,根据个人感觉调整DialogPanel的显示区域
  2. Canvas目录下新建一个空物体,添加GridLayoutGroup组件,用作问答类对话加载答案选项,这里重命名为AnswerGrid
  3. 新建一个空物体,用来挂载接下来的对话系统控制组件,这里重命名为DialogSystem

整体的层级图如下:

层级图

二、调整项目内Player的控制

根据自己的项目情况,让场景内放置好的人物能够实现简单的移动,这里以我下载好的Unity娘为例

  • MainCamera代码:ThirdPersonCamera.cs,找到FixedUpdate ()部分,取消鼠标控制摄像机(因为后边希望通过鼠标左键控制对话进度,当然有需要的同学可以分开写鼠标操作)

FixedUpdate ()

  • Player组件中,去除用不到的组件,并新建一个DialogTriggerEvent.cs,用于控制Player和NPC相遇弹出对话框,具体代码内容稍后再写

DialogTriggerEvent.cs

三、逻辑代码拆分

构思图

四、对话系统控制组件和他的自定义Inspector显示

1.为场景中的对话系统控制器挂载代码(DialogSystemController.cs),控制NPC图片和姓名的显示

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class DialogSystemController : MonoBehaviour
{[SerializeField]public List<NPCItem> npcItemArray = new List<NPCItem>();public static DialogSystemController Instance;void Awake(){Instance = this;}
}[System.Serializable]
public class NPCItem
{[SerializeField]public string ID;[SerializeField]public Sprite icon;[SerializeField]public string name;
}

2.Assets目录下,新建Editor文件夹,新建DialogSystemEditor,依赖于DialogSystemController.cs,自定义Inspector面板显示内容

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;[CustomEditor(typeof(DialogSystemController))]
public class DialogSystemEditor : Editor
{private ReorderableList _npcItemArray;private void OnEnable(){_npcItemArray = new ReorderableList(serializedObject, serializedObject.FindProperty("npcItemArray"), true, true, true, true);//自定义列表名称_npcItemArray.drawHeaderCallback = (Rect rect) =>{GUI.Label(rect, "NPC Array");};//定义元素的高度_npcItemArray.elementHeight = 88;//自定义绘制列表元素_npcItemArray.drawElementCallback = (Rect rect, int index, bool selected, bool focused) =>{//根据index获取对应元素 SerializedProperty item = _npcItemArray.serializedProperty.GetArrayElementAtIndex(index);rect.height -= 4;rect.y += 2;EditorGUI.PropertyField(rect, item, new GUIContent("Index " + index));};//当删除元素时候的回调函数,实现删除元素时,有提示框跳出_npcItemArray.onRemoveCallback = (ReorderableList list) =>{if (EditorUtility.DisplayDialog("Warnning", "Do you want to remove this element?", "Remove", "Cancel")){ReorderableList.defaultBehaviours.DoRemoveButton(list);}};}public override void OnInspectorGUI(){serializedObject.Update();//自动布局绘制列表_npcItemArray.DoLayoutList();serializedObject.ApplyModifiedProperties();}
}

3.通过PropertyDrawer来绘制PlayerItem的样式,注意这是对NPCItem类的绘制,不是DialogSystemController类。同样是编辑器类,需要放在Editor文件夹下

using UnityEngine;
using UnityEditor;
using UnityEngine.UI;[CustomPropertyDrawer(typeof(NPCItem))]
public class DialogSystemDrawer : PropertyDrawer
{public override void OnGUI(Rect position, SerializedProperty property, GUIContent label){using (new EditorGUI.PropertyScope(position, label, property)){//设置属性名宽度EditorGUIUtility.labelWidth = 60;position.height = EditorGUIUtility.singleLineHeight;var iconRect = new Rect(position){width = 64,height = 64};var IDRect = new Rect(position){width = position.width - 80,x = position.x + 80};var nameRect = new Rect(IDRect){y = IDRect.y + EditorGUIUtility.singleLineHeight + 5};var iconProperty = property.FindPropertyRelative("icon");var IDProperty = property.FindPropertyRelative("ID");var nameProperty = property.FindPropertyRelative("name");iconProperty.objectReferenceValue = EditorGUI.ObjectField(iconRect, iconProperty.objectReferenceValue, typeof(Sprite), false);IDProperty.stringValue = EditorGUI.TextField(IDRect, IDProperty.displayName, IDProperty.stringValue);nameProperty.stringValue = EditorGUI.TextField(nameRect, nameProperty.displayName, nameProperty.stringValue);}}
}

效果如下(ID为场景中NPC的具体名字,Icon和Name自定义):

在这里插入图片描述

五、对话系统显示

1.为DialogPanel新增View代码,DialogPanelView.cs,主要任务是接收Conrtoller的代码,显示出对话系统的UI层

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class DialogPanelView : MonoBehaviour
{[Header("Player")]public Image PlayerImage;public Text PlayerName;    public Text PlayerText;[Header("NPC")]public Image NPCImage;public Text NPCName;public Text NPCText;[Header("Panels")]public GameObject playerDialogPanel;public GameObject NPCDialogPanel;public static DialogPanelView Instance;void Awake(){Instance = this;}void Start(){PlayerImage.GetComponent<Image>();        PlayerName.GetComponent<Text>();PlayerText.GetComponent<Text>();NPCImage.GetComponent<Image>();NPCName.GetComponent<Text>();NPCText.GetComponent<Text>();        }
}

2.再次为DialogPanel新增代码,TalkWin.cs,主要任务是通过DOTween插件实现主角和NPC的对话内容显示

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
using System;public class TalkWin : MonoBehaviour
{public int textID;public Text NPCTalkText;public Text playerTalkText;public GameObject answerGrid;public GameObject answer;private List<GameObject> answers = new List<GameObject>();public CommonTalkNode[] commonTalkNodes;public SwitchTalkNode[] switchTalkNodes;private Dictionary<int, CommonTalkNode> commonDic = new Dictionary<int, CommonTalkNode>();private Dictionary<int, SwitchTalkNode> switchDic = new Dictionary<int, SwitchTalkNode>();public static TalkWin instance;private void Awake(){instance = this;foreach (CommonTalkNode item in commonTalkNodes){commonDic.Add(item.ID, item);}foreach (SwitchTalkNode item in switchTalkNodes){switchDic.Add(item.ID, item);}}private void Start(){WhenMouseClick();GetComponent<Button>().onClick.AddListener(WhenMouseClick);}private void WhenMouseClick(){        //NPC为对话模式if (textID > 1000 && textID < 2000){UpdateTalkWinShow(commonDic[textID].NPCTalkText, commonDic[textID].playerTalkText, (float)commonDic[textID].charSpeed);textID = commonDic[textID].nextID;//Debug.Log(textID);}//NPC为问答模式else if (textID > 2000 && textID < 3000){UpdateTalkWinShow(switchDic[textID].NPCTalkText, switchDic[textID].playerTalkText, (float)switchDic[textID].charSpeed);CreateAnswerUI(switchDic[textID].switchText);GetComponent<Button>().interactable = false;}//结束对话,关闭对话Panel//BUG:KeyNotFoundException: The given key was not present in the dictionaryif (commonDic.ContainsKey(commonDic[textID].ID) && commonDic[textID].NPCID != CheckNPCID()){DialogTriggerEvent.Instance.DialogPanel.SetActive(false);}}public void WhenSwitchNodeGetAnswer(int number){textID = switchDic[textID].switchNextID[number];foreach (GameObject item in answers){Destroy(item);}GetComponent<Button>().interactable = true;WhenMouseClick();}public void UpdateTalkWinShow(string NPCTalkText, string playerTalkText, float charSpeed){        if(playerTalkText != "0")   //主角说话时,开启playerPanel,关闭NPCPanel{DialogPanelView.Instance.playerDialogPanel.SetActive(true);DialogPanelView.Instance.NPCDialogPanel.SetActive(false);this.playerTalkText.text = "";this.playerTalkText.DOText(playerTalkText, charSpeed * playerTalkText.Length);}else    //主角不说话时,关闭playerPanel,开启NPCPanel{DialogPanelView.Instance.playerDialogPanel.SetActive(false);DialogPanelView.Instance.NPCDialogPanel.SetActive(true);this.NPCTalkText.text = "";this.NPCTalkText.DOText(NPCTalkText, charSpeed * NPCTalkText.Length);}            }public void CreateAnswerUI(string[] switchText){for (int i = 0; i < switchText.Length; i++){GameObject go = Instantiate(answer, answerGrid.transform);go.GetComponent<QuestionUI>().SetAnswerUI(switchText[i],i);answers.Add(go);}}public string CheckNPCID(){return DialogTriggerEvent.Instance.NPCSingle;}
}
/// <summary>
/// 所有句子的父类
/// </summary>
public abstract class TalkNode
{// 通过NPCID进行定位public string NPCID;// 每个句子独有的IDpublic int ID;//NPC文本public string NPCTalkText;//player文本public string playerTalkText;// 字符速度public double charSpeed;public TalkNode(string NPCID, int ID, string NPCTalkText,string playerTalkText, double charSpeed){this.NPCID = NPCID;this.ID = ID;this.NPCTalkText = NPCTalkText;this.playerTalkText = playerTalkText;this.charSpeed = charSpeed;}
}[Serializable]
public class CommonTalkNode : TalkNode
{public int nextID;public CommonTalkNode(string NPCID , int ID, string NPCTalkText, string playerTalkText, double charSpeed, int nextID) : base(NPCID , ID, NPCTalkText, playerTalkText, charSpeed){this.nextID = nextID;}
}
[Serializable]
public class SwitchTalkNode : TalkNode
{public string[] switchText;public int[] switchNextID;public SwitchTalkNode(string NPCID, int ID, string NPCTalkText, string playerTalkText, double charSpeed, string[] switchText, int[] switchNextID) : base(NPCID, ID, NPCTalkText, playerTalkText, charSpeed){this.switchText = switchText;this.switchNextID = switchNextID;}
}

3.在场景中AnswerGrid下新建Image组件,重命名为Answer,调整至合适大小,并且加入Button组件,并为他挂载新的代码QuestionUI.cs,为其添加一个Text子物体AnswerText,添加为预制体,用于显示问答对话中的答案选项

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class QuestionUI : MonoBehaviour
{public Text text;public int number;public void Start(){GetComponent<Button>().onClick.AddListener(()=> { TalkWin.instance.WhenSwitchNodeGetAnswer(number); });}public void SetAnswerUI(string s,int i){text.text = s;number = i;}
}

Answer
4.Player触发对话系统代码DialogTriggerEvent.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class DialogTriggerEvent : MonoBehaviour
{public GameObject DialogPanel;List<NPCItem> npcList;GameObject[] NPCgameObject;public string NPCSingle = null; //传递到TalkWinpublic static DialogTriggerEvent Instance; void Awake(){Instance = this;}void Start(){NPCgameObject = GameObject.FindGameObjectsWithTag("NPC");if(DialogSystemController.Instance.npcItemArray.Count!=0){npcList = DialogSystemController.Instance.npcItemArray;}}//进入对话区域开启Panle,结束对话关闭Panelprivate void OnTriggerEnter(Collider collider){if(collider.tag == "NPCDialog"){DialogPanel.SetActive(true);GetNPCByID(collider.transform);}}//通过当前Player触发的Trigger判断是在和哪个NPC对话public void GetNPCByID(Transform transform){for (int i = 0; i < NPCgameObject.Length; i++){Transform targetNPC = transform.parent.Find(NPCgameObject[i].name);if(targetNPC)for (int j = 0; j < npcList.Count; j++){if (npcList[j].ID == targetNPC.name){NPCSingle = targetNPC.name; //传递到TalkWin//设置当前NPC的名字和图片DialogPanelView.Instance.NPCName.text = npcList[j].name;DialogPanelView.Instance.NPCImage.sprite = npcList[j].icon;DialogPanelView.Instance.NPCImage.rectTransform.position = new Vector3(289.85f, 292, 0);}                        }}}
}

六、对话内容填写

1.各个代码拖入其需要的GameObject

2.DialogPanel的TalkWin代码中填写对话内容

这里需要注意几点:

  • TalkWin.cs类中包括两个Dictionary,其中 Dictionary<int, CommonTalkNode> commonDic 用于存储正常你一句我一句的对话内容,Dictionary<int, SwitchTalkNode> switchDic 用于存储问答式的对话内容

  • NPC讲话时,playerTalkText的值填写0

  • Player讲话时,NPCTalkText的值什么也不填写

  • 对话结束后应跳转到一个两人都没有讲话内容的一条对话,保证在最后一次对话显示后,正常关闭对话UI

以下长图为我填写的对话内容,尤其注意ID 1006,ID 1108,ID 1201在这里是必须的

长图1
长图2
长图3

效果展示:

首先和NPC1对话

效果图1

和NPC1对话完再去找NPC2

效果图2

写在最后:

1.总体缺点比较明显,NPC多了再去代码的列表里修改内容,就太麻烦了,不好查改
2.只适合于和一个NPC的对话内容一次讲完的设定,如果和黑魂一样可以对话到一半走开,就不行了
3.两个Dictionary之间交换数据时unity会报错,目前还没有找到解决方案,具体BUG位置写在了TalkWin.cs中,欢迎道友指正
4.项目地址:Simple dialog system 门钥匙:hhhh


http://www.ppmy.cn/news/394944.html

相关文章

龙之谷和服务器中断,龙之谷2手游进不去怎么办 游戏进不去断开连接解决方法[多图]...

龙之谷2手游目前已经开服&#xff0c;很多玩家都发现进不去游戏&#xff0c;那么遇到这个问题如何解决呢&#xff0c;同时如何才能登陆游戏&#xff0c;下面一起来看看吧。 龙之谷2游戏进不去断开连接解决方法 1.登录超时 在登录游戏时&#xff0c;若遇到带有“登录超时”字样的…

龙之谷怎么进去服务器维护,龙之谷2手游进不去怎么办?龙之谷2登录超时解决方法...

龙之谷2手游进不去怎么办&#xff1f;有不少玩家发现龙之谷2游戏登不上&#xff0c;甚至有卡退的情况&#xff0c;下面小编为大家带来相关问题的解决办法&#xff0c;感兴趣的小伙伴快来一起看看吧&#xff01; 1.登录超时 在登录游戏时&#xff0c;若遇到带有“登录超时”字样…

win7 php乱码,打开网站php出现乱码问题的解决办法nsiserror解决办法win7黑屏解决办法龙之谷报错解决办...

问题&#xff1a;用浏览器打开写好的php文件&#xff0c;然后发现中文字符显示为问号&#xff0c;网站显示不正常 原因&#xff1a;出现乱码的问题的原因是数据库&#xff0c;浏览器和php文件采用的编码方式不相同 解决办法&#xff1a; 1.浏览器编码方式修改&#xff1a;(以谷…

AMD三核、六核安装SQL2000

主板&#xff1a;微星MAG B550M MORTAR 迫击炮 CPU&#xff1a;AMD锐龙R5 5600G 公司旧电脑坏了&#xff0c;年前装的电脑&#xff0c;用来装SQL管家婆的。 SQL2000在win10系统中装不上&#xff0c;后来装的SQL2005&#xff0c;总算可以运行管家婆了&#xff0c;但是BUG太多…

岗位介绍-引子(能力三核模型)

能力三核简介 能力三核 知识&#xff0c;就是你所懂的东西&#xff0c;需要有意识的、专门的学习和记忆才能获得&#xff0c;常常与我们的专业学习或工作内容相关&#xff0c;一般用户名词表示。广度和深度是它的评价标准&#xff0c;它的重要性常常被求职者夸大。知识不可迁移…

RK3588旗舰32T人工智能多网口边缘智能网关交换机

32T边缘智能网关发布&#xff0c;助力多行业数字化升级&#xff0c;运维降本增效&#xff0c;搭载RK3588旗舰芯 搭载瑞芯微RK3588芯片的边缘智能网关XM-RK3588&#xff0c;算力可扩展至32T&#xff0c;适用于电力能源、智慧交通、智慧城市、智慧安防、智慧医疗、工业互联网等领…

大赛报名 | 免费体验V853芯片!“华秋电子X全志在线开源硬件设计大赛”开始报名啦

由华秋电子联手全志在线发起的专属硬件工程师线上设计大赛来啦&#xff01;报名免费申请 V853芯片 pcb打板、贴片等服务项目赞助 &#xff0c;还有官方荣誉证书&#xff0c;专家团队指导等众多福利&#xff0c;等你来挑战&#xff01; 大赛背景 现如今&#xff0c;信息技术高…

三、中央处理器

功能 1、处理指令&#xff1a;完成取指令、分析指令和执行指令的操作&#xff0c;即程序的顺序控制 2、执行操作&#xff1a;条指令的功能往往由若干操作信号的组合来实现。CPU管理并产生由内存 取出的每条指令的操作信号&#xff0c;把各种操作信号送往相应的部件&#xff0c…