思考并回答以下问题:
游戏中持久化数据一般可分为两种:第一种是静态数据,例如Excel数据表中由策划人员编辑的数据,其特点是运行期间程序只需要读取,不需要修改;另一种是游戏存档数据,例如记录玩家在游戏过程中的进度,其特点是运行期间既需要读取,也需要修改,并且在版本升级的时候需要考虑老数据是否需要删除或者重置。
Excel
策划人员通常都会在Excel中配置静态数据,例如道具表,它由主键、道具名称、描述、功能和参数等一系列数据组成。前后端使用道具主键来进行数据的通信,最终前端将主键所包含的整个数据信息展示在游戏中。
EPPlus
在Windows下,提供了很多解析Excel的方法。但是作为一个跨平台引擎,可能需要在多个平台都能解析Excel,所以我们需要引用一个第三方DLL库EPPlus来处理跨平台解析Excel,首先,需要从它的网站上将其下载下来,接着将DLL文件拖入Unity即可使用了。也可以直接NuGet安装,新建一个项目,然后工具->NuGet包管理器->管理解决方案的NuGet程序包,最后将项目路径\packages\EPPlus.4.5.2.1\lib\net35下的EPPlus.dll放进Assets/Plugins下。
读取Excel
首先,我们需要创建Excel文件。如下图所示,可以分别在不同的工作表中添加数据,接着在代码中读取这两个工作表中的所有数据。
如ExcelDemo.cs代码所示,根据Excel文件的路径得到FileStream,并且创建ExcelPackage对象,接着就可以用它对Excel进行读取了。
1 | using System.Collections; |
在导航菜单栏中选择Excel->Load Excel命令,数据已经全部读取出来了,如下图所示。
写入Excel
首先,需要使用FileInfo来创建一个Excel文件,接着使用ExcelPackage来向Excel文件中写入数据,如下所示。
如代码所示,在ExcelPackage对象中添加worksheet后,即可调用worksheet.Cells对每个单元格的行、列赋值,最终保存即可。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
33using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using OfficeOpenXml;
using System;
public class WriteExcel
{
[ ]
static void LoadExcel()
{
//创建Excel文件
string path = Application.dataPath + "/Excel/" + DateTime.Now.ToString("yyyy-MM-dd--hh-mm-ss") + ".xlsx";
var file = new FileInfo(path);
using (ExcelPackage excel = new ExcelPackage(file))
{
//向表格中写入数据
ExcelWorksheet worksheet = excel.Workbook.Worksheets.Add("sheet1");
worksheet.Cells[1, 1].Value = "Company name1";
worksheet.Cells[1, 2].Value = "Address1";
worksheet = excel.Workbook.Worksheets.Add("sheet2");
worksheet.Cells[1, 1].Value = "Company name2";
worksheet.Cells[1, 2].Value = "Address2";
// 保存
excel.Save();
}
AssetDatabase.Refresh();
}
}
保存完单元格后,为了在Unity中立刻看到效果,需要调用AssetDatabase.Refresh()方法进行刷新。
JSON
游戏运行时,我们是无法通过EPPlus读取Excel的,不过我们可以将它保存成自定义格式,例如CSV、JSON和ScriptableObject等,使用的时候将它读取进来就可以了。Unity支持JSON的序列化和反序列化。需要注意的是,参与序列化的类必须在上方声明[Serializable]属性,并且支持类对象的相互嵌套。我们可以使用JsonUtility.ToJson()以及JsonUtility.FromJson<T>()来进行序列化以及反序列化。比较遗憾的是,它并不支持字典类型的序列化。相关代码如下所示。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
42using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;
public class JsonDemo
{
[ ]
static void LoadJson()
{
Data data = new Data();
data.name = "Data";
data.subData.Add(new SubData() { intValue = 1, boolValue = true, floatValue = 0.1f, stringValue = "one" });
data.subData.Add(new SubData() { intValue = 2, boolValue = true, floatValue = 0.1f, stringValue = "two" });
string json = JsonUtility.ToJson(data);
Debug.Log(json);
data = JsonUtility.FromJson<Data>(json);
Debug.LogFormat("name = {0}", data.name);
foreach (var item in data.subData)
{
Debug.LogFormat("intValue = {0} boolValue={0} floatValue={0} stringValue={0}", item.intValue, item.boolValue, item.floatValue, item.stringValue);
}
}
[ ]
public class Data
{
public string name;
public List<SubData> subData = new List<SubData>();
}
[ ]
public class SubData
{
public int intValue;
public bool boolValue;
public float floatValue;
public string stringValue;
}
}
如下图所示,将数据对象转成JSON字符串,再从JSON字符串还原数据对象,并且将数据输出。
JSON支持字典
Unity的JSON是不支持字典的,不过可以继承ISerializationCallbackReceiver接口,间接地实现字典序列化,相关代码如下所示。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
public class DictionaryJson
{
[ ]
static void SerializableDictionary()
{
SerializableDictionary<int, string> serializableDictionary = new SerializableDictionary<int, string>();
serializableDictionary[100] = "Unity";
serializableDictionary[200] = "好好学习";
serializableDictionary[300] = "天天向上";
string json = JsonUtility.ToJson(serializableDictionary);
Debug.Log(json);
serializableDictionary = JsonUtility.FromJson<SerializableDictionary<int, string>>(json);
Debug.Log(serializableDictionary[100]);
}
}
如下SerializableDictionary.cs代码所示,序列化两个List元素来保存键和值,接着将C#的泛型传入,这样键和值就更加灵活了,在OnBeforeSerialize()和OnAfterDeserialize()进行序列化和反序列化赋值操作。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
52using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SerializableDictionary<K, V> : ISerializationCallbackReceiver
{
[ ]
private List<K> m_keys;
[ ]
private List<V> m_values;
private Dictionary<K, V> m_Dictionary = new Dictionary<K, V>();
public V this[K key]
{
get {
if (!m_Dictionary.ContainsKey(key))
{
return default(V);
}
return m_Dictionary[key];
}
set {
m_Dictionary[key] = value;
}
}
public void OnAfterDeserialize()
{
int length = m_keys.Count;
m_Dictionary = new Dictionary<K, V>();
for (int i = 0;i<length;i++)
{
m_Dictionary[m_keys[i]] = m_values[i];
}
m_keys = null;
m_values = null;
}
public void OnBeforeSerialize()
{
m_keys = new List<K>();
m_values = new List<V>();
foreach (var item in m_Dictionary)
{
m_keys.Add(item.Key);
m_values.Add(item.Value);
}
}
}
LitJson
Enemy:需要序列化的对象,不能继承MonoBehavior。
1 | using UnityEngine; |
LitJson是.dll格式的,需要放在Plugins目录下。
1 | using UnityEngine; |
文件读取与写入
游戏中有很多数据需要在运行期间读取或者写入,最典型的就是游戏存档功能。Unity自己也提供了一套存档的API,但是功能比较单一,只支持保存int、float和string这三种类型。不过C#支持文件的读写,我们可以灵活地扩展它。
PlayerPrefs
PlayerPrefs是Unity自带的存档方法,它的优点是使用起来非常方便。引擎已经封装好GetKey以及SetKey的方法,并且还做保存数据的优化。由于保存数据可能是个耗时操作,频繁地保存可能会带来卡顿,所以Unity默认会在应用程序将切入后台时统一保存文件,开发者也可以强制调用PlayerPrefs.Save()来保存。
然而它的缺点就是,编辑模式下查看存档非常不方便, Windows的存档在HKCU\Software\\[company name]\\[product name]注册表中。如下代码所示,我们使用PlayerPrefs对数据进行保存和读取操作。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
31using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerPrefsDemo : MonoBehaviour
{
void Start ()
{
PlayerPrefs.SetInt("MyInt", 100);
PlayerPrefs.SetFloat("MyFloat", 200f);
PlayerPrefs.SetString("MyString", "Unity");
Debug.Log(PlayerPrefs.GetInt("MyInt", 0));
Debug.Log(PlayerPrefs.GetFloat("MyFloat", 0f));
Debug.Log(PlayerPrefs.GetString("MyString", "没有返回默认值"));
// 判断是否有某个键
if (PlayerPrefs.HasKey("MyInt"))
{
Debug.Log("has MyInt");
}
// 删除某个键
PlayerPrefs.DeleteKey("MyInt");
// 删除所有键
PlayerPrefs.DeleteAll();
// 强制保存数据
PlayerPrefs.Save();
}
}
EditorPrefs
在编辑器模式下,Unity也提供了一组存档功能,它不需要考虑运行时的效率,所有没有采用PlayerPrefs优化的方式,而是立即就保存了。相关代码如下所示。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
30using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
public class Script_08_06
{
[ ]
static void Save()
{
EditorPrefs.SetInt("MyInt", 100);
EditorPrefs.SetFloat("MyFloat", 200f);
EditorPrefs.SetString("MyString", "Unity");
Debug.Log(EditorPrefs.GetInt("MyInt", 0));
Debug.Log(EditorPrefs.GetFloat("MyFloat", 0f));
Debug.Log(EditorPrefs.GetString("MyString", "没有返回默认值"));
//判断是否有某个键
if (EditorPrefs.HasKey("MyInt"))
{
}
//删除某个键
EditorPrefs.DeleteKey("MyInt");
//删除所有键
EditorPrefs.DeleteAll();
}
}
PlayerPrefs保存复杂结构
PlayerPrefs可以保存字符串,结合JSON的序列化和反序列功能,它就可以保存各种复杂的数据结构了。另外,保存存档取决于硬件当时的条件,完全有保存不上的情况,所以可以通过try…catch来捕获保存时的错误异常。
如下代码所示,使用JsonUtility.ToJson()方法将对象保存成JSON字符串,读取的时候再使用JsonUtility.FromJson将JSON字符串还原为类对象。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
38using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Script_08_07 : MonoBehaviour
{
void Start()
{
//保存游戏存档
Record record = new Record();
record.stringValue = "Unity";
record.intValue = 200;
record.names = new List<string>() { "test1", "test2" };
string json = JsonUtility.ToJson(record);
//可以使用try…catch来捕获异常
try
{
PlayerPrefs.SetString("record", json);
}
catch (System.Exception err)
{
Debug.Log("Got: " + err);
}
//读取存档
record = JsonUtility.FromJson<Record>(PlayerPrefs.GetString("record"));
Debug.LogFormat("stringValue = {0} intValue={1}", record.stringValue, record.intValue);
}
// 存档对象
[ ]
public class Record
{
public string stringValue;
public int intValue;
public List<string> names;
}
}
需要注意的是,凡是参与JSON序列化的对象都需要标记[System.Serializable]对象。
TextAsset
TextAsset是Unity提供的一个文本对象,它可以通过Resources.Load()或者AssetBundle来读取数据,其中数据是string格式的。当然,我们也可以按byte[]读取。它支持读取的文本格式包括.txt,.html,.htm,.xml,.bytes,.json,.csv,.yaml和.fnt。如下代码所示,我们来读取Resources目录下的MyText文本。1
2
3
4
5
6
7
8
9
10
11using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Script_08_08 : MonoBehaviour
{
void Start()
{
Debug.Log(Resources.Load<TextAsset>("MyText").text);
}
}
编辑期读写文本
Unity可以利用C#的File类来读写文本,此时只需要提供一个目录即可。这里需要注意的是,编辑器模式下读写文本是很方便的,但是一旦打包发布, Assets/目录都不存在了,运行时是无法读取它目录下的文本的。如下代码所示,通过File.WriteAllText()和File.ReadAllText()来对文本进行读取和写入。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Script_08_09
{
[ ]
static void Start ()
{
string path = Path.Combine(Application.dataPath, "test.txt");
//如果文件存在,就删除它
if (File.Exists(path))
{
File.Delete(path);
}
//写入文件
StringBuilder sb = new StringBuilder();
sb.AppendFormat("第一行:{0}", 100).AppendLine();
sb.AppendFormat("第二行:{0}", 200).AppendLine();
File.WriteAllText(path, sb.ToString());
//读取文件
Debug.Log(File.ReadAllText(path));
}
}
运行期读写文本
在游戏运行期间,只有Resources和StreamingAssets目录具有读取权限,其中Resources用来读取游戏资源,而StreamingAssets可使用File类来读取文件(除了个别平台外),但都是只读的,并不能写。只有Application.persistentDataPath目录是可读、可写的。分别在Resources和StreamingAssets目录下新建test.txt,分别写入“我是Resources目录”,“我是StreamingAssets目录”。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
36using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Text;
using System;
public class Script_08_10 : MonoBehaviour
{
//可读不可写
string m_ResourcesTxt = string.Empty;
//可读不可写
string m_StreamingAssetsTxt = string.Empty;
//可读可写
string m_PersistentDataTxt = string.Empty;
void Start()
{
m_ResourcesTxt = Resources.Load<TextAsset>("test").text;
m_StreamingAssetsTxt = File.ReadAllText(System.IO.Path.Combine(Application.streamingAssetsPath, "test.txt"));
}
void OnGUI()
{
GUILayout.Label(string.Format("<size=50>Resources : {0}</size>", m_ResourcesTxt));
GUILayout.Label(string.Format("<size=50>StreamingAssets : {0}</size>", m_StreamingAssetsTxt));
GUILayout.Label(string.Format("<size=50>PersistentDataPath : {0}</size>", m_PersistentDataTxt));
if (GUILayout.Button("<size=50>写入并读取时间</size>"))
{
string path = Path.Combine(Application.persistentDataPath, "test.txt");
File.WriteAllText(path, DateTime.Now.ToString());
m_PersistentDataTxt = File.ReadAllText(path);
}
}
}
执行如下:
PersistentDataPath目录
PersistentDataPath目录本身并没有什么问题,但是如果平常开发中也在这个目录下进行读写操作的话,就会比较麻烦,因为它在Windows以及Mac下的目录是很难找的。例如,开发过程中需要验证一下保存的文件是否正确,我们随时都需要很快地找到它。调用EditorUtility.RevealInFinder()方法,就可以立即定位到指定目录。1
2
3
4
5
6
7
8
9
10
11
12using System.Collections;
using UnityEngine;
using UnityEditor;
public class Script_08_11 : MonoBehaviour
{
[ ]
static void Open()
{
EditorUtility.RevealInFinder(Application.persistentDataPath);
}
}
我们需要一个不影响开发的存档,并且查看要非常方便(PlayerPref查看不方便)。可以自己写一个存档类,在编辑模式下将存档保存在Assets同级目录下,这样查看存档内容就方便多了。
如RecordUtil.cs所示,在编辑模式下将数据保存于Application.dataPath目录,而在真实环境下将数据保存在Application.persistentDataPath下。只有调用Save()方法时,数据才会被强制写入。
里面没有Update等回调方法,无需继承MonoBehavior,就是一个工具类。不是所有的类都需要继承MonoBehavior。作为组件的才需要。
Unity是可以直接访问硬盘上的文件和目录的。此时需要通过C#的函数来操作。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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System;
using System.Text;
// 这个类的作用是把游戏信息存到一个文件中,这儿没有使用Unity自带的PlayerPref类。
// 文件的目录,存储函数,得到函数
// 对外提供的函数是RecordUtil.Set(key, value)和RecordUtil.Get(key [,defaultValue])和RecordUtil.Delete();
// 因为是工具类,就用静态就好。
public class RecordUtil
{
//游戏存档保存的根目录
static string RecordRootPath
{
get
{
return Application.dataPath + "/../Record/";
return Application.persistentDataPath + "/Record/";
}
}
//游戏存档
static Dictionary<string, string> recordDic = new Dictionary<string, string>();
//标记某个游戏存档是否需要重新写入
static List<string> recordDirty = new List<string>();
//标记某个游戏存档是否需要删除
static List<string> deleteDirty = new List<string>();
//表示某个游戏存档读取时需要重新从文件中读取
static List<string> readDirty = new List<string>();
static private readonly UTF8Encoding UTF8 = new UTF8Encoding(false);
static RecordUtil()
{
readDirty.Clear();
if (Directory.Exists(RecordRootPath))
{
foreach (string file in Directory.GetFiles(RecordRootPath, "*.record", SearchOption.TopDirectoryOnly))
{
string name = Path.GetFileNameWithoutExtension(file);
if (!readDirty.Contains(name))
{
readDirty.Add(name);
Get(name);
}
}
}
}
//强制写入文件
public static void Save()
{
foreach (string key in deleteDirty)
{
try
{
string path = Path.Combine(RecordRootPath, key + ".record");
if (recordDirty.Contains(key))
{
recordDirty.Remove(key);
}
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (Exception ex)
{
Debug.LogError(ex.Message);
}
}
deleteDirty.Clear();
foreach (string key in recordDirty)
{
string value;
if (recordDic.TryGetValue(key, out value))
{
if (!readDirty.Contains(key))
{
readDirty.Add(key);
}
string path = Path.Combine(RecordRootPath, key + ".record");
recordDic[key] = value;
try
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllText(path, value, UTF8);
}
catch (Exception ex)
{
Debug.LogError(ex.Message);
}
}
}
recordDirty.Clear();
}
public static void Set(string key, string value)
{
recordDic[key] = value;
if (!recordDirty.Contains(key))
{
recordDirty.Add(key);
}
Save();
}
public static string Get(string key)
{
return Get(key, string.Empty);
}
public static string Get(string key, string defaultValue)
{
if (readDirty.Contains(key))
{
string path = Path.Combine(RecordRootPath, key + ".record");
try
{
string readstr = File.ReadAllText(path, UTF8);
recordDic[key] = readstr;
}
catch (Exception ex)
{
Debug.LogError(ex.Message);
}
readDirty.Remove(key);
}
string value;
if (recordDic.TryGetValue(key, out value))
{
return value;
}
else
{
return defaultValue;
}
}
public static void Delete(string key)
{
if (recordDic.ContainsKey(key))
{
recordDic.Remove(key);
}
if (!deleteDirty.Contains(key))
{
deleteDirty.Add(key);
}
Save();
}
}
在编辑模式下,数据发生改变时,会立刻写入文件并保存,而真实环境下出于性能考虑,以在某个特定的时间点保存数据。
如下代码所示,由于是自己写的存档类,处理起来会更加灵活。当然,我们也可仿照PlayerPrefs在应用程序即将进入后台时保存。当调用OnApplicationPause()时,表示应用进入后台后再保存数据。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
50using System.Collections;
using UnityEngine;
using UnityEditor;
/*
* 这个脚本是挂载在GameController上作为组件的。
*/
public class Script_08_12 : MonoBehaviour
{
// 脚本激活时就调用RecordUtil把数据写入。
// 也可以改成UGUI系统点击“保存进度”才写入。
void Start()
{
Setting setting = new Setting();
setting.stringValue = "测试字符串";
setting.intValue = 10000;
RecordUtil.Set("setting", JsonUtility.ToJson(setting));
}
private Setting m_Setting = null;
void OnGUI()
{
if (GUILayout.Button("<size=50>获取存档</size>"))
{
m_Setting = JsonUtility.FromJson<Setting>(RecordUtil.Get("setting"));
}
if (m_Setting != null)
{
GUILayout.Label(string.Format("<size=50> {0}, {1} </size>", m_Setting.intValue, m_Setting.stringValue));
}
}
void OnApplicationPause(bool pausestatus)
{
//当游戏即将进入后台时,保存存档
if (pausestatus)
{
RecordUtil.Save();
}
}
[ ]
class Setting
{
public string stringValue;
public int intValue;
}
}
默认情况下,应用进入后台后才会保存数据。如果应用进入后台之前发生了闪退现象,那么数据就无法保存了,所以某些非常重要的数据需要强制调用save()方法。
XML
XML在开发中使用也很频繁,此时要以标签的形式来组织数据结构。C#提供了创建、解析、修改和查询等方法,可以很方便地操作它。
创建XML
操作XML时,需要用到System.Xml命名空间。我们可以在运行时动态创建XML字符串,并且在节点下添加数据。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
33using UnityEngine;
using System.IO;
using System.Xml;
public class Script_08_13 : MonoBehaviour
{
void Start()
{
// 创建XmlDocument
XmlDocument xmlDoc = new XmlDocument();
XmlDeclaration xmlDeclaration = xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", null);
xmlDoc.AppendChild(xmlDeclaration);
// 在节点中写入数据
XmlElement root = xmlDoc.CreateElement("XmlRoot");
xmlDoc.AppendChild(root);
XmlElement group = xmlDoc.CreateElement("Group");
group.SetAttribute("username", "Unity");
group.SetAttribute("password", "123456");
root.AppendChild(group);
// 读取节点并输出XML字符串
using (StringWriter stringwriter = new StringWriter())
{
using (XmlTextWriter xmlTextWriter = new XmlTextWriter(stringwriter))
{
xmlDoc.WriteTo(xmlTextWriter);
xmlTextWriter.Flush();
Debug.Log(stringwriter.ToString());
}
}
}
}
输出结果:1
<XmlRoot><Group username="Unity" password="123456" /></XmlRoot>
读取与修改
XML可作为字符串来传递。可以动态读取XML字符串中的内容,并且修改它的内容,以重新生成新的XML字符串。
创建XmlDocument对象后,需要读取XML文件,通过循环可以遍历所有子节点对它们进行修改。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
38using UnityEngine;
using System.IO;
using System.Xml;
public class Script_08_14 : MonoBehaviour
{
void Start()
{
//xml字符串
string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XmlRoot><Group username=\"Unity\" password=\"123456\" /></XmlRoot>";
//读取字符串xml
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml);
// 遍历节点
XmlNode nodes = xmlDoc.SelectSingleNode("XmlRoot");
foreach (XmlNode node in nodes.ChildNodes)
{
string username = node.Attributes["username"].Value;
string password = node.Attributes["password"].Value;
Debug.LogFormat("username={0} password={1}", username, password);
//修改其中一条数据
node.Attributes["password"].Value = "88888888";
}
// 读取节点并输出XML字符串
using (StringWriter stringwriter = new StringWriter())
{
using (XmlTextWriter xmlTextWriter = new XmlTextWriter(stringwriter))
{
xmlDoc.WriteTo(xmlTextWriter);
xmlTextWriter.Flush();
Debug.Log(stringwriter.ToString());
}
}
}
}
结果如下:1
username=Unity password=123456
1 | <XmlRoot><Group username="Unity" password="88888888" /></XmlRoot> |
XML文件
XmlDocument类也提供了从文件中读取XML,或者将XML写入本地路径的方法。如下图所示,将XML写入本地文件,读取后再输出节点中的内容。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
62using UnityEngine;
using System.IO;
using System.Xml;
using UnityEditor;
public class Script_08_15
{
[ ]
static void WriteXml()
{
string xmlPath = Path.Combine(Application.dataPath, "test.xml");
// 如果XML文件已经存在,就删除它
if (File.Exists(xmlPath))
{
File.Delete(xmlPath);
}
// 创建XmlDocument
XmlDocument xmlDoc = new XmlDocument();
XmlDeclaration xmlDeclaration = xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", null);
xmlDoc.AppendChild(xmlDeclaration);
// 在节点中写入数据
XmlElement root = xmlDoc.CreateElement("XmlRoot");
xmlDoc.AppendChild(root);
//循环写入3条数据
for (int i = 0; i < 3; i++)
{
XmlElement group = xmlDoc.CreateElement("Group");
group.SetAttribute("id", i.ToString());
group.SetAttribute("username", "车斌Carl");
group.SetAttribute("password", "123456");
root.AppendChild(group);
}
// 写入文件
xmlDoc.Save(xmlPath);
AssetDatabase.Refresh();
}
[ ]
static void LoadXml()
{
string xmlPath = Path.Combine(Application.dataPath, "test.xml");
// XML文件只有存在,才能读取
if (File.Exists(xmlPath))
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(xmlPath);
//遍历节点
XmlNode nodes = xmlDoc.SelectSingleNode("XmlRoot");
foreach (XmlNode node in nodes.ChildNodes)
{
string id = node.Attributes["id"].Value;
string username = node.Attributes["username"].Value;
string password = node.Attributes["password"].Value;
Debug.LogFormat("id={0} username={1} password={2}", id, username, password);
}
}
}
}
test.xml1
2
3
4
5
6
<XmlRoot>
<Group id="0" username="Unity" password="123456" />
<Group id="1" username="Unity" password="123456" />
<Group id="2" username="Unity" password="123456" />
</XmlRoot>
1 | id=0 username=Unity password=123456 |
序列化与反序列化
StartXML.cs
1 | using UnityEngine; |
YAML
前面我们介绍了JSON和XML其实已经在大量使用了,当数据多了以后,JSON有个最大的问题,那就是可读性很差。XML比JSON的可读性会好一些。无论JSON还是XML,编辑都会很麻烦,它们的数据格式要求很严格,少写括号或者逗号都不行。
Unity就没有使用JSON或者XML来描述结构,采取的是YAML格式。如下所示,它的预览性以及编辑性都非常好,数据与变量通过冒号来连接。例如,游戏中一些服务器列表的配置,或者调试性的开关,不太方便配置在表格中的数据,或者修改比较频繁的数据都可以使用YAML来配置,随时用随时改。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15house:
family:
name: Doe
parents:
- John
- Jane
children:
- Paul
- Mark
- Simone
address:
number: 34
street: Main Street
city: Nowheretown
zipcode: 12345
YamlDotNet
YAML提供了.NET的类库,即YamlDotNet。Unity中直接提供了插件,它可以在Asset Store中免费下载到。YamlDotNet支持PC和移动端,下载后导入工程就可以使用了。
序列化和反序列化
YamlDotNet提供了运行时序列化和反序列化的接口。这里需要注意的是,对于参与序列化的类中的变量,其属性必须设置成get或者set,不然无法序列化。在程序运行中,可以序列化和反序列化数据。使用Serialize()和Deserialize()方法就可以进行序列化和反序列化操作。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
32using System.Collections.Generic;
using UnityEngine;
using YamlDotNet.Serialization;
public class Script_08_16 : MonoBehaviour
{
private void Start()
{
//创建对象
Data data = new Data();
data.name = "Unity";
data.password = "123456";
data.list = new List<string>() { "a", "b", "c" };
//序列化YAML字符串
Serializer serializer = new Serializer();
string yaml = serializer.Serialize(data);
Debug.LogFormat("serializer : \n{0}", yaml);
//反序列化成类对象
Deserializer deserializer = new Deserializer();
Data data1 = deserializer.Deserialize<Data>(yaml);
Debug.LogFormat("deserializer : name={0} password={1}", data1.name, data1.password);
}
}
class Data
{
public string name { get; set; }
public string password { get; set; }
public List<string> list { get; set; }
}
输出结果:1
2
3
4
5
6
7
8
9serializer:
name: Unity
password: 123456
list:
- a
- b
- c
deserializer : name=Unity password=123456
读取配置
在游戏中,一些服务器列表或者一些临时调试的配置信息,可能需要频繁地添加、删除和修改等。由于这些测试数据都是临时性的,就不太适合配置在Excel表格中。当包打出来以后,直接修改配置文件就可以立即生效到游戏中。在YAML中,可以使用”#”符号来表示注释部分。
yaml.txt1
2
3
4
5
6#服务器列表
ServerList : 192.168.1.1
#服务器端口
Port : 8080
#是否启动调试
Debug : true
1 | using System.Collections.Generic; |
小结
本章中,我们学习了游戏存档。存档可分为静态存档和动态存档。静态存档时,在游戏运行过程中,对它只能读取不能写入。这好比Excel表格数据,最终表格数据可在编辑模式下利用EPPlus转成程序可读文件类型。动态存档应用得就更多了,它在玩游戏的过程中记录玩家游戏的进度,或者一些设置选项, Unity提供了PlayerPrefs类来处理存档的读与写,我们也可以利用C#的File类来自行保存存档。