根据对话系统表格数据自动生成unity的ScriptableObject(基于EPPlus,附简单的任务对话系统设计)

前言

EPPlus更符合习惯,方便做扩展,dll放在文末链接
虽然有写好的插件,但还是自己写的方便扩展

任务对话系统概览,后面演示的表格数据基于触发式任务
在这里插入图片描述

步骤

1. 自动生成结构类和容器类

主要针对单列表的SO数据的通用自动化导入,首先做好表格
约定第一行描述,第二行字段名,之后通过反射去找同名字段赋值。每个sheet都会生成一个新SO,我的预想是一个sheet对应一个角色
在这里插入图片描述

public static class ExcelTool
{

    /// <summary>
    /// excel文件存放路径
    /// </summary>
    public static string EXCEL_PATH = Application.dataPath + "/GameData/";

    /// <summary>
    /// 数据结构类存放路径
    /// </summary>
    public static string DATA_CLASS_PATH = Application.dataPath + "/Scripts/ExcelData/";
    /// <summary>
    /// 容器类 
    /// </summary>
    public static string DATA_CONTAINER_PATH = Application.dataPath + "/Scripts/ExcelData/";


     /// <summary>
    /// 内容开始行
    /// </summary>
    public const int BEGINE_ROW = 4;
    /// <summary>
    /// 字段所在行
    /// </summary>
    public const int FIELD_ROW = 2;
    /// <summary>
    /// 类型所在行
    /// </summary>
    public const int TYPE_ROW = 3;


    /// <summary>
    /// 生成excel数据结构
    /// </summary>
    [MenuItem("Tools/生成excel数据结构")]
    private static void GenerateExcelInfo()
    {
        DirectoryInfo dInfo = new DirectoryInfo(EXCEL_PATH);
        FileInfo[] files = dInfo.GetFiles("*.xlsx", SearchOption.AllDirectories).Where(f => !f.Name.StartsWith("~$")).ToArray(); ;

        foreach (var file in files)
        {
            using (var package = new ExcelPackage(file))
            {
                string fileName = file.Name.Replace(file.Extension, "");
                foreach (var worksheet in package.Workbook.Worksheets)
                {
                    GenerateClass(worksheet, fileName);
                    break;
                }

                string assetPath = "Assets" + file.FullName.Replace(Application.dataPath.Replace("/", "\\"), "").Replace("\\", "/");
                GenerateContainer(fileName, assetPath);
            }
        }
    }

    private static void GenerateClass(ExcelWorksheet worksheet, string fileName)
    {
        string className = fileName;
        string str = "[System.Serializable]\npublic class " + className + "\n{\n";

        for (int i = 1; i <= worksheet.Dimension.End.Column; i++)
        {
            string fieldName = worksheet.Cells[FIELD_ROW, i].Value.ToString();
            string fieldType = worksheet.Cells[TYPE_ROW, i].Value.ToString();
            str += "\t public " + fieldType + " " + fieldName + ";\n";
        }

        str += "}";

        string path = Path.Combine(DATA_CLASS_PATH, className + ".cs");
        File.WriteAllText(path, str);

        AssetDatabase.Refresh();
    }

    private static void GenerateContainer(string fileName, string excelFilePath)
    {
        string className = fileName + "DataSO";
        string str = "using System.Collections.Generic;\nusing UnityEngine;\nusing UnityEditor;\n\n";
        str += "[CreateAssetMenu(fileName = \"" + className + "\", menuName = \"ExcelData/" + className + "\")]\n";
        str += "public class " + className + " : ScriptableObject\n{\n";
        str += "\tpublic List<" + fileName + "> data = new List<" + fileName + "" + ">();\n\n";
        str += "\t[MenuItem(\"Assets/导入表格数据\")]\n";
        str += "\tpublic static void ImportData()\n";
        str += "\t{\n";
        str += "\t\tExcelTool.ImportExcelData<" + className + ", " + fileName + ">(\"" + excelFilePath + "\");\n";
        str += "\t}\n";
        str += "}";

        string path = Path.Combine(DATA_CONTAINER_PATH, className + ".cs");
        File.WriteAllText(path, str);

        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 获取变量名所在行,应对不同规则
    /// </summary>
    /// <param name="table"></param>
    /// <param name="index"></param>
    /// <returns></returns>
    private static DataRow GetVariableNameRow(DataTable table)
    {
        return table.Rows[FIELD_ROW];
    }
    /// <summary>
    /// 获取变量类型所在行
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static DataRow GetVariableTypeRow(DataTable table)
    {
        return table.Rows[TYPE_ROW];
    }
}

生成好的结果如下
在这里插入图片描述
dialog要解析一些数据,所以重新写了

/// <summary>
    /// 对话片段
    /// </summary>
    [Serializable]
    public class DialogPiece
    {
        /// <summary>
        /// 约定:id增序,若无isEnd,则读取下一条
        /// </summary>
        public string id;
        [Header("对话内容")]
        public int faceIndex;
        // public Sprite face;
        public string npcName;

        /// <summary>
        /// 以此作为节点,没有该标识,就按顺序往下读
        /// </summary>
        public bool isEnd = false;
        /// <summary>
        /// 是否直接触发任务
        /// </summary>
        public bool isQuestStart = false;

        [TextArea]
        public string dialogText;

        public UnityEvent aftereTalkEvent;

        /// <summary>
        /// 任务
        /// </summary>
        public string questName;
        public QuestDataSO quest;

        // 回答选项
        public List<DialogOption> options = new();
        public string optionsString;

    }

2. 根据表格数据自动生成SO

这里需要选中生成的so文件,右键生成即可
规则: 工作表sheet名字作为SO文件名,行首为空跳过

因为要解析一些数据,所以加了内容
直接在表格写选项也可以,单独重写生成SO的方法即可

生成SO的方法

public static class ExcelTool
{ 
/// <summary>
    /// 导入excel  需要手动指定excel路径
    /// </summary>
    /// <typeparam name="T">最终保存的so类型</typeparam>
    /// <typeparam name="U">List列表元素的类型</typeparam>
    /// <param name="excelFilePath"></param>
    public static void ImportExcelData<T, U>(string excelFilePath, Action<T> OnFinish = null) where T : ScriptableObject
    {
        if (string.IsNullOrEmpty(excelFilePath))
        {
            Debug.LogError("Excel file  empty.");
            return;
        }

        FileInfo fileInfo = new FileInfo(excelFilePath);
        using (ExcelPackage package = new ExcelPackage(fileInfo))
        {
            foreach (ExcelWorksheet worksheet in package.Workbook.Worksheets)
            {
                // 如果整个sheet是空的,跳过
                if (worksheet.Dimension == null)
                {
                    continue;
                }

                string directoryPath = Path.GetDirectoryName(excelFilePath);
                string soPath = Path.Combine(directoryPath, worksheet.Name + ".asset");

                T excelData = AssetDatabase.LoadAssetAtPath<T>(soPath);
                if (excelData == null)
                {
                    excelData = ScriptableObject.CreateInstance<T>();
                    AssetDatabase.CreateAsset(excelData, soPath);
                }

                Type type = typeof(U);

                IList list = null;
                foreach (FieldInfo field in typeof(T).GetFields())
                {
                    if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(List<>))
                    {
                        // 找到list泛型字段
                        list = (IList)field.GetValue(excelData);
                        list.Clear(); // 清空列表,以便重新填充
                    }
                }

                // 自定义开始列和  字段名所在列  
                for (int i = BEGINE_ROW; i <= worksheet.Dimension.End.Row; i++)
                {
                    // 检查第一列是否为空
                    if (worksheet.Cells[i, 1].Value == null || string.IsNullOrEmpty(worksheet.Cells[i, 1].Value.ToString()))
                    {
                        continue;
                    }

                    object dataItem = Activator.CreateInstance(type);
                    for (int j = 1; j <= worksheet.Dimension.End.Column; j++)
                    {
                        string fieldName = worksheet.Cells[FIELD_ROW, j].Value.ToString();

                        FieldInfo property = type.GetField(fieldName);

                        if (property != null)
                        {
                            if (worksheet.Cells[i, j].Value == null || string.IsNullOrEmpty(worksheet.Cells[i, j].Value.ToString()))
                                continue;
                            string cellValue = worksheet.Cells[i, j].Value.ToString();
                            if (property.FieldType == typeof(int))
                            {
                                property.SetValue(dataItem, int.Parse(cellValue));
                            }
                            else if (property.FieldType == typeof(float))
                            {
                                property.SetValue(dataItem, float.Parse(cellValue));
                            }
                            else if (property.FieldType == typeof(string))
                            {
                                property.SetValue(dataItem, cellValue);
                            }
                            else if (property.FieldType == typeof(bool))
                            {
                                property.SetValue(dataItem, bool.Parse(cellValue));
                            }
                            else if (property.FieldType == typeof(double))
                            {
                                property.SetValue(dataItem, double.Parse(cellValue));
                            }
                            else if (property.FieldType == typeof(DateTime))
                            {
                                property.SetValue(dataItem, DateTime.Parse(cellValue));
                            }
                            else if (property.FieldType.IsEnum)
                            {
                                property.SetValue(dataItem, Enum.Parse(property.FieldType, cellValue));
                            }
                            else if (property.FieldType == typeof(long))
                            {
                                property.SetValue(dataItem, long.Parse(cellValue));
                            }
                        }
                    }
                    // 填充到so 的list
                    if (list != null) list.GetType().GetMethod("Add").Invoke(list, new[] { dataItem });
                }
                
                OnFinish?.Invoke(excelData);
                // 标记为脏,确保Unity保存更改  其实不需要这句
                UnityEditor.EditorUtility.SetDirty(excelData);
            }
            AssetDatabase.SaveAssets();
        }
    }
}

DataSO

[CreateAssetMenu(fileName = "DialogDataSO", menuName = "dialog/DialogDataSO", order = 0)]
public class DialogDataSO : ScriptableObject
{
    public List<DialogPiece> dialogPieceList = new List<DialogPiece>();

    // public NativeHashMap<string, DialogPiece> dialogueIndex = new();
    public Dictionary<string, DialogPiece> dialogueIndex = new();

    public QuestDataSO GetQuest()
    {
        QuestDataSO currentQuest = null;
        //循环对话中的任务,找到该任务并返回
        foreach (var piece in dialogPieceList)
        {
            if (piece.quest != null)
                currentQuest = piece.quest;
        }
        return currentQuest;
    }

    [MenuItem("Assets/导入表格数据")]
    public static void ImportData()
    {
        ExcelTool.ImportExcelData<DialogDataSO, DialogPiece>("Assets/GameData/Farm/Dialog/dialog.xlsx", (dialogDataSO) =>
        {
            UpdateData(dialogDataSO);
        });
    }


    public static void UpdateData(DialogDataSO dialogDataSO)
    {
        var dialogueIndex = dialogDataSO.dialogueIndex;
        var dialogPieceList = dialogDataSO.dialogPieceList;
        dialogueIndex.Clear();
        //一旦信息有所更新,就会将信息存储在字典中
        foreach (var piece in dialogPieceList)
        {
            if (!dialogueIndex.ContainsKey(piece.id))
                dialogueIndex.Add(piece.id, piece);

            // 自动将任务名转为对应so   考虑对话和任务放在同一文件夹,就无需深度搜索
            if (!piece.questName.IsNullOrEmpty())
            {
                string[] guids = UnityEditor.AssetDatabase.FindAssets(piece.questName, new[] { "Assets/GameData/Farm/Quest" }); // 搜索"Assets/So"及其子文件夹下的所有名为questName的资源
                if (guids.Length > 0)
                {
                    string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[0]); // 将第一个匹配的资源的GUID转换为资源路径
                    piece.quest = UnityEditor.AssetDatabase.LoadAssetAtPath<QuestDataSO>(path); // 加载资源
                }

            }

            // 将optionsString解析
            if (!piece.optionsString.IsNullOrEmpty())
            {
                piece.options = ParseOptionsString(piece.optionsString);
            }
        }

        List<DialogOption> ParseOptionsString(string optionsString)
        {
            List<DialogOption> options = new List<DialogOption>();
            // 分割字符串以获取单独的选项
            string[] optionStrings = optionsString.Split(',');

            foreach (string optionString in optionStrings)
            {
                // 进一步分割以获取属性值
                string[] properties = optionString.Trim().Split('|');
                if (properties.Length == 3)
                {
                    DialogOption option = new DialogOption();
                    option.text = properties[0].Trim();
                    option.targetID = properties[1].Trim();
                    option.takeQuest = properties[2].Trim().ToUpper() == "TRUE";

                    options.Add(option);
                }
            }
            return options;
        }
    }

    //如果是在Unity编辑器中,则字典随时改变时则进行修改,如果是打包则字典信息不会更改
#if UNITY_EDITOR
    void OnValidate()//一旦这个脚本中的数据被更改时会自动调用
    {
        UpdateData(this);
    }
#else
    void Awake()//保证在打包执行的游戏里第一时间获得对话的所有字典匹配 
    {
        dialogueIndex.Clear();
        foreach (var piece in dialoguePieces)
        {
            if (!dialogueIndex.ContainsKey(piece.ID))
                dialogueIndex.Add(piece.ID, piece);
        }
    }
#endif
}

结尾

链接:https://pan.baidu.com/s/17w-Rss_ExtoDV63cgaeSUQ?pwd=8uly
提取码:8uly

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值