当前位置: 首页 > article >正文

Unity的四种数据持久化方式

目录

什么是数据持久化

数据持久化之PlayerPrefs

 概述

API及用法

电脑中存放的位置

优缺点 

主要用处

封装PlayerPrefs

数据持久化之XML

XML是什么

读取XML信息

C#读取XML的方法有几种 

读取xml文件信息

 读取元素和属性信息

总结

写入XML信息

选择存储目录

存储xml文件

修改xml文件

总结

XML序列化

什么是序列化和反序列化

xml序列化

自定义节点名 或 设置属性

总结

XML反序列化

自定义类继承IXmlSerializable

扩展Dictionary支持序列号反序列化

封装

优缺点

优点

缺点

主要用处

数据持久化之Json

Json是什么

Json和Xml的异同

JsonUnity

   在文件中存读字符串

JsonUnity序列化

JsonUtlity进行反序列化

        注意事项

        总结

LitJson

使用LitJson进行序列化

使用LitJson反序列化

注意事项

总结

封装

数据持久化之2进制

        2进制是什么

    数据持久化之2进制的好处

各类型数据转字节数据

        2进制文件读写的本质

        数据和字节数据相互转换

文件操作相关

                1.判断文件是否存在

                2.创建文件

                3.写入文件

                4.读取文件

                5.删除文件

                6.复制文件

                7.文件替换

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

文件操作相关文件流

        什么是文件流     

                1.打开或创建指定文件

                2.重要属性和方法

                3.写入字节

                4.读取字节

文件夹相关操作

        1.判断文件夹是否存在

        2.创建文件夹

        3.删除文件夹

        4.查找文件夹和文件

        5.移动文件夹

    DirectoryInfo和FileInfo

c#类对象的序列化

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

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

c#类对象的反序列化

c#2进制数据加密

Unity添加菜单栏按钮

Excel DLL包的导入

Excel数据读取

        打开Excel表

        获取Excel表中单元格的信息

封装

        ExcelTool

        BinaryDataMgr

结尾


什么是数据持久化

        数据持久化就是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称  ;通俗来讲就是将数据存到硬盘,硬盘中数据读到游戏中,也就是传统意义上的存盘

数据持久化之PlayerPrefs

 概述

        PlayerPrefs,是Unity引擎内置的主要用来存储和读取玩家偏好设定的一个类,这是官方手册里写的它的主要用途。但其实它不只可以存储玩家的偏好设定,也可以用来存储简单的数据。

API及用法

        #region 存储相关
        //PlayerPrefs的数据存储 类似于键值对存储 一个键对应一个值
        //提供了存储3种数据的方法 int float string
        //键: string类型 
        //值:int float string 对应3种API

        PlayerPrefs.SetInt("myAge", 26);
        PlayerPrefs.SetFloat("myHeight", 188.5f);
        PlayerPrefs.SetString("myName", "Danny");

        //直接调用Set相关方法 只会把数据存到内存里
        //当游戏结束时 Unity会自动把数据存到硬盘中
        //如果游戏不是正常结束的 而是崩溃 数据是不会存到硬盘中的
        //只要调用该方法 就会马上存储到硬盘中
        PlayerPrefs.Save();

        //PlayerPrefs是有局限性的 它只能存3种类型的数据
        //如果你想要存储别的类型的数据 只能降低精度 或者上升精度来进行存储
        bool sex = true;
        PlayerPrefs.SetInt("sex", sex ? 1 : 0);

        //如果不同类型用同一键名进行存储 会进行覆盖
        PlayerPrefs.SetFloat("myAge", 20.2f);
        #endregion

        #region 读取相关
        //注意 运行时 只要你Set了对应键值对
        //即使你没有马上存储Save在本地
        //也能够读取出信息

        //int
        int age = PlayerPrefs.GetInt("myAge");
        print(age);
        //前提是 如果找不到myAge对应的值 就会返回函数的第二个参数 默认值
        age = PlayerPrefs.GetInt("myAge", 100);
        print(age);

        //float
        float height = PlayerPrefs.GetFloat("myHeight", 1000f);
        print(height);

        //string
        string name = PlayerPrefs.GetString("myName");
        print(name);

        //第二个参数 默认值 对于我们的作用
        //就是 在得到没有的数据的时候 就可以用它来进行基础数据的初始化

        //判断数据是否存在
        if( PlayerPrefs.HasKey("myName") )
        {
            print("存在myName对应的键值对数据");
        }
        #endregion

        #region 删除数据
        //删除指定键值对
        PlayerPrefs.DeleteKey("myAge");
        //删除所有存储的信息
        PlayerPrefs.DeleteAll();
        #endregion

电脑中存放的位置

PlayerPrefs将数据存储在了电脑的注册表中。

打开方式:

  • Win + r 输入regedit进入注册表界面

  • HKEY_CURRENT_USER/Software/Unity

  • 若你在编辑器中测试的,就进入UnityEditor,找到你工程的:公司/工程名 文件夹

优缺点 

 优点

        PlayerPrefs是数据持久化系列中最简单的一部分内容,简单快捷易懂

缺点

  • 重复工作繁多
  • 自定义数据类型都需要自己去实现读取功能,而且代码的相似度极高
  • 数据容易被修改、只要找到文件位置,就可以轻易的进行数据修改

主要用处

  • 单独使用他的原生功能
  • 适合存储一些对安全性要求不高的简单数据
  • 但也不能小看它,对他进行简单的封装也可以变的方便又安全

封装PlayerPrefs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

/// <summary>
/// PlayerPrefs数据管理类 统一管理数据的存储和读取
/// </summary>
public class PlayerPrefsDataMgr
{
    private static PlayerPrefsDataMgr instance = new PlayerPrefsDataMgr();

    public static PlayerPrefsDataMgr Instance
    {
        get
        {
            return instance;
        }
    }

    private PlayerPrefsDataMgr()
    {

    }

    /// <summary>
    /// 存储数据
    /// </summary>
    /// <param name="data">数据对象</param>
    /// <param name="keyName">数据对象的唯一key 自己控制</param>
    public void SaveData( object data, string keyName )
    {
        //就是要通过 Type 得到传入数据对象的所有的 字段
        //然后结合 PlayerPrefs来进行存储

        #region 第一步 获取传入数据对象的所有字段
        Type dataType = data.GetType();
        //得到所有的字段
        FieldInfo[] infos = dataType.GetFields();
        #endregion

        #region 第二步 自己定义一个key的规则 进行数据存储
        //我们存储都是通过PlayerPrefs来进行存储的
        //保证key的唯一性 我们就需要自己定一个key的规则

        //我们自己定一个规则
        // keyName_数据类型_字段类型_字段名
        #endregion

        #region 第三步 遍历这些字段 进行数据存储
        string saveKeyName = "";
        FieldInfo info;
        for (int i = 0; i < infos.Length; i++)
        {
            //对每一个字段 进行数据存储
            //得到具体的字段信息
            info = infos[i];
            //通过FieldInfo可以直接获取到 字段的类型 和字段的名字
            //字段的类型 info.FieldType.Name
            //字段的名字 info.Name;

            //要根据我们定的key的拼接规则 来进行key的生成
            //Player1_PlayerInfo_Int32_age
            saveKeyName = keyName + "_" + dataType.Name + 
                "_" + info.FieldType.Name + "_" + info.Name;

            //现在得到了Key 按照我们的规则
            //接下来就要来通过PlayerPrefs来进行存储
            //如何获取值
            //info.GetValue(data)
            //封装了一个方法 专门来存储值 
            SaveValue(info.GetValue(data), saveKeyName);
        }

        PlayerPrefs.Save();
        #endregion
    }

    private void SaveValue(object value, string keyName)
    {
        //直接通过PlayerPrefs来进行存储了
        //就是根据数据类型的不同 来决定使用哪一个API来进行存储
        //PlayerPrefs只支持3种类型存储 
        //判断 数据类型 是什么类型 然后调用具体的方法来存储
        Type fieldType = value.GetType();

        //类型判断
        //是不是int
        if( fieldType == typeof(int) )
        {
            //为int数据加密
            int rValue = (int)value;
            rValue += 10;
            PlayerPrefs.SetInt(keyName, rValue);
        }
        else if (fieldType == typeof(float))
        {
            PlayerPrefs.SetFloat(keyName, (float)value);
        }
        else if (fieldType == typeof(string))
        {
            PlayerPrefs.SetString(keyName, value.ToString());
        }
        else if (fieldType == typeof(bool))
        {
            //自己顶一个存储bool的规则
            PlayerPrefs.SetInt(keyName, (bool)value ? 1 : 0);
        }
        //如何判断 泛型类的类型呢
        //通过反射 判断 父子关系
        //这相当于是判断 字段是不是IList的子类
        else if( typeof(IList).IsAssignableFrom(fieldType) )
        {
            //父类装子类
            IList list = value as IList;
            //先存储 数量 
            PlayerPrefs.SetInt(keyName, list.Count);
            int index = 0;
            foreach (object obj in list)
            {
                //存储具体的值
                SaveValue(obj, keyName + index);
                ++index;
            }
        }
        //判断是不是Dictionary类型 通过Dictionary的父类来判断
        else if( typeof(IDictionary).IsAssignableFrom(fieldType) )
        {
            //父类装自来
            IDictionary dic = value as IDictionary;
            //先存字典长度
            PlayerPrefs.SetInt(keyName, dic.Count);
            //遍历存储Dic里面的具体值
            //用于区分 表示的 区分 key
            int index = 0;
            foreach (object key in dic.Keys)
            {
                SaveValue(key, keyName + "_key_" + index);
                SaveValue(dic[key], keyName + "_value_" + index);
                ++index;
            }
        }
        //基础数据类型都不是 那么可能就是自定义类型
        else
        {
            SaveData(value, keyName);
        }
    }

    /// <summary>
    /// 读取数据
    /// </summary>
    /// <param name="type">想要读取数据的 数据类型Type</param>
    /// <param name="keyName">数据对象的唯一key 自己控制</param>
    /// <returns></returns>
    public object LoadData( Type type, string keyName )
    {
        //不用object对象传入 而使用 Type传入
        //主要目的是节约一行代码(在外部)
        //假设现在你要 读取一个Player类型的数据 如果是object 你就必须在外部new一个对象传入
        //现在有Type的 你只用传入 一个Type typeof(Player) 然后我在内部动态创建一个对象给你返回出来
        //达到了 让你在外部 少写一行代码的作用

        //根据你传入的类型 和 keyName
        //依据你存储数据时  key的拼接规则 来进行数据的获取赋值 返回出去

        //根据传入的Type 创建一个对象 用于存储数据
        object data = Activator.CreateInstance(type);
        //要往这个new出来的对象中存储数据 填充数据
        //得到所有字段
        FieldInfo[] infos = type.GetFields();
        //用于拼接key的字符串
        string loadKeyName = "";
        //用于存储 单个字段信息的 对象
        FieldInfo info;
        for (int i = 0; i < infos.Length; i++)
        {
            info = infos[i];
            //key的拼接规则 一定是和存储时一模一样 这样才能找到对应数据
            loadKeyName = keyName + "_" + type.Name +
                "_" + info.FieldType.Name + "_" + info.Name;

            //有key 就可以结合 PlayerPrefs来读取数据
            //填充数据到data中 
            info.SetValue(data, LoadValue(info.FieldType, loadKeyName));
        }
        return data;
    }

    /// <summary>
    /// 得到单个数据的方法
    /// </summary>
    /// <param name="fieldType">字段类型 用于判断 用哪个api来读取</param>
    /// <param name="keyName">用于获取具体数据</param>
    /// <returns></returns>
    private object LoadValue(Type fieldType, string keyName)
    {
        //根据 字段类型 来判断 用哪个API来读取
        if( fieldType == typeof(int) )
        {
            //解密 减10
            return PlayerPrefs.GetInt(keyName, 0) - 10;
        }
        else if (fieldType == typeof(float))
        {
            return PlayerPrefs.GetFloat(keyName, 0);
        }
        else if (fieldType == typeof(string))
        {
            return PlayerPrefs.GetString(keyName, "");
        }
        else if (fieldType == typeof(bool))
        {
            //根据自定义存储bool的规则 来进行值的获取
            return PlayerPrefs.GetInt(keyName, 0) == 1 ? true : false;
        }
        else if( typeof(IList).IsAssignableFrom(fieldType) )
        {
            //得到长度
            int count = PlayerPrefs.GetInt(keyName, 0);
            //实例化一个List对象 来进行赋值
            //用了反射中双A中 Activator进行快速实例化List对象
            IList list = Activator.CreateInstance(fieldType) as IList;
            for (int i = 0; i < count; i++)
            {
                //目的是要得到 List中泛型的类型 
                list.Add(LoadValue(fieldType.GetGenericArguments()[0], keyName + i));
            }
            return list;
        }
        else if( typeof(IDictionary).IsAssignableFrom(fieldType) )
        {
            //得到字典的长度
            int count = PlayerPrefs.GetInt(keyName, 0);
            //实例化一个字典对象 用父类装子类
            IDictionary dic = Activator.CreateInstance(fieldType) as IDictionary;
            Type[] kvType = fieldType.GetGenericArguments();
            for (int i = 0; i < count; i++)
            {
                dic.Add(LoadValue(kvType[0], keyName + "_key_" + i),
                         LoadValue(kvType[1], keyName + "_value_" + i));
            }
            return dic;
        }
        else
        {
            return LoadData(fieldType, keyName);
        }

    }
}

数据持久化之XML

XML是什么

  • 全称:可扩展性标记语言(EXtensible Markup Language)
  • XML是国际通用的
  • 他是被设计来用于传输和存储数据的一种文本特殊格式
  • 文件名后缀一般为.xml

读取XML信息

C#读取XML的方法有几种 
  • 1.XmlDocument    (把数据加载到内存中,方便读取)
  • 2.XmlTextReader  (以流形式加载,内存占用更少,但是是单向只读,使用不是特别方便,除非有特殊需求,否则不会使用)
  • 3.Linq  
  • 使用XmlDocument类读取是较方便最容易理解和操作的方法
读取xml文件信息
XmlDocument xml = new XmlDocument();
//通过XmlDocument读取xml文件 有两个API
//1.直接根据xml字符串内容 来加载xml文件
//存放在Resorces文件夹下的xml文件加载处理
TextAsset asset = Resources.Load<TextAsset>("TestXml");
print(asset.text);
//通过这个方法 就能够翻译字符串为xml对象
xml.LoadXml(asset.text);

//2.是通过xml文件的路径去进行加载
xml.Load(Application.streamingAssetsPath + "/TestXml.xml");
 读取元素和属性信息
//节点信息类
//XmlNode 单个节点信息类
//节点列表信息
//XmlNodeList 多个节点信息类

//获取xml当中的根节点
XmlNode root = xml.SelectSingleNode("Root");
//再通过根节点 去获取下面的子节点
XmlNode nodeName = root.SelectSingleNode("name");
//如果想要获取节点包裹的元素信息 直接 .InnerText
print(nodeName.InnerText);

XmlNode nodeAge = root.SelectSingleNode("age");
print(nodeAge.InnerText);

XmlNode nodeItem = root.SelectSingleNode("Item");
//第一种方式 直接 中括号获取信息
print(nodeItem.Attributes["id"].Value);
print(nodeItem.Attributes["num"].Value);
//第二种方式 
print(nodeItem.Attributes.GetNamedItem("id").Value);
print(nodeItem.Attributes.GetNamedItem("num").Value);

//这里是获取 一个节点下的同名节点的方法
XmlNodeList friendList = root.SelectNodes("Friend");

//遍历方式一:迭代器遍历
//foreach (XmlNode item in friendList)
//{
//    print(item.SelectSingleNode("name").InnerText);
//    print(item.SelectSingleNode("age").InnerText);
//}
//遍历方式二:通过for循环遍历
//通过XmlNodeList中的 成员变量 Count可以得到 节点数量
for (int i = 0; i < friendList.Count; i++)
{
    print(friendList[i].SelectSingleNode("name").InnerText);
    print(friendList[i].SelectSingleNode("age").InnerText);
}
总结

1.读取XML文件

  • XmlDocument xml = new XmlDocument();
  • 读取文本方式1   xml.LoadXml(传入xml文本字符串)
  • 读取文本方式2   xml.Load(传入路径)

2.读取元素和属性

  • 获取单个节点 : XmlNode node = xml.SelectSingleNode(节点名)
  • 获取多个节点 : XmlNodeList nodeList = xml.SelectNodes(节点名)
  • 获取节点元素内容:node.InnerText
  • 获取节点元素属性:

        1.item.Attributes["属性名"].Value
        2.item.Attributes.GetNamedItem("属性名").Value

通过迭代器遍历或者循环遍历XmlNodeList对象 可以获取到各单个元素节点

写入XML信息

选择存储目录

 注意:存储xml文件 在Unity中一定是使用各平台都可读可写可找到的路径

  1. Resources 可读 不可写 打包后找不到  ×
  2.  Application.streamingAssetsPath 可读 PC端可写 找得到  ×
  3.  Application.dataPath 打包后找不到  ×
  4.  Application.persistentDataPath 可读可写找得到   √
string path = Application.persistentDataPath + "/TextXml.xml";
print(Application.persistentDataPath);
存储xml文件
  • 关键类 XmlDocument 用于创建节点 存储文件
  • 关键类 XmlDeclaration 用于添加版本信息
  • 关键类 XmlElement 节点类

存储有5步

//1.创建文本对象
XmlDocument xml = new XmlDocument();

//2.添加固定版本信息
//这一句代码 相当于就是创建<?xml version="1.0" encoding="UTF-8"?>这句内容
XmlDeclaration xmlDec = xml.CreateXmlDeclaration("1.0", "UTF-8", "");
//创建完成过后 要添加进入 文本对象中
xml.AppendChild(xmlDec);

//3.添加根节点
XmlElement root = xml.CreateElement("Root");
xml.AppendChild(root);

//4.为根节点添加子节点
//加了一个 name子节点
XmlElement name = xml.CreateElement("name");
name.InnerText = "Danny";
root.AppendChild(name);

XmlElement atk = xml.CreateElement("atk");
atk.InnerText = "10";
root.AppendChild(atk);

XmlElement listInt = xml.CreateElement("listInt");
for (int i = 1; i <= 3; i++)
{
    XmlElement childNode = xml.CreateElement("int");
    childNode.InnerText = i.ToString();
    listInt.AppendChild(childNode);
}
root.AppendChild(listInt);

XmlElement itemList = xml.CreateElement("itemList");
for (int i = 1; i <= 3; i++)
{
    XmlElement childNode = xml.CreateElement("Item");
    //添加属性
    childNode.SetAttribute("id", i.ToString());
    childNode.SetAttribute("num", (i * 10).ToString());
    itemList.AppendChild(childNode);
}
root.AppendChild(itemList);

//5.保存
xml.Save(path);
修改xml文件
//1.先判断是否存在文件
if( File.Exists(path) )
{
    //2.加载后 直接添加节点 移除节点即可
    XmlDocument newXml = new XmlDocument();
    newXml.Load(path);

    //修改就是在原有文件基础上 去移除 或者添加
    //移除
    XmlNode node;// = newXml.SelectSingleNode("Root").SelectSingleNode("atk");
    //这种是一种简便写法 通过/来区分父子关系
    node = newXml.SelectSingleNode("Root/atk");
    //得到自己的父节点
    XmlNode root2 = newXml.SelectSingleNode("Root");
    //移除子节点方法
    root2.RemoveChild(node);

    //添加节点
    XmlElement speed = newXml.CreateElement("moveSpeed");
    speed.InnerText = "20";
    root2.AppendChild(speed);

    //改了记得存
    newXml.Save(path);
}
总结

1.路径选取
        在运行过程中存储 只能往可写且能找到的文件夹存储
        故 选择了Application.persistentDataPath

2.存储xml关键类
        XmlDocument  文件  
           创建节点 CreateElement
           创建固定内容方法 CreateXmlDeclaration
           添加节点 AppendChild
           保存 Save
        XmlDeclaration 版本
        XmlElement 元素节点  
           设置属性方法SetAttribute

3.修改
        RemoveChild移除节点

XML序列化

什么是序列化和反序列化

序列化:把对象转化为可传输的字节序列过程称为序列化(序列化就是把想要存储的内容转换为字节序列用于存储或传递)
反序列化:把字节序列还原为对象的过程称为反序列化(反序列化就是把存储或收到的字节序列信息解析读取出来使用)

xml序列化
//1.第一步准备一个数据结构类
Lesson1Test lt = new Lesson1Test();
//2.进行序列化
//  关键知识点
//  XmlSerializer 用于序列化对象为xml的关键类
//  StreamWriter 用于存储文件  
//  using 用于方便流对象释放和销毁

//第一步:确定存储路径
string path = Application.persistentDataPath + "/Lesson1Test.xml";
print(Application.persistentDataPath);
//第二步:结合 using知识点 和 StreamWriter这个流对象 来写入文件
// 括号内的代码:写入一个文件流 如果有该文件 直接打开并修改 如果没有该文件 直接新建一个文件
// using 的新用法 括号当中包裹的声明的对象 会在 大括号语句块结束后 自动释放掉 
// 当语句块结束 会自动帮助我们调用 对象的 Dispose这个方法 让其进行销毁
// using一般都是配合 内存占用比较大 或者 有读写操作时  进行使用的 
using ( StreamWriter stream = new StreamWriter(path) )
{
    //第三步:进行xml文件序列化
    XmlSerializer s = new XmlSerializer(typeof(Lesson1Test));
    //这句代码的含义 就是通过序列化对象 对我们类对象进行翻译 将其翻译成我们的xml文件 写入到对应的文件中
    //第一个参数 : 文件流对象 
    //第二个参数: 想要备翻译 的对象
    //注意 :翻译机器的类型 一定要和传入的对象是一致的 不然会报错
    s.Serialize(stream, lt);
}
自定义节点名 或 设置属性

//可以通过特性 设置节点或者设置属性 并且修改名字
[XmlArray("IntList")]
[XmlArrayItem("Int32")]
public List<int> listInt;

[XmlElement("testPublic123123")]
public int testPublic;

[XmlAttribute("Test1")]
public int test1 = 1;
总结

序列化流程

  1. 有一个想要保存的类对象
  2. 使用XmlSerializer 序列化该对象
  3. 通过StreamWriter 配合 using将数据存储 写入文件

注意:

  1. 只能序列化公共成员
  2. 不支持字典序列化
  3. 可以通过特性修改节点信息 或者设置属性信息
  4. Stream相关要配合using使用

XML反序列化

#region 知识点一 判断文件是否存在
string path = Application.persistentDataPath + "/Lesson1Test.xml";
if( File.Exists(path) )
{
    #region 知识点二 反序列化
    //关键知识
    // 1.using 和 StreamReader
    // 2.XmlSerializer 的 Deserialize反序列化方法

    //读取文件
    using (StreamReader reader = new StreamReader(path))
    {
        //产生了一个 序列化反序列化的翻译机器
        XmlSerializer s = new XmlSerializer(typeof(Lesson1Test));
        Lesson1Test lt = s.Deserialize(reader) as Lesson1Test;
    }
    #endregion
}
#endregion

#region 总结
//1.判断文件是否存在 File.Exists
//2.文件流获取 StreamReader reader = new StreamReader(path)
//3.根据文件流 XmlSerializer通过Deserialize反序列化 出对象

//注意:List对象 如果有默认值 反序列化时 不会清空 会往后面添加
#endregion

自定义类继承IXmlSerializable

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using UnityEngine;

public class TestLesson3 : IXmlSerializable
{
    public int test1;
    public string test2;

    //返回结构
    public XmlSchema GetSchema()
    {
        return null;
    }

    //反序列化时 会自动调用的方法
    public void ReadXml(XmlReader reader)
    {
        //在里面可以自定义反序列化 的规则
        //1.读属性
        //this.test1 = int.Parse(reader["test1"]);
        //this.test2 = reader["test2"];

        //2.读节点
        //方式一
        //reader.Read();//这时是读到的test1节点
        //reader.Read();//这时是读到的test1节点包裹的内容
        //this.test1 = int.Parse(reader.Value);//得到当前内容的值
        //reader.Read();//这时读到的是尾部包裹节点
        //reader.Read();//这时是读到的test2节点
        //reader.Read();//这时是读到的test2节点包裹的内容
        //this.test2 = reader.Value;
        //方式二
        //while(reader.Read())
        //{
        //    if( reader.NodeType == XmlNodeType.Element )
        //    {
        //        switch (reader.Name)
        //        {
        //            case "test1":
        //                reader.Read();
        //                this.test1 = int.Parse(reader.Value);
        //                break;
        //            case "test2":
        //                reader.Read();
        //                this.test2 = reader.Value;
        //                break;
        //        }
        //    }
        //}

        //3.读包裹元素节点
        XmlSerializer s = new XmlSerializer(typeof(int));
        XmlSerializer s2 = new XmlSerializer(typeof(string));
        //跳过根节点
        reader.Read();
        reader.ReadStartElement("test1");
        test1 = (int)s.Deserialize(reader);
        reader.ReadEndElement();

        reader.ReadStartElement("test2");
        test2 = s2.Deserialize(reader).ToString();
        reader.ReadEndElement();
    }

    //序列化时 会自动调用的方法
    public void WriteXml(XmlWriter writer)
    {
        //在里面可以自定义序列化 的规则

        //如果要自定义 序列化的规则 一定会用到 XmlWriter中的一些方法 来进行序列化
        //1.写属性
        //writer.WriteAttributeString("test1", this.test1.ToString());
        //writer.WriteAttributeString("test2", this.test2);

        //2.写节点
        //writer.WriteElementString("test1", this.test1.ToString());
        //writer.WriteElementString("test2", this.test2);

        //3.写包裹节点
        XmlSerializer s = new XmlSerializer(typeof(int));
        writer.WriteStartElement("test1");
        s.Serialize(writer, test1);
        writer.WriteEndElement();

        XmlSerializer s2 = new XmlSerializer(typeof(string));
        writer.WriteStartElement("test2");
        s2.Serialize(writer, test2);
        writer.WriteEndElement();
    }
}

public class Lesson3 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 IXmlSerializable是什么
        //C# 的XmlSerializer 提供了可拓展内容 
        //可以让一些不能被序列化和反序列化的特殊类能被处理
        //让特殊类继承 IXmlSerializable 接口 实现其中的方法即可
        #endregion

        #region 知识点二 自定义类实践
        TestLesson3 t = new TestLesson3();
        t.test2 = "123";
        string path = Application.persistentDataPath + "/TestLesson3.xml";
        //序列化
        using (StreamWriter writer = new StreamWriter(path))
        {
            //序列化"翻译机器"
            XmlSerializer s = new XmlSerializer(typeof(TestLesson3));
            //在序列化时  如果对象中的引用成员 为空 那么xml里面是看不到该字段的
            s.Serialize(writer, t);
        }
        //反序列化
        using (StreamReader reader = new StreamReader(path))
        {
            //序列化"翻译机器"
            XmlSerializer s = new XmlSerializer(typeof(TestLesson3));
            TestLesson3 t2 = s.Deserialize(reader) as TestLesson3;
        }
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

扩展Dictionary支持序列号反序列化

using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using UnityEngine;

public class SerizlizerDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{
    public XmlSchema GetSchema()
    {
        return null;
    }

    //自定义字典的 反序列化 规则
    public void ReadXml(XmlReader reader)
    {
        XmlSerializer keySer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSer = new XmlSerializer(typeof(TValue));

        //要跳过根节点
        reader.Read();
        //判断 当前不是元素节点 结束 就进行 反序列化
        while (reader.NodeType != XmlNodeType.EndElement)
        {
            //反序列化键
            TKey key = (TKey)keySer.Deserialize(reader);
            //反序列化值
            TValue value = (TValue)valueSer.Deserialize(reader);
            //存储到字典中
            this.Add(key, value);
        }
    }

    //自定义 字典的 序列化 规则
    public void WriteXml(XmlWriter writer)
    {
        XmlSerializer keySer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSer = new XmlSerializer(typeof(TValue));

        foreach (KeyValuePair<TKey, TValue> kv in this)
        {
            //键值对 的序列化
            keySer.Serialize(writer, kv.Key);
            valueSer.Serialize(writer, kv.Value);
        }
    }
}

封装

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using UnityEngine;

public class XmlDataMgr
{
    private static XmlDataMgr instance = new XmlDataMgr();

    public static XmlDataMgr Instance => instance;

    private XmlDataMgr() { }

    /// <summary>
    /// 保存数据到xml文件中
    /// </summary>
    /// <param name="data">数据对象</param>
    /// <param name="fileName">文件名</param>
    public void SaveData(object data, string fileName)
    {
        //1.得到存储路径
        string path = Application.persistentDataPath + "/" + fileName + ".xml";
        //2.存储文件
        using(StreamWriter writer = new StreamWriter(path))
        {
            //3.序列化
            XmlSerializer s = new XmlSerializer(data.GetType());
            s.Serialize(writer, data);
        }
    }

    /// <summary>
    /// 从xml文件中读取内容 
    /// </summary>
    /// <param name="type">对象类型</param>
    /// <param name="fileName">文件名</param>
    /// <returns></returns>
    public object LoadData(Type type, string fileName)
    {
        //1。首先要判断文件是否存在
        string path = Application.persistentDataPath + "/" + fileName + ".xml";
        if( !File.Exists(path) )
        {
            path = Application.streamingAssetsPath + "/" + fileName + ".xml";
            if (!File.Exists(path))
            {
                //如果根本不存在文件 两个路径都找过了
                //那么直接new 一个对象 返回给外部 无非 里面都是默认值
                return Activator.CreateInstance(type);
            }
        }
        //2.存在就读取
        using (StreamReader reader = new StreamReader(path))
        {
            //3.反序列化 取出数据
            XmlSerializer s = new XmlSerializer(type);
            return s.Deserialize(reader);
        }
    }
    
}

优缺点

优点
  • XML是国际通用的规则
  • 跨平台(游戏、软件、网页等等都可以用)
  • 文件结构浅显易懂,非常容易编辑和理解
  • 可以用于网络通讯进行交换数据
缺点
  • 重复工作繁多,且代码相似度极高
  • 自定义数据类型,都需要自己去实现存储和读取的功能
  • 数据容易被修改,只要找到位置就可以轻易修改数据
主要用处

        网络游戏

  • 可以用于存储一些简单的不重要的数据
  • 可以用于传输信息(基本不会大范围使用,因为比较耗流量)

        单机游戏

  • 用于存储游戏相关数据
  • 用于配置游戏数据

数据持久化之Json

Json是什么

        JSONJavaScript Object Notation  JS对象简谱 , 是一种轻量级的数据交换格式.

        主要在网络通讯中用于传输数据,或本地数据存储和读取(在游戏中可以把游戏数据按照Json格式标准存储在Json文档中,再将Json文档存储在硬盘上或者传输给远端,达到数据持久化或者数据传输的目的

        易于人阅读和编写,同时也易于机器解析和生成,并有效的提高网络传输效率

Json和Xml的异同

        共同点

  •                 都是纯文本
  •                 都有层级结构
  •                 都具有描述性

        不同点

  •                 Json配置更简单
  •                 Json在某种情况下读写更快

JsonUnity

          JsonUnity是Unity自带的公共类

  •         将内存中对象序列化为Json格式的字符串
  •          将Json字符串反序列化为类对象
   在文件中存读字符串

        1.存储字符串到指定路径文件中
                第一个参数 填写的是 存储的路径
                第二个参数 填写的是 存储的字符串内容
                注意:第一个参数 必须是存在的文件路径 如果没有对应文件夹 会报错

        File.WriteAllText(Application.persistentDataPath + "/Test.json", "json文件");
        print(Application.persistentDataPath);

        2.在指定路径文件中读取字符串

        string str = File.ReadAllText(Application.persistentDataPath + "/Test.json");
        print(str);
JsonUnity序列化

        序列化:把内存中的数据 存储到硬盘上

        方法:JsonUtility.ToJson(对象)

public class MrZhong
{
    public string name;
    public int age;
    public bool sex;
    public float testF;
    public double testD;

    public int[] ids;
    public List<int> ids2;
    public Dictionary<int, string> dic;
    public Dictionary<string, string> dic2;

    public Student s1;
    public List<Student> s2s;

    [SerializeField]
    private int privateI = 1;
    [SerializeField]
    protected int protectedI = 2;
}
 MrZhong t = new MrZhong();
 t.name = "Danny";
 t.age = 18;
 t.sex = false;
 t.testF = 1.4f;
 t.testD = 1.4;

 t.ids = new int[] { 1, 2, 3, 4 };
 t.ids2 = new List<int>() { 1, 2, 3 };
 t.dic = new Dictionary<int, string>() { { 1, "123" }, { 2, "234" } };
 t.dic2 = new Dictionary<string, string>() { { "1", "123" }, { "2", "234" } };

 t.s1 = null;//new Student(1, "小红");
 t.s2s = new List<Student>() { new Student(2, "小明"), new Student(3, "小强") };

 //Jsonutility提供了线程的方法 可以把类对象 序列化为 json字符串
 string jsonStr = JsonUtility.ToJson(t);
 File.WriteAllText(Application.persistentDataPath + "/MrZhong.json", jsonStr);

注意:

  •         float序列化时看起来会有一些误差
  •         自定义类需要加上序列化特性[System.Serializable]
  •         想要序列化私有变量 需要加上特性[SerializeField]
  •        JsonUtility不支持字典
  •        JsonUtlity存储null对象不会是null 而是默认值的数据
     
JsonUtlity进行反序列化

        反序列化:把硬盘上的数据 读取到内存中

        方法: JsonUtility.FromJson(字符串) 

//读取文件中的 Json字符串
jsonStr = File.ReadAllText(Application.persistentDataPath + "/MrZhong.json");
//使用Json字符串内容 转换成类对象
MrZhong t2 = JsonUtility.FromJson(jsonStr, typeof(MrZhong)) as MrZhong;
//推荐这种写法
MrZhong t3 = JsonUtility.FromJson<MrZhong>(jsonStr);

        注意: 如果Json中数据少了,读取到内存中类对象中时不会报错

        注意事项

//1.JsonUtlity无法直接读取数据集合
public class RoleData
{
    public List<RoleInfo> list;
}
[System.Serializable]
public class RoleInfo
{
    public int hp;
    public int speed;
    public int volume;
    public string resName;
    public int scale;
}

[System.Serializable]
public class RoleInfo
{
    public int hp;
    public int speed;
    public int volume;
    public string resName;
    public int scale;
}
jsonStr = File.ReadAllText(Application.streamingAssetsPath + "/RoleInfo2.json");
print(jsonStr);
//List<RoleInfo> roleInfoList = JsonUtility.FromJson<List<RoleInfo>>(jsonStr);
RoleData data = JsonUtility.FromJson<RoleData>(jsonStr);

//2.文本编码格式需要时UTF-8 不然无法加载
        总结
  1. 必备知识点 —— File存读字符串的方法 ReadAllText和WriteAllText
  2. JsonUtlity提供的序列化反序列化方法 ToJson 和 FromJson
  3. 自定义类需要加上序列化特性[System.Serializable]
  4. 私有保护成员 需要加上[SerializeField]
  5. JsonUtlity不支持字典
  6. JsonUtlity不能直接将数据反序列化为数据集合
  7. Json文档编码格式必须是UTF-8

LitJson

        LitJson它是一个第三方库,用于处理Json的序列化和反序列化

  •         LitJson是C#编写的,体积小、速度快、易于使用
  •         它可以很容易的嵌入到我们的代码中
  •         只需要将LitJson代码拷贝到工程中即可

        获取LitJson

  •         1.前往LitJson官网
  •        2.通过官网前往GitHub获取最新版本代码
  •         3.讲代码拷贝到Unity工程中 即可开始使用LitJson
使用LitJson进行序列化

        方法:JsonMapper.ToJson(对象)

Danny t = new Danny();
t.name = "Danny";
t.age = 18;
t.sex = true;
t.testF = 1.4f;
t.testD = 1.4;

t.ids = new int[] { 1, 2, 3, 4 };
t.ids2 = new List<int>() { 1, 2, 3 };
//t.dic = new Dictionary<int, string>() { { 1, "123" }, { 2, "234" } };
t.dic2 = new Dictionary<string, string>() { { "1", "123" }, { "2", "234" } };

t.s1 = null;//new Student(1, "小红");
t.s2s = new List<Student2>() { new Student2(2, "小明"), new Student2(3, "小强") };

string jsonStr = JsonMapper.ToJson(t);
print(Application.persistentDataPath);
File.WriteAllText(Application.persistentDataPath + "/Danny.json", jsonStr);

        注意:

  •         相对JsonUtlity不需要加特性
  •         不能序列化私有变量
  •         支持字典类型,字典的键 建议都是字符串 因为 Json的特点 Json中的键会加上双引号
  •         需要引用LitJson命名空间
  •         LitJson可以准确的保存null类型
使用LitJson反序列化

         方法:JsonMapper.ToObject(字符串)

jsonStr = File.ReadAllText(Application.persistentDataPath + "/Danny.json");
//JsonData是LitJson提供的类对象 可以用键值对的形式去访问其中的内容
JsonData data = JsonMapper.ToObject(jsonStr);
print(data["name"]);
print(data["age"]);
//通过泛型转换 更加的方便 建议使用这种方式
Danny t = JsonMapper.ToObject<Danny>(jsonStr);

        注意:

  •         类结构需要无参构造函数,否则反序列化时报错
  •         字典虽然支持 但是键在使用为数值时会有问题 需要使用字符串类型
注意事项
//1.LitJson可以直接读取数据集合
jsonStr = File.ReadAllText(Application.streamingAssetsPath + "/RoleInfo.json");
RoleInfo2[] arr = JsonMapper.ToObject<RoleInfo2[]>(jsonStr);

List<RoleInfo2> list = JsonMapper.ToObject<List<RoleInfo2>>(jsonStr);

jsonStr = File.ReadAllText(Application.streamingAssetsPath + "/Dic.json");
Dictionary<string, int> dicTest = JsonMapper.ToObject<Dictionary<string, int>>(jsonStr);

//2.文本编码格式需要是UTF-8 不然无法加载
总结
  •         LitJson提供的序列化反序列化方法 JsonMapper.ToJson和ToObject<>
  •         LitJson无需加特性
  •         LitJson不支持私有变量
  •         LitJson支持字典序列化反序列化
  •         LitJson可以直接将数据反序列化为数据集合
  •         LitJson反序列化时 自定义类型需要无参构造
  •         Json文档编码格式必须是UTF-8

封装

using LitJson;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

/// <summary>
/// 序列化和反序列化Json时  使用的是哪种方案
/// </summary>
public enum JsonType
{
    JsonUtlity,
    LitJson,
}

/// <summary>
/// Json数据管理类 主要用于进行 Json的序列化存储到硬盘 和 反序列化从硬盘中读取到内存中
/// </summary>
public class JsonMgr
{
    private static JsonMgr instance = new JsonMgr();
    public static JsonMgr Instance => instance;

    private JsonMgr() { }

    //存储Json数据 序列化
    public void SaveData(object data, string fileName, JsonType type = JsonType.LitJson)
    {
        //确定存储路径
        string path = Application.persistentDataPath + "/" + fileName + ".json";
        //序列化 得到Json字符串
        string jsonStr = "";
        switch (type)
        {
            case JsonType.JsonUtlity:
                jsonStr = JsonUtility.ToJson(data);
                break;
            case JsonType.LitJson:
                jsonStr = JsonMapper.ToJson(data);
                break;
        }
        //把序列化的Json字符串 存储到指定路径的文件中
        File.WriteAllText(path, jsonStr);
    }

    //读取指定文件中的 Json数据 反序列化
    public T LoadData<T>(string fileName, JsonType type = JsonType.LitJson) where T : new()
    {
        //确定从哪个路径读取
        //首先先判断 默认数据文件夹中是否有我们想要的数据 如果有 就从中获取
        string path = Application.streamingAssetsPath + "/" + fileName + ".json";
        //先判断 是否存在这个文件
        //如果不存在默认文件 就从 读写文件夹中去寻找
        if(!File.Exists(path))
            path = Application.persistentDataPath + "/" + fileName + ".json";
        //如果读写文件夹中都还没有 那就返回一个默认对象
        if (!File.Exists(path))
            return new T();

        //进行反序列化
        string jsonStr = File.ReadAllText(path);
        //数据对象
        T data = default(T);
        switch (type)
        {
            case JsonType.JsonUtlity:
                data = JsonUtility.FromJson<T>(jsonStr);
                break;
            case JsonType.LitJson:
                data = JsonMapper.ToObject<T>(jsonStr);
                break;
        }

        //把对象返回出去
        return data;
    }
}


数据持久化之2进制

        2进制是什么

        2进制是计算机技术中广泛采用的一种数制。2进制数据是用0和1两个数码来表示的数。他的基数是2,进位规则是“逢二进一”。

        计算机中存储的数据本质上都是2进制数的存储,在计算机中位(bit)是最小的存储单位。1位就是一个0或者一个1。

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

    数据持久化之2进制的好处

        通过2进制进行数据持久化的好处:

  •         安全性较高
  •         效率较高
  •         为网络通信做铺垫

各类型数据转字节数据

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

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

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

                变量的本质是2进制
                在内存中都以字节的形式存储着
                1byte = 8bit
                1bit(位)不是0就是1
                通过sizeof方法可以看到常用变量类型占用的字节空间长度

print("有符号");
print("sbyte" + sizeof(sbyte) + "字节");//1字节
print("int" + sizeof(int) + "字节");//4字节
print("short" + sizeof(short) + "字节");//2字节
print("long" + sizeof(long) + "字节");//8字节
print("无符号");
print("byte" + sizeof(byte) + "字节");//1字节
print("uint" + sizeof(uint) + "字节");//4字节
print("ushort" + sizeof(ushort) + "字节");//2字节
print("ulong" + sizeof(ulong) + "字节");//8字节
print("浮点");
print("float" + sizeof(float) + "字节");//4字节
print("double" + sizeof(double) + "字节");//8字节
print("decimal" + sizeof(decimal) + "字节");//16字节
print("特殊");
print("bool" + sizeof(bool) + "字节");//1字节
print("char" + sizeof(char) + "字节");//2字节

        2进制文件读写的本质

  • 它就是通过将各类型变量转换为字节数组
  • 将字节数组直接存储到文件
  • 一般人是看不懂存储的数据的
  • 不仅可以节约存储空间,提升效率
  • 还可以提升安全性
  • 而且在网络通信中我们直接传输的数据也是字节数据(2进制数据)

        数据和字节数据相互转换

                C#提供了一个公共类帮助我们进行转化
                我们只需要记住API即可
                类名:BitConverter
                命名空间:using System

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

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

                标准编码格式

                编码是用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。
                为保证编码的正确性,编码要规范化、标准化,即需有标准的编码格式。
                常见的编码格式有ASCII、ANSI、GBK、GB2312、UTF - 8、GB18030和UNICODE等。

  •  计算机中数据的本质就是2进制数据
  •  编码格式就是用对应的2进制数 对应不同的文字
  •  由于世界上有各种不同的语言,所有会有很多种不同的编码格式
  •  不同的编码格式 对应的规则是不同的
  •  如果在读取字符时采用了不统一的编码格式,可能会出现乱码
  •  游戏开发中常用编码格式 UTF-8
  •  中文相关编码格式 GBK
     

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

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

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

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

总结:我们可以通过BitConverter和Encoding类,将所有C#提供给我们的数据类型和字节数组之间进行相互转换了

文件操作相关

        代码中的文件操作是做什么

                在电脑上我们可以在操作系统中创建删除修改文件,可以增删查改各种各样的文件类型,代码中的文件操作就是通过代码来做这些事情

        文件相关操作公共类

                C#提供了一个名为File(文件)的公共类 ,让我们可以快捷的通过代码操作文件相关

                类名:File;命名空间: System.IO

        文件操作File类的常用内容

                1.判断文件是否存在
if(File.Exists(Application.dataPath + "/UnityTeach.zhong"))
{
    print("文件存在");
}
else
{
    print("文件不存在");
}
                2.创建文件
FileStream fs = File.Create(Application.dataPath + "/UnityTeach.zhong");
                3.写入文件
//将指定字节数组 写入到指定路径的文件中
byte[] bytes = BitConverter.GetBytes(999);
File.WriteAllBytes(Application.dataPath + "/UnityTeach.zhong", bytes);

//将指定的string数组内容 一行行写入到指定路径中
string[] strs = new string[] { "123", "Danny", "123123kdjfsalk", "123123123125243"};
File.WriteAllLines(Application.dataPath + "/UnityTeach2.zhong", strs);

//将指定字符串写入指定路径
File.WriteAllText(Application.dataPath + "/UnityTeach3.zhong", "Danny666\n哈哈哈哈123123131231241234123");
                4.读取文件
//读取字节数据
bytes = File.ReadAllBytes(Application.dataPath + "/UnityTeach.zhong");
print(BitConverter.ToInt32(bytes, 0));

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

//读取所有文本信息
print(File.ReadAllText(Application.dataPath + "/UnityTeach3.zhong"));
                5.删除文件
//注意 如果删除打开着的文件 会报错
File.Delete(Application.dataPath + "/UnityTeach.zhong");
                6.复制文件
//参数一:现有文件 需要是流关闭状态
//参数二:目标文件
File.Copy(Application.dataPath + "/UnityTeach2.zhong", Application.dataPath + "/Danny.tanglaoshi", true);
                7.文件替换
//参数一:用来替换的路径
//参数二:被替换的路径
//参数三:备份路径
File.Replace(Application.dataPath + "/UnityTeach3.zhong", Application.dataPath + "/Danny.tanglaoshi", Application.dataPath + "/Danny备份.tanglaoshi");
                8.以流的形式 打开文件并写入或读取
//参数一:路径
//参数二:打开模式
//参数三:访问模式
FileStream fs = File.Open(Application.dataPath + "/UnityTeach2.zhong", FileMode.OpenOrCreate, FileAccess.ReadWrite);

文件操作相关文件流

        什么是文件流     

                在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.Danny", FileMode.Create, FileAccess.ReadWrite);

                        方法二:File.Create
                        参数一:路径
                        参数二:缓存大小
                        参数三:描述如何创建或覆盖该文件(不常用)
                                Asynchronous 可用于异步读写
                                DeleteOnClose 不在使用时,自动删除
                                Encrypted 加密
                                None 不应用其它选项
                                RandomAccess 随机访问文件
                                SequentialScan 从头到尾顺序访问文件
                                WriteThrough 通过中间缓存直接写入磁盘

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

                        方法三:File.Open
                        参数一:路径
                        参数二:打开模式

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

//是否可读
if( fs.CanRead )
{
    print("文件可读");
}

//是否可写
if( fs.CanWrite )
{
    print("文件可写");

}

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

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

//缓存资源销毁回收
fs.Dispose();
                3.写入字节
print(Application.persistentDataPath);
using (FileStream fs = new FileStream(Application.persistentDataPath + "/Lesson3.Danny", FileMode.OpenOrCreate, FileAccess.Write))
{
    byte[] bytes = BitConverter.GetBytes(999);
    //方法:Write
    //参数一:写入的字节数组
    //参数二:数组中的开始索引
    //参数三:写入多少个字节
    fs.Write(bytes, 0, bytes.Length);

    //写入字符串时
    bytes = Encoding.UTF8.GetBytes("Danny哈哈哈哈");
    //先写入长度
    //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.Danny", 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();
}

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

using (FileStream fs3 = File.Open(Application.persistentDataPath + "/Lesson3.Danny", 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#提供给我们的文件夹操作公共类

                类名: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.查找上级文件夹信息

DirectoryInfo  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);//后缀名
}

c#类对象的序列化

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

                注意:如果要使用C#自带的序列化2进制方法,申明类时需要添加[System.Serializable]特性

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

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

                主要用于得到字节数组 可以用于网络传输
                新知识点
                        1.内存流对象   类名:MemoryStream    命名空间:System.IO
                        2.2进制格式化对象   类名:BinaryFormatter    命名空间:System.Runtime.Serialization.Formatters.Binary、  通过其中的序列化方法即可进行序列化生成字节数组
                        主要方法:序列化方法 Serialize

//测试用例
[System.Serializable]
public class Person
{
    public int age = 1;
    public string name = "Danny";
    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;
}
Person p = new Person();
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.Danny", bytes);
    //关闭内存流
    ms.Close();
}
        方法二:使用文件流进行存储

                主要用于存储到文件中

using (FileStream fs = new FileStream(Application.dataPath + "/Lesson5_2.Danny", FileMode.OpenOrCreate, FileAccess.Write))
{
    //2进制格式化程序
    BinaryFormatter bf = new BinaryFormatter();
    //序列化对象 生成2进制字节数组 写入到内存流当中
    bf.Serialize(fs, p);
    fs.Flush();
    fs.Close();
}

c#类对象的反序列化

        反序列化之 反序列化文件中数据

        主要类    FileStream文件流类    BinaryFormatter 2进制格式化类

        主要方法    Deserizlize   通过文件流打开指定的2进制数据文件

using (FileStream fs = File.Open(Application.dataPath + "/Lesson5_2.Danny", 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.Danny");
//申明内存流对象 一开始就把字节数组传输进去
using (MemoryStream ms = new MemoryStream(bytes))
{
    //申明一个 2进制格式化类
    BinaryFormatter bf = new BinaryFormatter();
    //反序列化
    Person p = bf.Deserialize(ms) as Person;

    ms.Close();
}

c#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.Danny", bytes);
}

//解密
byte[] bytes2 = File.ReadAllBytes(Application.dataPath + "/Lesson7.Danny");
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();
}

        优点

                效率高

                节约空间

                安全性高

        缺点

                可读性差

                无法从数据文件看懂数据或修改数据

        主要用处

                网络游戏

                        可以用于存储客户端数据

                        可以用于传输信息

                单机游戏

                        用于储存游戏相关数据

                        用于配置游戏数据

Unity添加菜单栏按钮

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

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

        特性名:MenuItem   命名空间:UnityEditor

  •         规则一:一定是静态方法
  •         规则二:我们这个菜单栏按钮 必须有至少一个子路径(斜杠) 不然会报错 它不支持只有一个菜单栏入口
  •         规则三:这个特性可以用在任意的类当中
[MenuItem("GameTool/Danny")]
private static void Danny()
{
    Debug.Log("测试测试");
    //刷新Project窗口内容
    //类名:AssetDatabase
    //命名空间:UnityEditor
    //方法:Refresh

    Directory.CreateDirectory(Application.dataPath + "/Danny");
    //刷新资源管理器
    AssetDatabase.Refresh();
    #endregion
}

        Editor文件夹可以放在项目的任何文件夹下,可以有多个,放在其中的内容,项目打包时不会被打包到项目中,一般编辑器相关代码都可以放在该文件夹中

Excel DLL包的导入

       Excel表本质上也是一堆数据,只不过它有自己的存储读取规则,如果我们想要通过代码读取它,那么必须知道它的存储规则

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

        Dll文件:库文件,你可以理解为它是许多代码的集合,将相关代码集合在库文件中可以方便迁移和使用,有了某个DLL文件,我们就可以使用其中已经写好的代码

        而Excel的DLL包就是官方已经把解析Excel表的相关类和方法写好了,方便用户直接使用

        导入官方提供的Excel相关DLL文件,将DLL文件放置在Editor文件夹

Excel数据读取

        打开Excel表

                主要知识点:

  •         FileStream读取文件流
  •         IExcelDataReader类,从流中读取Excel数据
  •         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表中单元格的信息

                主要知识点:

  •         FileStream读取文件流
  •         IExcelDataReader类,从流中读取Excel数据
  •         DataSet 数据集合类 将Excel数据转存进其中方便读取
  •         DataTable 数据表类 表示Excel文件中的一个表
  •         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();
    }
}

封装

        ExcelTool
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 + ".ExcelData", 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
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<PlayerInfoContainer, PlayerInfo>();
    }

    /// <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 + ".ExcelData", 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 + ".ExcelData", 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 + ".ExcelData") )
            return default(T);

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

        return obj;
    }
}

结尾

以上是本人在项目中及平时积累的知识归纳总结!!!

如果喜欢我的文章,欢迎关注、点赞、转发、评论,大家的支持是我最大的动力

如有疑问或BUG,请加入QQ群一起交流探讨!!!

技术交流QQ群:1011399921!!!


http://www.kler.cn/a/469378.html

相关文章:

  • Electron使用记录
  • Functions
  • Eureka原理
  • 2024年终总结及计划
  • 从configure.ac到构建环境:解析Mellanox OFED内核模块构建脚本
  • OpenKit 介绍
  • unity学习9:unity的Asset 导入和导出
  • Go语言的 的设计模式(Design Patterns)基础知识
  • 富芮坤FR800X系列之软件开发工具链(如IDE、编译器、调试器等)
  • 【大模型】7 天 AI 大模型学习
  • 『SQLite』表的创建、修改和删除
  • Centos中常见的几个问题及其解决办法
  • 【微服务】SpringBoot 国际化适配方案使用详解
  • 陪诊陪护助浴系统源码:JAVA养老护理助浴陪诊小程序医院陪护陪诊小程序APP源码
  • 安卓漏洞学习(十六):unicorn在逆向中的使用
  • CESS 的 2024:赋能 AI,塑造去中心化数据基础
  • 基于springboot的社区团购系统设计(源码+数据库+文档)
  • Ungoogled Chromium127 编译指南 MacOS篇(三)- 安装Xcode
  • 【深度学习|地学应用】深度学习在热融滑塌研究中的应用(二)
  • AI Infra
  • SAP SD学习笔记25 - 品目阶层(产品层次结构)、品揃Module(分类模块)
  • 与 Oracle Dataguard 相关的进程及作用分析
  • 【新年特辑】使用 React + TypeScript 开发新年祝福网页
  • 41.3 将重查询记录增量更新到consul和redis中
  • 常用LabVIEW算法及应用
  • 第08章 存储管理(二)