Unity复习学习随笔(11):二进制存储

目录

什么是数据持久化?

二进制是什么?

学习二进制读写数据的原因

各类型数据转字节数据

回顾

不同变量类型

变量的本质

二进制文件读写的本质

各类型数据和字节数据相互转换

1.将各类型转换为字节

2.将字节数组转换为各个类型

标准编码格式

1.将字符串以指定编码格式转为字节

2.字节数组以指定编码格式转为字符串

文件操作相关

文件相关

代码中的文件操作是什么

文件相关操作公共类

文件操作File类的常用内容

1.判断文件是否存在

2.创建文件

3.写入文件

将指定字节数组写入到指定路径的文件中

将指定的string数组内容一行行写入到指定路径中

将指定字符串写入指定路径

4.读取文件

读取字节数据

读取所有行信息

读取所有文本信息

5.删除文件

6.复制文件

7.文件替换

8.以流的形式,打开文件并写入或读取

文件流相关

什么是文件流

FileStream文件流常用方法

1.打开或创建指定文件

[方法一:new FileStream](#方法一:new FileStream)

方法二:File.Create

方法三:File.Open

2.重要属性和方法

文本字节长度

是否可写

是否可读

将字节写入文件,当写入后一定执行一次

关闭流,当文件读写完毕后一定执行

缓存资源销毁回收

3.写入字节

4.读取字节

方法一:挨个读取字节数组

方法二:一次性读取再挨个读取

如何更加安全的使用

文件夹相关

文件夹操作是什么?

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

1.判断文件夹是否存在

2.创建文件夹

3.删除文件夹

4.查找文件夹和文件

得到指定路径下所有文件夹名

得到指定路径下所有文件名

5.移动文件夹

DirectoryInfo和FileInfo

1.创建文件夹方法的返回值

全路径

文件名

2.查找上级文件夹信息

全路径

文件名

重要方法

得到所有子文件夹的目录信息

FileInfo文件信息类

C#类对象的序列化和反序列化

序列化

声明类对象

将对象进行二进制序列化

方法一:使用内存流得到二进制字节数组

方法二:使用文件流进行存储

反序列化

反序列化文件中的数据

反序列化网络传输过来的二进制数据

加密

何时加密?何时解密?

加密是否一定安全?

常见加密算法

实现一个简单的自定义读写类

主要用处


什么是数据持久化?

数据持久化就是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称

简单来说就是将游戏数据存储到硬盘,硬盘中数据读取到游戏中,也就是传统意义上的存盘

二进制是什么?

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

它的基数为2,进位规则是"逢二进一"。

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

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

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

学习二进制读写数据的原因

前面学习的xml和json,都是用特定的字符串组合规则来读写数据的。

清晰易懂是他们的共同好处,但是也是一把双刃剑,如果我们用xml和json存储数据,如果不进行加密,那么只要玩家找到对应的存储信息,就能够快速修改其中的内容。

而且由于他们把数据转换成了对应的xml或者json字符串,我们最终在存储数据时存储的都是字符串数据,在读写时效率较低,内存和硬盘空间占用较大

即:xml和json的安全性和效率较低

二进制的好处:

1.安全性较高

2.效率较高

3.为网络通信做铺垫

各类型数据转字节数据

回顾

不同变量类型

  • 有符号:sbyte int short long
  • 无符号:byte uint ushort ulong
  • 浮点:float double decimal
  • 特殊:bool char string

变量的本质

变量的本质就是二进制,它们在内存中都以字节的形式存储着

1byte = 8bit 1bit不是0就是1 通过sizeof方法可以看到常用的变量类型占用的字节空间长度

二进制文件读写的本质

通过将各类型变量转换为字节数组,将字节数组直接存储到文件中

不仅可以节约存储空间,提升效率,还可以提升安全性

在网络通信中也是使用字节数据进行传输的

各类型数据和字节数据相互转换

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

我们只需记住API即可

命名空间:System

1.将各类型转换为字节

注意:decimal类型和string类型不能直接进行转换

cs 复制代码
byte[] bytes = BitConverter.GetBytes(99);

2.将字节数组转换为各个类型

注:第二个参数为该字节的第几个索引开始

cs 复制代码
int i = BitConverter.ToInt32(bytes, 0);

标准编码格式

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

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

常见的编码格式有ASCII、ANSI、GBK、GB2312、UTF-8、Unicode等等

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

游戏开发中常见编码格式:UTF-8

中文相关编码格式:GBK

英文相关编码格式:ASCII

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

类名:Encoding

命名空间:System.Text

1.将字符串以指定编码格式转为字节

cs 复制代码
byte[] bytes1 = Encoding.UTF8.GetBytes("123123123");

2.字节数组以指定编码格式转为字符串

cs 复制代码
string s = Encoding.UTF8.GetString(bytes1);

文件操作相关

文件相关

代码中的文件操作是什么

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

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

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

文件相关操作公共类

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

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

类名:File

命名空间:System.IO

文件操作File类的常用内容

1.判断文件是否存在
cs 复制代码
if(File.Exists(Application.dataPath + "/Test.txt"))
{
    print("文件存在");
}
else
{
    print("不存在");
}
2.创建文件
cs 复制代码
FileStream fs = File.Create(Application.dataPath + "/test.txt");
3.写入文件
将指定字节数组写入到指定路径的文件中
cs 复制代码
byte[] bytes = BitConverter.GetBytes(999);
File.WriteAllBytes(Application.dataPath + "/test.txt", bytes);
将指定的string数组内容一行行写入到指定路径中
cs 复制代码
string[] strs = new string[] { "123", "456", "123123123" };
File.WriteAllLines(Application.dataPath + "/test2.txt",strs);
将指定字符串写入指定路径
cs 复制代码
File.WriteAllText(Application.dataPath + "/test3.txt", "123123123\n123123123");
4.读取文件
读取字节数据
cs 复制代码
byte[] bytes1 = File.ReadAllBytes(Application.dataPath + "/test.txt");
print(BitConverter.ToInt32(bytes1));
读取所有行信息
cs 复制代码
string[] strs1 = File.ReadAllLines(Application.dataPath + "/test2.txt");
for (int i = 0; i < strs1.Length; i++)
{
    print(strs1[i]);
}
读取所有文本信息
cs 复制代码
string str = File.ReadAllText(Application.dataPath + "/test3.txt");
print(str);
5.删除文件

注意:如果删除打开着的文件会报错

cs 复制代码
File.Delete(Application.dataPath + "/test.txt");
6.复制文件

参数一:现有文件,需要是流关闭状态

参数二:目标文件

cs 复制代码
File.Copy(Application.dataPath + "/test2.txt", Application.dataPath + "/test4.txt");
7.文件替换

参数一:用来替换的路径

参数二:被替换的路径

参数三:备份路径

cs 复制代码
File.Replace(Application.dataPath + "/test2.txt", Application.dataPath + "/test4.txt", Application.dataPath + "/test4(old).txt");
8.以流的形式,打开文件并写入或读取

参数一:路径

参数二:打开模式

参数三:访问模式

cs 复制代码
FileStream fs = File.Open(Application.dataPath + "/test2.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite);

文件流相关

什么是文件流

在C#中提供了一个文件流类:FileStream类

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

File只能整体读写文件,而FileStream可以以读写字节的形式进行处理文件

简单来说:文件内的数据类比于一条数据流,可以通过FileStream一部分一部分的去读写这一串数据流。

比如:先存储一个int类型的,再存储一个 bool类型,最后再存一个char类型

我们就可以利用FileStream进行逐个的读写操作

FileStream文件流常用方法

类名:FileStream

命名空间:System.IO

1.打开或创建指定文件
方法一:new FileStream

参数一:路径

参数二:打开模式

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

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

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

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

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

Truncate:打开并清空文件内容

参数三:访问模式

参数四:共享权限

None:谢绝共享

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

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

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

cs 复制代码
FileStream fs = new FileStream(Application.dataPath + "/test1.txt",
FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.ReadWrite);
方法二:File.Create

参数一:路径

参数二:缓存大小

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

Asynchronous:可用于异步读写

DleteOnClose:不在使用时,自动删除

Encrypted:加密

None:不应用其他选项

RandomAccess:随机访问文件

SequentialScan:从头到尾顺序访问文件

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

cs 复制代码
FileStream fs2 = File.Create(Application.dataPath + "/test2.txt",1024,FileOptions.None);
方法三:File.Open

参数一:路径

参数二:打开模式

cs 复制代码
FileStream fs3 = File.Open(Application.dataPath + "/test1.txt", FileMode.OpenOrCreate);
2.重要属性和方法
文本字节长度
cs 复制代码
print(fs.Length);
是否可写
cs 复制代码
print(fs.CanWrite);
是否可读
cs 复制代码
print(fs.CanRead);
将字节写入文件,当写入后一定执行一次
cs 复制代码
fs.Flush();
关闭流,当文件读写完毕后一定执行
cs 复制代码
fs.Close();
缓存资源销毁回收
cs 复制代码
fs.Dispose();
3.写入字节

方法:Write

参数一:写入的字节数组

参数二:数组中的开始索引

参数三:写入多少个字节

cs 复制代码
fs.Write(bytes, 0, bytes.Length);

写入字符串时,先写入长度,再写入字符串具体内容

cs 复制代码
bytes = Encoding.UTF8.GetBytes("123123123");
int length = bytes.Length;
fs.Write(BitConverter.GetBytes(length), 0, BitConverter.GetBytes(length).Length);
fs.Write(bytes,0, length);

//避免数据丢失,一定要执行
fs.Flush();
//释放资源
fs.Dispose();
4.读取字节
方法一:挨个读取字节数组

读取第一个整形

参数一:用于存储读取的字节数组的容器

参数二:容器中开始的位置

参数三:读取多少个字节装入容器

返回值:当前流索引前进了几个位置

cs 复制代码
byte[] bytes2 = new byte[4];

fs.Read(bytes2, 0, 4);
int i = BitConverter.ToInt32(bytes2);
print(i);

读取第二个字符串

cs 复制代码
int index = fs2.Read(bytes2, 0, 4);
int length = BitConverter.ToInt32(bytes2);
bytes2 = new byte[length];
fs.Read(bytes2 , 0, length);
string s = Encoding.UTF8.GetString(bytes2);
方法二:一次性读取再挨个读取
cs 复制代码
byte[] bytes1 = new byte[fs1.Length];

fs1.Read(bytes1,0, (int)fs1.Length);
fs1.Dispose();
//读取整数
print(BitConverter.ToInt32(bytes1, 0));
//读取字符串字节数组长度
int length = BitConverter.ToInt32(bytes1, 4);
string s = Encoding.UTF8.GetString(bytes1, 8, length);

如何更加安全的使用

using关键字重要用法:

using(声明一个引用对象)

{

使用对象

}

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

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

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

注意:为了文件操作安全,建议都使用using来进行处理

通过FileStream读写时一定要注意,读的规则一定要和写一致

我们存储数据的先后顺序时我们自己定义的规则

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

文件夹相关

文件夹操作是什么?

平时我们可以再操作系统的文件管理系统中,通过一些操作增删查改文件夹

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

类名:Directory

命名空间:System.IO

1.判断文件夹是否存在
cs 复制代码
if(Directory.Exists(Application.persistentDataPath + "/test"))
{
    print("存在");
}
else
{
    print("不存在");
}
2.创建文件夹
cs 复制代码
DirectoryInfo info = Directory.CreateDirectory(Application.persistentDataPath + "/test");
3.删除文件夹

参数一:路径

参数二:是否删除非空目录,如果为true,将删除整个目录,

如果是false,仅当该目录为空才可删除

cs 复制代码
Directory.Delete(Application.persistentDataPath + "/test", true);
4.查找文件夹和文件
得到指定路径下所有文件夹名
cs 复制代码
string[] strings = Directory.GetDirectories(Application.dataPath);
for (int i = 0; i < strings.Length; i++)
{
    print(strings[i]);
}
得到指定路径下所有文件名
cs 复制代码
string[] strings = Directory.GetFiles(Application.dataPath);
for (int i = 0; i < strings.Length; i++)
{
    print(strings[i]);
}
5.移动文件夹

如果第二个参数所在的路径已经存在了一个文件夹,那么会报错

移动会把文件夹中的所有内容一起移到新的路径

cs 复制代码
Directory.Move(Application.persistentDataPath + "test",Application.dataPath+"/111");

DirectoryInfo和FileInfo

DirectoryInfo目录信息类

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

它主要出现在两个地方:

1.创建文件夹方法的返回值
cs 复制代码
DirectoryInfo info1 = Directory.CreateDirectory(Application.dataPath + "/111");
全路径
cs 复制代码
print(info1.FullName);
文件名
cs 复制代码
print(info1.Name);
2.查找上级文件夹信息
cs 复制代码
DirectoryInfo info2 = Directory.GetParent(Application.dataPath + "/111");
全路径
cs 复制代码
print(info2.FullName);
文件名
cs 复制代码
print(info2.Name);
重要方法
得到所有子文件夹的目录信息
cs 复制代码
DirectoryInfo[] infos = info2.GetDirectories();
FileInfo文件信息类

我们可以通过DirectoryInfo得到该文件下的所有文件信息

cs 复制代码
FileInfo[] fileInfos = info2.GetFiles();

该两个类一般再多文件夹或者多文件操作时会用到

用法和Directory和File类大同小异

C#类对象的序列化和反序列化

序列化

声明类对象

如果要使用C#自带的序列化二进制方法

需要先添加[System.Serializable]特性

将对象进行二进制序列化

方法一:使用内存流得到二进制字节数组

主要用于得到字节数组,可以用于网络传输

1.内存流对象:MemoryStream

命名空间:System.IO

2.二进制格式化对象:BinaryFormatter

命名空间:System.Runtime.Serialization.Formatters.Binary

主要方法:序列化方法Serialize

cs 复制代码
public class Test : MonoBehaviour
{
    private void Start()
    {
        Student s = new Student();
        using(MemoryStream ms = new MemoryStream())
        {
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(ms, s);
            byte[] bytes = ms.GetBuffer();
            //存储逻辑
            File.WriteAllBytes(Application.dataPath+"/123.111", bytes);
            ms.Close();
        }
    }

}
[System.Serializable]
public class Student
{
    public int id =1;
    public string name="123"; public string description="111";
    public int age=16;
    public bool sex=false;
}
方法二:使用文件流进行存储
cs 复制代码
Student s = new Student();
using(FileStream fs = new FileStream(Application.dataPath + "/test1.test", FileMode.OpenOrCreate, FileAccess.Write))
{
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(fs, s);
    fs.Flush();
    fs.Close();
}

反序列化

反序列化文件中的数据

主要类:

FileStream、BinaryFormatter

主要方法:Deserialize

cs 复制代码
Student s = new Student();
using(FileStream fs = new FileStream(Application.dataPath + "/test1.test", FileMode.OpenOrCreate, FileAccess.Write))
{
    BinaryFormatter bf = new BinaryFormatter();
    s = (Student)bf.Deserialize(fs);
    fs.Close();
}

反序列化网络传输过来的二进制数据

主要类:

MemoryStream内存流类

BinaryFormatter 二进制格式化类

主要方法:Deserialize

目前没有网络传输,所以依旧从文件中读取

cs 复制代码
Student s = new Student();
byte[] bytes = File.ReadAllBytes(Application.dataPath + "/test1.test");

using(MemoryStream ms = new MemoryStream(bytes))
{
    BinaryFormatter formatter = new BinaryFormatter();
    s = formatter.Deserialize(ms) as Student;
    ms.Close();
}

加密

何时加密?何时解密?

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

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

这样如果第三方获取到我们的二进制数据,当他们不知道加密规则和解密密钥时就无法获取正确的数据,起到保证数据安全的作用

加密是否一定安全?

加密只是提高了破解门槛,没有100%保密的数据

通过各种尝试始终是可以破解加密规则的,只是时间问题,加密只能起到提升一定安全性的作用

常见加密算法

MD5、SHA1、HMAC、AES/DES/3DES算法等等

有很多的第三方加密算法库,可以直接获取用于在程序中对数据进行加密

实现一个简单的自定义读写类

cs 复制代码
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;

public class BinaryDataManager
{
    private static BinaryDataManager instance = new BinaryDataManager();
    public static BinaryDataManager Instance => instance;

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

    private BinaryDataManager() { }

    /// <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 + ".data", FileMode.OpenOrCreate, FileAccess.Write))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(fs, obj);
            fs.Flush();
            fs.Close();
        }
    }

    /// <summary>
    /// 读取二进制数据转换成对象
    /// </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 + ".data"))
            return default(T);

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

主要用处

网络游戏:用于存储客户端数据、用于传输信息

单机游戏:用于存储游戏相关数据、用于配置游戏数据

注:可以自行实现一个通过excel表格配置数据,然后直接自动生成数据类和数据文件

补充:如何在Unity中添加菜单栏功能

为编辑器菜单栏添加新的选项入口

可以通过Unity提供我们的MenuItem特性在菜单栏添加选项按钮

特性名:MenuItem

命名空间:UnityEditor

例:

cs 复制代码
[MenuItem("GameTool/Test")]
private static void TestFunc()
{
    Debug.Log("Test");
}

注意:

必须是一个静态方法

菜单栏必须要有两级或以上的层级,不然会报错

可以不继承Monobehaviour

刷新Project窗口内容

类名:AssetDatabase

命名空间:UnityEditor

方法:Refresh

cs 复制代码
AssetDatabase.Refresh();

Editor文件夹

editor文件夹可以放在项目的任何文件夹下,可以有多个

放在其中的内容,项目打包时不会被打包到项目中

一般编辑器相关代码都可以放在该文件夹中

补充:Excel数据读取

Excel表的本质

Excel表本质上也是一堆数据,只不过它有自己的存储读取规则

如果我们想要通过代码读取它,那么必须知道它的存储规则

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

对于此类dll,我们一般只在编辑器中使用,所以最好放置在Editor文件夹内

打开Excel表

cs 复制代码
[MenuItem("GameTool/打开Excel表")]
private static void OpenExcel()
{
    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++)
        {
            print("表名"+result.Tables[i].TableName);
        }
        fs.Close();
    }
}

获取Excel表中单元格信息

cs 复制代码
 [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];
             for (int j = 0; j < table.Columns.Count; j++)
             {
                 //得到行
                 DataRow row = table.Rows[j];

                 for(int k = 0; k < table.Columns.Count; k++)
                 {
                     Debug.Log(row[k].ToString());
                 }
             }
         }
         fs.Close();
     }
 }

获取Excel表中信息的意义

既然我们能获取到Excel表中的所有数据,那么我们就可以通过表中的数据来动态生成相关数据

为什么不直接读取Excel表?

1.读取效率较低

2.数据安全性低

相关推荐
Jack___Xue2 小时前
LangGraph学习笔记(六)---LangGraph ReAct应用
笔记·学习·react.js
星期五不见面2 小时前
嵌入式学习!(一)C++学习-leetcode(21)-26/1/29
学习·算法·leetcode
呱呱巨基2 小时前
Linux 第一个系统程序 进度条
linux·c++·笔记·学习
好奇龙猫3 小时前
【人工智能学习-AI入试相关题目练习-第十七次】
人工智能·学习
我材不敲代码3 小时前
机器学习入门02——新手学习的第一个回归算法:线性回归
学习·机器学习·回归
●VON3 小时前
React Native for OpenHarmony:构建高性能、高体验的 TextInput 输入表单
javascript·学习·react native·react.js·von
●VON3 小时前
React Native for OpenHarmony:ActivityIndicator 动画实现详解
javascript·学习·react native·react.js·性能优化·openharmony
-Springer-3 小时前
STM32 学习 —— 个人学习笔记1(STM32简介)
笔记·stm32·学习
崇山峻岭之间4 小时前
Matlab学习记录40
开发语言·学习·matlab