三,游戏本地数据处理方案

前言

这段时间刚好看悠游视频,学习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以上版本 使用CreateOpenXmlReader
           using (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. 不同字段类型的 字段长度问题
    类型bytecharintshortlongfloatdoublebool
    字节长度11428481
    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

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值