先前在网络通信一块其实已经用到二进制了,了解了下感觉二进制的好处多多,所以这里再系统性学习一下。

第一章:概述

关于二进制

2进制是计算技术中广泛采用的一种数制。2进制数据是用0和1两个数码来表示的数。它

的基数为2,进位规则是“逢二进一”。

计算机中存储的数据本质上都是2进制数的存储,在计算机中位(bit)是最小的存储单

位。1位就是一个0或者一个1。

也就是说一个文件的数据本质上都是由n个0和1组合而成的,通过不同的解析规则最终呈

现在我们的眼前。

2进制的好处

之前我已经学习过Json和xml了。

这两种的内存占用,读取性能都不如二进制,但是可读性强。

然而就可读性强来说,也许开发者配置修改的时候会方便一些,但是如果不希望玩家看到和修改的话,这反而是个缺点。

使用二进制,不经过解析就只是一堆0和1了,自带加密效果,人类完全看不懂。

所以相比Json和xml,2进制有以下好处

1.安全性较高

2.效率较高

3.广泛被运用到网络通信内

第2章: 基础知识

各类型转字节数组

知识点一 回顾C#知识——不同变量类型

不同变量类型

有符号 sbyte int short long

无符号 byte uint ushort ulong

浮点 float double decimal

特殊 bool char string

知识点二 回顾C#知识——变量的本质

变量的本质,或者说计算机所有数据的本质都是2进制

在内存中都以字节的形式存储着

1byte = 8bit

1bit(位)不是0就是1

通过sizeof方法可以看到常用变量类型占用的字节空间长度

其中string的存在比较特殊,因为它的长度是可变的。

知识点三 2进制文件读写的本质

它就是通过将各类型变量转换为字节数组

将字节数组直接存储到文件中

一般人是看不懂存储的数据的

不仅可以节约存储空间,提升效率

还可以提升安全性

而且在网络通信中我们直接传输的数据也是字节数据(2进制数据)

知识点四 各类型数据和字节数据相互转换

C#提供了一个公共类帮助我们进行转化

我们只需要记住API即可

类名:BitConverter

命名空间:using System

//1.将各类型转字节
byte[] bytes = BitConverter.GetBytes(256);

//2.字节数组转各类型
int i = BitConverter.ToInt32(bytes, 0);

知识点五 标准编码格式(针对string)

对于string类型,有一种叫编码格式的存在来对其进行二进制和显示字符的转换。

编码是用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。

为保证编码的正确性,编码要规范化、标准化,即需有标准的编码格式。

常见的编码格式有ASCII、ANSI、GBK、GB2312、UTF - 8、GB18030和UNICODE等。

简单的来说:

计算机中数据的本质就是2进制数据

编码格式就是用对应的2进制数 对应不同的文字

由于世界上有各种不同的语言,所有会有很多种不同的编码格式

不同的编码格式 对应的规则是不同的

如果在读取字符时采用了不统一的编码格式,可能会出现乱码

游戏开发中常用编码格式 UTF-8(方便游戏的多国语言汉化)

中文相关编码格式 GBK

英文相关编码格式 ASCII

在C#中有一个专门的编码格式类 来帮助我们将字符串和字节数组进行转换

类名:Encoding

需要引用命名空间:using System.Text;

//1.将字符串以指定编码格式转字节
byte[] bytes2 = Encoding.UTF8.GetBytes("字符串");

//2.字节数组以指定编码格式转字符串
string s = Encoding.UTF8.GetString(bytes2);

总结

我们可以通过BitConverter和Encoding类

将所有C#提供给我们的数据类型和字节数组之间进行相互转换了

我们需要熟练掌握其中的API

文件操作相关—文件夹

知识点一 文件夹操作是指什么?

平时我们可以在操作系统的文件管理系统中

通过一些操作增删查改文件夹

我们目前要学习的就是通过代码的形式

来对文件夹进行增删查改的操作

知识点二 C#提供给我们的文件夹操作公共类

类名:Directory

命名空间:using System.IO

//1.判断文件夹是否存在
if( Directory.Exists(Application.dataPath + "/数据持久化四"))
{
    print("存在文件夹");
}
else
{
    print("文件夹不存在");
}

//2.创建文件夹
DirectoryInfo info = Directory.CreateDirectory(Application.dataPath + "/数据持久化四");

//3.删除文件夹
//参数一:路径
//参数二:是否删除非空目录,如果为true,将删除整个目录,如果是false,仅当该目录为空时才可删除
//Directory.Delete(Application.dataPath + "/数据持久化四");

//4.查找文件夹和文件
//得到指定路径下所有文件夹名
string[] strs = Directory.GetDirectories(Application.dataPath);
for (int i = 0; i < strs.Length; i++)
{
    print(strs[i]);
}

//得到指定路径下所有文件名
strs = Directory.GetFiles(Application.dataPath);
for (int i = 0; i < strs.Length; i++)
{
    print(strs[i]);
}

//5.移动文件夹
//如果第二个参数所在的路径 已经存在了一个文件夹 那么会报错
//移动会把文件夹中的所有内容一起移到新的路径
//Directory.Move(Application.dataPath + "/数据持久化四", Application.dataPath + "/123123123");

知识点三 DirectoryInfo和FileInfo

DirectoryInfo目录信息类

我们可以通过它获取文件夹的更多信息

它主要出现在两个地方

//1.创建文件夹方法的返回值
DirectoryInfo dInfo = Directory.CreateDirectory(Application.dataPath + "/数据持久化123");
//全路径
print(dInfo.FullName);
//文件名
print(dInfo.Name);

//2.查找上级文件夹信息
dInfo = Directory.GetParent(Application.dataPath + "/数据持久化123");
//全路径
print(dInfo.FullName);
//文件名
print(dInfo.Name);

//重要方法
//得到所有子文件夹的目录信息
DirectoryInfo[] dInfos = dInfo.GetDirectories();

//FileInfo文件信息类
//我们可以通过DirectoryInfo得到该文件下的所有文件信息
FileInfo[] fInfos = dInfo.GetFiles();
for (int i = 0; i < fInfos.Length; i++)
{
    print("**************");
    print(fInfos[i].Name);//文件名
    print(fInfos[i].FullName);//路径
    print(fInfos[i].Length);//字节长度
    print(fInfos[i].Extension);//后缀名
}

总结

Directory提供给我们了常用的文件夹相关操作的API

只需要熟练使用它即可

DirectoryInfo和FileInfo 一般在多文件夹和多文件操作时会用到

了解即可

目前用的相对较少 他们的用法和Directory和File类的用法大同小异

文件操作相关—文件

知识点一 代码中的文件操作是做什么

在电脑上我们可以在操作系统中创建删除修改文件

可以增删查改各种各样的文件类型

代码中的文件操作就是通过代码来做这些事情

知识点二 文件相关操作公共类

C#提供了一个名为File(文件)的公共类

让我们可以快捷的通过代码操作文件相关

类名:File

命名空间: System.IO

知识点三 文件操作File类的常用内容(代码演示)

//1.判断文件是否存在
if(File.Exists(Application.dataPath + "/UnityTeach.tang"))
{
    print("文件存在");
}
else
{
    print("文件不存在");
}

//2.创建文件
//FileStream fs = File.Create(Application.dataPath + "/UnityTeach.tang");

//3.写入文件
//将指定字节数组 写入到指定路径的文件中
byte[] bytes = BitConverter.GetBytes(999);
File.WriteAllBytes(Application.dataPath + "/UnityTeach.tang", bytes);
//将指定的string数组内容 一行行写入到指定路径中
string[] strs = new string[] { "123", "唐老狮", "123123kdjfsalk", "123123123125243"};
File.WriteAllLines(Application.dataPath + "/UnityTeach2.tang", strs);
//将指定字符串写入指定路径
File.WriteAllText(Application.dataPath + "/UnityTeach3.tang", "唐老狮哈\n哈哈哈哈123123131231241234123");

//4.读取文件
//读取字节数据
bytes = File.ReadAllBytes(Application.dataPath + "/UnityTeach.tang");
print(BitConverter.ToInt32(bytes, 0));

//读取所有行信息
strs = File.ReadAllLines(Application.dataPath + "/UnityTeach2.tang");
for (int i = 0; i < strs.Length; i++)
{
    print(strs[i]);
}

//读取所有文本信息
print(File.ReadAllText(Application.dataPath + "/UnityTeach3.tang"));

//5.删除文件
//注意 如果删除打开着的文件 会报错
File.Delete(Application.dataPath + "/UnityTeach.tang");

//6.复制文件
//参数一:现有文件 需要是流关闭状态
//参数二:目标文件
File.Copy(Application.dataPath + "/UnityTeach2.tang", Application.dataPath + "/唐老狮.tanglaoshi", true);

//7.文件替换
//参数一:用来替换的路径
//参数二:被替换的路径
//参数三:备份路径
File.Replace(Application.dataPath + "/UnityTeach3.tang", Application.dataPath + "/唐老狮.tanglaoshi", Application.dataPath + "/唐老狮备份.tanglaoshi");

//8.以流的形式 打开文件并写入或读取
//参数一:路径
//参数二:打开模式
//参数三:访问模式
//FileStream fs = File.Open(Application.dataPath + "/UnityTeach2.tang", FileMode.OpenOrCreate, FileAccess.ReadWrite);

总结

File类提供了各种方法帮助我们进行文件的基础操作,需要记住这些关键API

一般情况下想要整体读写内容 可以使用File提供的Write和Read相关功能

但是也由于它只能整体的读和写文件,如果涉及到对文件内容的细微调整,我们通常会选择使用下一节的文件流。

文件操作相关—文件流

知识点一 什么是文件流

在C#中提供了多个文件流可以应对不同数据处理的场合。

其中一个文件流类 FileStream类

它主要作用是用于读写文件的细节

我们之前学过的File只能整体读写文件

而FileStream可以以读写字节的形式处理文件

简单来说:

文件里面存储的数据就像是一条数据流(数组或者列表)

我们可以通过FileStream一部分一部分的读写数据流

比如我可以先存一个int(4个字节)再存一个bool(1个字节)再存一个string(n个字节)

利用FileStream可以以流式逐个读写

知识点二 FileStream文件流类常用方法

类名:FileStream

需要引用命名空间:System.IO

1.打开或创建指定文件

方法一:new FileStream

参数一:路径

参数二:打开模式

CreateNew:创建新文件 如果文件存在 则报错

Create:创建文件,如果文件存在 则覆盖

Open:打开文件,如果文件不存在 报错

OpenOrCreate:打开或者创建文件根据实际情况操作

Append:若存在文件,则打开并查找文件尾,或者创建一个新文件

Truncate:打开并清空文件内容

参数三:访问模式

参数四:共享权限

None 谢绝共享

Read 允许别的程序读取当前文件

Write 允许别的程序写入该文件

ReadWrite 允许别的程序读写该文件

FileStream fs = new FileStream(Application.dataPath + "/Lesson3.tang", FileMode.Create, FileAccess.ReadWrite);

方法二:File.Create

参数一:路径

参数二:缓存大小

参数三:描述如何创建或覆盖该文件(不常用)

Asynchronous 可用于异步读写

DeleteOnClose 不在使用时,自动删除

Encrypted 加密

None 不应用其它选项

RandomAccess 随机访问文件

SequentialScan 从头到尾顺序访问文件

WriteThrough 通过中间缓存直接写入磁盘

FileStream fs2 = File.Create(Application.dataPath + "/Lesson3.tang");

方法三:File.Open

参数一:路径

参数二:打开模式

FileStream fs3 = File.Open(Application.dataPath + "/Lesson3.tang", FileMode.Open);

2.重要属性和方法
FileStream fs = File.Open(Application.dataPath + "Lesson3.tang", FileMode.OpenOrCreate);
//文本字节长度
print(fs.Length);

//是否可写
if( fs.CanRead )
{

}

//是否可读
if( fs.CanWrite )
{

}

//将字节写入文件 当写入后 一定执行一次
fs.Flush();

//关闭流 当文件读写完毕后 一定执行
fs.Close();

//缓存资源销毁回收
fs.Dispose();

3.写入字节
print(Application.persistentDataPath);
using (FileStream fs = new FileStream(Application.persistentDataPath + "/Lesson3.tang", FileMode.OpenOrCreate, FileAccess.Write))
{

    byte[] bytes = BitConverter.GetBytes(999);
    //方法:Write
    //参数一:写入的字节数组
    //参数二:数组中的开始索引
    //参数三:写入多少个字节
    fs.Write(bytes, 0, bytes.Length);

    //写入字符串时
    bytes = Encoding.UTF8.GetBytes("唐老狮哈哈哈哈");
    //先写入长度
    //int length = bytes.Length;
    fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
    //再写入字符串具体内容
    fs.Write(bytes, 0, bytes.Length);

    //避免数据丢失 一定写入后要执行的方法
    fs.Flush();
    //销毁缓存 释放资源
    fs.Dispose();
}

4.读取字节
方法一:挨个读取字节数组
using (FileStream fs2 = File.Open(Application.persistentDataPath + "/Lesson3.tang", FileMode.Open, FileAccess.Read))
{
    //读取第一个整形
    byte[] bytes2 = new byte[4];

    //参数一:用于存储读取的字节数组的容器
    //参数二:容器中开始的位置
    //参数三:读取多少个字节装入容器
    //返回值:当前流索引前进了几个位置
    int index = fs2.Read(bytes2, 0, 4);
    int i = BitConverter.ToInt32(bytes2, 0);
    print("取出来的第一个整数" + i);//999
    print("索引向前移动" + index + "个位置");

    //读取第二个字符串
    //读取字符串字节数组长度
    index = fs2.Read(bytes2, 0, 4);
    print("索引向前移动" + index + "个位置");
    int length = BitConverter.ToInt32(bytes2, 0);
    //要根据我们存储的字符串字节数组的长度 来声明一个新的字节数组 用来装载读取出来的数据
    bytes2 = new byte[length];
    index = fs2.Read(bytes2, 0, length);
    print("索引向前移动" + index + "个位置");
    //得到最终的字符串 打印出来
    print(Encoding.UTF8.GetString(bytes2));
    fs2.Dispose();
}

方法二:一次性读取再挨个读取
print("***************************");
using (FileStream fs3 = File.Open(Application.persistentDataPath + "/Lesson3.tang", FileMode.Open, FileAccess.Read))
{
    //一开始就申明一个 和文件字节数组长度一样的容器
    byte[] bytes3 = new byte[fs3.Length];
    fs3.Read(bytes3, 0, (int)fs3.Length);
    fs3.Dispose();
    //读取整数
    print(BitConverter.ToInt32(bytes3, 0));
    //得去字符串字节数组的长度
    int length2 = BitConverter.ToInt32(bytes3, 4);
    //得到字符串
    print(Encoding.UTF8.GetString(bytes3, 8, length2));
}

知识点三 更加安全的使用文件流对象

using关键字重要用法

using (申明一个引用对象)
{
//使用对象执行逻辑
}

无论发生什么情况 当using语句块结束后

会自动调用该对象的销毁方法 避免忘记销毁或关闭流

using是一种更安全的使用方法

所以目前我们对文件流进行操作 为了文件操作安全 都用using来进行处理最好

总结

通过FIleStream读写时一定要注意

读的规则一定是要和写是一致的

我们存储数据的先后顺序是我们制定的规则

只要按照规则读写就能保证数据的正确性

C#类对象的序列化

知识点一 序列化类对象第一步—申明类对象

对于类对象的二进制序列化,C#官方提供了一个类,BinaryFormatter

相比Json和xml,Unity里面的原生二进制序列化能力是更强大的,它直接支持序列化字典。

注意:如果要使用C#自带的序列化2进制方法

申明类时需要添加[System.Serializable]特性

不过还需要注意的是,从 .NET 5.0 开始,BinaryFormatter 因为安全性问题被标记为过时(obsolete)。微软推荐使用更安全的替代方案,如 System.Text.Json.JsonSerializer 或 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 等。

知识点二 序列化类对象第二步—将对象进行2进制序列化

示例序列化对象脚本
[System.Serializable]
public class Person
{
    public int age = 1;
    public string name = "唐老狮";
    public int[] ints = new int[] { 1, 2, 3, 4, 5 };
    public List<int> list = new List<int>() { 1, 2, 3, 4 };
    public Dictionary<int, string> dic = new Dictionary<int, string>() { { 1,"123"},{ 2,"1223"},{ 3,"435345" } };
    public StructTest st = new StructTest(2, "123");
    public ClssTest ct = new ClssTest();
}

[System.Serializable]
public struct StructTest
{
    public int i;
    public string s;

    public StructTest(int i, string s)
    {
        this.i = i;
        this.s = s;
    }
}

[System.Serializable]
public class ClssTest
{
    public int i = 1;
}
方法一:使用内存流得到2进制字节数组

使用这个方式,可以在存储之前对字节先进行二次处理。

//主要用于得到字节数组 可以用于网络传输
//新知识点
//1.内存流对象
//类名:MemoryStream
//命名空间:System.IO
//2.2进制格式化对象
//类名:BinaryFormatter
//命名空间:System.Runtime.Serialization.Formatters.Binary、
//主要方法:序列化方法 Serialize
using (MemoryStream ms = new MemoryStream())
{
    //2进制格式化程序
    BinaryFormatter bf = new BinaryFormatter();
    //序列化对象 生成2进制字节数组 写入到内存流当中
    bf.Serialize(ms, p);
    //得到对象的2进制字节数组
    byte[] bytes = ms.GetBuffer();
    //存储字节
    File.WriteAllBytes(Application.dataPath + "/Lesson5.tang", bytes);
    //关闭内存流
    ms.Close();
}
方法二:使用文件流进行存储
//主要用于存储到文件中
using (FileStream fs = new FileStream(Application.dataPath + "/Lesson5_2.tang", FileMode.OpenOrCreate, FileAccess.Write))
{
    //2进制格式化程序
    BinaryFormatter bf = new BinaryFormatter();
    //序列化对象 生成2进制字节数组 写入到内存流当中
    bf.Serialize(fs, p);
    fs.Flush();
    fs.Close();
}

结果表明,person可以完全正常的序列化。

总结

C#提供的类对象2进制序列化主要类是 BinaryFormatter

通过其中的序列化方法即可进行序列化生成字节数组

C#类对象的反序列化

介绍

对于使用BinaryFormatter序列化的对象,反序列化就会变的十分简单了。

所以本节主要展示两种场景下的代码。

知识点一 反序列化之 反序列化文件中数据

主要类

FileStream文件流类

BinaryFormatter 2进制格式化类

主要方法

Deserizlize

通过文件流打开指定的2进制数据文件

using (FileStream fs = File.Open(Application.dataPath + "/Lesson5_2.tang", FileMode.Open, FileAccess.Read))
{
    //申明一个 2进制格式化类
    BinaryFormatter bf = new BinaryFormatter();
    //反序列化
    Person p = bf.Deserialize(fs) as Person;

    fs.Close();
}

知识点二 反序列化之 反序列化网络传输过来的2进制数据

主要类

MemoryStream内存流类

BinaryFormatter 2进制格式化类

主要方法

Deserizlize

目前没有网络传输 所以我们代码演示还是直接从文件中获取

byte[] bytes = File.ReadAllBytes(Application.dataPath + "/Lesson5_2.tang");
//申明内存流对象 一开始就把字节数组传输进去
using (MemoryStream ms = new MemoryStream(bytes))
{
    //申明一个 2进制格式化类
    BinaryFormatter bf = new BinaryFormatter();
    //反序列化
    Person p = bf.Deserialize(ms) as Person;

    ms.Close();
}

C#类对象的2进制数据加密

知识点一 何时加密?何时解密?

当我们将类对象转换为2进制数据时进行加密

当我们将2进制数据转换为类对象时进行解密

这样如果第三方获取到我们的2进制数据

当他们不知道加密规则和解密秘钥时就无法获取正确的数据

起到保证数据安全的作用

知识点二 加密是否是100%安全?

一定记住加密只是提高破解门槛,没有100%保密的数据

通过各种尝试始终是可以破解加密规则的,只是时间问题

加密只能起到提升一定的安全性

知识点三 常用加密算法

MD5算法

SHA1算法

HMAC算法

AES/DES/3DES算法

等等等

有很多的别人写好的第三发加密算法库

可以直接获取用于在程序中对数据进行加密

知识点四 用异或加密实现简单的二进制加密

Person p = new Person();
byte key = 199;
using (MemoryStream ms = new MemoryStream())
{
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(ms, p);
    byte[] bytes = ms.GetBuffer();
    //异或加密
    for (int i = 0; i < bytes.Length; i++)
    {
        bytes[i] ^= key;
    }
    File.WriteAllBytes(Application.dataPath + "/Lesson7.tang", bytes);
}

//解密
byte[] bytes2 = File.ReadAllBytes(Application.dataPath + "/Lesson7.tang");
for (int i = 0; i < bytes2.Length; i++)
{
    bytes2[i] ^= key;
}
using (MemoryStream ms = new MemoryStream(bytes2))
{
    BinaryFormatter bf = new BinaryFormatter();
    Person p2 = bf.Deserialize(ms) as Person;
    ms.Close();
}

第3章: 实践小项目

第1节: 前提知识

知识点补充—ExcelDll包导入

知识点一 了解Excel表的本质

Excel表本质上也是一堆数据

只不过它有自己的存储读取规则

如果我们想要通过代码读取它

那么必须知道它的存储规则

官网是专门提供了对应的DLL文件用来解析Excel文件的

Dll文件:库文件,你可以理解为它是许多代码的集合

将相关代码集合在库文件中可以方便迁移和使用

有了某个DLL文件,我们就可以使用其中已经写好的代码

而Excel的DLL包就是官方已经把解析Excel表的相关类和方法写好了

方便用户直接使用

知识点二 导入官方提供的Excel相关DLL文件

总的来说,ExcelDll文件放入工程目录调用即可

详细教程:

Unity 读取Excel表的内容_unity 读取excel数据-CSDN博客

下一节会重点讲解里面API的调用。

知识点补充—Excel数据读取

知识点一 打开Excel表

主要知识点:

1.FileStream读取文件流

2.IExcelDataReader类,从流中读取Excel数据

3.DataSet 数据集合类 将Excel数据转存进其中方便读取

[MenuItem("GameTool/打开Excel表")]
private static void OpenExcel()
{
    using (FileStream fs = File.Open(Application.dataPath + "/ArtRes/Excel/PlayerInfo.xlsx", FileMode.Open, FileAccess.Read ))
    {
        //通过我们的文件流获取Excel数据
        IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
        //将excel表中的数据转换为DataSet数据类型 方便我们 获取其中的内容
        DataSet result = excelReader.AsDataSet();
        //得到Excel文件中的所有表信息
        for (int i = 0; i < result.Tables.Count; i++)
        {
            Debug.Log("表名:" + result.Tables[i].TableName);
            Debug.Log("行数:" + result.Tables[i].Rows.Count);
            Debug.Log("列数:" + result.Tables[i].Columns.Count);
        }
        fs.Close();
    }
}

知识点二 获取Excel表中单元格的信息

主要知识点:

1.FileStream读取文件流

2.IExcelDataReader类,从流中读取Excel数据

3.DataSet 数据集合类 将Excel数据转存进其中方便读取

4.DataTable 数据表类 表示Excel文件中的一个表

5.DataRow 数据行类 表示某张表中的一行数据

[MenuItem("GameTool/读取Excel里的具体信息")]
private static void ReadExcel()
{
    using (FileStream fs = File.Open(Application.dataPath + "/ArtRes/Excel/PlayerInfo.xlsx", FileMode.Open, FileAccess.Read))
    {
        IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
        DataSet result = excelReader.AsDataSet();

        for (int i = 0; i < result.Tables.Count; i++)
        {
            //得到其中一张表的具体数据
            DataTable table = result.Tables[i];
            //得到其中一行的数据
            //DataRow row = table.Rows[0];
            //得到行中某一列的信息
            //Debug.Log(row[1].ToString());
            DataRow row;
            for (int j = 0; j < table.Rows.Count; j++)
            {
                //得到每一行的信息
                row = table.Rows[j];
                Debug.Log("*********新的一行************");
                for (int k = 0; k < table.Columns.Count; k++)
                {
                    Debug.Log(row[k].ToString());
                }
            }
        }

        fs.Close();
    }
}

知识点三 获取Excel表中信息对于我们的意义?

既然我们能够获取到Excel表中的所有数据

那么我们可以根据表中数据来动态的生成相关数据

1.数据结构类

2.容器类

3.2进制数据

为什么不直接读取Excel表而要把它转成2进制数据

1.提升读取效率

2.提升数据安全性

第2节: 小项目实现

需求分析

初步分析

回到我们游戏里面的实际用途,我们希望实现功能:

假设我们有一个CSV游戏配置的文件

1.CSV表可以被二进制序列化为二进制文件,防止玩家修改和查看,提高安全性。

2.被序列化的二进制文件又可以被二进制反序列化回游戏,供游戏读取调用数据。

所以,我们需要实现:

根据一个CSV文件,生成:

1.数据结构类的脚本

2.数据容器类的脚本

3.根据CSV数据序列化的二进制文件。

4.根据序列化的二进制文件,反序列化回数据容器类。

实现思路

就像大部分CSV文件都会实现的那样,我们一列代表了一个数据类型,一行代表了一个数据对象。

CSV表里面转换到C#的时候,统一处理为了string,生成数据结构类需要我们每个数据类型的变量名字和数据类型,生成数据容器类需要我们确定哪个数据类型是我们的主键。

由于我们可以决定我们的读取从第几行第几列开始,因此,我们完全把前几行进行约定,写成我们需要的数据,供生成脚本的时候读取。

我们也可以很容易的确定,

1和2的实现只需要按C#的类脚本进行格式书写即可

3的实现,我们也可以根据CSV里面的数据类型直接一个一个的进行序列化即可。

4的实现,我们可以运用反射,根据我们创建的1和2,拿到1的变量集合,然后按照集合对着3一个一个的读取即可。

实现

CSV约定

第一行:字段名

第二行:字段类型(字段类型一定不要配置错误,字段类型目前只支持int float bool string)如果想要再添加类型,需要在ExcelTool的GenerateExcelBinary方法中和BinaryDataMgr的LoadTable方法当中对应添加读写的逻辑

第三行:主键是哪一个字段 需要通过key来标识主键

第四行:描述信息(只是给别人看,不会有别的作用,可以写中文)

第五行~第n行:就是具体数据信息

举例:

id

name

money

atk

atkRange

offsetTime

nextLev

imgRes

res

atkType

eff

int

string

int

int

int

float

int

string

string

int

string

key

唯一ID

塔的名字

塔的价值

攻击力

攻击范围

攻击间隔时间

下一等级的塔ID

塔图片路径

塔预设体路径

攻击类型
1代表单体攻击
2代表群体攻击

特效路径

1

加农炮1

200

5

4

1

2

TowerImg/1_1

Tower/1_1

1

eff/Fire1

2

加农炮2

300

10

4

1

3

TowerImg/1_2

Tower/1_2

1

eff/Fire1

3

加农炮3

400

20

4

1

0

TowerImg/1_3

Tower/1_3

1

eff/Fire1

4

机枪炮1

200

2

4

0.2

5

TowerImg/2_1

Tower/2_1

1

eff/Fire1

5

机枪炮2

300

4

4

0.2

6

TowerImg/2_2

Tower/2_2

1

eff/Fire1

6

机枪炮3

400

6

4

0.2

0

TowerImg/2_3

Tower/2_3

1

eff/Fire1

7

魔法炮1

200

3

4

0.5

8

TowerImg/3_1

Tower/3_1

2

eff/Fire2

8

魔法炮2

300

6

4

0.5

9

TowerImg/3_2

Tower/3_2

2

eff/Fire2

9

魔法炮3

400

9

4

0.5

0

TowerImg/3_3

Tower/3_3

2

eff/Fire2

ExcelTool

描述

负责对CSV的交互,实现功能:数据结构类脚本的生成,数据容器类脚本的生成,CSV文件的二进制序列化。

生成的数据结构脚本以CSV表名为准,容器类脚本则会多加上Container。

可配置变量

1.Excel文件应该放置在ArtRes/Excel当中如果想要修改 就去修改ExcelTool当中的EXCEL_PATH路径变量

3.容器类和数据结构类的生成路径可以在ExcelTool当中修改DATA_CLASS_PATH和DATA_CONTAINER_PATH变量来进行更改

代码
using Excel;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;

public class ExcelTool
{
    /// <summary>
    /// excel文件存放的路径
    /// </summary>
    public static string EXCEL_PATH = Application.dataPath + "/ArtRes/Excel/";

    /// <summary>
    /// 数据结构类脚本存储位置路径
    /// </summary>
    public static string DATA_CLASS_PATH = Application.dataPath + "/Scripts/ExcelData/DataClass/";

    /// <summary>
    /// 容器类脚本存储位置路径
    /// </summary>
    public static string DATA_CONTAINER_PATH = Application.dataPath + "/Scripts/ExcelData/Container/";

    /// <summary>
    /// 真正内容开始的行号
    /// </summary>
    public static int BEGIN_INDEX = 4;

    [MenuItem("GameTool/GenerateExcel")]
    private static void GenerateExcelInfo()
    {
        //记在指定路径中的所有Excel文件 用于生成对应的3个文件
        DirectoryInfo dInfo = Directory.CreateDirectory(EXCEL_PATH);
        //得到指定路径中的所有文件信息 相当于就是得到所有的Excel表
        FileInfo[] files = dInfo.GetFiles();
        //数据表容器
        DataTableCollection tableConllection;
        for (int i = 0; i < files.Length; i++)
        {
            //如果不是excel文件就不要处理了
            if (files[i].Extension != ".xlsx" &&
                files[i].Extension != ".xls")
                continue;
            //打开一个Excel文件得到其中的所有表的数据
            using (FileStream fs = files[i].Open(FileMode.Open, FileAccess.Read))
            {
                IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
                tableConllection = excelReader.AsDataSet().Tables;
                fs.Close();
            }

            //遍历文件中的所有表的信息
            foreach (DataTable table in tableConllection)
            {
                //生成数据结构类
                GenerateExcelDataClass(table);
                //生成容器类
                GenerateExcelContainer(table);
                //生成2进制数据
                GenerateExcelBinary(table);
            }

        }
    }

    /// <summary>
    /// 生成Excel表对应的数据结构类
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelDataClass(DataTable table)
    {
        //字段名行
        DataRow rowName = GetVariableNameRow(table);
        //字段类型行
        DataRow rowType = GetVariableTypeRow(table);

        //判断路径是否存在 没有的话 就创建文件夹
        if (!Directory.Exists(DATA_CLASS_PATH))
            Directory.CreateDirectory(DATA_CLASS_PATH);
        //如果我们要生成对应的数据结构类脚本 其实就是通过代码进行字符串拼接 然后存进文件就行了
        string str = "public class " + table.TableName + "\n{\n";

        //变量进行字符串拼接
        for (int i = 0; i < table.Columns.Count; i++)
        {
            str += "    public " + rowType[i].ToString() + " " + rowName[i].ToString() + ";\n";
        }

        str += "}";

        //把拼接好的字符串存到指定文件中去
        File.WriteAllText(DATA_CLASS_PATH + table.TableName + ".cs", str);

        //刷新Project窗口
        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 生成Excel表对应的数据容器类
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelContainer(DataTable table)
    {
        //得到主键索引
        int keyIndex = GetKeyIndex(table);
        //得到字段类型行
        DataRow rowType = GetVariableTypeRow(table);
        //没有路径创建路径
        if (!Directory.Exists(DATA_CONTAINER_PATH))
            Directory.CreateDirectory(DATA_CONTAINER_PATH);

        string str = "using System.Collections.Generic;\n";

        str += "public class " + table.TableName + "Container" + "\n{\n";

        str += "    ";
        str += "public Dictionary<" + rowType[keyIndex].ToString() + ", " + table.TableName + ">";
        str += "dataDic = new " + "Dictionary<" + rowType[keyIndex].ToString() + ", " + table.TableName + ">();\n";

        str += "}";

        File.WriteAllText(DATA_CONTAINER_PATH + table.TableName + "Container.cs", str);

        //刷新Project窗口
        AssetDatabase.Refresh();
    }


    /// <summary>
    /// 生成excel2进制数据
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelBinary(DataTable table)
    {
        //没有路径创建路径
        if (!Directory.Exists(BinaryDataMgr.DATA_BINARY_PATH))
            Directory.CreateDirectory(BinaryDataMgr.DATA_BINARY_PATH);

        //创建一个2进制文件进行写入
        using (FileStream fs = new FileStream(BinaryDataMgr.DATA_BINARY_PATH + table.TableName + ".tang", FileMode.OpenOrCreate, FileAccess.Write))
        {
            //存储具体的excel对应的2进制信息
            //1.先要存储我们需要写多少行的数据 方便我们读取
            //-4的原因是因为 前面4行是配置规则 并不是我们需要记录的数据内容
            fs.Write(BitConverter.GetBytes(table.Rows.Count - 4), 0, 4);
            //2.存储主键的变量名
            string keyName = GetVariableNameRow(table)[GetKeyIndex(table)].ToString();
            byte[] bytes = Encoding.UTF8.GetBytes(keyName);
            //存储字符串字节数组的长度
            fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
            //存储字符串字节数组
            fs.Write(bytes, 0, bytes.Length);

            //遍历所有内容的行 进行2进制的写入
            DataRow row;
            //得到类型行 根据类型来决定应该如何写入数据
            DataRow rowType = GetVariableTypeRow(table);
            for (int i = BEGIN_INDEX; i < table.Rows.Count; i++)
            {
                //得到一行的数据
                row = table.Rows[i];
                for (int j = 0; j < table.Columns.Count; j++)
                {
                    switch (rowType[j].ToString())
                    {
                        case "int":
                            fs.Write(BitConverter.GetBytes(int.Parse(row[j].ToString())), 0, 4);
                            break;
                        case "float":
                            fs.Write(BitConverter.GetBytes(float.Parse(row[j].ToString())), 0, 4);
                            break;
                        case "bool":
                            fs.Write(BitConverter.GetBytes(bool.Parse(row[j].ToString())), 0, 1);
                            break;
                        case "string":
                            bytes = Encoding.UTF8.GetBytes(row[j].ToString());
                            //写入字符串字节数组的长度
                            fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
                            //写入字符串字节数组
                            fs.Write(bytes, 0, bytes.Length);
                            break;
                    }
                }
            }

            fs.Close();
        }

        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 获取变量名所在行
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static DataRow GetVariableNameRow(DataTable table)
    {
        return table.Rows[0];
    }

    /// <summary>
    /// 获取变量类型所在行
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static DataRow GetVariableTypeRow(DataTable table)
    {
        return table.Rows[1];
    }

    
    /// <summary>
    /// 获取主键索引
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static int GetKeyIndex(DataTable table)
    {
        DataRow row = table.Rows[2];
        for (int i = 0; i < table.Columns.Count; i++)
        {
            if (row[i].ToString() == "key")
                return i;
        }
        return 0;
    }
}

BinaryDataMgr

描述

负责二进制数据的获取和存储(这里的读取主要指的是配置型csv,存储主要是游戏存档数据)

可配置变量

存储2进制文件的路径可以在BinaryDataMgr当中的DATA_BINARY_PATH变量来进行修改

代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using UnityEngine;

/// <summary>
/// 2进制数据管理器
/// </summary>
public class BinaryDataMgr
{
    /// <summary>
    /// 2进制数据存储位置路径
    /// </summary>
    public static string DATA_BINARY_PATH = Application.streamingAssetsPath + "/Binary/";

    /// <summary>
    /// 用于存储所有Excel表数据的容器
    /// </summary>
    private Dictionary<string, object> tableDic = new Dictionary<string, object>();

    /// <summary>
    /// 数据存储的位置
    /// </summary>
    private static string SAVE_PATH = Application.persistentDataPath + "/Data/";

    private static BinaryDataMgr instance = new BinaryDataMgr();
    public static BinaryDataMgr Instance => instance;

    private BinaryDataMgr()
    {

    }

    public void InitData()
    {
        LoadTable<>();//这里负责读取初始化,设置你生成的容器类民和数据结构类名
    }

    /// <summary>
    /// 加载Excel表的2进制数据到内存中 
    /// </summary>
    /// <typeparam name="T">容器类名</typeparam>
    /// <typeparam name="K">数据结构类类名</typeparam>
    public void LoadTable<T,K>()
    {
        //读取 excel表对应的2进制文件 来进行解析
        using (FileStream fs = File.Open(DATA_BINARY_PATH + typeof(K).Name + ".tang", FileMode.Open, FileAccess.Read))
        {
            byte[] bytes = new byte[fs.Length];
            fs.Read(bytes, 0, bytes.Length);
            fs.Close();
            //用于记录当前读取了多少字节了
            int index = 0;

            //读取多少行数据
            int count = BitConverter.ToInt32(bytes, index);
            index += 4;

            //读取主键的名字
            int keyNameLength = BitConverter.ToInt32(bytes, index);
            index += 4;
            string keyName = Encoding.UTF8.GetString(bytes, index, keyNameLength);
            index += keyNameLength;

            //创建容器类对象
            Type contaninerType = typeof(T);
            object contaninerObj = Activator.CreateInstance(contaninerType);
            //得到数据结构类的Type
            Type classType = typeof(K);
            //通过反射 得到数据结构类 所有字段的信息
            FieldInfo[] infos = classType.GetFields();

            //读取每一行的信息
            for (int i = 0; i < count; i++)
            {
                //实例化一个数据结构类 对象
                object dataObj = Activator.CreateInstance(classType);
                foreach (FieldInfo info in infos)
                {
                    if( info.FieldType == typeof(int) )
                    {
                        //相当于就是把2进制数据转为int 然后赋值给了对应的字段
                        info.SetValue(dataObj, BitConverter.ToInt32(bytes, index));
                        index += 4;
                    }
                    else if (info.FieldType == typeof(float))
                    {
                        info.SetValue(dataObj, BitConverter.ToSingle(bytes, index));
                        index += 4;
                    }
                    else if (info.FieldType == typeof(bool))
                    {
                        info.SetValue(dataObj, BitConverter.ToBoolean(bytes, index));
                        index += 1;
                    }
                    else if (info.FieldType == typeof(string))
                    {
                        //读取字符串字节数组的长度
                        int length = BitConverter.ToInt32(bytes, index);
                        index += 4;
                        info.SetValue(dataObj, Encoding.UTF8.GetString(bytes, index, length));
                        index += length;
                    }
                }

                //读取完一行的数据了 应该把这个数据添加到容器对象中
                //得到容器对象中的 字典对象
                object dicObject = contaninerType.GetField("dataDic").GetValue(contaninerObj);
                //通过字典对象得到其中的 Add方法
                MethodInfo mInfo = dicObject.GetType().GetMethod("Add");
                //得到数据结构类对象中 指定主键字段的值
                object keyValue = classType.GetField(keyName).GetValue(dataObj);
                mInfo.Invoke(dicObject, new object[] { keyValue, dataObj });
            }

            //把读取完的表记录下来
            tableDic.Add(typeof(T).Name, contaninerObj);

            fs.Close();
        }
    }

    /// <summary>
    /// 得到一张表的信息
    /// </summary>
    /// <typeparam name="T">容器类名</typeparam>
    /// <returns></returns>
    public T GetTable<T>() where T:class
    {
        string tableName = typeof(T).Name;
        if (tableDic.ContainsKey(tableName))
            return tableDic[tableName] as T;
        return null;
    }

    /// <summary>
    /// 存储类对象数据
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="fileName"></param>
    public void Save(object obj, string fileName)
    {
        //先判断路径文件夹有没有
        if (!Directory.Exists(SAVE_PATH))
            Directory.CreateDirectory(SAVE_PATH);

        using (FileStream fs = new FileStream(SAVE_PATH + fileName + ".tang", FileMode.OpenOrCreate, FileAccess.Write))
        {
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(fs, obj);
            fs.Close();
        }
    }

    /// <summary>
    /// 读取2进制数据转换成对象
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="fileName"></param>
    /// <returns></returns>
    public T Load<T>(string fileName) where T:class
    {
        //如果不存在这个文件 就直接返回泛型对象的默认值
        if( !File.Exists(SAVE_PATH + fileName + ".tang") )
            return default(T);

        T obj;
        using (FileStream fs = File.Open(SAVE_PATH + fileName + ".tang", FileMode.Open, FileAccess.Read))
        {
            BinaryFormatter bf = new BinaryFormatter();
            obj = bf.Deserialize(fs) as T;
            fs.Close();
        }

        return obj;
    }
}