Unity 中的存档系统(本地存档)
思想
在游戏过程中,玩家的背包、登录、人物系统都与数据息息相关,无论是一开始就设定好的默认数据,还是可以动态存取的数据,都需要开发人员去管理。
游戏开发过程中,策划一般通过Excel表格配置一些内容来对游戏的一些行为经行数据的设定。表格有config默认数据,程序只需要读取即可;还可能建立model类数据需要在游戏中实例化对象来进行数据的增删改查.
想要看具体实现方法可以到页尾查看完整代码。
MVC架构中Model的CRUD操作也包含在存档类中(本地存档):
方法
excel转换成config默认数据(json文件)并通过对应的类读取数据可以参考我之前发的文章
以下我对它进行了改良,涵盖了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的结构不带数据也可以的):
需要注意:
Excel存放路径:
Config导出路径(Resources.Json)以及存档存储路径(编辑模式下在Assets/Records下,运行模式下在Application.persistentDataPath中)
2.通过编辑器导出对应的类型:
导出的文件:
导出类路径以及导出类:
测试
本地存档也修改了:
存档的优化:
在实际开发中,游戏存档一般不会在每一次数据修改就会改变,而是选择在一个特殊阶段(比如玩家退出游戏),或者是间隔时间存储,所以我们
一般使用一个字典先记录模型和对应的数据,通过一个公共方法控制文件的存储。
本文来自博客园,作者:C1earF,转载请注明原文链接:https://www.cnblogs.com/ameC1earF/p/17271104.html