游戏运行时,我们的游戏数据都是存在内存中的,一旦退出游戏,内存将被释放,这时游戏数据并没有保留下来。因此,利用数据持久化的操作可以将游戏数据从内存存储到硬盘中,从而实现游戏数据的保存。

这篇文章将会介绍几种数据持久化的方案。

值得一提的是,独立开发游戏一般选择Json,优点是人类可读易调试;而大型联机游戏一般选择二进制存储,人类虽然不可读不易调试,但是它的解析速度最快也更安全


PlayerPrefs

PlayerPrefsUnity官方封装好的键值对本地存储工具

基本操作

存储

键:string
值:intfloatstring

PlayerPrefs的存储方式是用类名PlayerPrefs.Setxxx来实现,其中xxx中填入要存储的值类型,参数依次传入键名要存储的值。下面是用PlayerPrefs存储的几个例子:

  • 存储int型到内存中
1
PlayerPrefs.SetInt(“myAge”, 18);
  • 存储float型到内存中
1
PlayerPrefs.SetFloat(“myHeight”, 165.5f);
  • 存储string型到内存中
1
PlayerPrefs.SetString(“myName”, "大鱼飞九草");

当游戏结束时,Unity自动把数据存在硬盘中。如果游戏不是正常结束而是崩溃,数据是不会存到硬盘中的。因此,可以调用这个方法立马存储到硬盘中:

1
PlayerPrefs.Save();

另外,PlayerPrefs有一定的局限性,因为只存在三种类型的存储方式,因此,在存储其他数据时会降低精度。

注意:

  • 用同一键名存储的值会被覆盖

读取

读取的顺序是先从内存找,再到硬盘中找。

PlayerPrefs的读取方式是用类名PlayerPrefs.Getxxx来实现,其中xxx中填入要读取的值类型,参数传入键名。下面是用PlayerPrefs读取的例子:

  • 读取int类型
1
int age = PlayerPrefs.GetInt("myAge");

还有一种重载:

1
2
// 参数2:默认值
age = PlayerPrefs.GetInt("myAge", 100);

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

读取floatstring类型的也都相似。

注意:当里面没有值时,会返回一个默认值。int对应0float对应0string对应""

判断键是否存在

1
2
3
4
if(PlayerPrefs.HasKey("myName"))
{
// 内部逻辑
}

删除

  1. 删除指定键值对
1
PlayerPrefs.DeleteKey("myKey");
  1. 删除所有存储的信息
1
PlayerPrefs.DeleteAll();

存储位置

不同平台PlayerPrefs的存储位置不一样。

  • Windows
    • 位置:HKCU\SoftWare\[公司名称]\[产品名称]项下的注册表中
  • Android
    • 位置:/data/data/包名/shared_prefs/包名.xml
  • IOS
    • 位置:/Library/Preferences/[应用ID].plist

XML

XML是一种特殊格式的文件,用于传输和存储数据 。XML是一种树形结构根节点。

XML基本语法

  1. 创建XML文件:把后缀名改为.xml

  2. 注释:

  • 单行注释
1
<!-- 在其中书写注释相关内容 -->
  • 多行注释
1
2
3
4
<!--
***
***
-->
  1. 固定内容:一定要写在第一行
1
2
3
<!-- version代表版本 encoding代表编码格式 -->
<!-- 编码格式:ASCII码 与 UTF-8 就是两种不同的编码格式 -->
<?xml version = "1.0" encoding = "UTF-8"?>
  1. 基本语法

xml的基本语法是<元素标签> </元素标签>配对出现。可以理解为树形结构:必须要有且仅有一个根节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<PlayerInfo>
<name>meimei酱</name>
<age>19</age>
<sex>false</sex>
<ItemList>
<Item>
<id>1</id>
<num>10</num>
</Item>
<Item>
<id>2</id>
<num>20</num>
</Item>
</ItemList>
</PlayerInfo>

注意:特殊符号在xml中的写法

符号 xml中写作
< &lt
> &gt
& &amp
&apos
‘’ &quot

XML属性

区别属性和元素:

  • 元素:一个节点之中包裹的东西,即<>元素</>
  • 属性:写在节点内部的东西,即<Friend 属性>

它俩表示的意思一样,只是两种写法。

1
2
<!-- 属性必须用引号包裹 单引号双引号都可以 -->
<Friend name = "小明" age = '8'>我的朋友</Friend>

如果使用属性记录信息,不想使用元素记录,如下:

1
<Frind name = "小明" age = "8"/>

验证是否有错

复制到该网址验证:
xml - 菜鸟教程

C#读取存储Xml

  1. Xml文件存放的位置:

    • 只读不写:Resource / StreamingAssets 文件夹下
    • 动态存储:Application.persistentDataPath 路径下
  2. 读取方法:

    • XmlDocument(较方便且容易操作)
    • XmlTextReader
    • Linq

读取

读取Xml文件信息

1
2
3
4
5
6
7
8
9
10
11
12
using System.Xml; // 需要引用的命名空间

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

// 2. 直接根据xml字符串内容 来加载xml文件
// 存放在Resources文件夹下的xml文件加载处理(TestXml是一个在Resources里的xml文件)
TextAsset asset = Resources.Load<TextAsset>("TestXml");
xml.LoadXml(asset.text);

// 3. 通过xml文件的路径去进行加载(TestXml是一个在StreamAssets里的xml文件)
xml.Load(Application.streamingAssetsPath + "/TestXml.xml");

读取元素和属性信息

  1. 关键的两个类:
  • 节点信息类(单个) XmlNode
  • 节点列表信息类(多个)XmlNodeList
  1. 读取元素:
1
2
3
4
5
6
// 1. 获取xml当中的根节点 参数:固定为"Root"
XmlNode root = xml.SelectSingleNode("Root");
// 2. 在通过根节点 去获取下面的子节点 参数:要获取的节点的内容的字符串
XmlNode nodeName = root.SelectSingleNode("name");
// 3. 获取节点包裹的元素信息,直接.InnerText
nodeName.InnerText
  1. 读取属性:
1
2
3
4
5
6
7
// 1. 通过根节点 去获取下面的子节点
XmlNode nodeItem = root.SelectSingleNode("Item");
// 2. 通过这个节点得到里面的属性
// 方式1:通过[]得到 []里写的是键
nodeItem.Attributes["id"].Value;
// 方式2:通过 GetNameItem() 得到
nodeItem.Attributes.GetNameItem("id").Value

注意:如果要得到一个同名节点的信息,不要通过XmlNode,这样只能得到第一个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 得到信息
XmlNodeList friendLit = root.SlectNodes("Friend");

// 2. 遍历
// 方式一:迭代器遍历
foreach(XmlNode item in friendList)
{
print(item.SelectSingleNode("name").InnerText);
}
// 方式二:for循环
for(int i = 0; i < friendList.Count; i ++)
{
print(friendList[i].SelectSingleNode("name").InnerText);
}

存储

  1. 存储路径:Application.persistentDataPath
  2. 关键类:
  • XmlDocument 用于创建节点 存储文件
  • XmlDeclaration 用于添加版本信息
  • XmlElement 节点类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System.Xml;

// 0. 准备存储路径
string path = Application.persistentDataPath + "/PlayerInfo2.xml";


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


// 2. 添加固定版本信息
// 参数1:版本号
// 参数2:编码格式
// 参数3:只用传一个空字符串
XmlDeclaration xmlDec = xml.CreateXmlDeclaration("1.0", "UTF-8", "");
// 创建完成后 要添加进 文本对象中
xml.AppendChild(xmlDec);


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


// 4.添加子节点

// 4.1 为根节点添加子节点
XmlElement name = xml.CreateElement("name");
name.InnerText = "小明";
root.AppendChild(name);

// 4.2 为子节点添加子节点
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);

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


// 5. 保存
xml.Save(path);

修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1. 先判断是否存在文件
if( File.Exists(path) ) // File是自带的类
{
// 2. 加载后 直接添加节点 移除节点即可
XmlDocument newXml = new XmlDocument();
newXml.Load(path);

// 修改就是在原有的基础上 移除或者添加

// 移除
// 写法1:
XmlNode node = newXml.SelectStingleNode("Root").SelectStingleNode("name");
// 写法2:
XmlNode node = newXml.SelectStingleNode("Root/name");
// 移除子节点
XmlNode root2 = newXml.SelectSingleNode("Root");
root2.RemoveChild(node);

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

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

XML序列化与反序列化

序列化

序列化是把内存中的数据存储到硬盘的过程

  1. 几个关键类:
  • XmlSerializer 用于序列化对象为xml
  • StreamWriter 用于存储文件
  • using 用于方便流对象释放和销毁
  1. 序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.IO; // 使用 StreamWriter 时要引用的命名空间
using System.Xml.Serialization;

public class Lesson1Test
{
// 这是我想要保存的类对象
}

public class Main : MonoBehaviour
{
void Start()
{
// 1. 准备一个想要保存的类
Lesson1Test lt = new Lesson1Test();


// 2. 序列化
// 2.1 确定存储路径
string path = Application.persistentDataPath + "/Test.xml";

// 2.2 写入文件
// 括号内的代码:写入一个文件流,如果有该文件直接打开并修改,如果没有直接新建
// using 的新用法:括号当中包裹的声明的对象 会在 大括号语句块结束后自动释放
using ( StreamWriter stream = new StreamWriter(path) )
{
// 声明一个序列化翻译器
XmlSerializer s = new XmlSerializer(typeof(Lesson1Test));

// 通过Serialize方法进行序列化翻译
// 参数1:文件流对象
// 参数2:想要被翻译的对象
// 注意:翻译机器的类型要和传入的对象是一致的
s.Serialize(stream, lt);
}
}
}

注意:序列化只支持public,不支持字典

  1. 以属性方式存储:
    加上一个特性:[XmlAttribute()] ,括号里面可以传想要的属性名(字符串形式)

  2. 给元素改名字:

  • 普通元素:[XmlElement()]
  • List:[XmlArray()]
  • List里面的元素:[XmlArrayItem()]

反序列化

反序列化是把硬盘数据还原为内存上数据的过程

  1. 反序列化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.IO;
using System.Xml.Serialization;

// 0.写出要反序列化对象所在路径
string path = Application.persistentDataPath + "/Lesson1Test.xml";
// 1. 判断文件是否存在
if( File.Exists(path) )
{
// 2. 反序列化
// 2.1 读取文件
using (StreamReader reader = new StreamReader(path))
{
// 声明一个反序列化翻译器
XmlSerializer s = new XmlSerializer(typeof(Lesson1Test));

// 通过DeSerialize方法进行反序列化翻译
// 参数:想要反序列化的对象
Lesson1Test lt = s.DeSerialize(reader) as Lesson1Test;
}
}

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

Ixmlserializable 接口

  1. 定义:IxmlserializableXmlSerializer提供的可拓展内容。他能让一些不能被序列化和反序列化的特殊类能被处理。

  2. 自定义类实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class Test1 : Ixmlserializable
{
public int test1;

// 下面是实现这个接口后要实现的方法

public XmlSchema GetSchema()
{
return null;
}

public void ReadXml(XmlReader reader)
{
// 反序列化时自动调用
// 1. 读属性
this.test1 = int.Parse(reader["test1"]);
// 2. 读节点
// 方式1:
reader.Read(); // 这时是读到根节点
reader.Read(); // 这时是读到test1前面包裹节点
this.test1 = int.Parse(reaser.Value()); // 得到当前内容的值
reader.Read(); // 这时是读到test1尾部包裹节点
// 方式2:
while( reader.Read() )
{
if( reader.NodeType == XmlNodeType.Element )
{
switch(reader.Name)
{
case "test1":
reader.Read();
this.test1 = int.Parse(reader.Value);
break;
}
}
}
// 3. 读包裹元素节点
XmlSerializer s = new XmlSerializer(typeof(int));
// 跳过根节点
reader.Read();
reader.ReadStartElement("test1");
test1 = (int)s.Deserialize(reader);
reader.ReadEndElement();
}

public void WriteXml(XmlWriter writer)
{
// 序列化时自动调用
// 1. 写属性
writer.WriteAttributeString("test1", this.test1.toString());
// 2. 写节点
writer.WriteElementString("test1", this.test1.toString());
// 3. 写包裹节点
Xmlserializer s = new Xmlserializer(typeof(int));
writer.WriteStartElement("test1");
s.Serialize(writer, test1);
writer.WriteEndElement();
}
}

public class Main: MonoBehaviour
{
void Start()
{
Test1 t = new Test1();
// 省略序列化和反序列化内容
}
}

让Dictionary支持序列化

思路:继承Dictionary,然后实现Ixmlserializable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class SerializerDictionary<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.EndElemenrt)
{
TKey key = (TKey)keySer.Deserialize(reader);
TValue value = (TValue)keySer.Deserialize(reader);
this.Add(key, value);
}

reader.Read();
}

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);
}
}
}

public class Main: MonoBehaviour
{
void Start()
{
TestLesson4 tl4 = new TestLesso4()
tl4.dic = new SerializerDictionary<int, string>();
// 省略序列化和反序列化
}
}

Json

Json是一种特殊的文件格式,主要用于传输数据、本地数据存储读取
xml的区别是Json配置更简单、某些情况下读写更快速。

Json基本语法

  1. 创建Json文件:把后缀名改为.json

  2. 注释:

  • 单行注释
1
// 注释内容
  • 多行注释
1
2
3
4
/*
***
***
*/
  1. 基本语法:

Json格式是一种键值对结构

符号 含义
{} 对象
[] 数组
: 键值对 对应关系
, 数据分割
"" 键名、字符串

键值对表示:"键名" : 值内容,其中,值可以是数字、字符串、bool值、数组、对象、null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 大括号 包裹的就代表一个对象
{
// 冒号 代表 键值对的对应关系
// 逗号 代表 成员变量的间隔符
"name":"meimei酱",
"age":19,
"sex":false,
"height":165.5,
// 中括号 代表 数组
"ids":[1,2,3,4],
"students":[{"name":"玛丽亚", "age":20, "sex":true},
{"name":"小明", "age":18, "sex":false}],
"home":{"address":"成都","street":"春熙路"},
"son":null
}

在线转Json网站:bejson

C#读取存储Json

存读字符串

  • 存储字符串到指定路径文件中
1
2
3
4
using System.IO;
// 参数1:要存储的路径
// 参数2:要存储的字符串内容
File.WriteAllText(Application.persistentDataPath + "/Test.json", "小草存储的json文件");

注意:直接写文件名和后缀,不能自己加一个文件夹

  • 在指定的路径文件中读取字符串
1
2
3
using System.IO;
// 参数:要读取的路径
string str = File.ReadAllText(Application.persistentDataPath + "/Test.json");

JsonUtility

JsonutilityUnity自带的用于解析Json公共类。它可以将内存中的对象序列化为Json格式的字符串,也可以将Json字符串反序列化为类对象。

  • 序列化:内存 -> 硬盘

JsonUtility提供了现成的方法,可以把类对象序列化为 json字符串,下面是一个实例:

1
2
3
4
5
using System.IO;
Person p = new Person();
// 存储类中成员略
string jsonStr = JsonUtility.ToJson(p);
File.WriteAllText(Application.persistentDataPath + "/Person.json", jsonStr);

注意:

  1. float序列化时看起来会有一些误差,不过读出时不影响。
  2. 自定义类需要加上序列化特性[System.Serializable]
  3. 想要序列化私有变量,需要加上特性[SerializeField]
  4. JsonUtility不支持字典
  5. 存储null时会有一个默认值
  • 反序列化:硬盘 -> 内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System.IO;
// 读取文件中的json字符串
jsonStr = File.ReadAllText(Application.persistentDataPath + "/Person.json");

// 使用json字符串内容 转换为类对象

// 方法1
// 参数1:要读取的json文件
// 参数2:json文件中对应的类的类型
Person p = JsonUtility.FromJson(jsonStr, typeof(Person)) as Person;

// 方法2
// 参数:要读取的json文件
Person p1 = JsonUtility.FromJson<Person>(jsonStr);

注意:如果json中数据少了,不会报错

另外,JsonUtility无法直接读取数据集合,需要用类对象包裹一层,编码格式必须是UTF-8

LitJson

LitJson是一个第三方库,用于处理Json的序列化和反序列化。它体积小、速度快、易于使用,使用时只需要把他的代码拷贝到工程中即可。建议使用LitJson

  • 获取:直接浏览器搜索LitJson -> 官网中访问GitHUb -> 右侧release下载最新版本 -> 拷贝src/Litjson放进sript文件夹(可以删除除了cs文件外的其他文件)

  • 序列化

1
2
3
4
5
6
7
using LitJson;
using System.IO;

Animal a = new Animal();
// 存储类中成员略
string jsonStr = JsonMapper.ToJson(a);
File.WriteAllText(Application.persistentDataPath + "/Animal.json", jsonStr);

注意:

  1. 通过LitJson存储的字符串表现上有点不一样,不过反序列化回来不影响
  2. 可以存储字典,字典的键建议用字符串
  3. 不能序列化私有变量
  4. 需要引用LitJson命名空间
  5. 能够准确的存储null
  • 反序列化
1
2
3
4
5
6
7
8
9
10
11
using LitJson;
using System.IO;

jsonStr = File.ReadAllText(Application.persistentDataPath + "/Animal.json");
// JsonData是LitJson提供的类对象,可以用键值对的形式去访问其中的内容
jsonData data = jsonMapper.ToObject(jsonStr);
// 方法一:索引器里面填类对象的成员变量名
print(data["name"]);
// 方法二:泛型方法
Animal a = JsonMapper.ToObject<Animal>(jsonStr);
print(a.name);

注意:

  1. 虽然支持字典类型,但是键一定是字符串类型才能使用。
  2. 类结构需要无参构造函数,否则反序列化的时候会报错。

JsonUtility不同的是,LitJson可以直接读取数据集合。相同点是文本编码格式是UTF-8

二进制

(未完待续)