游戏中持久化数据一般可分为两种:

  • 第一种是静态数据,例如Excel数据表中由策划人员编辑的数据,其特点是运行期间程序只需要读取,不需要修改;
  • 第二种是存档数据,例如记录玩家在游戏过程中的进度,其特点是运行期间既需要读取,也需要修改,并且在版本升级的时候还需要考虑存档的删除或重置

Excel

策划人员通常都会在Excel中配置静态数据,例如道具表,它由主键、道具名称、描述、功能和参数等一系列数据组成。前后端使用道具主键来进行数据的通信,最终前端将主键所包含的整个数据信息展示在游戏中

EPPlus

EPPlus是一个可以有跨平台解析Excel能力第三方DLL库,目前已经升级到EPPlus5且收费

官网

Excel spreadsheet library for .NET Framework/Core - EPPlus Software

从网上找到比较古早的3.0.0.2,X86版本的DLL,直接放在了Assets——Plugins文件夹内

提供一个下载网址:epplus.dll File Download & Fix For All Windows OS (pconlife.com)

读取Excel

创建两个工作表,把工作簿命名为“test.xlsx”

工作表1

工作表2

根据Excel文件路径得到FileStream,并且创建ExcelPackage对象接着就可以用它对Excel进行读取了。

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using OfficeOpenXml;

public class ExcelLoader
{
[MenuItem("Excel/Load Excel")]
static void LoadExcel()
{
string path = Application.dataPath + "/PersistentData/test.xlsx";
//读取Excel文件
using(FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using(ExcelPackage excel = new ExcelPackage(fs))
{
ExcelWorksheets worksheets = excel.Workbook.Worksheets;
//遍历所有工作表
for (int i = 1; i <= worksheets.Count; i++)
{
ExcelWorksheet worksheet = worksheets[i];
int colCount = worksheet.Dimension.End.Column;//纵列数
//获取当前工作表的名字
Debug.LogFormat("Sheet {0}", worksheet.Name);
for (int row = 1,count = worksheet.Dimension.End.Row; row <= count; row++)
{
for(int col = 1;col <= colCount; col++)
{
//读取每个单元格的数据
var text = worksheet.Cells[row, col].Text ?? "";//空合并运算符,如果当前Cell不为null则返回Text,否则返回""
Debug.LogFormat("下标:{0},{1} 内容:{2}", row, col, text);
}
}
}
}
}
}
}

在导航栏中选择Excel→Load Excel命令,工作表1和工作表2相同的内容会合并。

相同的消息会合并

写入Excel

首先,需要使用FileInfo来创建一个文件,接着使用ExcelPackage来向Excel文件中写入数据

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using OfficeOpenXml;
using System;

public class ExcelCreator
{
[MenuItem("Excel/Write Excel")]
static void CreateExcel()
{
//创建Excel文件
string path = Application.dataPath + "/PersistentData/" + 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();
}
}

创建完Excel为了在Unity中立刻看到效果,需要调用AssetDatabase.Refresh()进行刷新

生成的Excel

JSON

游戏运行时,我们是无法通过EPPlus读取Excel的,不过我们可以提前把Excel转化成可以运行时存取的格式,例如CSV、JSON和ScriptableObject等。

Unity支持JSON的序列化和反序列化。参与序列化的类必须在类上方声明[System.Serializable],并且支持类对象的相互嵌套(比如之前的PlayableAsset)。我们可以使用JsonUtility.ToJson()以及JsonUtility.FromJson<T>()来进行序列化以及反序列化。但是并不支持字典类型的序列化

通过下面的脚本,将数据对象转成Json字符串,再从Json字符串还原数据对象

新建JsonDataProcessing脚本

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;

public class JsonDataProcessing
{
[MenuItem("Excel/Json")]
static void LoadJson()
{
DataForJson dataForJson = new DataForJson();
dataForJson.name = "DataForJson";
dataForJson.subDatas.Add(
new SubDataForJson { intValue = 1, boolValue = false, floatValue = 0.12f, stringValue = "abbb" });
dataForJson.subDatas.Add(
new SubDataForJson { intValue = 2, boolValue = true, floatValue = 0.16f, stringValue = "abcd" });

string json = JsonUtility.ToJson(dataForJson);
Debug.Log(json);
dataForJson = JsonUtility.FromJson<DataForJson>(json);
Debug.LogFormat("name = {0}", dataForJson.name);
foreach (var item in dataForJson.subDatas)
{
Debug.LogFormat("intValue = {0},boolVaule = {1},floatValue = {2},stringValue = {3}", item.intValue, item.boolValue, item.floatValue, item.stringValue);
}
}
}
[Serializable]
public class DataForJson
{
public string name;
public List<SubDataForJson> subDatas = new List<SubDataForJson>();
}

[Serializable]
public class SubDataForJson
{
public int intValue;
public bool boolValue;
public float floatValue;
public string stringValue;
}

JsonDebug

JSON支持字典

Unity的JSON是不支持字典的,不过可以继承ISerializationCallBackReceiver接口,间接实现字典序列化,此部分知识在之前的第四章(游戏脚本)脚本序列化讲到过

新建SerializableDictionaryTest脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class SerializableDictionaryTest
{
[MenuItem("Excel/Load Dictionary")]
static void DictionaryTest()
{
SerializableDictionary<int, string> serializableDictionary = new SerializableDictionary<int, string>();

serializableDictionary[100] = "ATAO2017";
serializableDictionary[200] = "好好学习";
serializableDictionary[300] = "天天向上";

string json = JsonUtility.ToJson(serializableDictionary);//会调用OnBeforeSerialize,然后根据生成的list转化JSON
Debug.Log(json);

serializableDictionary = JsonUtility.FromJson<SerializableDictionary<int, string>>(json);//会调用OnAfterDeserialize,根据生成的Json转化Dictionary
Debug.Log(serializableDictionary[100]);
}
}

SerializableDictionary如下所示,序列化两个List元素来保存键和值,使用泛型Dictionary,这样键和值就更加灵活了,使用C#索引器来更方便地给字典赋值,然后在OnBeforeSerializeOnAfterDeserialize进行序列化和反序列化赋值操作

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SerializableDictionary<K,V> : ISerializationCallbackReceiver
{
[SerializeField] private List<K> m_Keys;
[SerializeField] 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);//在泛型中default表示此类型的默认值,比如说如果这个V是int,则default(V)是0.
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);
}
}
}