什么是数据持久化
数据持久化就是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称 ;通俗来讲就是将数据存到硬盘,硬盘中数据读到游戏中,也就是传统意义上的存盘
数据持久化之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中一定是使用各平台都可读可写可找到的路径
- Resources 可读 不可写 打包后找不到 ×
- Application.streamingAssetsPath 可读 PC端可写 找得到 ×
- Application.dataPath 打包后找不到 ×
- 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;
总结
序列化流程
- 有一个想要保存的类对象
- 使用XmlSerializer 序列化该对象
- 通过StreamWriter 配合 using将数据存储 写入文件
注意:
- 只能序列化公共成员
- 不支持字典序列化
- 可以通过特性修改节点信息 或者设置属性信息
- 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是什么
JSON: JavaScript 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 不然无法加载
总结
- 必备知识点 —— File存读字符串的方法 ReadAllText和WriteAllText
- JsonUtlity提供的序列化反序列化方法 ToJson 和 FromJson
- 自定义类需要加上序列化特性 [System.Serializable]
- 私有保护成员 需要加上 [SerializeField]
- JsonUtlity不支持字典
- JsonUtlity不能直接将数据反序列化为数据集合
- 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!!!