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

其余内容请自行查看源码

相关推荐
云草桑2 分钟前
Net 模拟退火,遗传算法,禁忌搜索,神经网络 ,并将 APS 排程算法集成到 ABP vNext 中
c#·.net·制造
范小多21 分钟前
mysql实战 C# 访问mysql(连载三)
数据库·mysql·oracle·c#
技术小甜甜29 分钟前
[Godot排错] 上传 Google Play Console 封闭测试时签名证书不匹配错误的解决方案
游戏引擎·godot·游戏开发
我是唐青枫4 小时前
C# 泛型数学:解锁真正的类型安全数值运算
c#·.net
故事不长丨9 小时前
C#定时器与延时操作的使用
开发语言·c#·.net·线程·定时器·winform
变身缎带9 小时前
Unity中的NetworkManager基于protobuf, Socket-TCP
tcp/ip·unity·游戏引擎
阿桂有点桂9 小时前
C#使用VS软件打包msi安装包
windows·vscode·c#
c#上位机9 小时前
halcon图像增强之分段灰度拉伸2
c#·上位机·halcon·机器视觉
yue0089 小时前
C# Directory的用法介绍
开发语言·c#
c#上位机12 小时前
halcon图像增强之自动灰度拉伸
图像处理·算法·c#·halcon·图像增强