前言

这段时间刚好看悠游视频,学习MMORPG的游戏制作,开这个篇章的主要是为了记录下自己的学习历程,以及自己的一些理解和思考,主要会把学习到的一些比较重要的东西记录下。

使用的环境

  1. Unity版本 2020.2.3f1c1
  2. 使用到第三方Dll ExcelDataReader类库
  3. Zlib数据压缩类库
  4. 课程下载地址 http://www.u3dol.com/index_CourseOne.html

代码工程在文末

游戏本地数据处理方案

一个游戏从总体考虑说白了就是一个MVC,C(Control) 控制处理玩家输入,V(View)显示游戏画面,而其中的M(Model)就是游戏数据部分了,所以一个好的数据处理方案,能让读写修改数据变得简单,让游戏处理更加方便。

而游戏数据主要是指资源数据,如音频数据,图片数据,数值相关数据 等等

而今天要说的就是数值相关数据 ,而数值相关数据也主要分成两大类

  • 本地数据:一旦配置好(主要是策划配置),在玩家玩的过程中,数据内容本不会发生变化,主要是一些本地数据,如商品,道具等等
  • 用户数据:随着玩家各种操作不断变化,主要是用户的数据,如用户信息,用户资源等等

当然这篇主要是想奖将本地数据的处理方案,用户的数据我们后面再说。

本地数据类设计

本地数据的结构大致分两大部分,三层结构

  • 两大部分

    1. DBModel部分 主要是 数据管理类 相关
    2. Entity部分 主要是 数据实体类 相关
  • 三层结构
    1. AbstractDBModel,AbstractEntity 抽象类层
    2. XXXDBModel,XXXEntity 自动生成的类层
    3. XXXDBmodelExt,XXXEntityExt 自定义类层

具体结构类图如下:

1. 抽象类 层

这层主要是用来抽象一些公共处理方法(加载data文件,获取数据实体信息等) 和 一些公共数据实体属性(编号ID等)

1. AbstractDBModel<T,P>类
  1. 定义约束

    • Where T : class, new(). //T 必须是类 并且具有无参构造方法
    • Where P: AbstractEntity //P 必须是AbstractEntity(抽象数据实体类)子类
  2. 提供懒加载模式单例,供其他地方访问 Entity数据实体信息
  3. 加载data文件数据到内存
    • 这里通过依赖反转将路径依赖的文件名(子类实现设置FileName属性值),和数据实体的解析(子类实现MakeEntity方法)交给 DBModel子类去实现。
    • 这里Data文件为加密的byte自定义数组,后面会详细说明。
    • GameDataTableParser类可以理解为解析data文件数据,这个后面也会详细说明。
  4. .提供外部结构 访问数据

具体的代码如下:

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个步骤

  1. 读取Excel文件转化为DataTable
  2. DataTable内容转化为Byte数组
  3. 异或加密Byte数组
  4. Zlib压缩Byte数组
  5. 将Byte数组保存为data文件

其中:

  1. 读取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以上版本 使用CreateOpenXmlReaderusing (IExcelDataReader excelReader =  ExcelReaderFactory.CreateBinaryReader(stream)){DataSet result = excelReader.AsDataSet();dataTable = result.Tables[0];}}return dataTable;
    }
    
  2. 从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数据并转化为其他基础类型数据

    技术含量不高,但是需要注意以下几点

    1. 不同字段类型的 字段长度问题
    类型 byte char int short long float double bool
    字节长度 1 1 4 2 8 4 8 1
    1. ushort,uint,ulong 是无符号整数, 字段的长度并没有变化
    2. bool类型字段是通过写入 1个byte 位 实现 ,其中1表示true ,0表示false
    3. 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
    }
    
  3. 异或加密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]);
    }
    
  4. Zlib压缩Byte数组
    这里使用到了zLib插件进行byte数组压缩,具体如何压缩,这里就不做赘述,有兴趣的同学自己去看看。(主要是我也没看,手动滑稽)

    具体代码如下:

     buffer = ZlibHelper.CompressBytes(buffer);
    
  5. 将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步

  1. 读取data文件保存到byte数组
  2. Zlib解压byte数组
  3. 异或解密byte数组
  4. 读取Byte数据并存入string[row,column]内存中

其中:

  1. 读取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);
    }
    
  2. Zlib解压Byte数组
    具体代码如下:

     buffer = ZlibHelper.DeCompressBytes(buffer);
    
  3. 异或解密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]);
    }
    
  4. 读取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编辑器主要有两个功能

  1. 选择Excel游戏数据文件,生成data文件和对应的 XXXDBModel,XXXEntity类代码文件
  2. 一键将 自动生成的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 将 自动生成的实体和实体管理类(简单操作) 与 根据具体需求自定义实体和实体管理类(操作更结合业务)结合使用。这里也给我们提示,将简单重复的使用代码自动生成,将复杂业务相关的提取出来单独处理。

谢谢大家,共勉。

代码工程下载

只含代码部分

整个Unity项目Demo下载

Github工程地址 https://github.com/Wsxiaojian/MMORPG

三,游戏本地数据处理方案相关推荐

  1. 棋牌游戏高防服务器三种安全防护方案

    游戏服务器三种安全防护方案房卡游戏是一种深受广大朋友喜欢的娱乐游戏,以消耗房卡的形式获利,成本低房卡消耗快其中利润很高,属于暴利行业,暴利行业总是无法避免同行的恶意竞争和攻击小组的胁迫,很多初创房卡游 ...

  2. 第三代大数据处理方案Flink

    Apache Flink Flink作为第三代流计算引擎,同采取了DAG Stage拆分的思想构建了存粹的流计算框架.被人们称为第三代大数据处理方案.该计算框架和Spark设计理念出发点恰好相反. S ...

  3. 干货:揭秘手机游戏运营推广方案,赚钱必备!

    干货:揭秘手机游戏运营推广方案,赚钱必备! 一:搜索引擎:百度.360.搜狗.UC 优势:用户定位精准,质量高. 劣势:地方性精准流量有限,通用词的流量又太杂,且价格贵. 因为产品是地方类游戏APP, ...

  4. ssiOS应用架构谈 本地持久化方案及动态部署

    本文转载至 http://casatwy.com/iosying-yong-jia-gou-tan-ben-di-chi-jiu-hua-fang-an-ji-dong-tai-bu-shu.html ...

  5. iOS应用架构谈-本地持久化方案及动态部署

    iOS应用架构谈-开篇 iOS应用架构谈-view层的组织和调用方案 iOS应用架构谈-网络层设计方案 iOS应用架构谈-本地持久化方案及动态部署 iOS应用架构谈-组件化方案 前言 嗯,你们要的大招 ...

  6. 游戏 UI 自动化测试方案 Airtest Project

    谷歌发布了一款由网易研发的游戏 UI 自动化测试方案:Airtest Project.谷歌方面表示 Airtest 是安卓游戏开发最强大.最全面的自动测试方案之一. 从 Airtest 官网上可以看到 ...

  7. 【热更新】游戏热更新方案

    游戏热更新方案 热更新演化 热更新方案 [1] 进程切换 1.1 利用fork.exec切换 1.2 利用网关切换 1.3 微服务 - 进程切换注意要点 [2] 动态库替换 [3] 脚本语言热更新 热 ...

  8. 3年级计算机的知识能力,三年级信息技术考核评价方案

    三年级信息技术考核评价方案 根据<中小学信息技术课程指导纲要>的要求,结合信息技术教材中的内容及信息技术课的主要特点--实践性,为培养学生的创新精神,每节课都会让学生亲自上机操作.评价采取 ...

  9. 高性能、免运维,博云开源云原生本地存储方案:Carina

    2021 年 10 月 11 日,博云正式开源 Carina 本地存储方案,Carina 基于 Kubernetes 及 LVM 实现,提供了数据库与中间件等有状态应用在 Kubernetes 中运行 ...

最新文章

  1. 禁止input输入框输入指定内容
  2. Rust 算法排位记-选择排序图示与代码实现
  3. require include php5中最新区别,百度上好多错的。
  4. java怎么写自定义布局_java-Android设置自定义首选项布局
  5. 使用el-image-viewer的预览功能
  6. 英特尔固态硬盘测试软件,英特尔固态硬盘工具(Intel SSD Datacenter Tool)
  7. 小学生数据分析《西游记》发现大BUG
  8. oracle 更改system.dbf,oracle数据文件system01.dbf上有坏块,如何修复
  9. iOS 调用TouchID 身份验证
  10. 微信公众号内测开放个人订阅号认证!
  11. Node.js 网站内容抓取及Mysql存取Demo
  12. 工业相机之镜头基础知识
  13. 16.【Linux】window和linux下文件格式相互转换
  14. 超级账本hyperledger fabric第五集:共识排序及源码阅读
  15. PTA 帅到没朋友 (20分)
  16. http://mirrors.aliyun.com/centos/7/os/x86_64/repodata/repomd.xml: [Errno 14] curl#6 - “Could not res
  17. 软件工程——成本效益分析
  18. dvd光盘安装linux系统,从单DVD光盘上安装openSUSE
  19. 如何向icloud上传文件_如何将苹果手机iCloud网盘中的文件分享给好友?
  20. 使用dom4j解析xml 遇到困难

热门文章

  1. Java精品项目系统100期生活旅行分享网站
  2. 天正菜单栏不见了怎么显示出来_windows7系统下天正建筑工具栏不见了如何解决...
  3. 用HTML5做的导航条(步骤非常详细)
  4. 【记录】ChatGPT|图片预览魔法咒语魔改,使用 ChatGPT 返回大量可以跳转的链接
  5. 用python画雪花飘落_python-turtle-画雪花-2种方法及效果的详解
  6. Regression----李宏毅
  7. 互联网出海淘金,HMS生态开辟了新航线
  8. rabbitmq 笔记
  9. [原创]RPA在人力资源(HR)行业场景分享-中科云创CEO每日分享
  10. 电子器件热设计知识点