前言
这段时间刚好看悠游视频,学习MMORPG的游戏制作,开这个篇章的主要是为了记录下自己的学习历程,以及自己的一些理解和思考,主要会把学习到的一些比较重要的东西记录下。
使用的环境
- Unity版本 2020.2.3f1c1
- 使用到第三方Dll ExcelDataReader类库
- Zlib数据压缩类库
- 课程下载地址 http://www.u3dol.com/index_CourseOne.html
游戏本地数据处理方案
一个游戏从总体考虑说白了就是一个MVC,C(Control) 控制处理玩家输入,V(View)显示游戏画面,而其中的M(Model)就是游戏数据部分了,所以一个好的数据处理方案,能让读写修改数据变得简单,让游戏处理更加方便。
而游戏数据主要是指资源数据,如音频数据,图片数据,数值相关数据 等等
而今天要说的就是数值相关数据 ,而数值相关数据也主要分成两大类
- 本地数据:一旦配置好(主要是策划配置),在玩家玩的过程中,数据内容本不会发生变化,主要是一些本地数据,如商品,道具等等
- 用户数据:随着玩家各种操作不断变化,主要是用户的数据,如用户信息,用户资源等等
当然这篇主要是想奖将本地数据的处理方案,用户的数据我们后面再说。
本地数据类设计
本地数据的结构大致分两大部分,三层结构
- 两大部分
- DBModel部分 主要是 数据管理类 相关
- Entity部分 主要是 数据实体类 相关
- 三层结构
- AbstractDBModel,AbstractEntity 抽象类层
- XXXDBModel,XXXEntity 自动生成的类层
- XXXDBmodelExt,XXXEntityExt 自定义类层
具体结构类图如下:
1. 抽象类 层
这层主要是用来抽象一些公共处理方法(加载data文件,获取数据实体信息等) 和 一些公共数据实体属性(编号ID等)
1. AbstractDBModel<T,P>类
- 定义约束
- Where T : class, new(). //T 必须是类 并且具有无参构造方法
- Where P: AbstractEntity //P 必须是AbstractEntity(抽象数据实体类)子类
- 提供懒加载模式单例,供其他地方访问 Entity数据实体信息
- 加载data文件数据到内存
- 这里通过依赖反转将路径依赖的文件名(子类实现设置FileName属性值),和数据实体的解析(子类实现MakeEntity方法)交给 DBModel子类去实现。
- 这里Data文件为加密的byte自定义数组,后面会详细说明。
- GameDataTableParser类可以理解为解析data文件数据,这个后面也会详细说明。
- .提供外部结构 访问数据
具体的代码如下:
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 抽象数据实体数据管理类
/// </summary>
/// <typeparam name="T">实体数据管理类</typeparam>
/// <typeparam name="P">实体数据信息类</typeparam>
public abstract class AbstractDBModel<T, P>
where T : class, new()
where P : AbstractEntity
{
/// <summary>
/// 数据集合
/// </summary>
protected List<P> m_PDataList;
/// <summary>
/// 数据集合
/// </summary>
protected Dictionary<int, P> m_PDataDic;
#region 单例
private static T instance;
/// <summary>
/// 单例
/// </summary>
public static T Instance
{
get
{
if (instance == null)
{
instance = new T();
}
return instance;
}
}
#endregion
/// <summary>
/// 构造方法
/// </summary>
protected AbstractDBModel()
{
m_PDataList = new List<P>();
m_PDataDic = new Dictionary<int, P>();
//加载数据
LoadData();
}
/// <summary>
/// 加载数据
/// </summary>
private void LoadData()
{
//路径后期修改为Application.persistentDataPath/Data/LocalData
//读取文件data数据
string path = string.Format("{0}/DataToExcel/{1}",
Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("/Assets") + 1),
FileName);
using (GameDataTableParser parser = new GameDataTableParser(path))
{
while (parser.Eof ==false)
{
//创建实体
P p = MakeEntity(parser);
m_PDataList.Add(p);
m_PDataDic[p.ID] = p;
//下一个
parser.Next();
}
}
}
#region 子类实现
/// <summary>
/// 子类实现 文件名
/// </summary>
protected abstract string FileName { get; }
/// <summary>
/// 子类实现 创建实体
/// </summary>
protected abstract P MakeEntity(GameDataTableParser parser);
#endregion
#region 对外访问 获取数据
/// <summary>
/// 获取所有信息
/// </summary>
/// <returns></returns>
public List<P> GetAll()
{
return m_PDataList;
}
/// <summary>
/// 获取单个数据
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public P Get(int id)
{
if (m_PDataDic.ContainsKey(id))
{
return m_PDataDic[id];
}
return null;
}
#endregion
}
2. AbstractEntity 类
主要为所有数据实体子类提供公共属性ID,这也可以在DBModel中通过ID去查找之类
具体代码如下:
/// <summary>
/// 抽象实体信息类
/// </summary>
public abstract class AbstractEntity
{
/// <summary>
/// 编号
/// </summary>
public int ID {get; set;}
}
2. 自动生成的类 层
这层主要是通过生成Data数据文件的同时代码去自动生成的,一些基础的实体和实体数据管理类,具体如何生成我们后面详细说明。
1. XXXEntity类 实际数据实体
实际数据实体类主要数据记录和具体业务相关
具体我们看个例子,ProductEntity,商品实体类 主要包含商品的基础信息
具体代码如下:
/// <summary>
/// Product实体类
/// </summary>
public partial class ProductEntity : AbstractEntity
{
/// <summary>
/// 商品名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 商品价格
/// </summary>
public int Piece { get; set; }
/// <summary>
/// 商品图片名称
/// </summary>
public string PicName { get; set; }
/// <summary>
/// 商品描述
/// </summary>
public string Desc { get; set; }
/// <summary>
/// 测试坐标
/// </summary>
public string Pos { get; set; }
}
2. XXXDBModel类 实际数据管理类
这一部分主要是继承AbstractDBModel<T,P>抽象类,并实现其中FileName属性,和MakeEntity方法。
具体我们看个例子,ProductDBModel ,商品实体数据管理类继承至 AbstractDBModel<ProductDBModel, ProductEntity>
类,然后实现FileName属性 返回 “Product.data” data文件名,并且实现MakeEntity方法,从GameDataTableParser读取一行数据转化为ProductEntity实体。
具体代码如下:
/// <summary>
/// Product数据管理类
/// </summary>
public partial class ProductDBModel : AbstractDBModel<ProductDBModel, ProductEntity>
{
/// <summary>
/// 文件名称
/// </summary>
protected override string FileName { get { return "Product.data"; } }
/// <summary>
/// 创建实体
/// </summary>
/// <param name="parser"></param>
/// <returns></returns>
protected override ProductEntity MakeEntity(GameDataTableParser parser)
{
ProductEntity entity = new ProductEntity();
entity.ID = parser.GetFileValue(parser.FieldName[0]).ToInt();
entity.Name = parser.GetFileValue(parser.FieldName[1]);
entity.Piece = parser.GetFileValue(parser.FieldName[2]).ToInt();
entity.PicName = parser.GetFileValue(parser.FieldName[3]);
entity.Desc = parser.GetFileValue(parser.FieldName[4]);
entity.Pos = parser.GetFileValue(parser.FieldName[5]);
return entity;
}
}
3. 自定义类 层
这一层主要包含用户自定义的数据实体管理类,和自定义的数据实体类,这里需要注意的是使用到了 partial (C#语言关键字),
partial:定义的类可以在多个地方被定义,最后编译的时候会被当作一个类来处理。
主要用在下面3种情况
- 类型特别大,不宜放在一个文件中实现
- 一个类型中的一部分代码为自动化工具生成的代码,不宜与我们自己编写的代码混合在一起
- 需要多人合作编写一个类
这里主要是使用到第二点,因为我们XXXDBModel,XXXEntity都是自动生成的,但是自动生成的部分有时候并不能完全满足我们需要,所以就需要使用到扩充类,使用 partial 关键字,让 自动生成部分 和 自定义部分 可以分开两个文件,但最后编译合并到一起。
1. XXXEntityExt类 实际数据实体扩展类
扩充XXXEntity一些不能表示的数据类型部分。
具体我们看下面例子,ProductEntityExt 商品信息扩展类
其中的Pos坐标 是Vector3类型,但是在我们基础类型中并不存在,所有我们使用string字符串将x,y,z3个字段拼接起来,如1_1_1就表示坐标Vector3(1,1,1);
具体代码如下:
/// <summary>
/// Product实体类
/// </summary>
public partial class ProductEntity : AbstractEntity
{
/// <summary>
/// 例子 获取保存的坐标值
/// 等等
/// </summary>
public Vector3 RelPos
{
get
{
string[] posStr = Pos.Split('_');
if(posStr.Length == 3)
{
return new Vector3(posStr[0].ToFloat(), posStr[1].ToFloat(), posStr[2].ToFloat());
}
return Vector3.zero;
}
}
}
2. XXXDBModelExt类 实际数据实体管理扩展类
扩充XXXDBModel类主要用于一些特殊的获取数据,或者对实体数据进行一些处理。
具体我们看下面例子,ProductDBModelExt 商品数据实体管理扩展类
其中我写了一个例子,获取最高价格的商品GetHighestPiece方法,用于获取当前商品中的最高价格,当然这只是我举个例子,实际就要看真正的业务场景,比如获取某种类型的所有商品等等,都可以写在扩充类中。
具体的代码如下:
/// <summary>
/// Product数据管理类 扩展
/// </summary>
public partial class ProductDBModel : AbstractDBModel<ProductDBModel, ProductEntity>
{
/// <summary>
/// 例子 获取最高价格的 商品
/// </summary>
/// <returns></returns>
public ProductEntity GetHighestPiece()
{
int MaxPiece =-999;
ProductEntity highestProd = null;
for (int i = 0; i <m_PDataList.Count ; i++)
{
if(MaxPiece < m_PDataList[i].Piece)
{
MaxPiece = m_PDataList[i].Piece;
highestProd = m_PDataList[i];
}
}
return highestProd;
}
}
本地数据配置与生成
1. 文件格式选择
大部分都是使用Excel来给策划配置本地数据,主要是Excel的可视化操作,一些公式的使用,能很大的提高策划配置数值的效率,但是使用Exce直接作为游戏本地数据有很多缺点
- Excel文件格式同样文件内容,占用空间更大
- Excel文件动态读取相对更加麻烦,速度也更加慢
所以大部分都在游戏中使用其他格式文件(如二进制,Json,Xml)这里主要是将数据转化为Byte数组然后写入文件保存,相比于Json,Xml来说文件的体积更小,读取速度更快。
2. 内容格式规定
Excel文件内容格式需要作为规定优先确定好,方便后面将Excel转化为其他格式,以及自动化生成实体信息类和实体数据控制类代码(这个后面会详细说明)。
其中一个Excel一个文件对应一个游戏的实体类,默认第一个sheet页即为游戏数据内容 (注:Excel文件格式为 Excel97-2003工作薄 )
Excel的文件格式为
第一行 实体字段变量名
第二行 实体字段类型 (主要类型为int,float,long,string)
第三行 实体字段介绍名称
第四行以后 为实际数据内容
举个例子,Product.xml 商品数据信息 , 如下图所示
3.Excel转化为本地数据文件(data文件)
将策划配置好的Excel转化为本地数据文件(data文件),主要分成5个步骤
- 读取Excel文件转化为DataTable
- DataTable内容转化为Byte数组
- 异或加密Byte数组
- Zlib压缩Byte数组
- 将Byte数组保存为data文件
其中:
-
读取Excel文件,将第一个sheet页内容读取为DataTable类型,这里使用到第三方Dll库,ExcelDataReader类库。
具体代码如下:/// <summary> /// 读取Excel表格数据 /// </summary> /// <returns></returns> private static DataTable LoadExcelData(string path) { if (string.IsNullOrEmpty(path)) return null; DataTable dataTable = null; using (FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read)) { //2003版本 使用CreateBinaryReader //2007以上版本 使用CreateOpenXmlReader using (IExcelDataReader excelReader = ExcelReaderFactory.CreateBinaryReader(stream)) { DataSet result = excelReader.AsDataSet(); dataTable = result.Tables[0]; } } return dataTable; }
-
从DataTable中读取内容转化为byte数组
具体代码如下byte[] buffer = null; string[,] dataArr;//字段名称 字段类型 字段描述 using (MMO_MemoryStream ms = new MMO_MemoryStream()) { int row = dt.Rows.Count; int column = dt.Columns.Count; dataArr = new string[column,3]; //先写入行列 ms.WriteInt(row); ms.WriteInt(column); for (int i = 0; i < row; i++) { for (int j = 0; j < column; j++) { //第一是字段名称 第二行是字段类型 第三行是字段描述 if (i < 3) { dataArr[j,i] = dt.Rows[i][j].ToString().Trim(); } //写入内容 ms.WriteUTF8String(dt.Rows[i][j].ToString().Trim()); } } buffer = ms.ToArray(); }
注:MMO_MemoryStream类
MMO_MemoryStream 主要是继承至 MemoryStream,主要实现两个功能- 将各种基础类型数据转换为byte数值写入流
- 从流中读取byte数据并转化为其他基础类型数据
技术含量不高,但是需要注意以下几点
- 不同字段类型的 字段长度问题
类型 byte char int short long float double bool 字节长度 1 1 4 2 8 4 8 1 - ushort,uint,ulong 是无符号整数, 字段的长度并没有变化
- bool类型字段是通过写入 1个byte 位 实现 ,其中1表示true ,0表示false
- string 类型字段写入byte中时,是先写入一个ushort字段( string字段总长度),再写入string内容数据,读取的时候也是先去读一个ushort字段,然后再读取ushort长度的string内容数据。
具体代码如下:
using System; using System.IO; using System.Text; /// <summary> /// 转换byte数组 short ushort int uint long ulong float double bool string /// </summary> public class MMO_MemoryStream : MemoryStream { public MMO_MemoryStream() { } public MMO_MemoryStream(byte[] buffer) : base(buffer) { } #region short /// <summary> /// 从流中读取一个 short 字段 /// </summary> /// <returns></returns> public short ReadShort() { byte[] buffer = new byte[2]; base.Read(buffer, 0, 2); return BitConverter.ToInt16(buffer, 0); } /// <summary> /// 往流中写一个 short 字段 /// </summary> /// <param name="data"></param> public void WriteShort(short data) { byte[] buffer = BitConverter.GetBytes(data); base.Write(buffer, 0, 2); } #endregion #region ushort /// <summary> /// 从流中读取一个 ushort 字段 /// </summary> /// <returns></returns> public ushort ReaduUShort() { byte[] buffer = new byte[2]; base.Read(buffer, 0, 2); return BitConverter.ToUInt16(buffer, 0); } /// <summary> /// 往流中写一个 ushort 字段 /// </summary> /// <param name="data"></param> public void WriteUShort(ushort data) { byte[] buffer = BitConverter.GetBytes(data); base.Write(buffer, 0, 2); } #endregion #region int /// <summary> /// 从流中读取一个 int 字段 /// </summary> /// <returns></returns> public int ReadInt() { byte[] buffer = new byte[4]; base.Read(buffer, 0, 4); return BitConverter.ToInt32(buffer, 0); } /// <summary> /// 往流中写一个 int 字段 /// </summary> /// <param name="data"></param> public void WriteInt(int data) { byte[] buffer = BitConverter.GetBytes(data); base.Write(buffer, 0, 4); } #endregion #region uint /// <summary> /// 从流中读取一个 uint 字段 /// </summary> /// <returns></returns> public uint ReaduUInt() { byte[] buffer = new byte[4]; base.Read(buffer, 0, 4); return BitConverter.ToUInt32(buffer, 0); } /// <summary> /// 往流中写一个 uint 字段 /// </summary> /// <param name="data"></param> public void WriteUInt(uint data) { byte[] buffer = BitConverter.GetBytes(data); base.Write(buffer, 0, 4); } #endregion #region long /// <summary> /// 从流中读取一个 long 字段 /// </summary> /// <returns></returns> public long ReadLong() { byte[] buffer = new byte[8]; base.Read(buffer, 0, 8); return BitConverter.ToInt64(buffer, 0); } /// <summary> /// 往流中写一个 long 字段 /// </summary> /// <param name="data"></param> public void WriteLong(long data) { byte[] buffer = BitConverter.GetBytes(data); base.Write(buffer, 0, 8); } #endregion #region ulong /// <summary> /// 从流中读取一个 uint 字段 /// </summary> /// <returns></returns> public ulong ReaduULong() { byte[] buffer = new byte[8]; base.Read(buffer, 0, 8); return BitConverter.ToUInt64(buffer, 0); } /// <summary> /// 往流中写一个 ulong 字段 /// </summary> /// <param name="data"></param> public void WriteULong(ulong data) { byte[] buffer = BitConverter.GetBytes(data); base.Write(buffer, 0, 8); } #endregion #region float /// <summary> /// 从流中读取一个 float 字段 /// </summary> /// <returns></returns> public float ReadFloat() { byte[] buffer = new byte[4]; base.Read(buffer, 0, 4); return BitConverter.ToSingle(buffer, 0); } /// <summary> /// 往流中写一个 float 字段 /// </summary> /// <param name="data"></param> public void WriteFloat(float data) { byte[] buffer = BitConverter.GetBytes(data); base.Write(buffer, 0, 4); } #endregion #region double /// <summary> /// 从流中读取一个 double 字段 /// </summary> /// <returns></returns> public double ReadDouble() { byte[] buffer = new byte[8]; base.Read(buffer, 0, 8); return BitConverter.ToDouble(buffer, 0); } /// <summary> /// 往流中写一个 double 字段 /// </summary> /// <param name="data"></param> public void WriteDouble(double data) { byte[] buffer = BitConverter.GetBytes(data); base.Write(buffer, 0, 8); } #endregion #region Bool /// <summary> /// 从流中读取一个bool数据 /// </summary> /// <returns></returns> public bool ReadBool() { return base.ReadByte() == 1; } /// <summary> /// 往流中写一个 bool 字段 /// </summary> /// <param name="value"></param> public void WriteBool(bool data) { base.WriteByte((byte)(data == true ? 1 : 0)); } #endregion #region string /// <summary> /// 从流中读取一个 string 字段 /// </summary> /// <returns></returns> public string ReadUTF8String() { ushort count = ReaduUShort(); byte[] buffer = new byte[count]; base.Read(buffer, 0, count); return Encoding.UTF8.GetString(buffer); } /// <summary> /// 往流中写一个 string 字段 /// </summary> /// <param name="data"></param> public void WriteUTF8String(string data) { byte[] buffer = Encoding.UTF8.GetBytes(data); if (buffer.Length > 65535) { throw new InvalidCastException("字符串超出范围"); } WriteUShort((ushort)buffer.Length); base.Write(buffer, 0, buffer.Length); } #endregion }
-
异或加密Byte数组
将byte数组内容通过设置的异或因子进行异或来实现加密数据为什么选用异或?
将相同的异或因子同时做两次异或就能得到原本的数据,对于加密过的数据,通过使用相同的异或因子再次进行异或操作就能实现解密。对于加密解密操作更加简单。具体代码如下:
//.data文件的xor加解密因子 private static byte[] xorScale = new byte[] { 45, 66, 38, 55, 23, 254, 9, 165, 90, 19, 41, 45, 201, 58, 55, 37, 254, 185, 165, 169, 19, 171 }; int iScaleLen = xorScale.Length; for (int i = 0; i < buffer.Length; i++) { buffer[i] = (byte)(buffer[i] ^ xorScale[i % iScaleLen]); }
-
Zlib压缩Byte数组
这里使用到了zLib插件进行byte数组压缩,具体如何压缩,这里就不做赘述,有兴趣的同学自己去看看。(主要是我也没看,手动滑稽)具体代码如下:
buffer = ZlibHelper.CompressBytes(buffer);
-
将byte数组保存为data文件
这里就使用了FileStrem将Byte数据写入到data文件中,没有太多要说的具体代码如下:
FileStream fs = new FileStream(string.Format("{0}{1}.data", filePath, fileName),FileMode.Create); fs.Write(buffer,0,buffer.Length); fs.Close();
4.自动生成XXXEntity类,XXXDBModel类代码文件
前面提到了,本地数据类中自动生成部分中,XXXEntity类,XXXDBModel类就是在生成data文件的同时,自动化生成代码文件的。主要是根据Excel中前三行格式(第一行 字段变量名 第二行 字段类型 第三行 字段描述)来生成。
这里需要注意的是使用 string.fromat 函数,如果格式化字符串中包含 ”{ 或 }“ 需要使用使用“{{ 或 }}“来表示。
-
自动生成XXXEntity类代码文件
具体代码如下:/// <summary> /// 自动创建数据实体类 /// </summary> /// <param name="filePath"></param> /// <param name="fileName"></param> /// <param name="dataArr"></param> private static void CreateEntity(string filePath, string fileName, string[,] dataArr) { if (dataArr == null) return; string savePath = string.Format("{0}/Create", filePath); if (Directory.Exists(savePath) == false) { Directory.CreateDirectory(savePath); } StringBuilder sb = new StringBuilder(); sb.Append("//***********************************************************"); sb.AppendLine(); sb.Append(string.Format("// 描述:{0}实体类", fileName)); sb.AppendLine(); sb.Append("// 作者:fanwei "); sb.AppendLine(); sb.Append(string.Format("// 创建时间:{0} ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))); sb.AppendLine(); sb.Append("// 版本:1.0 "); sb.AppendLine(); sb.Append("// 备注:此代码为工具生成 请勿手工修改"); sb.AppendLine(); sb.Append("//***********************************************************"); sb.AppendLine(); sb.Append("/// <summary>"); sb.AppendLine(); sb.Append(string.Format("/// {0}实体类",fileName)); sb.AppendLine(); sb.Append("/// </summary>"); sb.AppendLine(); sb.Append(string.Format("public partial class {0}Entity : AbstractEntity", fileName)); sb.AppendLine(); sb.Append("{"); sb.AppendLine(); for (int i = 1; i < dataArr.GetLength(0); i++) { sb.Append(" /// <summary>"); sb.AppendLine(); sb.Append(string.Format(" /// {0}", dataArr[i,2])); sb.AppendLine(); sb.Append(" /// </summary>"); sb.AppendLine(); sb.Append(string.Format(" public {0} {1} {{ get; set; }}", dataArr[i, 1], dataArr[i, 0])); sb.AppendLine(); } sb.Append("}"); sb.AppendLine(); //写入文件 using (FileStream fs = new FileStream(string.Format("{0}/{1}Entity.cs", savePath, fileName), FileMode.Create)) { using (StreamWriter sw = new StreamWriter(fs)) { sw.Write(sb.ToString()); } } }
-
自动生成XXXDBModel类代码文件
具体代码如下:/// <summary> /// 自动生成数据管理类 /// </summary> /// <param name="filePath"></param> /// <param name="fileName"></param> /// <param name="dataArr">字段信息数组</param> private static void CreateDBModel(string filePath,string fileName,string [,] dataArr) { if (dataArr == null) return; string savePath = string.Format("{0}/Create", filePath); if (Directory.Exists(savePath) == false) { Directory.CreateDirectory(savePath); } StringBuilder sb = new StringBuilder(); sb.Append("//***********************************************************"); sb.AppendLine(); sb.Append(string.Format("// 描述:{0}数据管理类", fileName)); sb.AppendLine(); sb.Append("// 作者:fanwei "); sb.AppendLine(); sb.Append(string.Format("// 创建时间:{0} ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))); sb.AppendLine(); sb.Append("// 版本:1.0 "); sb.AppendLine(); sb.Append("// 备注:此代码为工具生成 请勿手工修改"); sb.AppendLine(); sb.Append("//***********************************************************"); sb.AppendLine(); sb.Append("/// <summary>"); sb.AppendLine(); sb.Append(string.Format("/// {0}数据管理类",fileName)); sb.AppendLine(); sb.Append("/// </summary>"); sb.AppendLine(); sb.Append(string.Format("public partial class {0}DBModel : AbstractDBModel<{0}DBModel, {0}Entity>", fileName)); sb.AppendLine(); sb.Append("{"); sb.AppendLine(); sb.Append(" /// <summary>"); sb.AppendLine(); sb.Append(" /// 文件名称"); sb.AppendLine(); sb.Append(" /// </summary>"); sb.AppendLine(); sb.Append(string.Format(" protected override string FileName {{ get {{ return \"{0}.data\"; }} }}",fileName)); sb.AppendLine(); sb.Append(" /// <summary>"); sb.AppendLine(); sb.Append(" /// 创建实体"); sb.AppendLine(); sb.Append(" /// </summary>"); sb.AppendLine(); sb.Append(" /// <param name=\"parser\"></param>"); sb.AppendLine(); sb.Append(" /// <returns></returns>"); sb.AppendLine(); sb.Append(string.Format(" protected override {0}Entity MakeEntity(GameDataTableParser parser)", fileName)); sb.AppendLine(); sb.Append(" {"); sb.AppendLine(); sb.Append(" ProductEntity entity = new ProductEntity();"); sb.AppendLine(); for (int i = 0; i < dataArr.GetLength(0); i++) { sb.Append(string.Format(" entity.{0} = parser.GetFileValue(parser.FieldName[{1}]){2};", dataArr[i,0],i, ChangeToType(dataArr[i,1]))); sb.AppendLine(); } sb.Append(" return entity;"); sb.AppendLine(); sb.Append(" }"); sb.AppendLine(); sb.Append("}"); sb.AppendLine(); //写入文件 using (FileStream fs = new FileStream(string.Format("{0}/{1}DBModel.cs", savePath, fileName), FileMode.Create)) { using(StreamWriter sw = new StreamWriter(fs)) { sw.Write(sb.ToString()); } } }
本地数据解析与使用
本地数据解析
前面讲AbstractDBModel中LoadData的时候,使用到了 GameDataTableParser 去解析data文件,这里就详细说下GameDataTableParser 如何去解析data文件的。
1. data文件的格式
大概结构如下,前两个int值是表示数据总共的行和列数,后面就是按每行数据,循环。
第一个Int值 数据行数Row
第二个Int值 数据列数Column
第一行数据 字段1值长度 字段1值(string) 字段2值长度 字段2值(string)等等
第二行数据 字段1值长度 字段1值(string) 字段2值长度 字段2值(string)等等
。。。。。
等等
2. GameDataTableParser解析
前面保存data文件的时候我们使用到多个步骤,如异或加密,zlib压缩,这里解析的话我们也要使用相反的步骤去处理。具体步骤大概为一下4步
- 读取data文件保存到byte数组
- Zlib解压byte数组
- 异或解密byte数组
- 读取Byte数据并存入string[row,column]内存中
其中:
-
读取data文件保存到byte数组
比较简单使用FileStream读取对于的data文件
具体代码如下:byte[] buffer = null; using (FileStream fs = new FileStream(path, FileMode.Open)) { buffer = new byte[fs.Length]; fs.Read(buffer, 0, buffer.Length); }
-
Zlib解压Byte数组
具体代码如下:buffer = ZlibHelper.DeCompressBytes(buffer);
-
异或解密Byte数组
前面也提到了,异或解密和加密是同一样的操作,不过必须保证异或因子相同
具体代码如下://.data文件的xor加解密因子 private static byte[] xorScale = new byte[] { 45, 66, 38, 55, 23, 254, 9, 165, 90, 19, 41, 45, 201, 58, 55, 37, 254, 185, 165, 169, 19, 171 }; int iScaleLen = xorScale.Length; for (int i = 0; i < buffer.Length; i++) { buffer[i] = (byte)(buffer[i] ^ xorScale[i % iScaleLen]); }
-
读取byte数据并存入string[row,column]内存中
这里主要根据data的结构使用MMO_MemoryStream类来读取的string内容的
具体代码如下:using (MMO_MemoryStream ms = new MMO_MemoryStream(buffer)) { //前面两个数是 行 列 m_Row = ms.ReadInt(); m_Column = ms.ReadInt(); m_FieldName = new string[m_Column]; m_FieldNameDic = new Dictionary<string, int>(); m_GameData = new string[m_Row, m_Column]; for (int i = 0; i < m_Row; i++) { for (int j = 0; j < m_Column; j++) { string str = ms.ReadUTF8String(); if (i == 0) { //第一行 字段名 m_FieldName[j] = str; m_FieldNameDic[str] = j; } else if (i > 2) { //实际内容 m_GameData[i, j] = str; } } } }
3. 本地数据使用
主要使用对应实体数据管理类来使用和访问本地数据的。由于实体数据管理类是单例,所以使得访问本地数据变得很简单。
具体举个例子,如前面说到的ProductModel 商品实体信息管理类,具体使用访问商品数据
如下代码所示:
//获取所有商品
List<ProductEntity> datas = ProductDBModel.Instance.GetAll();
for (int i = 0; i < datas.Count; i++)
{
Debug.Log(datas[i].Name);
}
//获取5号商品
ProductEntity prod5 = ProductDBModel.Instance.Get(5);
//获取最高价格商品
ProductEntity highPieceProd = ProductDBModel.Instance.GetHighestPiece();
编辑器工具
这边写了一个Unity编辑器主要有两个功能
- 选择Excel游戏数据文件,生成data文件和对应的 XXXDBModel,XXXEntity类代码文件
- 一键将 自动生成的XXXDBModel,XXXEntity类代码文件 copy 到指定的代码路径下
具体菜单如下图:
1. ExcelToData 工具
界面啥的都比较简单,主要功能如下:(具体的编辑器代码我就不放了,要的自己在文末代码工程下载)
- 选择Excel文件,选择需要转换的Excel文件
- ExcelToData,读取选择的Excel文件的游戏数据,并生成data文件和对应的 XXXDBModel,XXXEntity类代码文件
- 查看data文件,选择需要查看的data文件,查看内容是否转换成功
主要界面如下图所示:
2. CopyCreateAllFile 工具
这里是将自动生成的DBModel,Entity类文件全部 copy到 工程项目DBModel,Entity类放置的代码文件夹中
需要注意的是几个路径问题:
Excel文件放置路径 工程目录下的 ExcelToData 文件夹下
data文件生成路径 工程目录下的 ExcelToData 文件夹下
DBModel,Entity类自动生成路径 工程目录下的 ExcelToData/Create 文件夹下
DBModel,Entity类放置的代码路径 工程目录下的 Assets/Script/Data/LocalData/Create 文件夹下
结语
以上就是整个本地数据的处理方案,主要从本地数据的类结构设计,如何使用Excel生成对应data文件,如何在游戏中动态读取data文件,
以及通过工具去生成对应data文件这几个方面来阐述。
通过工具将Excel数据转化为data文件,同时自动化生成对应实体和实体管理类代码,可以减少很多重复性代码,与重复性劳动,能大幅度提高数据修改与数据结构变更带来的额外工作量。
通过使用 关键字 Partial 将 自动生成的实体和实体管理类(简单操作) 与 根据具体需求自定义实体和实体管理类(操作更结合业务)结合使用。这里也给我们提示,将简单重复的使用代码自动生成,将复杂业务相关的提取出来单独处理。
谢谢大家,共勉。
代码工程下载
Github工程地址 https://github.com/Wsxiaojian/MMORPG