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

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

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

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

目录

1.介绍

2.核心脚本

对话管理器

对话事件

对话配置脚本

对话节点脚本 

3.使用指北

路径配置

关于特性

关于接口 

关于UI 

其余内容请自行查看源码


     Github:  Haki-sheep/Haki-sheep-UnityTools at DialogTools-dev1.0 

        演示视频:

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

1.介绍

         这个对话系统并不是可视化编辑节点(像是NodeCanvas插件那种),但也支持一键将Excel表转为So文件,通过配表的方式轻量化这一过程

        首先,算上DEMO一共632行,去掉以后可能 不到四百行 所以十分轻巧

        但是由于代码量摆在那,所以目前本对话系统只支持小玩具, 今后我说不定会将其拓展为课编辑节点的系统,当然,目前我个人使用起来还是比较方便的,毕竟是自己编写的系统

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

2.核心脚本

对话管理器

        对话流程如下:

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

}

对话事件

这里我就展示其中一种事件,检查某一样子东西玩家是否已经应有 从而跳过对话

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;
    }
}

对话配置脚本

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>();
}

对话节点脚本 

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

其余内容请自行查看源码

相关文章:

  • deepseek搭建本地私有知识库dify安装介绍docker compose图文教程
  • Spring漏洞再现
  • 卷积神经网络 - 关于LeNet-5的若干问题的解释
  • 【机器学习/大模型/八股文 面经 (一)】
  • 深度学习技术与应用的未来展望:从基础理论到实际实现
  • Spark Driver生成过程详解
  • Pyecharts功能详解与实战示例
  • CCF编程能力等级认证GESP—C++4级—20250322
  • 传统复古怀旧标签标识Logo设计PSAI无衬线英文字体安装包 Myfonts – Hebden Recut Font Family
  • 「DP」专题训练(持续更新中)
  • 基于linuxC结合epoll + TCP 服务器客户端 + 数据库实现一个注册登录功能
  • 在IDEA中快速注释所有console.log
  • 四种跨模态行人重识别可视化方法
  • Ubuntu22.04搭建freeradius操作说明
  • 实时图像处理:让你的应用更智能
  • 【Bug记录】node-sass安装失败解决方案
  • Thinkphp(TP)框架漏洞攻略
  • Docker 可视化工具 Portainer
  • 【2025】基于springboot+spark的电影推荐系统(源码、万字文档、图文修改、调试答疑)
  • 【Tomcat】部署及优化
  • 俄乌交换205名被俘人员,俄方人员已抵达白俄罗斯
  • 博裕基金拟收购“全球店王”北京SKP最多45%股权
  • 台湾花莲县海域发生5.7级地震,震源深度15公里
  • 新剧|《执法者们》《亲爱的仇敌》5月7日开播
  • 云南石屏举办茶文化交流活动:弘扬企业家精神,激发市场活力
  • 取消了“仅退款”,商家就可以高枕无忧了吗?