【Unity】2D 对话模块的实现

news2024/9/20 15:26:32

对话模块主要参考 【Unity教程】剧情对话系统 实现。

在这次模块的构建将基于 unity ui 组件 和 C#代码实现一个从excel 文件中按照相应规则读取数据并展示的逻辑。这套代码不仅能实现正常的对话,也实现了对话中可以通过选择不同选项达到不同效果的分支对话功能。

整一套代码分为分为两部分,一部分和库存模块一样通过 Collider 2D 和 Unity Event 构建了一个范围内可互动的功能,这一部分可以参考之前的库存模块。

剩下一部分就是对话框模块整体逻辑,先看一下效果:

在这里插入图片描述

在这里插入图片描述

从上图中,可以看出整个对话框可以分为五个部分:头像、说话人名字、普通对话内容、跳转到下一句的按钮、和 选择对话框。可以简单将 普通对话内容和跳转按钮 划分成一个逻辑组件,而选择对话框划分成另一个逻辑组件,这样划分的原因在于两者实现方式不一样。

接下来内容将分为三个段落,我将自下而上一一实现:从Excel中读取对话内容的方式、普通对话实现 和 选择对话 这三个步骤:

从Excel中读取对话内容的方式

整一个Excel的内容分为了五个部分,如下图所述,分别是、

对话的id,用来快速定位到一个对话类,可以方便我们进行查找和使用

说话人的名字,用来展示在 对话框中的UI组件

说话人的头像,也是用来展示在对话框中的 UI 和 名字不同的是他是代表一个文件的路径

对话内容,用来展示在 对话框中的UI组件

下一句对话id,用来做跳转使用

在这里插入图片描述

定义好具体数据的Excel之后,需要将Excel导出成Unity可以识别的编码格式,否则在Unity中会被识别成乱码(这一步可以通过 txt 文本另存为的方式进行变更)。将另存为的文本保存在unity 项目中的 Assets/Resources/Dialogue/路径下,以便项目能够读取到。

在这里插入图片描述

接下来,在C#中定义一个对应的class,用来接住Excel读取出的数据,并在C#中通过Resources.Load的方法来读取。Resoruces.Load的方法会将整个文本以 string的方式进行读取,所以还需要对每一行文本进行拆分,才能处理成我们需要的对话类。最终,将 对话 id 和 对话实体保存到一个 dictionary里,方便后面的步骤进行调用。


    // 对话类
    class Dialogue
    {
        public int dialogueId;
        public string characterName;
        public string characterIcon;
        public string dialogue;
        public List<int> toDialogueIdList;

        public Dialogue(int dialogueId, string characterName, string characterIcon, string dialogue, List<int> toDialogueIdList)
        {
            this.dialogueId = dialogueId;
            this.characterName = characterName;
            this.characterIcon = characterIcon;
            this.dialogue = dialogue;
            this.toDialogueIdList = toDialogueIdList;
        }
    }
    
//

    public void eventDialogueFileProcess(int eventId)
    {   
        // 文件处理成 对话 id 和 对话对象 的映射
            dialogueDictionary.Clear();
            string filePath = dialogueFilePrefix + eventId;
            TextAsset textFile = Resources.Load<TextAsset>(filePath);
            processDialogueFile(textFile);    }

    private void processDialogueFile(TextAsset dialogueFile)
    {
        string[] dialogueArrays = dialogueFile.text.Split("\r\n");
        foreach(string strDialogue in dialogueArrays)
        {
            if(string.IsNullOrEmpty(strDialogue))
            {
                continue;
            }
            string[] cols = strDialogue.Split(',');
            string[] strToDialogueIds = cols[4].Split('|');
            List<int> toDialogueIdList = new List<int>();
            foreach(string strToDialogueId in strToDialogueIds)
            {   
                if(string.IsNullOrEmpty(strToDialogueId))
                {
                    break;
                }
                Debug.Log(strToDialogueId);
                toDialogueIdList.Add(int.Parse(strToDialogueId));
            }
            Dialogue dialogue = new Dialogue(int.Parse(cols[0]), cols[1], cols[2], cols[3], toDialogueIdList);
            dialogueDictionary.Add(dialogue.dialogueId, dialogue);
        }
    }
    

普通对话实现

普通对话实现的主要方式就是将当前对话的 id 保存为一个成员对象,这样在触发按钮的点击事件之后,便能通过事件监听的方式调用这个对话id来获取下一段对话。


            // 正常对话
            int nextDialogueId = dialogueIdList[0];
            if(dialogueDictionary.TryGetValue(nextDialogueId, out Dialogue dialogue))
            {
                // 隐藏多选
                dialogueMutliBG.enabled = false;
                // 显示正常对话
                dialogueText.enabled = true;
                nextButton.gameObject.SetActive(true);
                characterIcon.sprite = Resources.Load<Sprite>(characterIconPrefix + dialogue.characterIcon);
                characterName.text = dialogue.characterName;
                dialogueText.text = dialogue.dialogue;
                currentDialogueIndex = dialogue.dialogueId; 
            }

选择对话

选择对话相比普通对话来说,实现有一些复杂,主要在于需要用C# 的 delegate 代理一个函数,来达成下一步对话的操作。这样做的原因在于选择对话每一个对话都会有一个下一个对话的选项,导致没有办法直接使用普通对话定义的当前对话id变量。


            // 可选对话选择
            List<Dialogue> dialogues = new List<Dialogue>();
            foreach(int index in dialogueIdList)
            {
                if(dialogueDictionary.TryGetValue(index, out Dialogue dialogue))
                {
                    dialogues.Add(dialogue);
                }
            }
            // 隐藏正常对话
            dialogueText.enabled = false;
            nextButton.gameObject.SetActive(false);
            // 显示多选
            dialogueMutliBG.enabled = true;
            foreach(Dialogue dialogue in dialogues)
            {
                // 依次生成对话 Button
                optionButton = Instantiate(optionButton, dialogueMutliBG.transform);
                TextMeshProUGUI textMeshPro = optionButton.GetComponentInChildren<TextMeshProUGUI>();
                textMeshPro.text = dialogue.dialogue;
                // 添加事件监听
                optionButton.GetComponent<Button>().onClick.AddListener(delegate 
                    {
                        // 点完以后 销毁选择对话框
                        Button[] optionalButtons = dialogueMutliBG.GetComponentsInChildren<Button>();
                        foreach(Button button in optionalButtons)
                        {
                            Destroy(button.gameObject);
                        }
                        // 显示下一句对话
                        showDialogue(dialogue.toDialogueIdList);
                        
                        
                    });
            }

将这两部分合入一个函数中,通过下一段对话id 的数量进行判断到底是 普通对话还是选择对话,合并后的代码如下:


using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class DialogueControl : MonoBehaviour
{

    class Dialogue
    {
        public int dialogueId;
        public string characterName;
        public string characterIcon;
        public string dialogue;
        public List<int> toDialogueIdList;

        public Dialogue(int dialogueId, string characterName, string characterIcon, string dialogue, List<int> toDialogueIdList)
        {
            this.dialogueId = dialogueId;
            this.characterName = characterName;
            this.characterIcon = characterIcon;
            this.dialogue = dialogue;
            this.toDialogueIdList = toDialogueIdList;
        }
    }

    public Image characterIcon;
    public TextMeshProUGUI characterName;
    public TextMeshProUGUI dialogueText;

    private static string dialogueFilePrefix = "Dialogue/event_dialogue_";
    private static string characterIconPrefix = "Character/";
    private Dictionary<int, Dialogue> dialogueDictionary = new Dictionary<int, Dialogue>();

    private int currentDialogueIndex = 1;

    public Image dialogueMutliBG;
    public Button optionButton;
    public Button nextButton;

    public int lastEvent;

    // Start is called before the first frame update
    void Start()
    {
        transform.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public void eventDialogueFileProcess(int eventId)
    {   
        transform.gameObject.SetActive(true);
        // 文件处理成 对话 id 和 对话对象 的映射
        if(!lastEvent.Equals(eventId))
        {
            dialogueDictionary.Clear();
            string filePath = dialogueFilePrefix + eventId;
            TextAsset textFile = Resources.Load<TextAsset>(filePath);
            processDialogueFile(textFile);
        }

        // 显示第一段对话
        List<int> dialogueIdList = new List<int>(1);
        dialogueIdList.Add(1);
        showDialogue(dialogueIdList);
        lastEvent = eventId;
    }

    private void processDialogueFile(TextAsset dialogueFile)
    {
        string[] dialogueArrays = dialogueFile.text.Split("\r\n");
        foreach(string strDialogue in dialogueArrays)
        {
            if(string.IsNullOrEmpty(strDialogue))
            {
                continue;
            }
            string[] cols = strDialogue.Split(',');
            string[] strToDialogueIds = cols[4].Split('|');
            List<int> toDialogueIdList = new List<int>();
            foreach(string strToDialogueId in strToDialogueIds)
            {   
                if(string.IsNullOrEmpty(strToDialogueId))
                {
                    break;
                }
                Debug.Log(strToDialogueId);
                toDialogueIdList.Add(int.Parse(strToDialogueId));
            }
            Dialogue dialogue = new Dialogue(int.Parse(cols[0]), cols[1], cols[2], cols[3], toDialogueIdList);
            dialogueDictionary.Add(dialogue.dialogueId, dialogue);
        }
    }

    private void showDialogue(List<int> dialogueIdList)
    {
        if(dialogueIdList.Count == 0)
        {
            return;
        }

        if(dialogueIdList.Count > 1)
        {
            // 可选对话选择
            List<Dialogue> dialogues = new List<Dialogue>();
            foreach(int index in dialogueIdList)
            {
                if(dialogueDictionary.TryGetValue(index, out Dialogue dialogue))
                {
                    dialogues.Add(dialogue);
                }
            }
            // 隐藏正常对话
            dialogueText.enabled = false;
            nextButton.gameObject.SetActive(false);
            // 显示多选
            dialogueMutliBG.enabled = true;
            foreach(Dialogue dialogue in dialogues)
            {
                // 依次生成对话 Button
                optionButton = Instantiate(optionButton, dialogueMutliBG.transform);
                TextMeshProUGUI textMeshPro = optionButton.GetComponentInChildren<TextMeshProUGUI>();
                textMeshPro.text = dialogue.dialogue;
                // 添加事件监听
                optionButton.GetComponent<Button>().onClick.AddListener(delegate 
                    {
                        // 点完以后 销毁选择对话框
                        Button[] optionalButtons = dialogueMutliBG.GetComponentsInChildren<Button>();
                        foreach(Button button in optionalButtons)
                        {
                            Destroy(button.gameObject);
                        }
                        // 显示下一句对话
                        showDialogue(dialogue.toDialogueIdList);
                        
                        
                    });
            }
        }
        else 
        {
            // 正常对话
            int nextDialogueId = dialogueIdList[0];
            if(dialogueDictionary.TryGetValue(nextDialogueId, out Dialogue dialogue))
            {
                // 隐藏多选
                dialogueMutliBG.enabled = false;
                // 显示正常对话
                dialogueText.enabled = true;
                nextButton.gameObject.SetActive(true);
                characterIcon.sprite = Resources.Load<Sprite>(characterIconPrefix + dialogue.characterIcon);
                characterName.text = dialogue.characterName;
                dialogueText.text = dialogue.dialogue;
                currentDialogueIndex = dialogue.dialogueId;
                
            }
        }
    }

    public void nextDialogue()
    {
        if(dialogueDictionary.TryGetValue(currentDialogueIndex, out Dialogue dialogue))
        {
            if(dialogue.toDialogueIdList.Count == 0)
            {
                transform.gameObject.SetActive(false);
                nextButton.gameObject.SetActive(false);
            }
            showDialogue(dialogue.toDialogueIdList);
        }
    }

}

来看一下最终效果

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1005618.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器视觉康耐视visionpro-CogFitLineTool拟合直线错误上的理解

视频详细解答&#xff1a;康耐视VisionPro高级脚本系列教程-2.拟合指教脚本遍历编写 什么是直线&#xff1f; 直线&#xff1a;那么直线是由无线的点组成的。特点是无端点&#xff1b;不能度量&#xff0c;两边无限延长。错误示例子如下&#xff1a; 首先上面是错误的理解&…

UG\NX CAM二次开发 设置工序切削区域 UF_CAMGEOM_append_items

文章作者:代工 来源网站:NX CAM二次开发专栏 简介: UG\NX CAM二次开发 设置工序切削区域 UF_CAMGEOM_append_items 效果: 代码: static int init_proc(UF_UI_selection_p_t select, void* user_data) { int errorCode = 0; int num_triples = 1;//UF_UI_mas…

来可LCWLAN-600P产品使用和常见问题说明

01LCWLAN-600P简介 LCWLAN-600P是来可电子最新生产的一款CAN转WiFi设备&#xff0c;该设备的主要功能是将CAN数据转换成网络数据并通过无线网络转发出去。设备支持8~30V宽压供电&#xff0c;出厂默认配置为AP模式&#xff0c;设备供电后可在电脑的WiFi搜索栏搜索到名称为LCWLA…

2023年意大利富时MIB指数研究报告

第一章 指数概况 1.1 指数基本情况 富时MIB指数&#xff08;意大利语&#xff1a;Financial Times Stock Exchange Milano Indice di Borsa, FTSE MIB&#xff09;&#xff0c;2009年6月之前称为S&P/MIB&#xff0c;是意大利证券交易所的基准股票市场指数。该指数于2004年…

[其他]IDEA中Maven项目配置国内源

配置国内源主要解决了,在maven项目中pom.xml下载jar包失败或过慢的问题. 在IDEA中的设置分成两种,设置当前项目与新创项目. 我们就需要两种都进行设置,不然只有在当前项目配置了国内源,新创项目的时候还是默认的状态. 由于下面两种设置的界面与方法是一致的,下面以当前项目设…

jmeter采集ELK平台海量业务日志( 采用Scroll)

由于性能测试需要&#xff0c;需采集某业务系统海量日志&#xff08;百万以上&#xff09;来使用&#xff0c;做稳定性压测使用。但Elasticsearch的结果分页size单次最大为10000&#xff08;运维同事为保证ES安全&#xff09;。为了能够快速采集ELK平台业务日志&#xff0c;可以…

Python解释器和Pycharm的傻瓜式安装部署

给我家憨憨写的python教程 有惊喜等你找噢 ——雁丘 Python解释器Pycharm的安装部署 关于本专栏一 Python解释器1.1 使用命令提示符编写Python程序1.2 用记事本编写Python程序 二 Pycharm的安装三 Pycharm的部署四 Pycharm基础使用技巧4.1 修改主题颜色4.2 修改字体4.3 快速修…

# (1462. 课程表 IV leetcode)广搜+拓扑-------------------Java实现

&#xff08;1462. 课程表 IV leetcode&#xff09;广搜拓扑-------------------Java实现 题目表述 你总共需要上 numCourses 门课&#xff0c;课程编号依次为 0 到 numCourses-1 。你会得到一个数组 prerequisite &#xff0c;其中 prerequisites[i] [ai, bi] 表示如果你想…

【Fiddler】mac m1 机器上使用 fiddler 抓取接口

mac m1 机器上使用 fiddler 抓取接口&#xff08;非虚拟机模式&#xff09; author: jwensh date:2023.09.12 文章目录 mac m1 机器上使用 fiddler 抓取接口&#xff08;非虚拟机模式&#xff09;1. 环境准备2. 进行配置3. 使用情况 1. 环境准备 想要抓取 mac 上浏览器的接口&a…

香橙派配合IIC驱动OLED 使用SourceInsight解读源码

OLED屏幕介绍 & 硬件接线 OLED也是老熟人了&#xff0c;详细的介绍见&#xff1a; IIC 协议 和 OLED_iic oled-CSDN博客 再次回顾香橙派硬件接线&#xff1a; 根据之前的学习了解到&#xff0c;OLED屏幕的驱动是需要IIC协议的&#xff0c;在学习C52时我使用了代码模拟IIC协…

C++之map如何实例化类对象(一百九十九)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

总结 NAT 机制的工作流程及优缺点

什么是NAT NAT定义 **NAT&#xff08;Network Address Translator&#xff0c;网络地址转换&#xff09;**是用于在本地网络中使用私有地址,在连接互联网时转而使用全局IP地址的技术. 实际上是为解决IPv4地址短缺而开发的技术: NAT技术作为当前解决IP地址不够用的主要手段&a…

pgzrun 拼图游戏制作过程详解(3)

3. 绘制完整的拼图 建立Gird列表存储小拼图的基本信息 Gird[] for i in range(6):for j in range(4):SquareActor("girl_06")Square.leftSquare_size*jSquare.topSquare_size*iGird.append(Square) 修改draw()绘制函数 建立循环绘制Gird列表中的所有小拼图 def d…

laravel安装初步使用学习 composer安装

一、什么是laravel框架 Laravel框架可以开发各种不同类型的项目&#xff0c;内容管理系统&#xff08;Content Management System&#xff0c;CMS&#xff09;是一种比较典型的项目&#xff0c;常见的网站类型&#xff08;如门户、新闻、博客、文章等&#xff09;都可以利用CM…

stm32---基本定时器(TIM6,TIM7)

STM32F1的定时器非常多&#xff0c;由两个基本定时器&#xff08;TIM6&#xff0c;TIM7&#xff09;、4个通用定时器&#xff08;TIM2-TIM5&#xff09;和两个高级定时器&#xff08;TIM&#xff11;&#xff0c;TIM&#xff18;&#xff09;组成。基本定时器的功能最为简单&am…

优维低代码实践:菜单

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 优维…

并查集基础与二分搜索树的特性

并查集基础 一、概念及其介绍 并查集是一种树型的数据结构&#xff0c;用于处理一些不相交集合的合并及查询问题。 并查集的思想是用一个数组表示了整片森林&#xff08;parent&#xff09;&#xff0c;树的根节点唯一标识了一个集合&#xff0c;我们只要找到了某个元素的的…

卫星物联网生态建设全面加速,如何抓住机遇?

当前&#xff0c;卫星通信无疑是行业最热门的话题之一。近期发布的华为Mate 60 Pro“向上捅破天”技术再次升级&#xff0c;成为全球首款支持卫星通话的大众智能手机&#xff0c;支持拨打和接听卫星电话&#xff0c;还可自由编辑卫星消息。 据悉&#xff0c;华为手机的卫星通话…

开源日报 0829 | 改变面试方式:拒绝死板问题,推崇真实情境

poteto/hiring-without-whiteboards Stars: 38.3k License: MIT 这个项目是一个不进行 “白板” 面试的公司 (或团队) 的列表。“白板” 在这里被用作一种隐喻&#xff0c;代表与糟糕的面试实践相关联的 CS 知识问答题。该项目中列出的公司和团队使用类似日常工作情境下的面试…

成功解决Selenium 中116版本的chromedriver找不到问题

Selenium 中的Google&#xff08;谷歌浏览器&#xff09;最新版本chromedriver 文章目录 Selenium 中的Google&#xff08;谷歌浏览器&#xff09;最新版本chromedriver1.当前作者的谷歌浏览器版本2.当前驱动官网的最新版本3.当不想降低浏览器版本继续使用谷歌浏览器的办法 1.当…