Unity 实现一个简易可拓展性的对话系统

本人能力有限,一切实现仅供参考,如有不足还请斧正

起因是我看到学校社团内有人做了对话系统的分享,我想了想之前没写过这种东西,而Fungus插件教程太老了,NodeCanvas插件学习成本又比较高,我就干脆寻找资料 加上自己迭代一下,花了一天时间完成了这个对话系统

目录

1.介绍

2.核心脚本

对话管理器

对话事件

对话配置脚本

对话节点脚本

3.使用指北

路径配置

关于特性

关于接口

关于UI

其余内容请自行查看源码


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样式之类的自行配置即可

其余内容请自行查看源码

相关推荐
一只一只35 分钟前
Unity之UGUI Button按钮组件详细使用教程
unity·游戏引擎·ugui·button·ugui button
bjzhang751 小时前
Dorisoy.AMS--一款采用C# WinForm框架+SQLite数据库的企业/机构资产管理解决方案
sqlite·c#·资产管理
零度@2 小时前
Java消息中间件-Kafka全解(2026精简版)
java·kafka·c#·linq
2501_941882483 小时前
在开普敦跨区域部署环境中构建高可靠分布式配置中心的设计思路与实现实践
开发语言·c#
神米米3 小时前
Maya快速安装UE4 布料权重绘制插件PhysX导出apx
游戏引擎·ue4·maya
WarPigs4 小时前
Unity阴影
unity·游戏引擎
zxy28472253014 小时前
利用C#的BotSharp本地部署第一个大模型AI Agent示例(1)
人工智能·c#·对话·ai agent·botsharp
刘97534 小时前
【第25天】25c#今日小结
java·开发语言·c#
一只一只4 小时前
Unity之Invoke
unity·游戏引擎·invoke
玩泥巴的5 小时前
基于.NET操作Excel COM组件生成数据透视报表
c#·.net·excel·二次开发·com互操作