引言

在游戏开发的世界中,存档系统是不可或缺的一部分,它让玩家的游戏进度得以保存,增强了游戏的可玩性和用户体验。今天分享一下如何在Unity中实现游戏存档的功能,特别是PlayerPrefsJSON存档这两种常见方式,帮助开发者理解它们的适用场景、优缺点以及如何有效使用它们来构建强大的存档机制。

随着游戏复杂度的提升,存档系统的设计和实现变得尤为重要。合理的存档策略能够显著提升玩家的满意度和忠诚度。然而,Unity自带的PlayerPrefs虽然简单易用,但在处理复杂数据或大量数据时显得力不从心。而JSON作为一种轻量级的数据交换格式,因其易于人阅读和编写,以及良好的跨平台特性,在游戏存档中得到了广泛应用。因此,本文将简单介绍这两种方法,帮助开发者根据项目需求做出最佳选择。

Unity存档系统的用途

  • 保存游戏进度:让玩家能够在退出游戏后重新加载之前的游戏状态。
  • 设置和配置保存:存储玩家的游戏设置,如音量、难度等。
  • 成就和解锁内容:记录玩家的成就和已解锁的内容,作为游戏进度的一部分。
  • 跨平台同步:在支持云存档的情况下,实现不同设备间的游戏进度同步。

PlayerPrefs

PlayerPrefs存储数据

PlayerPrefs是Unity提供的一个简单的数据持久化系统,用于保存和加载玩家偏好设置和游戏数据。它仅支持存储和检索整型(int)浮点型(float)字符串(string)数据。

Unity的PlayerPrefs是Unity引擎提供的一个用于存储和检索玩家偏好设置和游戏状态数据的简单数据持久化系统。它允许开发者跨会话(即游戏运行之间)保存和加载简单的数据类型,如整型(int)、浮点型(float)和字符串(string)。这些数据通常存储在设备的本地存储中,如注册表(在Windows上)或偏好设置文件(在macOS和iOS上)等,具体取决于目标平台。

PlayerPrefs的特点

  1. 简单易用PlayerPrefs API 非常直观,易于上手。只需几行代码就可以实现数据的保存和加载。
  2. 跨平台PlayerPrefs 数据在不同的Unity支持平台上具有高度的可移植性。无论游戏在哪个平台上运行,PlayerPrefs 都能以相同的方式工作。
  3. 限制:尽管PlayerPrefs 非常方便,但它也有一些限制。首先,它只支持整型、浮点型和字符串数据类型。其次,由于数据存储在一个统一的命名空间中,不同游戏或应用可能会无意中覆盖彼此的PlayerPrefs 数据(尽管这可以通过在键名中包含唯一标识符来避免)。最后,PlayerPrefs 数据的存储量也有限制,虽然这个限制对于大多数用途来说都足够大,但在处理大量数据时可能会成为问题。
优点缺点
使用简单,无需额外配置。数据类型有限,不支持复杂数据结构(如数组、列表、字典等)。
跨平台兼容性好,适用于大多数Unity支持的平台。存储容量有限,不适合存储大量数据。
安全性较低,数据容易被用户修改。

PlayerPrefs的基本用法

保存数据

使用PlayerPrefs.SetIntPlayerPrefs.SetFloatPlayerPrefs.SetString等静态方法将数据保存到PlayerPrefs中。调用PlayerPrefs.Save()可以手动保存更改,但通常这不是必需的,因为Unity会在游戏结束时自动保存PlayerPrefs数据。

1
2
3
public static void SetInt(string key, int value); // key 键 value 值
public static void SetFloat(string key, float value);
public static void SetString(string key, string value);
1
PlayerPrefs.SetInt("score", 50);

加载数据

使用PlayerPrefs.GetIntPlayerPrefs.GetFloatPlayerPrefs.GetString等静态方法从PlayerPrefs中检索数据。这些方法需要一个键名作为参数,并返回与该键名关联的数据。如果指定的键名不存在,则可以提供一个默认值作为第二个参数。

1
2
3
public static int GetInt(string key);
public static float GetFloat(string key);
public static string GetString(string key);
1
2
int highScore = PlayerPrefs.GetInt("HighScore", 0); // 如果HighScore不存在,则返回0
string username = PlayerPrefs.GetString("Username", "Guest"); // 如果Username不存在,则返回"Guest"

删除数据

使用PlayerPrefs.DeleteKey方法可以删除PlayerPrefs中的单个键值对。PlayerPrefs.DeleteAll方法会删除PlayerPrefs中的所有数据。

1
2
3
PlayerPrefs.DeleteKey("HighScore"); // 删除HighScore键值对
// 或者
PlayerPrefs.DeleteAll(); // 删除所有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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using UnityEngine;

public class PlayerData : MonoBehaviour
{
public string playerName; // 角色名称
public float playerHealth; // 角色血量
public Vector3 playerPosition; // 角色位置
public int Score; // 得分

void Awake()
{
LoadFromPlayerPrefs(); // 游戏开始时加载调用存储的数据
}

// 当保存并退出按钮被触发时执行
public void SaveAndBack()
{
// 其他代码
SaveByPlayerPrefs(); // 保存数据
}

// 保存数据
void SaveByPlayerPrefs()
{
PlayerPrefs.SetString("PlayerName", playerName);
PlayerPrefs.SetFloat("PlayerHealth", playerHealth);
PlayerPrefs.Setint("Score", Score);
PlayerPrefs.SetFloat("PlayerPo_x", playerPosition.x);
PlayerPrefs.SetFloat("PlayerPo_y", playerPosition.y);
PlayerPrefs.SetFloat("PlayerPo_z", playerPosition.z);
PlayerPrefs.Save();
}

// 加载数据
void LoadFromPlayerPrefs()
{
playerName = PlayerPrefs.GetString("PlayerName", "None");
playerHealth = PlayerPrefs.GetFloat("PlayerHealth", 100f);
Score = PlayerPrefs.Getint("Score", 0);
playerPosition = new(PlayerPrefs.GetFloat("PlayerPo_x", 0f),
PlayerPrefs.GetFloat("PlayerPo_y", 0f),
PlayerPrefs.GetFloat("PlayerPo_z", 0f));
}

}

注意事项

  • 由于PlayerPrefs数据存储在设备的本地存储中,因此它可能受到设备存储空间或用户隐私设置的影响。
  • 在使用PlayerPrefs时,建议为键名添加前缀或后缀,以确保它们在不同游戏或应用之间保持唯一性。
  • 虽然PlayerPrefs提供了基本的数据持久化功能,但对于需要处理复杂数据结构或大量数据的游戏来说,可能需要考虑使用更高级的数据存储解决方案,如数据库或文件系统。

适用范围

PlayerPrefs适合用来存储暂时性数据,如:

  • 玩家设定偏好
  • 简单的数据
  • 游戏原型制作时暂时的存储方案
总之,PlayerPrefs设计的初衷就不是为了真正的玩家存档而服务的,但它有其存在的理由,如何使用好这个工具,才是我们作为游戏制作者应该掌握的关键。

相关资料 -> https://docs.unity3d.com/ScriptReference/PlayerPrefs.html

JSON

JSONJavaScript Object Notation(JavaScript对象注释/表示法)的简称,是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。在Unity中,我们可以通过序列化/反序列化对象到JSON字符串的方式来实现存档功能。

参考资料 -> https://www.json.org/json-zh.html

格式

JSON是一种基于文本的格式,用于表示数据。它使用键值对来表示对象,使用数组来表示一组值。例如,一个表示玩家的JSON对象可能如下所示:

1
2
3
4
5
6
7
8
9
{
"name": "张三",
"level": 10,
"gold": 1000,
"equipment": [
{"name": "剑", "type": "武器"},
{"name": "盾", "type": "防具"}
]
}

特点

优点

  • 支持复杂数据结构,如数组、列表、字典等。
  • 数据量大小无限制(受限于系统存储空间)。
  • 安全性较高,可通过加密等手段保护数据。
  • 跨平台兼容性好,JSON是标准的文本格式,易于在不同平台间交换数据。

缺点

  • 需要编写额外的序列化/反序列化代码。
  • 相对于PlayerPrefs,使用上稍显复杂。

与PlayerPrefs对比

  • JSON更适合存储复杂数据结构和大量数据。
  • JSON提供了更高的灵活性和可扩展性。
  • 在处理简单数据类型和少量数据时,PlayerPrefs可能更便捷。

JsonUtility

在Unity中,要将对象保存为JSON字符串,需要对其进行序列化;同样,要从JSON字符串恢复对象,需要对其进行反序列化。Unity提供了JsonUtility类来简化这一过程。

  • 序列化:使用JsonUtility.ToJson()方法将对象转换为JSON字符串。
  • 反序列化:使用JsonUtility.FromJson<T>()方法将JSON字符串转换回对象。

使用JsonUtility进行存档

定义可序列化的类

在Unity中,只有标记为[Serializable]的类才能被JsonUtility序列化。因此,需要为要存档的数据定义可序列化的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Serializable]
public class PlayerData
{
public string name;
public int level;
public int gold;
public List<Equipment> equipment;
}

[Serializable]
public class Equipment
{
public string name;
public string type;
}

需要注意,并不是所有数据都可以被正常的序列化:

序列化并保存数据

在Unity中,可以通过以下步骤将PlayerData对象序列化为JSON字符串,并保存到文件中:

1
2
3
4
5
6
7
8
9
public void SavePlayerData(PlayerData playerData, string fileName)
{
// 将数据序列化
string json = JsonUtility.ToJson(playerData);
// 获取存储文件地址
string filePath = Path.Combine(Application.persistentDataPath, fileName + ".json");
// 将JSON写入文件中
File.WriteAllText(filePath, json);
}

读取并反序列化数据

当需要加载存档时,可以从文件中读取JSON字符串,并使用JsonUtility.FromJson<T>()方法将其反序列化为PlayerData对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public PlayerData LoadPlayerData(string fileName)
{
// 获取存储文件地址
string filePath = Path.Combine(Application.persistentDataPath, fileName + ".json");
// 判断是否有对应文件
if (File.Exists(filePath))
{
// 读取JSON字符串
string json = File.ReadAllText(filePath);
// 将JSON反序列化
PlayerData playerData = JsonUtility.FromJson<PlayerData>(json);
return playerData;
}
else
{
// 没有文件打印提示
Debug.LogError($"File {filePath} not found!");
return null;
}
}

示例

首先创建一个公有的静态类:

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.IO;
using UnityEngine;

namespace SaveSystemTutorial
{
public static class SaveSystem
{
// 存档
public static void SaveByJson(string saveFileName, object data)
{
var json = JsonUtility.ToJson(data);
var path = Path.Combine(Application.persistentDataPath, saveFileName);
/* Application.persistentDataPath用来提供一个存储永久数据的路径,并且
当我们将游戏打包发布在不同平台上时,这个路径会随着我们发布的平台自动变更 */

Debug.Log($"save data to {path}."); // 这里我们可以输出这个路径找到文件

File.WriteAllText(path, json);
}

// 读档
public static T LoadFromJson<T>(string saveFileName)
{
var path = Path.Combine(Application.persistentDataPath, saveFileName);

var json = File.ReadAllText(path);
var data = JsonUtility.FromJson<T>(json);

return data;
}

// 删除
public static void DeleteSaveFile(string saveFileName)
{
var path = Path.Combine(Application.persistentDataPath, saveFileName);

File.Delete(path);
}
}
}

再其他脚本中使用SaveSystemTutorial命名空间并实现存储数据函数:

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
using UnityEngine;
using SaveSystemTutorial;

public class Player : MonoBehaviour
{
public string name; // 角色名称
public float health; // 角色血量
public Vector3 position; // 角色位置
public int score; // 得分
public List<GameObject> enermy; // 敌人列表

[System.Serializable]
class SaveData
{
public string playerName; // 角色名称
public float playerHealth; // 角色血量
public Vector3 playerPosition; // 角色位置
public int Score; // 得分
public List<GameObject> Enermy; // 敌人列表
}

// 存储数据
public void SaveAndBack()
{
SaveData data = new()
{
playerName = name;
playerHealth = health;
playerPosition = position;
Score = score;
Enermy = enermy;
};

SaveSystem.SaveByJson("player.save", data);
}

// 调用数据
public void LoadData()
{
// 获取文件地址判断文件是否存在
var Path = Application.persistentDataPath + "\\player.save";
SaveData data;
if (File.Exists(HistoryPath))
{
data = SaveSystem.LoadFromJson<SaveData>("player.save");

// 将数据赋回
name = data.playerName;
health = data.playerHealth;
position = data.playerPosition;
score = data.Score;
enermy = data.Enermy;
}
else Debug.Log("Not Found!");
}
}

之后通过输出的文件路径就可以找到我们的存档啦:

注意事项

  1. 数据安全性:直接以明文形式保存JSON文件可能会带来数据泄露的风险。在需要保护用户数据的情况下,应考虑对JSON数据进行加密。
  2. 跨平台兼容性Application.persistentDataPath会自动处理不同平台上的文件路径差异,使得存档系统能够跨平台工作。
  3. 性能考虑:对于大型游戏或频繁存档的场景,需要关注序列化/反序列化操作对性能的影响。
  4. 错误处理:在文件读写过程中,应添加适当的错误处理逻辑,以应对文件不存在、磁盘空间不足等异常情况。

使用JSON文件而不是PlayerPrefs的原因

前面提到过PlayerPrefs可以存储字符串类型的数据,而JSON数据实际上就是字符串,所以PlayerPrefs实际上也可以存储JSON数据。

不建议用PlayerPrefs存JSON的原因主要有以下几点:

存储效率与灵活性

PlayerPrefs的存储方式相对简单直接,但这也限制了其存储效率和灵活性。当需要存储大量数据时,PlayerPrefs可能会因为频繁地读写磁盘而导致性能下降。此外,由于PlayerPrefs只支持简单的数据类型,因此在处理复杂数据结构时,往往需要将数据转换为字符串进行存储,这在读取时又需要反序列化回原始数据结构,增加了额外的处理步骤和可能的性能开销。相比之下,直接使用JSON文件或其他数据库系统存储复杂数据,可以更加高效和灵活地管理数据。

安全性问题

虽然PlayerPrefs本身在数据安全方面并没有直接的漏洞,但由于其存储的数据类型简单且易于访问,因此如果游戏或应用程序中涉及敏感信息(如用户密码、支付信息等),则不建议使用PlayerPrefs进行存储。此外,由于PlayerPrefs的数据通常存储在用户设备的文件系统中,因此如果设备被恶意软件攻击或用户拥有足够的权限,那么存储在PlayerPrefs中的数据就有可能被非法读取或篡改。对于需要保护用户数据安全的场景,建议使用更加安全的数据存储方案。

可扩展性和可维护性

随着游戏或应用程序的不断发展,可能需要存储的数据量也会不断增加,数据结构也会变得更加复杂。如果一直使用PlayerPrefs来存储这些数据,那么随着数据的增加和数据结构的复杂化,代码的可读性、可维护性和可扩展性都会受到影响。相比之下,使用JSON文件或其他数据库系统来存储数据,可以更加方便地管理数据结构和数据变更,提高代码的可读性、可维护性和可扩展性。

所以,虽然PlayerPrefs在存储简单数据方面具有一定的优势,但在需要处理复杂数据结构、大量数据或敏感信息时,建议使用更加高效、灵活和安全的数据存储方案。对于JSON数据的存储和读取,建议使用Unity内置的JsonUtility类或其他第三方JSON库来实现序列化和反序列化操作,并将数据存储在文件系统或数据库中以便后续管理和使用。

适用范围

联网

  • 优秀的网络数据交换载体
  • 云存档

本地存储

  • 非敏感而需要大量读取的数据,如:Mod数据
  • 玩家的偏好设置等

相关资料 -> https://docs.unity3d.com/ScriptReference/JsonUtility.html

尾声

通过本文章的介绍,我们深入了解了Unity中的两种主要存档方式——PlayerPrefs和JSON存档。每种方法都有其独特的优势和适用场景。开发者应根据项目的具体需求、数据复杂度以及目标平台的特性来选择最合适的存档策略。无论是追求简单快捷的PlayerPrefs,还是注重数据复杂性和安全性的JSON存档,都能在Unity中找到实现方法,为玩家带来更加流畅和丰富的游戏体验。

—end—