《游戏存档系统的实现与设计》
在游戏开发中,存档系统是至关重要的功能模块之一,它允许玩家保存游戏进度,随时退出游戏并在后续继续游戏。一个良好的存档系统不仅能提升玩家体验,还能为游戏增添更多乐趣与沉浸感。本文将深入剖析一个基于 C# 的游戏存档系统的设计与实现,从设计思路到代码实现,再到实际使用,全方位进行讲解,即使是初学者也能轻松理解和上手。
一、设计思路
(一)系统架构
构建一个通用且灵活的游戏存档系统,采用分层架构,将系统划分为以下几个关键模块:
- 存档数据管理模块 :由
GameData类负责,它作为存档数据的载体,定义了存档中包含的各种游戏状态信息,如存档名称、存档时间、当前场景名称、游戏内货币以及物品库存等。 - 存档操作接口模块 :定义
ISaveManager接口和ISaveSystem接口。ISaveManager用于规范游戏内各个需要存档功能的组件的行为,规定它们必须实现LoadData和SaveData方法,以便将自身数据存入或从GameData中读取数据。ISaveSystem则抽象出存档系统的共性操作,包括保存数据到指定路径和从指定路径加载数据,为后续具体存档格式的实现提供统一规范。 - 数据处理器模块 :
DataHandler类作为数据处理的核心,它负责管理存档目录、与具体的存档系统交互,完成游戏数据的保存、加载以及存档的删除操作。 - 存档系统实现模块 :提供两种具体存档实现方式,
BinarySaveSystem类实现二进制格式存档,JsonSaveSystem类实现 JSON 格式存档(且支持加密),它们都遵循ISaveSystem接口的规范。 - 存档管理协调模块 :
SaveManager类作为整个存档系统的中央管理器,以单例模式存在,负责协调存档的创建、保存、加载和删除操作,它整合上述各个模块,使整个存档系统高效、有序地运行。
(二)设计原则
- 单一职责原则 :每个类或接口都有明确且单一的职责,例如
GameData仅负责存储存档数据,ISaveSystem仅定义存档操作规范,DataHandler专注数据的读写处理等,这样使得系统结构清晰,便于维护和扩展。 - 开闭原则 :通过定义
ISaveSystem接口,在不修改现有代码的基础上,可以轻松添加新的存档格式实现,如后续若要引入 XML 格式存档,只需新增一个实现该接口的 XML 存档类即可,增强了系统的可扩展性。 - 依赖倒置原则 :高层模块(如
SaveManager)不依赖于低层模块(如具体的BinarySaveSystem或JsonSaveSystem)的具体实现,而是依赖于ISaveSystem接口,这样降低了模块之间的耦合度,提高了系统的灵活性。
二、类图展示
以下是该游戏存档系统的类图:
三、代码实现详解
(一)存档数据管理模块(GameData)
using System;
using System.Collections.Generic;
/// <summary>
/// 存档数据结构,包含游戏状态信息
/// </summary>
[Serializable]
public class GameData
{
// 存档名称
public string saveName;
// 存档时间
public string saveTime;
// 当前场景名称
public string levelName;
// 游戏内货币
public int currency;
// 物品库存
public List<string> inventory;
public GameData()
{
// 初始化存档时间
saveTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
// 初始化物品库存
inventory = new List<string>();
}
}
GameData 类通过 [Serializable] 特性标记,使其具备序列化能力,以便能够将其中的数据转换为特定格式(如 JSON 或二进制)进行存储。其构造函数初始化了存档时间和物品库存列表,为每个新创建的存档提供基础数据结构。
(二)存档操作接口模块(ISaveManager 和 ISaveSystem)
1. ISaveManager
public interface ISaveManager
{
void LoadData(GameData data);
void SaveData(GameData data);
}
ISaveManager 接口规定了实现该接口的类必须具备加载数据(LoadData)和保存数据(SaveData)的方法,游戏内需要存档功能的各个组件(如玩家属性组件、场景管理组件等)可以通过实现此接口,在存档过程中将自身数据整合到 GameData 中或从 GameData 中恢复自身数据。
2. ISaveSystem
using System;
using System.IO;
/// <summary>
/// 保存系统的接口
/// </summary>
public interface ISaveSystem
{
// 保存数据到指定路径
void Save<T>(T data, string savePath) where T : class;
// 从指定路径加载数据
T Load<T>(string savePath) where T : class;
}
ISaveSystem 接口定义了存档系统必须实现的保存(Save)和加载(Load)操作,其中泛型的使用使得存档系统能够灵活处理不同类型的数据对象,只要是类类型(where T : class)即可。
(三)数据处理器模块(DataHandler)
using System;
using System.IO;
using UnityEngine;
/// <summary>
/// 负责存档数据的读写操作,支持加密和文件管理
/// </summary>
public class DataHandler
{
// 存档目录路径
private string saveDirectory;
// 存档系统实现
private ISaveSystem saveSystem;
public DataHandler(string saveDirectory, ISaveSystem saveSystem)
{
this.saveDirectory = saveDirectory;
this.saveSystem = saveSystem;
// 确保存档目录存在
if (!Directory.Exists(saveDirectory))
{
Directory.CreateDirectory(saveDirectory);
}
}
/// <summary>
/// 保存游戏数据
/// </summary>
/// <param name="data"></param>
/// <param name="slotName"></param>
public void Save<T>(T data, string slotName, string fileExtension) where T : class
{
string savePath = Path.Combine(saveDirectory, $"{slotName}{fileExtension}");
saveSystem.Save(data, savePath);
}
/// <summary>
/// 加载游戏数据
/// </summary>
/// <param name="slotName"></param>
/// <returns></returns>
public T Load<T>(string slotName, string fileExtension) where T : class
{
string savePath = Path.Combine(saveDirectory, $"{slotName}{fileExtension}");
return saveSystem.Load<T>(savePath);
}
/// <summary>
/// 删除存档
/// </summary>
/// <param name="slotName"></param>
public void Delete(string slotName, string fileExtension)
{
string savePath = Path.Combine(saveDirectory, $"{slotName}{fileExtension}");
if (File.Exists(savePath))
{
File.Delete(savePath);
Debug.Log($"存档 {slotName} 删除成功!");
}
else
{
Debug.Log($"没有找到存档 {slotName}!");
}
}
}
DataHandler 类接收存档目录路径和具体存档系统实现作为参数进行初始化,并确保存档目录存在。它的 Save 方法根据传入的数据、存档槽位名称和文件扩展名构建完整存档路径,调用存档系统的保存方法完成数据存储;Load 方法同样依据存档槽位名称和文件扩展名构建路径,利用存档系统的加载方法读取数据并返回;Delete 方法负责删除指定的存档文件,通过检查文件是否存在来决定是执行删除操作还是提示找不到存档。
(四)存档系统实现模块
1. BinarySaveSystem
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
public class BinarySaveSystem : ISaveSystem
{
public void Save<T>(T data, string savePath) where T : class
{
try
{
using (FileStream fileStream = new FileStream(savePath, FileMode.Create))
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(fileStream, data);
Debug.Log("二进制存档成功!");
}
}
catch (Exception e)
{
Debug.LogError("二进制存档失败:" + e.Message);
}
}
public T Load<T>(string savePath) where T : class
{
try
{
if (File.Exists(savePath))
{
using (FileStream fileStream = new FileStream(savePath, FileMode.Open))
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
return (T)binaryFormatter.Deserialize(fileStream);
}
}
else
{
Debug.Log("没有找到二进制存档文件!");
return null;
}
}
catch (Exception e)
{
Debug.LogError("二进制加载失败:" + e.Message);
return null;
}
}
}
BinarySaveSystem 类实现了 ISaveSystem 接口,采用二进制格式进行存档。在 Save 方法中,利用 BinaryFormatter 将传入的数据对象序列化,并写入到指定的文件流中;Load 方法则是检查文件存在性后,通过 BinaryFormatter 从文件流中反序列化出数据对象并返回,过程中添加了异常捕获,以应对可能出现的文件操作错误等情况,并给出相应的调试信息提示。
2. JsonSaveSystem
using UnityEngine;
using System;
using System.IO;
public class JsonSaveSystem : ISaveSystem
{
private string encryptionKey = "your-encryption-key-1234567890123456";
private string encryptionIV = "your-iv-12345678";
public void Save<T>(T data, string savePath) where T : class
{
try
{
string json = JsonUtility.ToJson(data, true);
string encryptedJson = AESUtility.Encrypt(json, encryptionKey, encryptionIV);
File.WriteAllText(savePath, encryptedJson);
Debug.Log("加密后的 JSON 存档成功!");
}
catch (Exception e)
{
Debug.LogError("加密存档失败:" + e.Message);
}
}
public T Load<T>(string savePath) where T : class
{
try
{
if (File.Exists(savePath))
{
string encryptedJson = File.ReadAllText(savePath);
string json = AESUtility.Decrypt(encryptedJson, encryptionKey, encryptionIV);
return JsonUtility.FromJson<T>(json);
}
else
{
Debug.Log("没有找到加密的 JSON 存档文件!");
return null;
}
}
catch (Exception e)
{
Debug.LogError("加密存档加载失败:" + e.Message);
return null;
}
}
}
JsonSaveSystem 类同样遵循 ISaveSystem 接口规范,采用 JSON 格式存档且支持加密。Save 方法先利用 Unity 提供的 JsonUtility.ToJson 方法将数据对象转换为 JSON 字符串,然后调用 AESUtility.Encrypt 方法(后文会提及该工具类)对 JSON 字符串进行加密,最后将加密后的字符串写入指定文件。Load 方法则是先读取文件内容,解密后使用 JsonUtility.FromJson 方法将 JSON 字符串还原为对应的数据对象,并且在整个操作流程中加入了异常处理和调试信息输出。
(五)存档管理协调模块(SaveManager)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>
/// 存档系统的中央管理器,协调存档的创建、保存、加载和删除操作
/// </summary>
public class SaveManager : MonoBehaviour
{
// 单例模式,确保只有一个存档管理器实例
public static SaveManager Instance;
// 数据处理器
public DataHandler dataHandler;
public List<ISaveManager> saveTargets;
public bool EncryptData = true;
// 用于选择存档格式
[Tooltip("选择存档格式")] // 添加悬停提示
public enum SaveFormat { Json, Binary }
public SaveFormat saveFormat = SaveFormat.Json;
private void Awake()
{
// 单例模式初始化
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
// 初始化存档系统
string saveDirectory = Path.Combine(Application.persistentDataPath, "Saves");
ISaveSystem saveSystem;
// 根据 saveFormat 选择存档系统
if (saveFormat == SaveFormat.Json)
{
saveSystem = new JsonSaveSystem();
}
else
{
saveSystem = new BinarySaveSystem();
}
dataHandler = new DataHandler(saveDirectory, saveSystem);
// 自动查找所有实现 ISaveManager 的组件
saveTargets = FindObjectsOfType<MonoBehaviour>(true).OfType<ISaveManager>().ToList();
}
// 保存游戏
public void SaveGame(string slotName)
{
GameData gameData = new GameData();
foreach (var saveTarget in saveTargets)
{
saveTarget.SaveData(gameData);
}
// 根据 saveFormat 设置文件扩展名
string fileExtension = saveFormat == SaveFormat.Json ? ".json" : ".bin";
dataHandler.Save(gameData, slotName, fileExtension);
}
// 加载游戏
public void LoadGame(string slotName)
{
// 根据 saveFormat 设置文件扩展名
string fileExtension = saveFormat == SaveFormat.Json ? ".json" : ".bin";
GameData gameData = dataHandler.Load<GameData>(slotName, fileExtension);
foreach (var saveTarget in saveTargets)
{
saveTarget.LoadData(gameData);
}
}
// 删除存档
public void DeleteGame(string slotName)
{
// 调用数据处理器删除存档
// 根据 saveFormat 设置文件扩展名
string fileExtension = saveFormat == SaveFormat.Json ? ".json" : ".bin";
dataHandler.Delete(slotName, fileExtension);
}
}
SaveManager 类以单例模式存在,确保整个游戏过程中只有一个存档管理器实例在运行,方便全局调用。在 Awake 方法中完成单例初始化、存档系统的初始化(根据选择的存档格式创建对应的存档系统对象,并初始化数据处理器),以及自动查找游戏内所有实现了 ISaveManager 接口的组件,将它们添加到 saveTargets 列表中,以便在存档和加载时统一协调这些组件的数据操作。
SaveGame 方法创建一个新的 GameData 对象,然后依次调用 saveTargets 中各个组件的 SaveData 方法,将它们的数据汇总到 GameData 中,接着根据选择的存档格式确定文件扩展名,借助数据处理器完成存档数据的保存。
LoadGame 方法依据存档格式构建文件扩展名,通过数据处理器加载对应的存档数据到 GameData 对象中,再循环调用 saveTargets 中各组件的 LoadData 方法,使它们从 GameData 中恢复自身数据,实现游戏状态的加载。
DeleteGame 方法同样是根据存档格式确定文件扩展名后,调用数据处理器的删除方法来移除指定的存档文件。
(六)加密工具模块(AESUtility)
using System;
using System.IO;
using System.Security.Cryptography; // 引入 AES 加密算法相关类
using System.Text;
/// <summary>
/// AES 加密工具
/// </summary>
public class AESUtility
{
/// <summary>
/// AES 加密方法
/// </summary>
/// <param name="plainText">需要加密的原始字符串(明文)</param>
/// <param name="key">加密使用的密钥(需与解密密钥相同)</param>
/// <param name="iv">初始化向量(增加加密随机性,需与解密 IV 相同)</param>
/// <returns>Base64 编码的加密字符串(密文)</returns>
public static string Encrypt(string plainText, string key, string iv)
{
using (Aes aes = Aes.Create()) // 创建 AES 加密对象
{
// 确保秘钥和 IV 的长度为 16 字节
aes.Key = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32)); // 密钥长度为 32 字节(256 位)
aes.IV = Encoding.UTF8.GetBytes(iv.PadRight(16).Substring(0, 16)); // IV 长度为 16 字节(128 位)
// 创建加密器
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using (MemoryStream ms = new MemoryStream()) // 创建内存流用于存储加密数据
{
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
// 创建加密流,将数据写入内存流时进行加密
{
using (StreamWriter sw = new StreamWriter(cs)) // 创建写入器,将字符串写入加密流
{
sw.Write(plainText); // 将明文写入加密流
}
// 将加密后的字节数组转换为 Base64 字符串(便于存储和传输)
return Convert.ToBase64String(ms.ToArray());
}
}
}
}
/// <summary>
/// AES 解密方法
/// </summary>
/// <param name="cipherText">需要解密的 Base64 编码字符串(密文)</param>
/// <param name="key">初始化向量(需与加密 IV 相同)</param>
/// <param name="iv">解密后的原始字符串(明文)</param>
/// <returns></returns>
public static string Decrypt(string cipherText, string key, string iv)
{
using (Aes aes = Aes.Create()) // 创建 AES 解密对象
{
// 确保密钥和 IV 的长度为 16 字节
aes.Key = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32)); // 密钥长度为 32 字节(256 位)
aes.IV = Encoding.UTF8.GetBytes(iv.PadRight(16).Substring(0, 16)); // IV 长度为 16 字节(128 位)
// 创建解密器
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(cipherText)))
// 将 Base64 字符串转换为字节数组,并创建内存流
{
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
// 创建解密流,从内存流读取时进行解密
{
using (StreamReader sr = new StreamReader(cs)) // 创建读取器,从解密流读取明文
{
return sr.ReadToEnd(); // 返回解密后的字符串
}
}
}
}
}
}
AESUtility 类提供了 AES 加密和解密功能。Encrypt 方法中,先根据传入的密钥和初始化向量(IV)生成符合 AES 算法要求长度的密钥和 IV,然后创建加密器,利用内存流和加密流将明文数据进行加密,并最终将加密后的字节数组转换为 Base64 编码字符串,便于存储和传输。Decrypt 方法则是逆过程,将 Base64 编码的密文字符串转换回字节数组,通过解密流和内存流进行解密,还原出原始的明文字符串。该工具类为 JSON 格式存档的加密功能提供了底层支持,保障了存档数据的安全性。
四、实际使用示例
(一)设置存档格式与初始化
在 Unity 编辑器中,将 SaveManager 脚本挂载到一个空的游戏对象上。然后在Inspector 面板中,根据实际需求选择存档格式(JSON 或二进制),通常对于需要数据安全性的场景(如防止玩家篡改存档),选择 JSON 格式并开启加密功能较为合适;若追求存档效率和文件大小(如存档数据量较大且对安全性要求不高),二进制格式是不错的选择。
(二)为游戏组件添加存档功能
对于游戏内需要存档功能的组件,例如玩家属性组件(管理玩家生命值、经验值、等级等)、背包组件(管理玩家携带的物品)等,让它们实现 ISaveManager 接口。以下以一个简单的玩家属性组件为例:
using UnityEngine;
public class PlayerAttributes : MonoBehaviour, ISaveManager
{
public int health;
public int experience;
public int level;
public void LoadData(GameData data)
{
// 从 GameData 中恢复玩家属性数据
if (data != null)
{
health = data.currency; // 假设此处存档时将生命值存储在 currency 字段,需根据实际设计调整
experience = data.inventory.Count; // 同样,仅为示例,实际应与存档时对应
level = data.levelName.Length; // 示例,实际应合理映射
}
}
public void SaveData(GameData data)
{
// 将玩家属性数据保存到 GameData 中
data.currency = health;
data.inventory.Add(experience.ToString());
data.levelName = level.ToString();
}
}
在上述示例中,PlayerAttributes 类实现了 ISaveManager 接口,在 SaveData 方法中将自身的玩家属性数据(生命值、经验值、等级)存储到 GameData 的相应字段中;在 LoadData 方法中则从 GameData 中读取这些数据,恢复玩家属性。需要注意的是,此处仅为示例,实际项目中应根据具体的设计合理安排数据的存储和读取逻辑,确保数据的一致性和准确性。
(三)存档与加载操作
-
存档操作 :在游戏过程中,当玩家希望保存游戏进度时,可以通过调用
SaveManager.Instance.SaveGame("slot1");这样的代码来实现存档,其中"slot1"是存档槽位名称,可以根据需要自定义,如"slot2"、"autosave"等。SaveManager会协调各个实现了ISaveManager接口的组件,将它们的数据汇总到GameData中,并根据设置的存档格式将数据保存到对应的文件中。 -
加载操作 :当玩家想要继续之前的游戏进度时,使用
SaveManager.Instance.LoadGame("slot1");来加载存档,SaveManager会依据存档格式从对应文件中读取数据到GameData,然后将这些数据分发给各个组件,使它们恢复到存档时的状态,继续游戏。 -
删除存档操作 :若玩家不再需要某个存档,可以执行
SaveManager.Instance.DeleteGame("slot1");,这样就能将对应的存档文件从存储设备中删除,释放存储空间。
五、总结与展望
本文详细介绍了基于 C# 的游戏存档系统的设计与实现,从系统架构的搭建、各个模块的划分以及核心类的代码实现,到实际的使用示例,全方位展示了如何构建一个通用、灵活且安全的游戏存档系统。通过合理的接口设计和模块划分,该系统不仅支持多种存档格式(JSON 和二进制),还能方便地进行扩展,以满足不同类型游戏的存档需求。


被折叠的 条评论
为什么被折叠?



