草庐IT

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

ameC1earF 2023-03-30 原文

思想

在游戏过程中,玩家的背包、登录、人物系统都与数据息息相关,无论是一开始就设定好的默认数据,还是可以动态存取的数据,都需要开发人员去管理。
游戏开发过程中,策划一般通过Excel表格配置一些内容来对游戏的一些行为经行数据的设定。表格有config默认数据,程序只需要读取即可;还可能建立model类数据需要在游戏中实例化对象来进行数据的增删改查.

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

方法

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

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

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

使用

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

需要注意:

Excel存放路径:

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

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


导出的文件:

导出类路径以及导出类:


测试




本地存档也修改了:

存档的优化:

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

完整代码

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 = "";
        int num = 0;
        string[] properties = GetProperties(worksheet);
        for (int col = 1; col <= properties.Length; col++)
        {
            string[] temp = GetValues(worksheet, col);
            num = 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, num);
        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)
    {
        string str = "";
        string[] strs;
        List<string> listStr = new List<string>();
        strs = json.Split(',');
        listStr.Clear();
        for (int j = 0; j < valueNum; j++)
        {
            listStr.Add("{" + string.Format("{0},{1}", strs[j], strs[j + valueNum]) + "}");
        }
        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;
    }
}

有关Unity 中的存档系统(本地存档)的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  6. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  7. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  8. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  9. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐