Unity 中的存档系统(本地存档)

思想

在游戏过程中,玩家的背包、登录、人物系统都与数据息息相关,无论是一开始就设定好的默认数据,还是可以动态存取的数据,都需要开发人员去管理。

游戏开发过程中,策划一般通过Excel表格配置一些内容来对游戏的一些行为经行数据的设定。表格有config默认数据,程序只需要读取即可;还可能建立model类数据需要在游戏中实例化对象来进行数据的增删改查.

想要看具体实现方法可以到页尾查看完整代码。

MVC架构中Model的CRUD操作也包含在存档类中(本地存档):
image

方法

excel转换成config默认数据(json文件)并通过对应的类读取数据可以参考我之前发的文章

https://www.cnblogs.com/ameC1earF/p/17270090.html

以下我对它进行了改良,涵盖了config默认数据以及类的转换以及model动态数据类文件的生成以及数据的存取。

Json格式的数据类:

DataList
using System.Collections.Generic;
using System;

[Serializable]
public class DataList<T>
{
    public List<T> datas = new List<T>();
}

导出类代码:

导出工具类
using UnityEngine;
using UnityEditor;
using System.IO;
using OfficeOpenXml;
using System.Collections.Generic;
using System;
using System.Text;
/// <summary>
/// 导出模式
/// </summary>
public enum ExporterMode
{
    /// <summary>
    /// 表格数据,策划配置的默认数据
    /// </summary>
    Config,
    /// <summary>
    /// 模型数据,服务器或者本地可以修改的数据
    /// </summary>
    Model,
}
/// <summary>
/// 使用EPPlus获取表格数据,同时导出对应的Json以及Class.
/// </summary>
public class ExcelExporter
{

    /// <summary>
    /// ExcelConfig路径
    /// </summary>
    private const string excelConfigPath = "../Assets/Excels/Configs";
    /// <summary>
    /// ExcelModel路径
    /// </summary>
    private const string excelModelPath = "../Assets/Excels/Models";

    private const string configPath = "../Assets/Resources/Json";
    private const string configClassPath = "../Assets/Scripts/Configs";
    private const string modelPath = "../Assets/Records";
    private const string modelClassPath = "../Assets/Scripts/Models";

    /// <summary>
    /// 属性行
    /// </summary>
    private const int propertyIndex = 2;
    /// <summary>
    /// 类型行
    /// </summary>
    private const int typeIndex = 3;
    /// <summary>
    /// 值行
    /// </summary>
    private const int valueIndex = 4;


    [MenuItem("Tools/ExportExcelConfigs")]
    private static void ExportConfigs()
    {
        try
        {
            string path = string.Format("{0}/{1}", Application.dataPath, excelConfigPath);

            FileInfo[] files = FilesUtil.LoadFiles(path);

            foreach (var file in files)
            {
                //过滤文件
                if (file.Extension != ".xlsx") continue;
                ExcelPackage excelPackage = new ExcelPackage(file);
                ExcelWorksheets worksheets = excelPackage.Workbook.Worksheets;
                //只导表1
                ExcelWorksheet worksheet = worksheets[1];

                ExportJson(worksheet, Path.GetFileNameWithoutExtension(file.FullName), ExporterMode.Config);
                ExportClass(worksheet, Path.GetFileNameWithoutExtension(file.FullName), ExporterMode.Config);

            }
            AssetDatabase.Refresh();
        }
        catch (Exception e)
        {
            Debug.LogError(e.ToString());
        }
    }
    [MenuItem("Tools/ExportExcelModels")]
    private static void ExportModels()
    {
        try
        {
            string path = string.Format("{0}/{1}", Application.dataPath, excelModelPath);

            FileInfo[] files = FilesUtil.LoadFiles(path);

            foreach (var file in files)
            {
                //过滤文件
                if (file.Extension != ".xlsx") continue;
                ExcelPackage excelPackage = new ExcelPackage(file);
                ExcelWorksheets worksheets = excelPackage.Workbook.Worksheets;
                //只导表1
                ExcelWorksheet worksheet = worksheets[1];

                ExportJson(worksheet, Path.GetFileNameWithoutExtension(file.FullName), ExporterMode.Model);
                ExportClass(worksheet, Path.GetFileNameWithoutExtension(file.FullName), ExporterMode.Model);

            }
            AssetDatabase.Refresh();
        }
        catch (Exception e)
        {
            Debug.LogError(e.ToString());
        }
    }

    /// <summary>
    /// 导出类
    /// </summary>
    private static void ExportClass(ExcelWorksheet worksheet, string fileName, ExporterMode mode)
    {
        string[] properties = GetProperties(worksheet);
        StringBuilder sb = new StringBuilder();
        sb.Append("using System;\t\n");
        sb.Append("[Serializable]\t\n");
        sb.Append($"public class {fileName}{mode.ToString()} ");//类名
        if (mode == ExporterMode.Model)//模型类继承模型接口
            sb.Append(": IModel");
        sb.Append("\n");
        sb.Append("{\n");

        for (int col = 1; col <= properties.Length; col++)
        {
            string fieldType = GetType(worksheet, col);
            string fieldName = properties[col - 1];
            sb.Append($"\tpublic {fieldType} {fieldName};\n");
        }

        sb.Append("}\n\n");

        FilesUtil.SaveFile(string.Format("{0}/{1}", Application.dataPath, mode == ExporterMode.Config ? configClassPath : modelClassPath),
        string.Format("{0}{1}.cs", fileName, mode.ToString()), sb.ToString());


    }
    /// <summary>
    /// 导出JSON
    /// </summary>
    private static void ExportJson(ExcelWorksheet worksheet, string fileName, ExporterMode mode)
    {
        string str = "";
        string[] properties = GetProperties(worksheet);
        int addNum = 0;
        for (int col = 1; col <= properties.Length; col++)
        {
            string[] temp = GetValues(worksheet, col);
            addNum = temp.Length;
            foreach (var value in temp)
            {
                str += GetJsonK_VFromKeyAndValues(properties[col - 1],
                    Convert(GetType(worksheet, col), value)) + ',';
            }
        }
        //获取key:value的字符串
        str = str.Substring(0, str.Length - 1);
        str = GetJsonFromJsonK_V(str, properties.Length, addNum);
        Debug.Log(str);

        str = GetUnityJsonFromJson(str);
        FilesUtil.SaveFile(string.Format("{0}/{1}", Application.dataPath, mode == ExporterMode.Config ? configPath : modelPath),
        string.Format("{0}{1}.{2}", fileName, mode.ToString(), mode == ExporterMode.Config ? "json" : "record"),
        str);
    }

    /// <summary>
    /// 获取属性
    /// </summary>
    private static string[] GetProperties(ExcelWorksheet worksheet)
    {
        string[] properties = new string[worksheet.Dimension.End.Column];
        for (int col = 1; col <= worksheet.Dimension.End.Column; col++)
        {
            if (worksheet.Cells[propertyIndex, col].Text == "")
                throw new System.Exception(string.Format("第{0}行第{1}列为空", propertyIndex, col));
            properties[col - 1] = worksheet.Cells[propertyIndex, col].Text;
        }
        return properties;
    }

    /// <summary>
    /// 获取值
    /// </summary>
    private static string[] GetValues(ExcelWorksheet worksheet, int col)
    {
        //容量减去前三行
        string[] values = new string[worksheet.Dimension.End.Row - 3];
        for (int row = valueIndex; row <= worksheet.Dimension.End.Row; row++)
        {
            values[row - valueIndex] = worksheet.Cells[row, col].Text;
        }
        return values;
    }

    /// <summary>
    /// 获取类型
    /// </summary>
    private static string GetType(ExcelWorksheet worksheet, int col)
    {
        return worksheet.Cells[typeIndex, col].Text;
    }

    /// <summary>
    /// 通过类型返回对应值
    /// </summary>
    private static string Convert(string type, string value)
    {
        string res = "";
        switch (type)
        {
            case "int": res = value; break;
            case "int32": res = value; break;
            case "int64": res = value; break;
            case "long": res = value; break;
            case "float": res = value; break;
            case "double": res = value; break;
            case "string": res = $"\"{value}\""; break;
            default:
                throw new Exception($"不支持此类型: {type}");
        }
        return res;
    }

    /// <summary>
    /// 返回key:value
    /// </summary>
    private static string GetJsonK_VFromKeyAndValues(string key, string value)
    {
        return string.Format("\"{0}\":{1}", key, value);
    }

    /// <summary>
    ///获取[key:value]转换为{key:value,key:value},再变成[{key:value,key:value},{key:value,key:value}]
    /// </summary>
    private static string GetJsonFromJsonK_V(string json, int valueNum, int addNum)
    {
        string str = "";
        string[] strs;
        List<string> listStr = new List<string>();
        strs = json.Split(',');
        listStr.Clear();

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < valueNum; i++)
        {
            sb.Clear();
            int j = i;
            while (j < strs.Length)
            {
                sb.Append(strs[j]);
                sb.Append(",");
                j += addNum;
            }
            str = sb.ToString();
            str = $"{{{str.Substring(0, str.Length - 1)}}}";
            listStr.Add(str);
        }

        str = "[";
        foreach (var l in listStr)
        {
            str += l + ',';
        }
        str = str.Substring(0, str.Length - 1);
        str += ']';
        return str;
    }

    /// <summary>
    /// 适应JsonUtility.FromJson函数的转换格式
    /// </summary>
    private static string GetUnityJsonFromJson(string json)
    {
        return "{" + "\"datas\":" + json + "}";
    }

}






存档类代码:

存档类
using UnityEngine;
using System.IO;
using System.Collections.Generic;
using System;


/// <summary>
/// 本地模式存档类
/// </summary>
public class Recorder : Singleton<Recorder>
{
    /// <summary>
    /// 不同模式下的存储路径
    /// </summary>
    private string RecordPath
    {
        get
        {
#if (UNITY_EDITOR || UNITY_STANDALONE)
            return string.Format("{0}/Records", Application.dataPath);
#else
            return string.Format("{0}/Records", Application.persistentDataPath);
#endif
        }
    }

    /// <summary>
    /// 用来临时存储存档的容器,便与定时存储而不是每一次修改都进行存储
    ///Key是文件名,Value是内容
    /// </summary>
    private Dictionary<string, string> _cache = new Dictionary<string, string>();

    public Recorder()
    {
        _cache.Clear();
        FileInfo[] files = FilesUtil.LoadFiles(RecordPath);
        foreach (var f in files)
        {
            string key = Path.GetFileNameWithoutExtension(f.FullName);
            string value = File.ReadAllText(f.FullName);
            _cache.Add(key, value);
        }
    }

    /// <summary>
    /// 通常不会修改一次数据就保存一次,间隔保存或者统一保存可以调用此方法
    /// 强制手动保存
    /// 将cache内容同步到本地文件
    /// </summary>
    public void ForceSave()
    {
        FileInfo[] files = FilesUtil.LoadFiles(RecordPath);
        foreach (var f in files)
        {
            string name = Path.GetFileNameWithoutExtension(f.Name);
            if (_cache.ContainsKey(name))
            {
                string path = string.Format("{0}/{1}.record", RecordPath, name);
                if (File.Exists(path)) File.Delete(path);
                //重新写入
                File.WriteAllText(path, _cache[name]);
            }
        }
    }

    /// <summary>
    /// 读取数据,dynamic表示你是从对象的cache中获取数据,还是读取静态存档的数据
    /// </summary>
    public DataList<T> LoadData<T>() where T : IModel
    {
        try
        {
            string fileContent = _cache[typeof(T).Name];
            DataList<T> dataList = JsonUtility.FromJson<DataList<T>>(fileContent);
            return dataList;
        }
        catch (Exception err)
        {
            throw new System.Exception(err.ToString());
        }

    }

    /// <summary>
    /// 存储数据,暂存在字典中或者持续存储到文件中
    /// 不建议每次更改数据都存储到文件中
    /// 非必要不使用save = true,建议使用ForceSave进行一次性的统一存储
    /// </summary>
    public void SaveData<T>(DataList<T> data, bool save = false) where T : IModel
    {
        string json = JsonUtility.ToJson(data);
        try
        {
            _cache[typeof(T).Name] = json;
            if (save)
            {
                string path = string.Format("{0}/{1}.record", RecordPath, typeof(T).Name);
                if (File.Exists(path)) File.Delete(path);
                //重新写入
                File.WriteAllText(path, json);
            }
        }
        catch (System.Exception)
        {
            throw;
        }


    }
    #region  CURD
    public void CreateData<T>(T data, bool save = false) where T : IModel
    {
        DataList<T> dataList = LoadData<T>();
        dataList.datas.Add(data);
        SaveData<T>(dataList, save);
    }
    public void UpdateData<T>(int index, T data, bool save = false) where T : IModel
    {
        try
        {
            DataList<T> dataList = LoadData<T>();
            dataList.datas[index] = data;
            SaveData<T>(dataList, save);
        }
        catch (Exception err)
        {
            throw new System.Exception(err.ToString());
        }
    }
    public T ReadData<T>(int index) where T : IModel
    {
        try
        {
            DataList<T> dataList = LoadData<T>();
            return dataList.datas[index];
        }
        catch (Exception err)
        {
            throw new System.Exception(err.ToString());
        }

    }
    public void DeleteData<T>(T data, bool save = false) where T : IModel
    {
        DataList<T> dataList = LoadData<T>();
        dataList.datas.Remove(data);
        SaveData<T>(dataList, save);
    }
    public void DeleteData<T>(int index, bool save = false) where T : IModel
    {
        try
        {
            DataList<T> dataList = LoadData<T>();
            dataList.datas.RemoveAt(index);
            SaveData<T>(dataList, save);
        }
        catch (System.Exception)
        {
            throw;
        }
    }
    #endregion
}

Config读取代码:

ConfigLoader
using UnityEngine;

public class ConfigLoader : Singleton<ConfigLoader>
{
    public DataList<T> LoadConfig<T>()
    {
        string json = Resources.Load<TextAsset>("Json/" + typeof(T).Name).text;
        DataList<T> dataList = JsonUtility.FromJson<DataList<T>>(json);
        return dataList;
    }
}

使用

1.写俩个Excel测试(这里同一个Excel分成俩份,一个表示默认配置数据,一个表示model的结构不带数据也可以的):

image

需要注意:

Excel存放路径:
image

Config导出路径(Resources.Json)以及存档存储路径(编辑模式下在Assets/Records下,运行模式下在Application.persistentDataPath中)
image
image

2.通过编辑器导出对应的类型:

image
导出的文件:
image
image

导出类路径以及导出类:
image
image
image

测试

image
image
image

image
image
本地存档也修改了:
image
image

存档的优化:

在实际开发中,游戏存档一般不会在每一次数据修改就会改变,而是选择在一个特殊阶段(比如玩家退出游戏),或者是间隔时间存储,所以我们
一般使用一个字典先记录模型和对应的数据,通过一个公共方法控制文件的存储。
image
image

posted @ 2023-03-30 01:08  C1earF  阅读(826)  评论(0编辑  收藏  举报