[Unity] 实现ScriptableObject数据同步Excel表格(对话系统数据管理,C# ExcelNPOI)

前言

        在制作游戏中需要管理各种各样的项目资源,其中游戏中的剧情文字也是一种需要管理的资源。自己刚开始接触游戏开发的时候,第一次看MStudio里面的对话系统教学,只讲了怎么写脚本同步UI的设置,并没有讲有什么方式去管理这些对话数据,视频里拿的是txt来演示存储对话数据,导致我制作的第一个游戏也是那txt来写的,一个对话就是一个txt,管理起来肯定是不方便的。

        如何存储文字数据呢?当然我们可以专门写一个系统去对接数据库,而对话系统的网络上也有很多插件,其中就有很多节点化,可分支的对话系统,但是系统太过庞大,如果只是对一个小项目显得的有点用牛刀杀鸡的感觉了。

        这里我们可以想到拿ScriptableObject管理数据,ScriptableObject是一种很方便存储不可变数据的数据结构。但是ScriptableObject是面向Unity的,有时候只是比赛的小组队的情况下,并不方便策划进编辑器里数据(策划不会用unity?/版本同步?)

        这时候就可以使用Excel表格了,让策划在表格中编写完数据后我们就可以一键导入成ScriptableObject,有需要再编辑是也可以导出到excel中发回给策划,或者策划直接进编辑器修改scrpitableObject也是可以的。这可以比较轻松的实现数据管理,excel也很方便策划的理解和使用,在比赛的一些中小项目中还是很实用的。

效果展示:

 功能实现:

        本文章使用了C# ExcelNPOI,这是一个开源的C#读写Excel、WORD等微软OLE2组件文档的项目,可以自行去网上下载,或者在本文结尾也会放出github网址,直接在里面下载即可。

        (这东西其实就是两个DLL,放入Plugins文件夹中里即可生效)

         这里拿我的项目中的对话系统举例,首先假设我们已经在unity中实现了一个对话系统,并使用了ScriptableObject进行数据管理,其中对话数据的代码如下:

[CreateAssetMenu(fileName = "DialogueSo")]
    public class DialogueData : ScriptableObject
    {
        public List<Sentence> data = new List<Sentence>();

        [System.Serializable]
        public class Sentence
        {
            public string character;
            [TextArea]
            public string content;
        }
    }

          这里的一个对话数据由一个句子的List构成,句子有角色名和对话内容组成,组成都为string,我们excel里也可以定义约束一下格式,在第一列中为该ScriptableObject的name,意味着一个新的对话数据开始,第二列为该角色名,第三列为具体的对话内容,其中第二第三列是不能为空的,第一列为空待变该对话仍在上一个定义的对话中,有内容代表新起了一段新对话。

        Excel里如下:

        对于的ScriptableObject如下:

         这里只是简单展示,当然也可以使用其他的规范,或者在第四第五行再添加一些额外的数据也是可以的(比如表示对话关联的其他事件,对话分支什么的)。确定了规范,就可以实现与Excel的同步了。

        在开始将同步前,先简单介绍一下一些NPOI的基本用法。

        首先需要将NPOI两个DLL放入Unity的Plugins文件夹,这个文件夹会被当做插件载入项目,就可以直接使用对应的命名空间了。

        在使用前,需要引入对应的两个命名空间,一般为NPOI.HSSF.UserModel和NPOI.SS.UserModel。需要注意NPOI的API只能在编辑器中使用,游戏最后打包是无法将逻辑带出去的(打包时会提示编译错误),所以需要加上#if UNITY_EDITOR的宏定义,或者将使用Excel的代码放入Unity的Editor文件夹。

#if UNITY_EDITOR
using NPOI.SS.UserModel;
using NPOI.HSSF.UserModel;
#endif

         从逻辑上讲,放入Editor文件夹更方便,因为数据同步本身就是在Unity编辑器中完成的。

        接着就是API的使用了,这里介绍一下本文章中使用的。更多详细的API可自行上网查找。

//创建一个excel文件实例,fs这里指为文件路径
var wk = new HSSFWorkbook(fs);
//将表格内容写入回路径
wk.Write(fs);
//获取excel中第index个sheet
var sheet = wk.GetSheetAt(0);
//在excel中创建一个新的sheet
var sheet = wk.CreateSheet();
//sheet的行数
sheet.LastRowNum

//读取第i行的内容,下标从0开始
var row = sheet.GetRow(i)
//读取该行中的第i个元素(可以用toString转换为字符串内容),下标从0开始
row.GetCell(i)
//创建行和行中元素
var row = sheet.CreateRow(i);
var cell = row.CreateCell(0);
cell.SetCellValue(string);

        然后我们就可以开始编写Excel脚本逻辑了。首先我们需要两个string,去表示ScriptableObject的保存文件夹路径和excel的文件路径。注意这里一个是文件夹的路径,一个是文件路径。

[FolderPath]
public string saveScriptableObjectPath; //对话数据文件夹存储路径
[FilePath]
public string excelPath;                //excel表格文件路径

       

        Excel 同步分为写出加载两种操作,一种是将所有的ScriptableObject写到Excel,另一种是Excel的数据载入进Unity,写出机制逻辑比较简单,就是先读取所有对话文件,然后对着行列一个个写入即可,代码如下。

public void WriteExcel()
{
    //空值判断
    if (string.IsNullOrEmpty(excelPath))
        return;
    if (!excelPath.Contains(".xlsx"))
    {
        Debug.Log("路径不是excel文件");
        return;
    }
    //打开文件流
    FileStream fs = File.Exists(excelPath) ? File.Open(excelPath, FileMode.Open) : File.Create(excelPath);
    //新建excel和sheet
    var wk = new HSSFWorkbook();
    var sheet = wk.CreateSheet();

    int i = 1; //省略掉第0行

    //遍历所有对话文件
    foreach (var dialogue in dialogueData.datas)
    {
        var row = sheet.CreateRow(i);
        var cell = row.CreateCell(0);
        //讲ScriptableObject的name写在第一列
        cell.SetCellValue(dialogue.ID.name);
        //遍历句子内容
        for (int j = 0; j < dialogue.ID.data.Count; j++)  
        {
            if (j != 0)
                row = sheet.CreateRow(i);
            //将名字写在第二列
            row.CreateCell(1).SetCellValue(dialogue.ID.data[j].character.ToString());
            //将对话内容写在第三列
            row.CreateCell(2).SetCellValue(dialogue.ID.data[j].content);
            i++;
        }
    }


    wk.Write(fs);
    fs.Close();
    fs.Dispose();
    Debug.Log("写入成功");
}

        加载机制同理,这里有一点不用的是,加载需要动Unity中的ScriptableObject的数据,所以只有新的ID数据(name)时需要创建ScriptableObject的实例并保存,如果已经存在的ID就不用删除再创建新的文件了,否者会导致资源的meta文件被刷新,原本游戏逻辑中已经引用了这个ScriptableObject数据的地方就会丢失(当然你要使用string做数据索引这点就不用担心了)

        最后不要忘记使用AssetDatabase.SaveAssets();AssetDatabase.Refresh();刷新一下unity资源列表。


public void ReadExcel()
{
    //空值判断
    if (string.IsNullOrEmpty(excelPath))
        return;
    if (!excelPath.Contains(".xlsx"))
    {
        Debug.Log("路径不是excel文件");
        return;
    }
    //打开文件流
    FileStream fs = File.Open(excelPath, FileMode.Open);
    //打开excel和sheet
    var wk = new HSSFWorkbook(fs);
    var sheet = wk.GetSheetAt(0);
    //这是ScriptableObject的实例
    DialogueData so = null;
    //开始遍历sheet每一行的数据,注意这里i=1跳过了第一行
    for (int i = 1; i < sheet.LastRowNum; i++)
    {
        Debug.Log("读取EXCEL 行数:" + i);
        var row = sheet.GetRow(i);
        //如果当前行第一列元素不为空,证明需要保存当前so,然后创建新的so
        if (row.GetCell(0) != null && !string.IsNullOrEmpty(row.GetCell(0).ToString()))
        {
            //只有第一次so为null
            if (so) //保存路径
            {
                //是否原来已经创建了ScriptableObject资源
                if (File.Exists(saveScriptableObjectPath + "/" + so.name + ".asset"))
                {
                    dialogueData.datas.Find(x => { return x.ID.name == so.name; }).ID.data = so.data;
                }
                else
                    AssetDatabase.CreateAsset(so, saveScriptableObjectPath + "/" + so.name + ".asset");
            }
            //创建实例,开始新的对话数据记录
            so = ScriptableObject.CreateInstance<DialogueData>();
            so.name = row.GetCell(0).ToString();
        }
        //加载excel第二第三列的数据
        if (so != null)
        {
            var se = new DialogueData.Sentence();
            se.character = row.GetCell(1).ToString();
            se.content = row.GetCell(2).ToString();
            so.data.Add(se);
        }
    }
    //退出循环时记得还有一个so的数据
    if (so)
    {
        if (File.Exists(saveScriptableObjectPath + "/" + so.name + ".asset"))
        {
            File.Delete(saveScriptableObjectPath + "/" + so.name + ".meta");
            File.Delete(saveScriptableObjectPath + "/" + so.name + ".asset");
        }
        AssetDatabase.CreateAsset(so, saveScriptableObjectPath + "/" + so.name + ".asset");
    }

    AssetDatabase.SaveAssets();
    AssetDatabase.Refresh();
}

        

        本文完结!最后本文章出现的代码和NPOI需要用到的两个DLL已放在github上,欢迎下载和讨论

         GitHub - sugarzo/Unity_ExcelFrame: 使用NPOI实现在unity中的ScriptableObject和Excel表格数据同步

猜你喜欢

转载自blog.csdn.net/m0_51776409/article/details/127624546