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

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

写在前边:

1.参考文章

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

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

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


正式开始

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

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

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

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

整体的层级图如下:

二、调整项目内Player的控制

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

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

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

三、逻辑代码拆分

四、对话系统控制组件和他的自定义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;}
}


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在这里是必须的



效果展示:

首先和NPC1对话

和NPC1对话完再去找NPC2

写在最后:

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

Unity 简单RPG对话系统——龙之谷2的NPC对话系统相关推荐

  1. 欢乐互娱庞池海:《龙之谷》项目性能优化经验分享

    欢乐互娱庞池海:<龙之谷>项目性能优化经验分享 在5月12日,UNITY 2017案例分享专场上,欢乐互娱技术引擎开发工程师娱庞池海分享了<龙之谷>项目性能优化经验.以下为分享 ...

  2. Unity模拟龙之谷人物行走简单控制

    我个人挺喜欢龙之谷(DN)的人物控制的(不是广告哈....),就是人物太萌了一点,动作.打击感都挺好的. 今天用Unity简单模仿了一下DN的人物控制,当然,游戏里面动作很多,我这里只做了简单的wal ...

  3. Unity模拟龙之谷人物控制(二)

    接着上一篇:Unity模拟龙之谷人物行走简单控制 这一篇是对上一篇的扩展,记下我的学习记录吧!避免遗忘. 这一篇对上一篇中的动作状态进行了扩展,主要是submachine的运用. 看下状态机吧: 关键 ...

  4. HTML5期末大作业:龙之谷网络游戏网站设计——龙之谷网络游戏(9页) 学生网游网页设计模板 大学生HTML网页制作作品 简单手游网页设计成品 dreamweaver学生网站模板

    HTML5期末大作业:龙之谷网络游戏网站设计--龙之谷网络游戏(9页) 学生网游网页设计模板 大学生HTML网页制作作品 简单手游网页设计成品 dreamweaver学生网站模板 常见网页设计作业题材 ...

  5. HTML+CSS+JS——龙之谷网络游戏(9页) 学生网游网页设计模板 大学生HTML网页制作作品 简单手游网页设计成品 dreamweaver学生网站模板

    HTML5期末大作业 文章目录 HTML5期末大作业 一.作品展示 二.文件目录 三.代码实现 四.获取更多源码 一.作品展示 二.文件目录 三.代码实现 <!DOCTYPE html> ...

  6. 阻击外挂——《龙之谷手游》安全测试的那点事

    作者:sheldon,腾讯游戏漏洞测试高级工程师 商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 原文链接:http://wetest.qq.com/lab/view/302.html ...

  7. 龙之谷手游WebVR技术分享

    目录: 一.项目体验 1.1.项目简介 1.2.功能介绍 1.3.游戏体验 二.技术方案 2.1.为什么使用WebVR 2.2.常用的WebVR解决方案 2.2.1.Mozilla的A-Frame方案 ...

  8. 使用Unity开发RPG游戏完整指南(全)

    使用Unity开发RPG游戏完整指南(全) - GameRes游资网 关注公众号 风色年代(itfantasycc) 200G Unity资料合集送上~ 本教程教大家如何使用Unity创建一个RPG游 ...

  9. 龙之谷2微信哪个服务器,龙之谷2微信区

    龙之谷2微信区是一款由腾讯出品原班人马打造的龙之谷正统续作动作奇幻RPG手游.找到属于自己的快乐与故事!曾经许下的约定,终于到了实现的时刻,龙之谷再登移动端,超爽快的动作体验,绚丽的技能特效,海量免费 ...

  10. HTML5期末大作业:网站——仿游戏官网(龙之谷)HTML+CSS+JavaScript

    HTML5期末大作业:网站--仿游戏官网(龙之谷)HTML+CSS+JavaScript ~ 学生HTML个人网页作业作品下载 ~ web课程设计网页规划与设计 ~大学生个人网站作业模板 ~简单个人网 ...

最新文章

  1. mysql可以做日期处理吗_mysql可以做日期处理吗
  2. 【ICML2021】计算机视觉中的自注意力机制教程,谷歌伯克利出品
  3. 一个普通80后的IT Pro去溜冰的感慨
  4. python对外发布的时间_Python如何进行时间处理
  5. lsof查看占用高_linux lsof命令查看文件占用进程
  6. 在线HTML编辑器使用入门(Kindeditor)
  7. mysql中两根竖线什么意思_SQL如何查询表字段值以竖线分割的数据
  8. set在python中什么意思_python中set是什么意思
  9. 蓝桥杯-9-1九宫格(java)
  10. 怎么在HTML中加个日历,HTML中如何添加日历插件(JQUERY)
  11. NetCore2.x 使用Log4Net(一)
  12. 【实战】使用Python部署机器学习模型的10个实践经验
  13. 元宵节快乐 | 启明云端邀您一起猜灯谜了
  14. 机器学习——SVM之交叉验证对参数(C,gamma)进行优化以及选择
  15. JavaFX UI控件教程(二十四)之Password Field
  16. [css] box-sizing常用的属性有哪些?分别有什么作用?
  17. linux搭建Django环境,linux下搭建python Django环境及启动一个示例
  18. PDFLib去水印办法
  19. cad刷新快捷键_47个快捷键、50个CAD技巧。
  20. 【产品】蓝绿发布、滚动发布和灰度发布对比

热门文章

  1. 如何安装windows NT虚拟机
  2. spark学习9:sparkStreaming
  3. 视频直播 linux 推流,linux rtmp服务器搭建推流
  4. stm32之蓝牙模块HC-05使用
  5. 明清徽州宗族的异姓承继
  6. 让信息带钩的六种武器——《让创意更有黏性》优秀读后感4300字
  7. 普通硬盘和固态硬盘的区别?
  8. 代码质量分析利器之SonarQube【史上最全】
  9. 网站速度这样优化,让你的网站飞起来
  10. 【数字信号处理】离散时间信号 ( 离散时间信号 与 连续时间信号 关系 | 序列表示法 | 列表法 | 函数表示法 | 图示法 )