数据持久化(Json,二进制,PlayerPrefs)

数据持久化

数据持久化概述

定义:内存中的数据模型与存储模型的相互转化。
类比:将游戏数据存储到硬盘,从硬盘中读取游戏数据。

1.数据持久化——JSON

1.Json简介

定义:JavaScript对象简谱,一种全国通用的轻量级的数据交换格式。主要在网络通信中传输数据,或本地数据的存储与读取。

特点:纯文本,层级结构,都具有描述性,键值对存储。

基本语法演示

//大括号包裹的代表一个类
{
    
    
	"name" : "Shawn",
	"age" : 18,
	"sex" : true,
	"height" : 1.8,
	//中括号代表数组
	"Students" : [{
    
    "name" : "小红","sex" : false},
				  {
    
    "name" : "小名","sex" : false}],
	"home":{
    
    "address" : "成都","street" : "春熙路"},
	"son" : null
}

Excel表转Json
访问网址:https://www.bejson.com/json/col2json


2.JsonUtility相关知识点

  • 1.定义:Unity自带的用于解析Json的公共类。
  • 2.作用:对象序列化为Json,Json反序列化为对象。
  • 3.用法:
    序列化
//存储字符串到指定路径文件中
//Param1:路径(可读写文件夹)
//Param2:文本内容
File.WriteAllText(Application.persistentDataPath + ”Test.json“,"hhhhhh");
//从指定路径中读取字符串
string str = File.ReadAllText(Application.persistentDataPath + "Test.json");

//将类对象序列化为Json字符串
Student s = new Student();
string str = JsonUtility.ToJson(s);

反序列化

//读取文件中的Json字符串并将其转换为类对象
类名 变量 = JsonUtility.FromJson<类名>(jsonStr);
  • 4.注意:
    – 1.自定义类序列化时需要加上特性 [System.Serializable]
    – 2.想要序列化私有的或者保护的变量可以加上特性 [SerializeField]
    – 3.JU不支持字典存储。
    – 4.JU存储null对象不会是null,而是变量类型默认值。
    – 5.JU无法直接读取(反序列化)数据集合(需要包装一层类)
    – 6.文本编辑器的编码格式必须是UTF-8,否则会报错。

3.LitJson相关知识

定义:第三方库,由C#编写,体积小速度快易于使用。
使用步骤

  • 1.进入其Github官方文档。
  • 2.获取代码引入项目即可。(最终拷贝的是LitJson文件夹核心代码模块,无需其他的文件)
  • 3.用法
    序列化
Student t = new Student();
//序列化(引入命名空间)
string jsonStr = JsonMapper.ToJson(t);
File.WriteAllText(Application.persistentDataPath + ”Test.json“,jsonStr);

反序列化

jsonStr = File.WriteAllText(Application.persistentDataPath + ”Test.json“);
//反序列化
JsonData data = JsonMapper.ToObject(jsonStr);

//需要用索引器去访问对象内容
data["name"];

//也可以通过泛型直接返回对象
Student s = JsonMapper.ToObject<Student>(jsonStr);

//LitJson可以直接读取数组,字典类型
Student[] stus = JsonMapper.ToObject<Student[]>(jsonStr);
List<Student> stus1 = JsonMapper.ToObject<List<Student>>();
Dictionary<string,int> dic = JsonMapper.ToObject<Dictionary<string,int>>(jsonStr);
  • 4.注意
    – 1.相对JsonUtility来说不需要加特性。
    – 2.不能序列号私有变量。
    – 3.支持字典类型,字典的键建议都是字符串。
    – 4.需要引用LitJson 命名空间。
    – 5.可以准确地保存null类型。
    – 6.反序列化时要保证指定对象存在无参构造函数。
    – 7.文本编码必须是UTF-8。

JsonUtility与LitJson对比
相同点:

  • 1.都是用于Json的序列化反序列化
  • 2.Json文档编辑器必须都是UTF-8
  • 3.都是通过静态类(JsonUtility和JsonMapper)进行方法调用

不同点:

  • 1.JU是Unity自带,LitJson是第三方,需要引入。
  • 2.JU使用自定义类需要加[System.Serializable]特性,LitJson不需要。
  • 3.JU支持私有变量(加特性[SerializField]),LitJson不需要。
  • 4.JU不支持字典,LitJson支持(key只能是字符串)。
  • 5.JU不能直接将数据反序列化为数据集合(字典),LitJson可以。
  • 6.JU堆自定义类不要求无参构造,LitJson要求必须存在无参构造。
  • 7.JU存储空对象时会存储默认值而不是null,LitJson会存null。

4.JsonMgr管理器的书写

/// <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.数据持久化——二进制

1.二进制简介

写在前面:C#变量的本质即为二进制。二进制文件读写的本质即为 将各类型变量转换为字节数组,将其直接存储到文件中,这样的作法不仅可以节约存储空间,提升效率,也可以提高安全性。

各类型数据与二进制相互转化

//将各类型转字节
byte[] bytes = BitConverter.GetBytes(256);
print(bytes[0]);//0
print(bytes[1]);//1(第二位的1代表二进制八位,即十进制的256)

//将字节数组转换为其他类型
//param2:从第几个索引开始
int i = BitConverter.ToInt32(bytes,0)

//以指定编码格式转换字节数组
byte[] bytes = Encoding.UTF8.GetBytes("我是猪");
//以指定编码格式转换其他类型
string s = Encoding.UTF8.GetString(bytes);

2.文件操作

API简介:

//判断文件是否存在
//Param:文件路径
bool isExist = File.Exists(Application.dataPath + "/data.json");

//创建文件
FileStream fs = File.Create(Application.dataPath + "/data.json");

//写入文件
byte[] bytes = BitConverter.GetBytes(999);
File.WriteAllBytes(Application.dataPath + "/data.json",bytes);

//写入字符串数组文件(自动空行)
string[] strs = new string[]("123","我是猪");
File.WriteAllLines(Application.dataPath + "/data.json",strs);

//将指定字符串存入指定路径(支持转义字符)
File.WriteAllText(Application.dataPath + "/data.json","哈哈哈哈\n嘿嘿嘿");

//读取文件(类比上述API,将Write改写为Read)

//删除文件
File.Delete(Application.dataPath + "/data.json");

//复制文件
//Param1:源文件
//Param2:目标文件
//Param3:如果已存在是否要覆盖原文件
File.Copy(Application.dataPath + "/data.json",Application.dataPath + "/data.guan",true);

//文件替换
//Param3:备份文件路径
File.Replace(Application.dataPath + "/data.json",Application.dataPath + "/data.guan",Application.dataPath + "/data备份.txt");

//打开文件并写入或读取
//Param2:文件模式,如果没有自动创建
//Param3:访问模式,只读只写
FileStream fs = File.Open(Application.dataPath + "/data.json",FileMode.OpenOrCreate,FileAccess.ReadWrite);

3.流

//打开或创建文件
FileStream fs = new FileStream(Application.dataPath + "/data.json",FileMode.CreateOrCreate,FileAccess.ReadWrite);

//文本字节长度
print(fs.Length);

//将字节写入文件,写入后,一定要执行一次
fs.Flush();
//关闭流,文件读写完毕执行
fs.Close();
//缓存资源的销毁回收
fs.Dispose();


//写入字节
FileStream fs = new FileStream(Application.persistentDataPath + "/Lesson.txt",FileMode.OpenOrCreate,FileAccess.Write);
byte[] test = new byte[1024];
byte[] bytes = BitConverter.GetBytes(999);
//Param2:开始字节索引
//Param3:写入多少字节
fs.Write(bytes,0,bytes.Length);

//写入字符串时,先写入长度
bytes = Encoding.UTF8.GetBytes("猪猪猪猪");
fs.Write(BitConverter.GetBytes(bytes.Length),0,4);
fs.Write(bytes,0,bytes.Length);
fs.Flush();
fs.Dispose();

//====================一个一个读取=======================
//读取字节
FileStream fs2 = File.Open(Application.persistentDataPath + "/Lesson.txt",FileMode.Open,FileAccess.Read);

//挨个读取字节数组
byte[] bytes2 = new byte[4];
int index = fs2.Read(bytes2,0,bytes.Length);
int i = BitConverter.ToInt32(bytes2,0);
print("取出来的第一个整数" + i);
print("索引下标" + index);

//读取字符串
index = fs2.Read(bytes2,0,4);
int length = BitConverter.ToInt32(bytes2,0);
//重新声明一个字节数组
bytes2 = new byte[length];
index = fs.Read(bytes2,0,length);
print(Encoding.UTF8.GetString(bytes2));
fs.Dispose();

//====================一次性读取========================
FileStream fs3 = File.Open(Application.persistentDataPath + "/Lesson.txt",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 length3 = BitConverter.ToInt32(bytes3,4);
//得到字符串
print(Encoding.UTF8.GetString(bytes3,8,length3));

//===============通过using改进IO===================
using(FileStream fs3 = File.Open(Application.persistentDataPath + "/Lesson.txt",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 length3 = BitConverter.ToInt32(bytes3,4);
	//得到字符串
	print(Encoding.UTF8.GetString(bytes3,8,length3));

}

4.文件夹

//判断文件夹是否存在
Directory.Exists(Application.dataPath + "/test");

//创建文件夹并返回
DirectoryInfo dirInfo = Directory.CreateDirectory(Application.dataPath + "/test");

//删除文件夹
//Param2:是否删除非空目录
Directory.Delete(Application.dataPath + "/test",true);

//得到指定路径下的所有文件夹名
string[] strs = Directory.GetDirectories(Application.dataPath);

//得到指定路径下所有文件名
strs = Directory.GetFiles(Application.dataPath);

//移动文件夹
Directory.Move(Application.dataPath + "/test",Application.dataPath + "/123");

//===============DirectoryInfo类==================

//目录信息类DirectoryInfo
//文件夹的全路径
dirInfo.FullName;
//文件夹名
dirInfo.Name;
//上级文件夹信息
dirInfo = Directory.GetParent(Application.dataPath + "/test")

//查找子文件夹以及文件信息
DirectoryInfo[] dirInfos = dirInfo.GetDirectories();
FileInfo[] fileInfos = dirInfo.GetFiles();

5.二进制序列化工具

补充知识1:Unity编辑器添加选项入口

//必须为静态方法,通过类名点方式调用
//必须存在两个或以上斜杠
//这个类可以用在任何类中
[MenuItem("GameTool/Test/GenerateExcelInfo")]
private static void Test(){
    
    
	Debug.Log("测试");
}

补充知识2:刷新Project窗口内容

AssetDatabase.Refresh();

补充知识3:Editor文件夹
项目打包时,该文件夹下的文件无法打包。通常可以把编辑器相关代码放置其中。

导入Excel的Dll文件
打开Excel表

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

获取Excel表中单元格的信息

[MenuItem("GameTool/读取具体信息")]
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];
			//得到某一个单元格的信息
			string ans = row[1].ToString();
			
			//遍历所有行的数据
			for (int j = 0;j < table.Rows.Count;j++){
    
    
				row = table.Rows[j];
				//得到每一个单元格的内容信息
				for (int k = 0;k < table.Columns.Count;k++){
    
    
					print(row[k].ToString());
				}
			}
		}
	}
}

需求分析:通过一张Excel表格生成以下信息:

  • 1.数据结构类
  • 2.装数据的容器
  • 3.二进制存储文件

写一个工具读取Excel中的信息

(放入Editor下,无需打包)

public class ExcelTool 
{
    
    
	//文件目录中Excel的路径
	public static string EXCEL_PATH = Application.dataPath + "/ArtRes/Excel/";

	//生成的数据结构类的路径
	public static string DATA_CLASS_PATH = Application.dataPath + "/Scripts/ExcelData/DataClass/";

	//生成的容器类的路径
	public static string DATA_CONTAINER_PATH = Application.dataPath + "/Scripts/ExcelData/DataContainer/";

	//生成的二进制文件路径
	public static string DATA_BINARY_PATH = Application.streamingAssetsPath + "/Binary/";

	//在编辑器中添加按钮
	[MenuItem("GameTool/GenerateExcel")]
	private static void GenerateExcelInfo()
	{
    
    
		//得到指定路径下所有文件信息(Excel表)
		FileInfo[] infos = Directory.CreateDirectory(EXCEL_PATH).GetFiles();;

		//数据表容器
		DataTableCollection tableCollection;
		
		foreach(FileInfo info in infos)
		{
    
    
			//处理excel文件
			if (info.Extension != ".xlsx" && info.Extension != ".xls")
			continue;

			//使用流的相关知识获取表内数据
			using(FileStream fs = info.Open(FileMode.Open,FileAccess.Read))
			{
    
    
				IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
				tableCollection = excelReader.AsDataSet().Tables;
				fs.Close();
			}
			//得到每一张表
			foreach(DataTable table in tableCollection)
			{
    
    
				//生成数据结构类
				GenerateExcelDataClass(table);
				//生成容器类
				GenerateExcelContainer(table);
	 			//生成二进制数据
	 			GenerateExcelBinary(table);
			}
		}
	}
}

生成数据结构类的逻辑方法

private static void GenerateExcelDataClass(DataTable table)
{
    
    
	//字段名字
	DataRow rowName = GetRowInfo(table,0);
	//字段类型
	DataRow rowType = GetRowInfo(table,1);

	if (!Directory.Exists(DATA_CLASS_PATH))
		Directory.CreateDirectory(DATA_CLASS_PATH);

	//字符串拼接生成代码
	string code = "public class " + table.TableName + "\n{\n";

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

	code += "}";

	File.WriteAllText(DATA_CLASS_PATH + table.TableName + ".cs", code);

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

//获得指定行所在行信息
//获得变量名所在行信息
private static DataRow GetRowInfo(DataTable table,int row)
{
    
    
	return table.Rows[row];
}

生成容器(字典)类

private static void GenerateExcelContainer(DataTable table)
{
    
    
	int keyIndex = GetKeyIndex(table);
	DataRow rowType = GetRowInfo(table,1);
	//没有路径则创建路径
	if (!Directory.Exists(DATA_CONTAINER_PATH))
		Directory.CreateDirectory(DATA_CONTAINER_PATH);

	//字符串拼接生成类
	string code = "using System.Collections.Generic;\n";
	code += "public class " + table.TableName + "Container" + "\n{\n";
	code += "	public Dictionary<" + rowType[keyIndex].ToString() + ", " + table.TableName + "> dataDic = new Dictionary<" + rowType[keyIndex].ToString() + ", " + table.TableName + ">();\n";
	code += "}";

	File.WriteAllText(Data_CONTAINER_PATH + table.TableName + "Container.cs", code);

	AssetDatabase.Refresh();
}

//找到主键所在的索引
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 default(int);
}

生成Excel二进制数据

(放在StreamingAssets包中,打包出去也要读取数据(只读))

private static void GenerateExcelBinary(DataTable table)
{
    
    
	//判断路径下是否存在文件夹
	if (!Directory.Exists(DATA_BINARY_PATH))
		Directory.CreateDirectory(DATA_BINARY_PATH);

	//创建一个二进制文件进行写入
	using(FileStream fs = new FileStream(DATA_BINARY_PATH + table.TableName + ".shawn",FileMode.OpenOrCreate,FileAccess.Write))
	{
    
    
		//1.优先存储我们要写多少行
		fs.Write(BitConverter.GetBytes(table.Rows.Count - 4),0,4);
		//2.存储主键的变量名(id)
		string keyName = GetRowInfo(table,0)[GetKeyIndex(table)].ToString();
		byte[] bytes = Encoding.UTF8.GetBytes(keyName);
		//存储字符串字节数组的长度
		fs.Write(BitConverter.GetBytes(bytes.Length),0,4);
		//存储字符串字节数组
		fs.Write(bytes,0,bytes.Length);
		
		//数据开始索引可定义为全局变量,后续可直接修改变量
		DataRow row;		
		//得到类型
		DataRow rowType = GetRowInfo(table,1);
		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(int.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();
}

使用二进制数据读取使用

3.数据持久化——PlayerPrefs

3.1 概述

概念:PlayerPrefs是可以用于读取玩家数据的公共类。

3.2 基础语法

存储相关

//存储三种数据类型
PlayerPrefs.SetInt("Age",18);
PlayerPrefs.SetFloat("Height",177.5f);
PlayerPrefs.SetString("Name","Shawn");

//调用Save会把数据存入硬盘
PlayerPrefs.Save();

读取相关

//运行时,不需要Save也能读取到
string name = PlayerPrefs.GetString("Name");
//如果找不到对应值,函数会返回第二个参数
int age = PlayerPrefs.GetInt("age",100);

删除相关

//删除指定键
PlayerPrefs.DeleteKey("age");
//删除所有信息
PlayerPrefs.DeleteAll();

3.3 PlayerPrefs存储位置

Windows

存储路径:HKCU\Software\[公司名称]\[产品名称] 下的注册表中
打开方式:

  • 1.运行 regedit
  • 2.进入HKEY_CURRENT_USER->SOFTWARE
  • 3.进入Unity->UnityEditor->公司名称->产品名称

Android
存储路径:/data/data/包名/shared_prefs/pkg-name.xml

IOS
存储路径:/Libraray/Preferences/[应用ID].plist

猜你喜欢

转载自blog.csdn.net/qq_55071342/article/details/127307129