关于 unity-save-system
unity-save-system 是用于设计 Unity 游戏存档与读取系统的参考技能,比较序列化、存储、迁移、云同步和自动存档方案,并说明如何避免数据丢失及平台存储问题。
主要功能
- 01序列化格式选择
- 02存档数据与 DTO 设计
- 03文件位置和 PlayerPrefs 指南
- 04版本管理与迁移模式
- 05本地优先的云存档同步
- 06自动存档与移动端持久化
适用场景
- 01为游戏存档数据选择格式
- 02按游戏系统组织存档文件
- 03数据结构变更后迁移旧存档
- 04将本地存档同步到云端
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-save-system安装后在 Claude Code 中输入/unity-save-system确认已加载。
附带文件references/save-load-implementation.md
详细介绍
存档/加载系统——决策模式
前置技能:
unity-lifecycle(OnApplicationPause、OnApplicationQuit、退出顺序)、unity-data-driven(JSON 序列化、版本控制)、unity-packages-services(Cloud Save API)、unity-async-patterns(BackgroundThreadAsync)
这些模式旨在解决最常见的存档系统故障:Claude 什么都用 PlayerPrefs,生成脆弱且没有版本控制的序列化代码,并忽略移动平台特有的持久化要求。
模式:序列化格式选择
适用场景:选择如何将存档数据序列化到磁盘
决策:
- JSON(
JsonUtility)——可读性强,便于调试,存档文件较小。不支持字典/多态。适合大多数游戏,是最佳默认选择。 - JSON(Newtonsoft)——支持完整的 JSON 功能(字典、多态、LINQ)。依赖项稍大。适用于存档数据较复杂的情况。
- 二进制(自定义或 MessagePack)——适用于对性能要求高、存档较大或需要防作弊(更难编辑)的场景。不可读。适合竞技类游戏或规模非常庞大的世界。
- BinaryFormatter——绝对不要使用。存在安全漏洞(反序列化时可执行任意代码)。Unity 和 Microsoft 已将其弃用。
代码框架(JSON 存档/加载):
using System.IO;
using UnityEngine;
public static class SaveFileIO
{
public static void SaveJson<T>(T data, string fileName)
{
string path = GetSavePath(fileName);
string json = JsonUtility.ToJson(data, prettyPrint: true);
// Write to temp file first, then rename (atomic write)
string tempPath = path + ".tmp";
File.WriteAllText(tempPath, json);
if (File.Exists(path))
File.Replace(tempPath, path, path + ".bak");
else
File.Move(tempPath, path);
}
public static T LoadJson<T>(string fileName) where T : new()
{
string path = GetSavePath(fileName);
if (!File.Exists(path)) return new T();
string json = File.ReadAllText(path);
return JsonUtility.FromJson<T>(json);
}
public static bool SaveExists(string fileName) =>
File.Exists(GetSavePath(fileName));
public static void DeleteSave(string fileName)
{
string path = GetSavePath(fileName);
if (File.Exists(path)) File.Delete(path);
if (File.Exists(path + ".bak")) File.Delete(path + ".bak");
}
static string GetSavePath(string fileName) =>
Path.Combine(Application.persistentDataPath, fileName);
}
注意:BinaryFormatter 存在安全漏洞——反序列化可以执行任意代码。Unity 和 Microsoft 已将其弃用。如果在现有代码中发现它,请立即替换。JsonUtility 无法序列化字典——请转换为 List<SerializableKeyValue<K,V>>,或使用 Newtonsoft。原子写入模式(先写入 .tmp,再重命名)可防止应用在写入过程中崩溃导致数据损坏。
模式:存档数据架构(DTO 模式)
适用场景:决定要序列化哪些内容以及如何组织数据
决策:
- 单个整体存档——使用一个包含所有内容的
SaveData类。加载/保存是原子操作。适合加载/保存速度快的小型游戏。 - 按系统拆分存档文件——
PlayerSave.json、InventorySave.json、WorldSave.json。可部分加载、减少写入量,也更容易单独迁移各个系统。
代码框架(ISaveable 接口):
// Save Data Transfer Object -- plain C# class, NOT MonoBehaviour
[System.Serializable]
public class GameSaveData
{
public int version = 1;
public PlayerSaveData player;
public InventorySaveData inventory;
public WorldSaveData world;
}
[System.Serializable]
public class PlayerSaveData
{
public float[] position = new float[3]; // Vector3 as array for JSON compat
public int health;
public int level;
public int xp;
public void FromPlayer(Transform t, int hp, int lvl, int xp)
{
position[0] = t.position.x;
position[1] = t.position.y;
position[2] = t.position.z;
health = hp; level = lvl; this.xp = xp;
}
public Vector3 GetPosition() => new(position[0], position[1], position[2]);
}
// Systems implement ISaveable to participate in save/load
public interface ISaveable
{
void GatherSaveData(GameSaveData data); // Write state to DTO
void ApplySaveData(GameSaveData data); // Read state from DTO
}
注意:存档 DTO 必须是带有 [System.Serializable] 的普通 C# 类,不能是 MonoBehaviours 或 ScriptableObjects。它们只能包含可序列化类型(基元类型、数组、列表、嵌套的 [Serializable] 类)。Unity 类型(如 Vector3)可通过 JsonUtility 正常序列化,但使用 Newtonsoft 时则不行,除非提供自定义转换器——为确保兼容性,请使用 float[]。
模式:存档文件位置
适用场景:选择存档文件的写入位置
决策:
Application.persistentDataPath(正确的默认选择)——可写入、应用更新后仍会保留,并且适用于相应平台。所有存档数据都应使用此路径。Application.streamingAssetsPath——在构建版本中为只读。切勿写入此处。仅用于捆绑的只读数据。PlayerPrefs——仅适用于键值数据。用于设置/偏好项,不适用于游戏状态。
代码框架:
// Always use Path.Combine -- never concatenate with "/"
string savePath = Path.Combine(Application.persistentDataPath, "saves", "slot1.json");
// Ensure directory exists before writing
string dir = Path.GetDirectoryName(savePath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
注意:persistentDataPath 在不同平台上的位置各不相同:AppData/LocalLow/<company>/<product>(Windows)、~/Library/Application Support/<bundleID>(macOS)、内部存储(Android/iOS)。在 WebGL 上,它使用 IndexedDB(异步,且在隐私浏览模式下可能失败)。始终使用 Path.Combine——macOS/Linux 支持正斜杠,但 Path.Combine 可安全地跨平台使用。
模式:PlayerPrefs 作用域
适用场景:使用 PlayerPrefs 存储设置和少量持久化值
决策:
- 直接调用 PlayerPrefs —— 设置少于 10 项,项目规模较小。实现简单。
- 封装类 —— 设置达到 10 项或更多,需要类型安全、变更事件和默认值。可避免键冲突。
脚手架(设置封装类):
public static class GameSettings
{
// Prefixed keys prevent collision between systems
private const string Prefix = "settings.";
public static float MasterVolume
{
get => PlayerPrefs.GetFloat(Prefix + "masterVolume", 1f);
set { PlayerPrefs.SetFloat(Prefix + "masterVolume", value); OnSettingsChanged?.Invoke(); }
}
public static bool Fullscreen
{
get => PlayerPrefs.GetInt(Prefix + "fullscreen", 1) == 1;
set { PlayerPrefs.SetInt(Prefix + "fullscreen", value ? 1 : 0); OnSettingsChanged?.Invoke(); }
}
public static int QualityLevel
{
get => PlayerPrefs.GetInt(Prefix + "quality", QualitySettings.GetQualityLevel());
set { PlayerPrefs.SetInt(Prefix + "quality", value); OnSettingsChanged?.Invoke(); }
}
public static event Action OnSettingsChanged;
public static void Save() => PlayerPrefs.Save();
public static void ResetToDefaults()
{
PlayerPrefs.DeleteKey(Prefix + "masterVolume");
PlayerPrefs.DeleteKey(Prefix + "fullscreen");
PlayerPrefs.DeleteKey(Prefix + "quality");
OnSettingsChanged?.Invoke();
}
}
注意事项:PlayerPrefs 的键是全局字符串,没有命名空间。使用带前缀的键("audio.masterVolume"、"video.fullscreen")来避免冲突。PlayerPrefs 没有 bool 类型,应使用 int(0/1)。在 Windows 上,PlayerPrefs 会将数据存储到 Windows 注册表中,因此应避免存储较大的值。PlayerPrefs.Save() 会在 OnApplicationQuit 时自动调用,但在移动设备上进行重要变更后应手动调用(应用可能会在未退出的情况下被终止)。
模式:存档版本控制与迁移
适用场景:游戏更新后存档格式发生变化(新增、重命名或重构字段)
决策:
- 版本字段 + 顺序迁移器 —— 适用于不兼容变更。在存档中存储
int version。加载时,按迁移链执行迁移:v1 -> v2 -> v3 -> 当前版本。 - 容错读取 —— 适用于仅添加字段的变更。新字段使用默认值,已移除的字段会被忽略。
JsonUtility会自动处理。
脚手架(迁移链):
public static class SaveMigrator
{
private static readonly int CurrentVersion = 3;
public static GameSaveData LoadAndMigrate(string json)
{
// Parse version from raw JSON first
var versionCheck = JsonUtility.FromJson<VersionOnly>(json);
int version = versionCheck.version;
if (version == CurrentVersion)
return JsonUtility.FromJson<GameSaveData>(json);
// Sequential migration using Newtonsoft for raw JSON manipulation
var jObj = Newtonsoft.Json.Linq.JObject.Parse(json);
if (version < 2) MigrateV1ToV2(jObj);
if (version < 3) MigrateV2ToV3(jObj);
jObj["version"] = CurrentVersion;
return Newtonsoft.Json.JsonConvert.DeserializeObject<GameSaveData>(jObj.ToString());
}
static void MigrateV1ToV2(Newtonsoft.Json.Linq.JObject data)
{
// v2 moved "playerX/Y/Z" into "player.position" array
var player = (Newtonsoft.Json.Linq.JObject)data["player"];
if (player != null && player["playerX"] != null)
{
float x = (float)player["playerX"];
float y = (float)player["playerY"];
float z = (float)player["playerZ"];
player["position"] = new Newtonsoft.Json.Linq.JArray(x, y, z);
player.Remove("playerX"); player.Remove("playerY"); player.Remove("playerZ");
}
}
static void MigrateV2ToV3(Newtonsoft.Json.Linq.JObject data)
{
// v3 added inventory system (default to empty)
if (data["inventory"] == null)
data["inventory"] = Newtonsoft.Json.Linq.JObject.FromObject(new InventorySaveData());
}
[System.Serializable]
private class VersionOnly { public int version; }
}
注意事项:迁移必须针对原始 JSON 执行(在反序列化为新的 C# 类型之前),因为旧类型定义可能已不再存在于代码中。结构发生变化时,请使用 Newtonsoft JObject。每个迁移步骤都是一个从版本 N 转换到 N+1 的函数,并按顺序组合执行。使用各版本保存的 JSON 测试迁移(存储测试样例)。
模式:云存档集成
适用场景:存档数据需要在多台设备间同步,或需要在本地存储之外持久保存
决策:
- Unity Cloud Save(UGS) —— 托管服务,支持键值或文件存储,每位玩家可免费使用 200KB。API 参见
unity-packages-services。最适合大多数独立游戏和中型游戏。 - 自定义后端 —— 完全自主控制、存储空间不限,可自定义冲突解决方式。需要自行构建和维护。
脚手架(本地优先并与云端同步):
public class SaveManager : MonoBehaviour
{
public async Awaitable Save(GameSaveData data)
{
// Always save locally first (fast, reliable)
SaveFileIO.SaveJson(data, "savegame.json");
// Then sync to cloud in background (best-effort)
try
{
var cloudData = new Dictionary<string, object>
{
{ "savegame", JsonUtility.ToJson(data) }
};
await Unity.Services.CloudSave.CloudSaveService.Instance
.Data.Player.SaveAsync(cloudData);
}
catch (Exception e)
{
Debug.LogWarning($"Cloud save failed (local save preserved): {e.Message}");
}
}
}
注意事项:云存档必须支持离线游玩。始终先在本地保存,再在后台同步到云端。妥善处理 CloudSaveException——网络故障绝不能妨碍本地游玩或导致游戏崩溃。冲突解决方面,“最后写入者胜出”最简单;如需合并逻辑,则使用时间戳。
模式:自动保存策略
适用场景:决定何时触发保存
决策:
- 检查点式 —— 在特定游戏事件时保存(关卡完成、休息点、手动保存)。触发明确,由设计师控制。传统做法。
- 定时自动保存 —— 每隔 N 秒或在场景切换时保存。可作为防崩溃的安全保障,也可与检查点结合使用。
- 事件驱动 —— 每当重要状态发生变化时保存(获得物品、完成任务)。粒度更细,但 I/O 更多。
脚手架(自动保存 + 移动设备安全保障):
public class AutoSaveManager : MonoBehaviour
{
[SerializeField] private float autoSaveInterval = 60f; // seconds
private float _timeSinceLastSave;
void Update()
{
_timeSinceLastSave += Time.unscaledDeltaTime;
if (_timeSinceLastSave >= autoSaveInterval)
{
_timeSinceLastSave = 0f;
_ = PerformAutoSave(); // Fire-and-forget with error handling
}
}
// Mobile: save on app background (OnApplicationQuit may not fire)
void OnApplicationPause(bool paused)
{
if (paused)
_ = PerformAutoSave();
}
void OnApplicationQuit()
{
// Synchronous save on quit (async may not complete)
var data = GatherSaveData();
SaveFileIO.SaveJson(data, "autosave.json");
}
async Awaitable PerformAutoSave()
{
try
{
await Awaitable.BackgroundThreadAsync();
var data = GatherSaveData();
SaveFileIO.SaveJson(data, "autosave.json");
await Awaitable.MainThreadAsync();
}
catch (Exception e)
{
Debug.LogError($"Auto-save failed: {e.Message}");
}
}
GameSaveData GatherSaveData()
{
var data = new GameSaveData();
// Gather from all ISaveable systems
return data;
}
}
注意事项:保存受 I/O 限制。在移动设备上,请使用 OnApplicationPause(true)——操作系统终止应用时,OnApplicationQuit 可能不会触发。在 OnApplicationQuit 中使用同步 I/O(进程退出前异步操作可能无法完成)。切勿在 Update 中同步保存——请使用 BackgroundThreadAsync 或计时器。交叉参考:unity-lifecycle 涵盖退出流程。
反模式
| 反模式 | 问题 | 修复方法 |
|---|---|---|
BinaryFormatter | 存在安全漏洞,且已弃用 | 使用 JsonUtility 或 Newtonsoft |
| 在 PlayerPrefs 中存储游戏状态 | 缺乏结构和版本控制,且可能发生键冲突 | 使用 JSON 文件 + persistentDataPath |
| 直接保存 MonoBehaviour | 无法序列化,且会使存档与场景耦合 | 使用带 [Serializable] 的纯 C# DTO |
| 存档数据中没有版本字段 | 格式变更时无法迁移 | 始终包含 int version |
| 在 Update 中同步保存 | 会造成帧卡顿,在移动设备上尤其明显 | 使用 BackgroundThreadAsync 或计时器 |
| 没有备份或原子写入 | 写入期间崩溃会导致存档损坏 | 写入 .tmp 文件,再以原子方式重命名 |
相关技能
- unity-lifecycle —— OnApplicationPause、OnApplicationQuit、退出序列时序
- unity-packages-services —— Unity Cloud Save API(请勿重复)
- unity-async-patterns —— 用于异步 I/O 的 BackgroundThreadAsync
- unity-data-driven —— JsonUtility 与 Newtonsoft 的对比、配置数据的版本控制模式