当前位置: 首页 > news >正文

开发笔记 | 优化对话管理器脚本与对话语音的实现

上回书说到,我们的对话系统目前只是实现了基础的功能,而且这些脚本是想到什么写什么,结构没什么规律,在未来想要扩展可就麻烦了,所以这篇文章就来记录一下代码的优化吧!


优化对话标志分支

从首先我们先从Advance方法入手,此前我们的cells的索引直接用数字表示,我们可以将其更改为常量。由于 C# 中并没有头文件的概念,所以我们将他们写入一个静态类中。

Advance方法的主要问题是对话行的创建和不同标志的运行方法杂糅在了一起,并不符合开闭原则。Advance方法总体来说的作用是这样的:找出对应对话行、执行各自对应的方法。所以很适合使用行为型模式策略模式来优化。

之前我们的对话行就是单纯的包含逗号的字符串,这样每次还得通过逗号进行分隔,所以我们可以新建一个对话行类用来存储这些数据,同时方便扩展。

此时我们的对话行列表可以替换为字典了,这样一来就不必每次调用Advance方法都遍历了。

在Awake中初始化字典时,要防止汉语行或者空行进入字典,我们可以使用int.TryParse转化,其返回bool值。注意在编写构造函数时,要区别一下“END”的对话行,其无法转化“跳转”编号。

之前更新选项的方法就可以写成这样。然后接下来我们就可以在另一个脚本中试着编写策略模式了。

这是我优化后的脚本。

//策略接口
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
interface DialogueUpdate
{public void LineUpdate(DialogueLine line, DialogueManager manager);
}
//上下文类
public class DifferentSymbols
{DialogueManager manager = null;//C#声明接口时,get和set表示外部可读可写该属性DialogueUpdate DialogueUpdate { get; set; }//字典类,symbol对应一个DifferentSymbol脚本中的DialogueUpdate类Dictionary<string, DialogueUpdate> differentSymbolAnalysis =new Dictionary<string, DialogueUpdate>{{"W", new UpdateW()},{"T", new UpdateT()},{"O", new UpdateO()},{"END", new UpdateEND()}};//用于执行的方法public void DialogueLineAnalysis(DialogueLine line){DialogueUpdate = differentSymbolAnalysis[line.symbol];DialogueUpdate.LineUpdate(line, manager);}/*构造函数*///一个DifferentSymbols绑定一个manager类public DifferentSymbols(DialogueManager _manager) {//让我们的manager指向一个DialogueManager类manager = _manager;}
}
/*具体策略*/
public class UpdateW : DialogueUpdate
{public void LineUpdate(DialogueLine line, DialogueManager manager){Upd(manager.FindCha(line.name), line.content, manager);manager.dialogueIndex = line.jump;}void Upd(Character character, string newContent, DialogueManager manager){if (character.portrait == null){manager.portrait.enabled = false;manager.avatar.enabled = false;}else{manager.portrait.enabled = true;manager.avatar.enabled = true;}manager.portrait.sprite = character.portrait;manager.avatar.sprite = character.portrait;manager.characterName.text = "【" + character.name + "】";manager.content.text = "  " + newContent;}
}
public class UpdateT : DialogueUpdate
{public void LineUpdate(DialogueLine line, DialogueManager manager){Upd(line.content, manager);manager.dialogueIndex = line.jump;}void Upd(string newContent, DialogueManager manager){manager.portrait.enabled = false;manager.avatar.enabled = false;manager.characterName.text = "";manager.content.text = "  " + newContent;}
}
public class UpdateO : DialogueUpdate
{public void LineUpdate(DialogueLine line, DialogueManager manager){manager.parentGroup.gameObject.SetActive(true);Upd(manager.dialogueIndex, manager);}void Upd(int index, DialogueManager manager){DialogueLine line = manager.dialogueLines[index];if (line.symbol != "O"){return;}//如果类不继承于MonoBehavior,则使用UnityEngine.Object.InstantiateGameObject option = UnityEngine.Object.Instantiate(manager.optionPref, manager.parentGroup);option.GetComponentInChildren<TMP_Text>().text = line.content;option.GetComponent<Button>().onClick.AddListener(delegate { OptionJump(line.jump, manager); });Upd(index + 1, manager);}void OptionJump(int target, DialogueManager manager){manager.parentGroup.gameObject.SetActive(false);manager.dialogueIndex = target;manager.Advance();}
}
public class UpdateEND : DialogueUpdate
{public void LineUpdate(DialogueLine line, DialogueManager manager){Application.Quit();}
}

这样一来,我们的代码就拥有了更好的扩展性,符合单一职责原则。

对话音频的加入

这里可以新建一个AudioDialogue类,用来存储对话行索引与音频片段的对应关系。

至于什么时候、播放什么音频片段,我们可以试着使用观察者模式,当我们对话发生变化时,向作为观察者的“音频管理器”发送消息播放音频,从而达到播放音频的效果。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.TextCore.Text;
//采用观察者模式,在日后加入多个观察者时再加入观察者的接口
public class AudioManager : MonoBehaviour
{AudioSource audioSource;//要执行的功能与dialogueManager的某些参数有关呢DialogueManager dialogueManager;[Header("对话音频列表")]public List<AudioDialogue> audioDialogues;void Awake(){audioSource = GetComponent<AudioSource>();dialogueManager = GetComponent<DialogueManager>();}public void executeUpdate(){if(FindAud(dialogueManager.dialogueIndex) != null){audioSource.clip = FindAud(dialogueManager.dialogueIndex).clip;audioSource.Play();}}//通过对话索引寻找列表中对应音频剪辑public AudioDialogue FindAud(int index){foreach (AudioDialogue audioDialogue in audioDialogues){if (audioDialogue.dialogueLineIndex == index){return audioDialogue;}}return null;}
}

​​​​​​​在这里,我们就将AudioManager这个类作为一个具体的观察者。如果后续需要更多的观察者,可以为executeUpdate方法提供一个接口,方便后续扩展。

之前的DialogueManager类就充当主题角色,包含通知观察者的方法。这样一来角色语音的功能就大功告成了,在挂载好脚本之后,我们只需要将对话行的索引以及mp4文件对应好就行了。

打字机效果

众所周知,我们的视觉小说只有加上打字机效果才不会显得僵硬。

在Unity的UI组件中,网格结构是实现UI可视化的核心结构,是字型渲染的基础。所以我们可以通过一定间隔时间,改变字符显示的最大数量并进行网格的刷新(ForceMeshUpdate)来实现打字机效果。

具体代码可以通过协程来实现。

    //执行打字机效果void ExecuteTypeText(){if (typeTextCoroutine != null){StopCoroutine(typeTextCoroutine);typeTextCoroutine = null;}typeTextCoroutine = StartCoroutine(TypeText());}//打字机效果IEnumerator TypeText(){//刷新网格content.ForceMeshUpdate();int total = content.textInfo.characterCount;int current = 0;while (current <= total){content.maxVisibleCharacters = current;current++;yield return new WaitForSeconds(intervalTime);}}

​​​​​​​之后就在Advance方法里更新文本框后直接调用执行打字效果的方法就好了。

最后我们的打字机效果也是成功实现了。


小结

总的来说,这次的优化紧随着上次的对话系统,添加了打字机效果和对话语音,新的unity包也在github上更新了。

如有补充交流欢迎留言。

参考列表:

使用Text Mesh Pro 实现打字机效果(bilibili):BV1Wr4y147nD

http://www.lryc.cn/news/600115.html

相关文章:

  • 13.使用C连接mysql
  • Jenkins中出现pytest: error: unrecognized arguments: --alluredir=report错误解决办法
  • 栈----1.有效的括号
  • 机器学习 KNN 算法,鸢尾花案例
  • 从Taro的Dialog.open出发,学习远程控制组件之【事件驱动】
  • C++ 多线程同步机制详解:互斥锁、条件变量与原子操作
  • Python Multiprocessing 进程池完全教程:从理论到实战
  • 数据结构(3)单链表
  • [尚庭公寓]14-找房模块
  • Canal 1.1.7的安装
  • 习题5.6 “数学黑洞“
  • PHP插件开发中的一个错误:JSON直接输出导致网站首页异常
  • 纸板留声机:用ESP32和NFC打造会唱歌的复古装置
  • 手语式映射:Kinova Gen3 力控机械臂自适应控制的研究与应用
  • 秒收蜘蛛池解析机制的原理
  • PPIO上线阿里旗舰推理模型Qwen3-235B-A22B-Thinking-2507
  • ATR2652SGNSS全频段低噪声放大器
  • PostgreSQL对象权限管理
  • GPU 驱动安装升级测试
  • [NPUCTF2020]ReadlezPHP
  • CSS 盒子模型学习版的理解
  • C语言第 9 天学习笔记:数组(二维数组与字符数组)
  • ODFM(正交频分复用)系统中加入汉明码(Hamming Code)的主要目的是增强抗误码能力,通过**前向纠错(FEC)**机制提高传输可靠性
  • KNN算法:从原理到实战全解析
  • Kubernetes深度解析:企业级容器编排平台的核心实践
  • 内存 管理
  • LeetCode 233:数字 1 的个数
  • ACL:访问控制列表
  • 大数据中心——解读60页IDC云数据中心机房运维服务解决方案【附全文阅读】
  • MMRotate ReDet ReFPN 报错 `assert input.type == self.in_type`