在Unity3D中实现存档系统

Unity引擎提供了很多工具类和接口,但是在游戏存档这方面为开发者提供的帮助十分优先,毕竟存档不是什么简单的事,不同游戏的存档方式差距还是比较大的。

像网上说的通过PlayerPrefsJsonUtility等类虽然确实能够提供一些存储的手段,但是局限性较大。前者只能存储整型、单精度浮点、字符串类型的数据,而后者对序列化对象有一定要求,需要开发者在设计类时就提前考虑好。

所以,最好的方法还是自己想办法实现一个存档系统,可以完美契合自己的需求,减少时间和空间上的付出

格式化

我的项目是一款类似MC的游戏,需要存储的数据很简单,就是每个游戏中物体的位置(一个三维向量)、朝向(一个四维向量)和激活状态(一个bool值)。之前搞算法竞赛的时候就经常接触格式化的输入输出,即规定好某行某列的数据是某意义的数值,所以就想到了用格式化的方法来储存数据。

Unity中所有游戏对象都继承了GameObject类,所以可以写一个静态方法来将想要储存的游戏对象转化成可以存储的字符串形式,至于“选择”的工作,可以另外写一个方法来筛选。

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
static public string ToText(List<GameObject> gameObjects)
{
string res="";
foreach(var go in gameObjects)
{
if(go.layer==20)
{
//Debug.Log(go);
string str = Formatting(go);
res += str;
}
}
return res;
}

static public string Formatting(GameObject go)
{
string res = go.GetComponent<Identify>().ID;
res += "%" + go.transform.position;
res += "%" + go.transform.rotation;
if(go.GetComponent<IUnit>()!=null)
{
res += "%" + go.GetComponent<IUnit>().isCultivated;
}
return res+"\n";
}

这里的20是一个Magic Number,代表游戏中“方块”所特有的渲染图层。由于暂时找不到很好的方法来区别方块和其他类型的GameObject,只能先用这个笨办法。这算是一个不好的习惯吧。

ToText接受一个GameObject数组,筛选出是方块的对象后调用Formatting方法来格式化方块。Formatting方法采使用简单的语句记录positionrotation两个属性,并判断该对象是否继承自IUnit(这个类是电子元件的基类,需要存储激活状态)。

实际上GameObject并不继承自IUnit,而是GameObject上的脚本组件继承自IUnit

然后是朴实无华的获取场景中所有类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static public List<GameObject> GetBlocks()
{
//获取场景中所有GameObject
GameObject[] gameObjects =(GameObject[]) GameObject.FindObjectsOfType(typeof(GameObject));
List<GameObject> glist = new List<GameObject>();
foreach(var go in gameObjects)
{
//这里保证选到的类不是任何类的子类,否则会出问题
if (go.transform.parent == null)
{
glist.Add(go);
}
}
return glist;
}

储存

1
2
3
4
5
6
7
8
9
10
11
static public void Save(int index)
{
List<GameObject> gameObjects = GetBlocks();
//Debug.Log(gameObjects.Length);
//Player:
GameObject player = GameObject.Find("Player");
string str = player.transform.position.ToString() + "%" + player.transform.rotation.ToString() + "\n";
str += ToText(gameObjects);
File.WriteAllText(path + string.Format("\\test{0}.txt", index), str);
Debug.Log("Save " + index + " is saved Successfully!");
}

调用各个函数,然后使用C#自带的File类来向文件写入数据。这里为了储存玩家的位置信息,决定在第一行储存玩家的positionrotation

格式化后的结果如图:

格式化后的信息

读取数据

读取数据就是格式化数据的逆过程,要从字符串中获得相应的数据,就要对读取出的字符串做一些操作,上面格式化时加入的%符号就是为了让字符串数据变得好处理。

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
static public Vector3 getPos(string str)
{
float x, y, z;
str = str.Trim(new char[] { '(',')'});
string[] nums = str.Split(',');
x = float.Parse(nums[0]);
y = float.Parse(nums[1]);
z = float.Parse(nums[2]);
return new Vector3(x,y,z);
}

static public Quaternion getRotation(string str)
{
float x, y, z, w;
str = str.Trim(new char[] { '(', ')' });
string[] nums = str.Split(',');
x = float.Parse(nums[0]);
y = float.Parse(nums[1]);
z = float.Parse(nums[2]);
w = float.Parse(nums[3]);
return new Quaternion(x,y,z,w);
}

static public bool getCul(string str)
{
return str.CompareTo("True")==0 ? true : false;
}

都是一些基础操作,去空格、去括号、读取值等等。

还需要一个for循环对每一行的数据调用上面三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for(int i = 1; i < strs.Length; i++)
//从1开始,因为第0个是玩家位置的数据
{
string str = strs[i];
// 对字符串进行分割
string[] word = str.Split("%");
string name = word[0];
Vector3 pos = getPos(word[1]);
Quaternion quaternion = getRotation(word[2]);
GameObject go = Resources.Load<GameObject>("Prefabs/" + name);
// 在游戏场景中实例化GameObject
Instantiate(go, pos, quaternion);
// 如果分割出的数据长度大于3,即有第四个数据就意味着这是一个元件,需要读取的数据更多
if(word.Length>3)
{
go.GetComponent<IUnit>().isCultivated = getCul(word[3]);
}
}

完整的Load

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
static public void Load(int index)
{
string[] strs;
string FilePath = path + string.Format("\\test{0}.txt", index);
// 读取前先判断文件是否存在
if (!File.Exists(FilePath))
{
File.Create(FilePath).Close(); // 这里链式调用的Close方法很重要
File.WriteAllText(FilePath, template.text);
}
strs = File.ReadAllLines(FilePath);
// 这一部分是玩家的数据
string playerInfo = strs[0];
string[] res = playerInfo.Split("%");
GameObject player = GameObject.Find("Player");
player.transform.position = getPos(res[0]);
player.transform.rotation = getRotation(res[1]);

for(int i = 1; i < strs.Length; i++)
{
string str = strs[i];
string[] word = str.Split("%");
string name = word[0];
Vector3 pos = getPos(word[1]);
Quaternion quaternion = getRotation(word[2]);
GameObject go = Resources.Load<GameObject>("Prefabs/" + name);
Instantiate(go, pos, quaternion);
if(word.Length>3)
{
go.GetComponent<IUnit>().isCultivated = getCul(word[3]);
}
}

上面用到了类中的一些静态量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SaveManager:MonoBehaviour
{
public static string path;
public static TextAsset template;

private void Start()
{
// 可持久化储存路径,在不同设备中会获得不同的值,可移植性更好
path = Application.persistentDataPath;

// template是一个模板存档文件,只存储一个简单的地图
template = Resources.Load<TextAsset>("template");
}
}

一些Issue

脚本上的技巧大概就是这些,但是写的时候还是遇到了一些问题。

文件流冲突

Load方法中,对文件的存在与否做了一个判断,这里出现过问题。

1
2
3
4
5
6
// 原先的写法
if (!File.Exists(FilePath))
{
File.Create(FilePath);
File.WriteAllText(FilePath, template.text);
}

之前的代码在创建文件时没有文件流关闭,导致每次创建文件时出bug,后来加了一个链式调用的.Close()就好了

静态字段未被赋值

上述所有静态方法和字段都在一个SaveManager类中,而这个类挂载在**场景B(游戏内)内。我在场景A(主菜单)**中添加了一个删除存档的按钮,执行一下语句,结果出现了问题

1
2
string filePath = path + string.Format("\\test{0}.txt", index);
File.WriteAllText(filePath, SaveManager.template.text);

然后发现,在这个函数中SaveManager.pathSaveManager.template都是空的,因为在场景A中,场景B挂载的脚本并没有运行,所以SaveManager类的静态字段没有被初始化。

当时为什么不在声明的时候给这两个变量赋值?我也忘了,好像IDE给我报错了,但现在我发现又可以这么写了,挖个坑以后研究吧

完整代码

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

public class SaveManager:MonoBehaviour
{
public static string path;
public static TextAsset template;

private void Start()
{
path = Application.persistentDataPath;
template = Resources.Load<TextAsset>("template");
}

static public void Load(int index)
{
string[] strs;
string FilePath = path + string.Format("\\test{0}.txt", index);

if (!File.Exists(FilePath))
{
File.Create(FilePath).Close();
File.WriteAllText(FilePath, template.text);
}

strs = File.ReadAllLines(FilePath);
string playerInfo = strs[0];
string[] res = playerInfo.Split("%");
GameObject player = GameObject.Find("Player");
player.transform.position = getPos(res[0]);
player.transform.rotation = getRotation(res[1]);
for(int i = 1; i < strs.Length; i++)
//foreach(string str in strs)
{
string str = strs[i];
string[] word = str.Split("%");
string name = word[0];
Vector3 pos = getPos(word[1]);
Quaternion quaternion = getRotation(word[2]);
GameObject go = Resources.Load<GameObject>("Prefabs/" + name);
Instantiate(go, pos, quaternion);
if(word.Length>3)
{
go.GetComponent<IUnit>().isCultivated = getCul(word[3]);
}
}
//Debug.Log(path);
}

static public Vector3 getPos(string str)
{
float x, y, z;
str = str.Trim(new char[] { '(',')'});
string[] nums = str.Split(',');
x = float.Parse(nums[0]);
y = float.Parse(nums[1]);
z = float.Parse(nums[2]);
return new Vector3(x,y,z);
}

static public Quaternion getRotation(string str)
{
float x, y, z, w;
str = str.Trim(new char[] { '(', ')' });
string[] nums = str.Split(',');
x = float.Parse(nums[0]);
y = float.Parse(nums[1]);
z = float.Parse(nums[2]);
w = float.Parse(nums[3]);
return new Quaternion(x,y,z,w);
}

static public bool getCul(string str)
{
return str.CompareTo("True")==0 ? true : false;
}

static public void Save(int index)
{
List<GameObject> gameObjects = GetBlocks();
//Debug.Log(gameObjects.Length);
//Player:
GameObject player = GameObject.Find("Player");
string str = player.transform.position.ToString() + "%" + player.transform.rotation.ToString() + "\n";
str += ToText(gameObjects);
File.WriteAllText(path + string.Format("\\test{0}.txt", index), str);
Debug.Log("Save " + index + " is saved Successfully!");
}

static public List<GameObject> GetBlocks()
{
GameObject[] gameObjects =(GameObject[]) GameObject.FindObjectsOfType(typeof(GameObject));
List<GameObject> glist = new List<GameObject>();
foreach(var go in gameObjects)
{
if (go.transform.parent == null)
{
glist.Add(go);
}
}
return glist;
}

static public string ToText(List<GameObject> gameObjects)
{
string res="";
foreach(var go in gameObjects)
{
if(go.layer==20)
{
//Debug.Log(go);
string str = Formatting(go);
res += str;
}
}
return res;
}

static public string Formatting(GameObject go)
{
string res = go.GetComponent<Identify>().ID;
res += "%" + go.transform.position;
res += "%" + go.transform.rotation;
if(go.GetComponent<IUnit>()!=null)
{
res += "%" + go.GetComponent<IUnit>().isCultivated;
}
return res+"\n";
}
}


在Unity3D中实现存档系统
http://zhouhf.top/2022/09/05/在Unity3D中实现存档系统/
作者
周洪锋
发布于
2022年9月5日
许可协议