本人能力有限,一切实现仅供参考,如有不足还请斧正
起因是我看到学校社团内有人做了对话系统的分享,我想了想之前没写过这种东西,而Fungus插件教程太老了,NodeCanvas插件学习成本又比较高,我就干脆寻找资料 加上自己迭代一下,花了一天时间完成了这个对话系统
目录

Github: Haki-sheep/Haki-sheep-UnityTools: 这里是咩咩所有的工具
演示视频:
Unity一个简易可拓展的对话系统
1.介绍
这个对话系统并不是可视化编辑节点(像是NodeCanvas插件那种),但也支持 一键将Excel表转为So文件,通过配表的方式轻量化这一过程
首先,算上DEMO一共632行,去掉以后可能 不到四百行 所以十分轻巧
但是由于代码量摆在那,所以目前本对话系统只支持小玩具, 今后我说不定会将其拓展为课编辑节点的系统,当然,目前我个人使用起来还是比较方便的,毕竟是自己编写的系统

其次 Base只涉及到了Odin插件EPPLUS 以及一个单例基类 无需其他支持

2.核心脚本
对话管理器
对话流程如下:
cs
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
public class DialogManager : SingltonMono<DialogManager>
{
#region 基础配置
//配置相关
private DialogConfig curDialogConfig;
private int nodeIndex;
public bool nodeNotOver => nodeIndex < curDialogConfig.nodeList.Count - 1;
//角色相关
private string characterName;
private Sprite characterAvatar;
//外部 可做替换
public Player player;
public DialogMainUI dialogMainUI;
public SelectUI selectUI;
#endregion
#region 对话流程
/// <summary>
/// 开始对话
/// </summary>
/// <param name="dialogConfig">想要对话角色的配置</param>
/// <param name="nodeIndex">从第几个节点开始对话</param>
public void StartDialog(DialogConfig dialogConfig, int nodeIndex = 0)
{
if (curDialogConfig == dialogConfig) return;//不要重复对话
curDialogConfig = dialogConfig;
this.nodeIndex = nodeIndex;
characterName = curDialogConfig.characterName;
characterAvatar = curDialogConfig.characterAvatar;
StartCoroutine(PlayNode(curDialogConfig.nodeList[nodeIndex]));
}
public void CeckCharacterInfo(DialogNode node, Image ui_characterAvator, Text ui_characterName)
{
//角色的信息
if (node.player)
{
ui_characterName.text = player.name;
ui_characterAvator.sprite = player.Avator;
}
else
{
ui_characterName.text = characterName;
ui_characterAvator.sprite = characterAvatar;
}
}
private IEnumerator PlayNode(DialogNode node)
{
dialogMainUI.Show();
CeckCharacterInfo(node, dialogMainUI.ui_characterAvator, dialogMainUI.ui_characterName);
//开始事件
OnEvent(node.onStartEventList);
yield return OnBlockEvent(node.onStartEventList);
//打字机
yield return Typing(node.content, dialogMainUI.ui_contentText);
//等待交互
while (!Input.GetMouseButtonDown(0)) { yield return null; }
//结束事件
OnEvent(node.onEndEventList);
yield return OnBlockEvent(node.onEndEventList);
if (nodeNotOver)
{
nodeIndex++;
StartCoroutine(PlayNode(curDialogConfig.nodeList[nodeIndex]));
}
else
{
CloseDialog();
}
}
private void OnEvent(List<IDialogEvent> dialogEvents)
{
foreach (IDialogEvent sEvent in dialogEvents)
{
sEvent.Execute();
}
}
private IEnumerator OnBlockEvent(List<IDialogEvent> dialogEvents)
{
foreach (IDialogEvent sBEvnt in dialogEvents)
{
IEnumerator enumerator = sBEvnt.ExecuteBlock();
if (enumerator == null) continue;
yield return enumerator;
}
}
public void CloseDialog()
{
StopAllCoroutines();
curDialogConfig = null;
nodeIndex = 0;
dialogMainUI.Hide();
}
#endregion
#region 打字机相关
public float delayBetweenContent = 0.1f;
private Dictionary<string, string> keywordDic = new Dictionary<string, string>();
public void SetKeyword(string key, string value)
{
keywordDic[key] = value;
}
public void RemoveKeyword(string key)
{
keywordDic.Remove(key);
}
private IEnumerator Typing(string content, Text ui_contentText)
{
StringBuilder builder = new StringBuilder();
foreach (var item in keywordDic)
{
content = content.Replace(item.Key, item.Value);
}
foreach (var s in content)
{
builder.Append(s);
ui_contentText.text = builder.ToString();
yield return new WaitForSeconds(delayBetweenContent);
}
}
#endregion
#region 资源管理
public T GetDialogConfig<T>(string path) where T : ScriptableObject, new()
{
return Resources.Load<T>(path);
}
#endregion
}
对话事件
这里我就展示其中一种事件,检查某一样子东西玩家是否已经应有 从而跳过对话
cs
using System.Collections;
[DialogEvent("CheckKeyWordEvent")]
public class CheckKeyWordEvent : IDialogEvent
{
public void ConverString(string excelString)
{
}
public void Execute()
{
//检查是否有选中物品 TODO:条件可以替换
if (Player.Instance.selectItem != null)
{
DialogManager.Instance.CloseDialog();
}
}
public IEnumerator ExecuteBlock()
{
return null;
}
}
对话配置脚本
cs
using Sirenix.OdinInspector;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Tools", fileName = "创建新角色")]
public class DialogConfig : SerializedScriptableObject
{
//角色名称
public string characterName;
//角色头像
public Sprite characterAvatar;
//显示索引,开启翻页
[ListDrawerSettings(ShowIndexLabels = true, ShowPaging = true)]
public List<DialogNode> nodeList = new List<DialogNode>();
}
对话节点脚本
cs
using System.Collections.Generic;
/// <summary>
/// 对话节点配置
/// </summary>
public class DialogNode
{
//是否是玩家
public bool player;
//说的内容
public string content;
//对话事件
public List<IDialogEvent> onStartEventList = new List<IDialogEvent>();
public List<IDialogEvent> onEndEventList = new List<IDialogEvent>();
}
3.使用指北
路径配置
这个文件填写你的Excel表和so文件想在的位置
但是我推荐将so文件放在Res下面 方便管理器读取

如果有报错就把你的DialogImprotSetting的路径放在这里面
关于特性
这个特性内填写你的事件名称即可 可以不和脚本一样 只需要和Excel表之中一样便可以读取
关于接口
事件需要继承这个接口


阻塞执行 里面直接return nul即可 因为外部会判断
当然你直接yield rerun null也可以,但是会造成延迟一帧后才执行其他语句

UI接口的话可以选择性继承,因为里面也没什么方法,可以自己写

关于UI
在DialogManager里有两个UI的对象,其实所有在外部这个注释下的字段都可以自行做替换

只要让Manager得到了你UI身上下面这些信息即可(方式自行选择比如事件中心或者订阅回调的方式)

剩下的UI样式之类的自行配置即可