【Unity游戏存档系统】
《游戏存档系统的实现与设计》
在游戏开发中,存档系统是至关重要的功能模块之一,它允许玩家保存游戏进度,随时退出游戏并在后续继续游戏。一个良好的存档系统不仅能提升玩家体验,还能为游戏增添更多乐趣与沉浸感。本文将深入剖析一个基于 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 和二进制),还能方便地进行扩展,以满足不同类型游戏的存档需求。