游戏运行时,我们的游戏数据都是存在内存中的,一旦退出游戏,内存将被释放,这时游戏数据并没有保留下来。因此,利用数据持久化的操作可以将游戏数据从内存存储到硬盘中,从而实现游戏数据的保存。
这篇文章将会介绍几种数据持久化的方案。
值得一提的是,独立开发游戏一般选择Json,优点是人类可读,易调试;而大型联机游戏一般选择二进制存储,人类虽然不可读不易调试,但是它的解析速度最快,也更安全。
PlayerPrefs
PlayerPrefs是Unity官方封装好的键值对本地存储工具类。
基本操作
存储
键:string
值:int、float、string
PlayerPrefs的存储方式是用类名PlayerPrefs.Setxxx来实现,其中xxx中填入要存储的值类型,参数依次传入键名、要存储的值。下面是用PlayerPrefs存储的几个例子:
1
| PlayerPrefs.SetInt(“myAge”, 18);
|
1
| PlayerPrefs.SetFloat(“myHeight”, 165.5f);
|
1
| PlayerPrefs.SetString(“myName”, "大鱼飞九草");
|
当游戏结束时,Unity会自动把数据存在硬盘中。如果游戏不是正常结束而是崩溃,数据是不会存到硬盘中的。因此,可以调用这个方法立马存储到硬盘中:
另外,PlayerPrefs有一定的局限性,因为只存在三种类型的存储方式,因此,在存储其他数据时会降低精度。
注意:
读取
读取的顺序是先从内存找,再到硬盘中找。
PlayerPrefs的读取方式是用类名PlayerPrefs.Getxxx来实现,其中xxx中填入要读取的值类型,参数传入键名。下面是用PlayerPrefs读取的例子:
1
| int age = PlayerPrefs.GetInt("myAge");
|
还有一种重载:
1 2
| age = PlayerPrefs.GetInt("myAge", 100);
|
第二个参数默认值对于我们的作用:在得到没有的数据的时候,可以用它来进行基础数据的初始化。
读取float、string类型的也都相似。
注意:当里面没有值时,会返回一个默认值。int对应0,float对应0,string对应""。
判断键是否存在
1 2 3 4
| if(PlayerPrefs.HasKey("myName")) { }
|
删除
- 删除指定键值对
1
| PlayerPrefs.DeleteKey("myKey");
|
- 删除所有存储的信息
1
| PlayerPrefs.DeleteAll();
|
存储位置
不同平台PlayerPrefs的存储位置不一样。
Windows
- 位置:
HKCU\SoftWare\[公司名称]\[产品名称]项下的注册表中
Android
- 位置:
/data/data/包名/shared_prefs/包名.xml
IOS
- 位置:
/Library/Preferences/[应用ID].plist
XML
XML是一种特殊格式的文件,用于传输和存储数据 。XML是一种树形结构根节点。
XML基本语法
创建XML文件:把后缀名改为.xml
注释:
- 固定内容:一定要写在第一行
1 2 3
|
<?xml version = "1.0" encoding = "UTF-8"?>
|
- 基本语法
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中写作 |
| < |
< |
| > |
> |
| & |
& |
| ‘ |
&apos |
| ‘’ |
" |
XML属性
区别属性和元素:
- 元素:一个节点之中包裹的东西,即
<>元素</>
- 属性:写在节点内部的东西,即
<Friend 属性>
它俩表示的意思一样,只是两种写法。
1 2
| <Friend name = "小明" age = '8'>我的朋友</Friend>
|
如果使用属性记录信息,不想使用元素记录,如下:
1
| <Frind name = "小明" age = "8"/>
|
验证是否有错
复制到该网址验证:
xml - 菜鸟教程
C#读取存储Xml
Xml文件存放的位置:
- 只读不写:
Resource / StreamingAssets 文件夹下
- 动态存储:
Application.persistentDataPath 路径下
读取方法:
XmlDocument(较方便且容易操作)
XmlTextReader
Linq
读取
读取Xml文件信息
1 2 3 4 5 6 7 8 9 10 11 12
| using System.Xml;
XmlDocument xml = new XmlDocument();
TextAsset asset = Resources.Load<TextAsset>("TestXml"); xml.LoadXml(asset.text);
xml.Load(Application.streamingAssetsPath + "/TestXml.xml");
|
读取元素和属性信息
- 关键的两个类:
- 节点信息类(单个)
XmlNode
- 节点列表信息类(多个)
XmlNodeList
- 读取元素:
1 2 3 4 5 6
| XmlNode root = xml.SelectSingleNode("Root");
XmlNode nodeName = root.SelectSingleNode("name");
nodeName.InnerText
|
- 读取属性:
1 2 3 4 5 6 7
| XmlNode nodeItem = root.SelectSingleNode("Item");
nodeItem.Attributes["id"].Value;
nodeItem.Attributes.GetNameItem("id").Value
|
注意:如果要得到一个同名节点的信息,不要通过XmlNode,这样只能得到第一个节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| XmlNodeList friendLit = root.SlectNodes("Friend");
foreach(XmlNode item in friendList) { print(item.SelectSingleNode("name").InnerText); }
for(int i = 0; i < friendList.Count; i ++) { print(friendList[i].SelectSingleNode("name").InnerText); }
|
存储
- 存储路径:
Application.persistentDataPath
- 关键类:
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;
string path = Application.persistentDataPath + "/PlayerInfo2.xml";
XmlDocument xml = new XmlDocument();
XmlDeclaration xmlDec = xml.CreateXmlDeclaration("1.0", "UTF-8", "");
xml.AppendChild(xmlDec);
XmlElement root = xml.CreateElement("Root"); xml.AppendChild(root);
XmlElement name = xml.CreateElement("name"); name.InnerText = "小明"; root.AppendChild(name);
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()); itemList.AppendChild(childNode); } root.AppendChild(itemList);
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
| if( File.Exists(path) ) { XmlDocument newXml = new XmlDocument(); newXml.Load(path); XmlNode node = newXml.SelectStingleNode("Root").SelectStingleNode("name"); 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序列化与反序列化
序列化
序列化是把内存中的数据存储到硬盘的过程
- 几个关键类:
XmlSerializer 用于序列化对象为xml
StreamWriter 用于存储文件
using 用于方便流对象释放和销毁
- 序列化
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; using System.Xml.Serialization;
public class Lesson1Test { }
public class Main : MonoBehaviour { void Start() { Lesson1Test lt = new Lesson1Test(); string path = Application.persistentDataPath + "/Test.xml"; using ( StreamWriter stream = new StreamWriter(path) ) { XmlSerializer s = new XmlSerializer(typeof(Lesson1Test)); s.Serialize(stream, lt); } } }
|
注意:序列化只支持public,不支持字典
以属性方式存储:
加上一个特性:[XmlAttribute()] ,括号里面可以传想要的属性名(字符串形式)
给元素改名字:
- 普通元素:
[XmlElement()]
- List:
[XmlArray()]
- List里面的元素:
[XmlArrayItem()]
反序列化
反序列化是把硬盘数据还原为内存上数据的过程
- 反序列化:
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;
string path = Application.persistentDataPath + "/Lesson1Test.xml";
if( File.Exists(path) ) { using (StreamReader reader = new StreamReader(path)) { XmlSerializer s = new XmlSerializer(typeof(Lesson1Test)); Lesson1Test lt = s.DeSerialize(reader) as Lesson1Test; } }
|
注意:List对象如果有默认值,反序列化时不会清空,会在后面添加
Ixmlserializable 接口
定义:Ixmlserializable是XmlSerializer提供的可拓展内容。他能让一些不能被序列化和反序列化的特殊类能被处理。
自定义类实践:
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) { this.test1 = int.Parse(reader["test1"]); reader.Read(); reader.Read(); this.test1 = int.Parse(reaser.Value()); reader.Read(); while( reader.Read() ) { if( reader.NodeType == XmlNodeType.Element ) { switch(reader.Name) { case "test1": reader.Read(); this.test1 = int.Parse(reader.Value); break; } } } XmlSerializer s = new XmlSerializer(typeof(int)); reader.Read(); reader.ReadStartElement("test1"); test1 = (int)s.Deserialize(reader); reader.ReadEndElement(); } public void WriteXml(XmlWriter writer) { writer.WriteAttributeString("test1", this.test1.toString()); writer.WriteElementString("test1", this.test1.toString()); 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基本语法
创建Json文件:把后缀名改为.json
注释:
- 基本语法:
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
存档
存档的具体步骤分为3步:
- 定义一个需要存档且可序列化的数据类
- 把对象转成
Json字符串
- 把字符串写入文件
定义一个需要存档且可序列化的数据类
1 2 3 4 5 6
| [System.Serializable] public void SaveData { public Vector3 playerPosition; public string mapBoundary; }
|
把对象转成Json字符串
把对象转为Json字符串有两种方法:
JsonUtility是Unity自带的用于解析Json的公共类。它可以将内存中的对象序列化为Json格式的字符串,也可以将Json字符串反序列化为类对象。
注意:JsonUtility不支持字典,存储null时会有一个默认值
LitJson是一个第三方库,用于处理Json的序列化和反序列化。它体积小、速度快、易于使用,使用时只需要把他的代码拷贝到工程中即可。建议使用LitJson。
获取:直接浏览器搜索LitJson -> 官网中访问GitHUb -> 右侧release下载最新版本 -> 拷贝src/Litjson放进sript文件夹(可以删除除了cs文件外的其他文件)
把字符串写入文件
要把字符串写入文件需要用到File类。使用时,要先引用命名空间:
1
| File.WriteAllText(Application.persistentDataPath, "内容")
|
其中,Application.persistentDataPath是Unity专门给游戏提供的唯一安全、永远可读写、不会被删除的文件夹路径。
读档
读档的具体步骤也分为3步:
- 确定路径
- 判断文件是否存在并读取文件,得到
json字符串
- 反序列化:
json字符串 -> 对象
确定路径
1
| string path = Path.Combine(Application.persistentDataPath, "SaveData.json");
|
其中,Path.Combine是一个为了适应各种平台的连接字符串的方法,
判断文件是否存在并读取文本
1 2 3 4
| if(File.Exists(path)) { string str = File.ReadAllText(path) }
|
反序列化:json字符串 -> 对象
1
| SaveData saveData = JsonUtility.FromJson<SaveData>(str);
|
1
| SaveData saveData = JsonMapper.ToObject<SaveData>(str);
|
二进制
(未完待续)