543 lines
20 KiB
Markdown
543 lines
20 KiB
Markdown
# ShrinkDataSaver
|
||
|
||
一个为 Unity C# 项目设计的模块化存档与设置管理系统。支持多存档槽、链式版本迁移、可选 AES-256 加密、关键模块保护、跨模块只读查询,以及完整的事件驱动架构。
|
||
|
||
## ✨ 特性概览
|
||
|
||
| 特性 | 说明 |
|
||
|------|------|
|
||
| 💾 **模块化存档** | 按模块拆分存档数据,注册即用,读写隔离 |
|
||
| 🔢 **多存档槽** | 槽位数量可配置,支持元数据轻量查询 |
|
||
| 🔄 **链式版本迁移** | 注册迁移规则后自动链式执行,失败时整体回滚 |
|
||
| 🔒 **可选加密** | AES-256-CBC + PBKDF2-SHA256,随机 Salt/IV,密钥由使用者管理 |
|
||
| ⚡ **事件驱动** | 8 种原生事件,覆盖保存/加载/删除/迁移的完整生命周期 |
|
||
| 🔍 **跨模块查询** | `QueryModule<T>` 只读查询其他模块的运行时数据 |
|
||
| 🛡️ **关键模块保护** | `CriticalModule` 标记的模块序列化失败将中止整个保存操作 |
|
||
| ⏱️ **自动保存** | 按模块配置的最小间隔自动触发保存 |
|
||
| ⚙️ **Settings 系统** | 独立于存档的键值对设置,防抖写入,本地持久化 |
|
||
| 🧷 **最近游玩槽位** | 自动记录最近一次成功进入的槽位,可用于“继续游戏” |
|
||
| 🛠️ **双副本备份** | 主文件原子替换,自动保留 `.bak1` / `.bak2` 双副本轮换 |
|
||
| ☁️ **云存档兼容** | 每槽单文件 `.sav`,模块级 `EnableCloudSync` 开关 |
|
||
| 🔗 **EventBus 集成** | 可选接入 ShrinkEventBus,所有事件自动桥接到事件总线 |
|
||
| 🖥️ **Editor 工具** | 中文可视化调试窗口,实时查看/操作设置与存档 |
|
||
|
||
## 📦 依赖
|
||
|
||
- Unity 2022.3+
|
||
- [UniTask](https://github.com/Cysharp/UniTask) `2.x`
|
||
- [Newtonsoft.Json](https://docs.unity3d.com/Packages/com.unity.nuget.newtonsoft-json@3.2/manual/index.html)(`com.unity.nuget.newtonsoft-json`)
|
||
|
||
## ⚙️ 安装
|
||
|
||
在项目的 `Packages/manifest.json` 中添加:
|
||
|
||
```json
|
||
{
|
||
"dependencies": {
|
||
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
|
||
"com.cneicy.shrink-datasaver": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.git"
|
||
}
|
||
}
|
||
```
|
||
|
||
或通过 Package Manager → `+` → `Add package from git URL` 输入:
|
||
|
||
```
|
||
https://git.crash.work/ShrinkSDK/ShrinkDataSaver.git
|
||
```
|
||
|
||
## 🚀 快速上手
|
||
|
||
### 第一步:创建配置资产
|
||
|
||
菜单 `ShrinkSDK` → `存档` → `创建设置`。
|
||
|
||
菜单会创建 `Assets/Resources/GameAssets/Runtime/Data/ShrinkSDK/ShrinkDataSaverSettings.asset`。
|
||
运行时优先从该路径加载,并保留旧的 `Assets/Resources/ShrinkDataSaverSettings.asset`
|
||
回退;也可以在 Bootstrap 组件上手动指定。
|
||
|
||
> ⚠️ 未找到配置文件时,控制台会输出警告并使用默认配置。
|
||
|
||
### 第二步:放置 Bootstrap
|
||
|
||
在首场景中创建 GameObject,挂载 `ShrinkDataSaverBootstrap` 组件:
|
||
|
||
- **Settings Override**:可选,手动拖入配置资产(留空则自动查找)
|
||
- **Current Save Version**:当前存档版本号(从 `1` 开始)
|
||
|
||
Bootstrap 会自动 `DontDestroyOnLoad`,并在应用退出 / 移动端切后台时自动写入 Settings。
|
||
|
||
从当前版本开始,真正的初始化逻辑已下沉到 `ShrinkDataSaverRuntime`。这意味着:
|
||
|
||
- 旧项目继续挂 `ShrinkDataSaverBootstrap` 也能正常工作
|
||
- 如果项目接入了 `ShrinkApp`,则可由宿主统一调用 `ShrinkDataSaverRuntime.Initialize(...)`
|
||
- 宿主已接管时,旧 bootstrap 会自动幂等退出,不重复初始化
|
||
|
||
### 第三步:注册存档模块
|
||
|
||
```csharp
|
||
// 方式 A:Lambda(轻量)
|
||
ShrinkSave.RegisterModule(
|
||
key: "inventory",
|
||
serialize: () => inventoryManager.GetData(),
|
||
deserialize: data => inventoryManager.LoadData(data)
|
||
);
|
||
|
||
// 方式 B:Lambda + 模块配置
|
||
ShrinkSave.RegisterModule(
|
||
key: "quests",
|
||
serialize: () => questSystem.GetData(),
|
||
deserialize: data => questSystem.LoadData(data),
|
||
config: new ModuleConfig
|
||
{
|
||
EnableCloudSync = true,
|
||
AutoSaveIntervalSeconds = 60f, // 每 60 秒自动保存
|
||
CriticalModule = true // 序列化失败将中止保存
|
||
}
|
||
);
|
||
|
||
// 方式 C:接口(结构化)
|
||
public class InventoryModule : ISaveModule<InventoryData>
|
||
{
|
||
public string Key => "inventory";
|
||
public InventoryData Serialize() => inventoryManager.GetData();
|
||
public void Deserialize(InventoryData data) => inventoryManager.LoadData(data);
|
||
}
|
||
ShrinkSave.RegisterModule(new InventoryModule());
|
||
```
|
||
|
||
### 第四步:保存与加载
|
||
|
||
```csharp
|
||
// 保存
|
||
await ShrinkSave.SaveSlotAsync(0, new SaveOptions
|
||
{
|
||
SlotName = "第一周目",
|
||
CaptureScreenshot = true
|
||
});
|
||
|
||
// 加载
|
||
await ShrinkSave.LoadSlotAsync(0);
|
||
|
||
// 删除
|
||
await ShrinkSave.DeleteSlotAsync(0);
|
||
```
|
||
|
||
---
|
||
|
||
## 📖 核心概念
|
||
|
||
### 事件系统
|
||
|
||
系统在关键操作时触发原生 C# 事件,所有事件携带完整载荷数据:
|
||
|
||
| 事件 | 触发时机 | 载荷 |
|
||
|------|----------|------|
|
||
| `OnSaveStarted` | 开始保存前 | SlotIndex, Timestamp |
|
||
| `OnSaveCompleted` | 保存成功后 | SlotIndex, ModuleNames[], Timestamp |
|
||
| `OnSaveFailed` | 保存失败时 | SlotIndex, ErrorMessage |
|
||
| `OnLoadStarted` | 开始加载前 | SlotIndex, Timestamp |
|
||
| `OnLoadCompleted` | 加载成功后 | SlotIndex, ModuleNames[], Version, Timestamp |
|
||
| `OnLoadFailed` | 加载失败时 | SlotIndex, ErrorMessage |
|
||
| `OnMigrationCompleted` | 版本迁移完成后 | SlotIndex, FromVersion, ToVersion |
|
||
| `OnDeleteCompleted` | 删除存档后 | SlotIndex |
|
||
|
||
```csharp
|
||
ShrinkSave.OnSaveCompleted += args =>
|
||
Debug.Log($"槽位 {args.SlotIndex} 保存成功,模块: {string.Join(", ", args.ModuleNames)}");
|
||
|
||
ShrinkSave.OnLoadFailed += args =>
|
||
ShowErrorDialog($"加载失败: {args.ErrorMessage}");
|
||
|
||
ShrinkSave.OnMigrationCompleted += args =>
|
||
Debug.Log($"存档从 v{args.FromVersion} 迁移到 v{args.ToVersion}");
|
||
```
|
||
|
||
### 模块配置
|
||
|
||
注册模块时可传入 `ModuleConfig` 控制行为:
|
||
|
||
```csharp
|
||
var config = new ModuleConfig
|
||
{
|
||
EnableCloudSync = false, // 不参与云存档(如本地设置)
|
||
AutoSaveIntervalSeconds = 30f, // 自动保存间隔(0 = 禁用)
|
||
CriticalModule = true // 序列化失败中止整个保存
|
||
};
|
||
ShrinkSave.RegisterModule("settings", () => data, d => data = d, config);
|
||
```
|
||
|
||
- **EnableCloudSync**:标记该模块是否参与云同步(供业务层查询)
|
||
- **AutoSaveIntervalSeconds**:Bootstrap 会取所有模块中最小的非零间隔,定时自动保存到当前已加载的槽位
|
||
- **CriticalModule**:标记为关键模块后,序列化异常会触发 `OnSaveFailed` 并中止保存;非关键模块异常仅跳过该模块
|
||
|
||
### 跨模块只读查询
|
||
|
||
模块间需要共享数据时,通过存档管理器提供的只读接口查询,避免直接耦合:
|
||
|
||
```csharp
|
||
// 查询其他模块的当前运行时数据(序列化快照)
|
||
var playerStats = ShrinkSave.QueryModule<PlayerStatsData>("playerStats");
|
||
if (playerStats != null)
|
||
Debug.Log($"玩家等级: {playerStats.Level}");
|
||
|
||
// 检查已加载存档中某模块是否包含特定键
|
||
bool hasCoins = ShrinkSave.HasKey("inventory", "coins");
|
||
|
||
// 检查模块是否已注册
|
||
bool registered = ShrinkSave.HasModule("quests");
|
||
```
|
||
|
||
### 最近游玩槽位
|
||
|
||
包内会在 `LoadSlotAsync(...)` 成功后自动记录最近一次成功进入的槽位,并在删除该槽位时自动回退到其他有效槽位:
|
||
|
||
```csharp
|
||
int recentSlot = ShrinkSave.GetRecentSlotIndex();
|
||
|
||
int continueSlot = await ShrinkSave.GetRecommendedContinueSlotAsync();
|
||
if (continueSlot >= 0)
|
||
{
|
||
await ShrinkSave.LoadSlotAsync(continueSlot);
|
||
}
|
||
```
|
||
|
||
### 版本迁移
|
||
|
||
每次存档结构变化时,注册迁移规则。**必须在 `LoadSlotAsync` 调用之前注册。**
|
||
|
||
```csharp
|
||
// 注册迁移(在游戏初始化时)
|
||
MigrationChain.Register(fromVersion: 1, toVersion: 2, data =>
|
||
{
|
||
// data 是整个存档的 JObject,包含所有模块
|
||
if (data["inventory"] is JObject inv)
|
||
{
|
||
inv["gold"] = inv["coins"];
|
||
inv.Remove("coins");
|
||
}
|
||
return data;
|
||
});
|
||
|
||
MigrationChain.Register(fromVersion: 2, toVersion: 3, data =>
|
||
{
|
||
if (data["quests"] is JObject q)
|
||
q["dailyQuestReset"] = 0;
|
||
return data;
|
||
});
|
||
|
||
// 同步更新 Bootstrap 上的 Current Save Version = 3
|
||
ShrinkSave.SetCurrentSaveVersion(3);
|
||
```
|
||
|
||
加载时自动检测版本差异,链式执行所有中间迁移(v1 → v2 → v3)。**迁移失败时自动回滚到迁移前的数据,不会丢失原始存档。**
|
||
|
||
### Settings 系统
|
||
|
||
独立于存档的键值对设置,本地持久化,不参与云存档:
|
||
|
||
```csharp
|
||
// 读写
|
||
ShrinkSettings.Set("MasterVolume", 0.8f);
|
||
ShrinkSettings.Set("Language", "zh-CN");
|
||
float volume = ShrinkSettings.Get("MasterVolume", defaultValue: 1f);
|
||
bool exists = ShrinkSettings.Has("MasterVolume");
|
||
ShrinkSettings.Remove("MasterVolume");
|
||
|
||
// 查看所有设置
|
||
var all = ShrinkSettings.GetAllRaw(); // IReadOnlyDictionary<string, JToken>
|
||
|
||
// 监听变更
|
||
ShrinkSettings.OnChanged += (key, value) => Debug.Log($"{key} = {value}");
|
||
ShrinkSettings.Watch<float>("MasterVolume", vol => ApplyVolume(vol));
|
||
```
|
||
|
||
变更时立刻触发回调,写入磁盘有防抖延迟(默认 300ms),防止高频调用产生大量 IO。
|
||
|
||
### 加密
|
||
|
||
可选加密,默认关闭。使用 AES-256-CBC + PBKDF2-SHA256(10000 次迭代),每次加密随机生成 Salt 和 IV:
|
||
|
||
```csharp
|
||
// 加密保存
|
||
await ShrinkSave.SaveSlotAsync(0, new SaveOptions
|
||
{
|
||
Encrypt = true,
|
||
EncryptionKey = "your-secret-key"
|
||
});
|
||
|
||
// 解密加载(需传入相同密钥)
|
||
await ShrinkSave.LoadSlotAsync(0, decryptionKey: "your-secret-key");
|
||
|
||
// 通过元数据判断是否加密
|
||
var meta = await ShrinkSave.GetMetaAsync(0);
|
||
if (meta.IsEncrypted)
|
||
ShowPasswordPrompt();
|
||
```
|
||
|
||
---
|
||
|
||
## 🔗 ShrinkEventBus 集成
|
||
|
||
> 详细文档见 [ShrinkDataSaver.Integration.EventBus](https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.EventBus)
|
||
|
||
项目中同时包含 `ShrinkDataSaver.Integration.EventBus` 时,由 Context 组件或宿主显式管理桥接生命周期,将原生事件发布到指定的 ShrinkEventBus 2.0 Bus:
|
||
|
||
```csharp
|
||
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||
public sealed partial class SaveUIManager : MonoBehaviour
|
||
{
|
||
[ShrinkSubscribe]
|
||
private void OnSaveCompleted(SaveCompletedEvent e)
|
||
{
|
||
ShowSaveIndicator(e.SlotIndex, e.ModuleNames);
|
||
}
|
||
|
||
[ShrinkSubscribe]
|
||
private void OnLoadFailed(LoadFailedEvent e)
|
||
{
|
||
ShowErrorDialog(e.ErrorMessage);
|
||
}
|
||
|
||
[ShrinkSubscribe]
|
||
private void OnSettingsChanged(SettingsChangedEvent e)
|
||
{
|
||
if (e.Key == "MasterVolume")
|
||
ApplyVolume(e.Get<float>());
|
||
}
|
||
}
|
||
```
|
||
|
||
普通 C# 对象通过 `EventBus.Attach(target)` 接入;MonoBehaviour 可使用 `ShrinkMonoEventScope`,也可在 `OnEnable`/`OnDisable` 中自行持有并释放 binding。桥接器本身由 `ShrinkDataSaverEventBusComponent` 或 `DataSaverEventBusBridge.Register()`/`Unregister()` 管理。
|
||
|
||
**EventBus 事件类型完整列表:**
|
||
|
||
`SettingsChangedEvent` · `SaveStartedEvent` · `SaveCompletedEvent` · `SaveFailedEvent` · `LoadStartedEvent` · `LoadCompletedEvent` · `LoadFailedEvent` · `MigrationCompletedEvent` · `SlotDeletedEvent`
|
||
|
||
---
|
||
|
||
## 🖥️ Editor 调试工具
|
||
|
||
菜单 `ShrinkSDK` → `存档` → `数据查看器`
|
||
|
||
| 标签页 | 功能 |
|
||
|--------|------|
|
||
| **设置** | 运行时查看所有设置项(键、值、类型),支持新增/修改/删除,一键保存/加载 |
|
||
| **存档槽** | 查看所有槽位元数据,每个槽位可直接加载/删除,支持加密参数 |
|
||
| **工具** | 打开持久化数据目录,定位配置资源文件,一键删除所有存档 |
|
||
|
||
---
|
||
|
||
## ☁️ Steam Auto-Cloud 配置
|
||
|
||
每个逻辑存档槽默认包含一个主文件和最多两个轮换备份文件:`.sav`、`.sav.bak1`、`.sav.bak2`。
|
||
|
||
在 Steamworks 后台(App Admin → Steam Cloud)配置同步路径:
|
||
|
||
| 字段 | 值 |
|
||
|------|-----|
|
||
| Root Path | `{userdata}` |
|
||
| Subdirectory | 指向 `persistentDataPath` 的相对路径 |
|
||
| File Pattern | `saves/*.sav*` |
|
||
|
||
通过 `ModuleConfig.EnableCloudSync = false` 可将特定模块(如本地设置)排除在云同步之外,供业务层在合并逻辑中过滤。
|
||
|
||
---
|
||
|
||
## 🔧 API 参考
|
||
|
||
### ShrinkSave(静态门面)
|
||
|
||
#### 模块注册
|
||
|
||
```csharp
|
||
ShrinkSave.RegisterModule(ISaveModule module, ModuleConfig config = null)
|
||
ShrinkSave.RegisterModule<T>(string key, Func<T> serialize, Action<T> deserialize, ModuleConfig config = null)
|
||
ShrinkSave.UnregisterModule(string key)
|
||
ShrinkSave.HasModule(string moduleName) → bool
|
||
ShrinkSave.GetModuleConfig(string key) → ModuleConfig
|
||
ShrinkSave.GetRegisteredModuleNames() → IReadOnlyCollection<string>
|
||
```
|
||
|
||
#### 存档操作
|
||
|
||
```csharp
|
||
ShrinkSave.SaveSlotAsync(int slotIndex, SaveOptions, CancellationToken) → UniTask
|
||
ShrinkSave.LoadSlotAsync(int slotIndex, string decryptionKey, ct) → UniTask
|
||
ShrinkSave.DeleteSlotAsync(int slotIndex, CancellationToken) → UniTask
|
||
ShrinkSave.SlotExistsAsync(int slotIndex, CancellationToken) → UniTask<bool>
|
||
ShrinkSave.LoadedSlot → int (-1 = 未加载)
|
||
```
|
||
|
||
#### 元数据查询
|
||
|
||
```csharp
|
||
ShrinkSave.GetAllMetaAsync(CancellationToken) → UniTask<SaveMeta[]>
|
||
ShrinkSave.GetMetaAsync(int slotIndex, ct) → UniTask<SaveMeta>
|
||
ShrinkSave.GetRecentSlotIndex() → int
|
||
ShrinkSave.GetRecommendedContinueSlotAsync(ct) → UniTask<int>
|
||
```
|
||
|
||
#### 跨模块查询
|
||
|
||
```csharp
|
||
ShrinkSave.QueryModule<T>(string moduleName) → T
|
||
ShrinkSave.HasKey(string moduleName, string key) → bool
|
||
```
|
||
|
||
#### 版本
|
||
|
||
```csharp
|
||
ShrinkSave.SetCurrentSaveVersion(int version)
|
||
ShrinkSave.GetMinAutoSaveInterval() → float
|
||
MigrationChain.Register(int from, int to, Func<JObject, JObject>)
|
||
```
|
||
|
||
#### 事件
|
||
|
||
```csharp
|
||
ShrinkSave.OnSaveStarted += Action<SaveStartedEventArgs>
|
||
ShrinkSave.OnSaveCompleted += Action<SaveCompletedEventArgs>
|
||
ShrinkSave.OnSaveFailed += Action<SaveFailedEventArgs>
|
||
ShrinkSave.OnLoadStarted += Action<LoadStartedEventArgs>
|
||
ShrinkSave.OnLoadCompleted += Action<LoadCompletedEventArgs>
|
||
ShrinkSave.OnLoadFailed += Action<LoadFailedEventArgs>
|
||
ShrinkSave.OnMigrationCompleted += Action<MigrationCompletedEventArgs>
|
||
ShrinkSave.OnDeleteCompleted += Action<SlotDeletedEventArgs>
|
||
```
|
||
|
||
### ShrinkSettings(静态门面)
|
||
|
||
```csharp
|
||
ShrinkSettings.Set<T>(string key, T value)
|
||
ShrinkSettings.Get<T>(string key, T defaultValue = default) → T
|
||
ShrinkSettings.Has(string key) → bool
|
||
ShrinkSettings.Remove(string key)
|
||
ShrinkSettings.GetAllRaw() → IReadOnlyDictionary<string, JToken>
|
||
ShrinkSettings.Watch<T>(string key, Action<T> callback)
|
||
ShrinkSettings.Unwatch(string key, Action<object> callback)
|
||
ShrinkSettings.SaveAsync(CancellationToken) → UniTask
|
||
ShrinkSettings.LoadAsync(CancellationToken) → UniTask
|
||
ShrinkSettings.OnChanged += Action<string, object>
|
||
```
|
||
|
||
---
|
||
|
||
## 🏗️ 架构说明
|
||
|
||
```
|
||
ShrinkDataSaver/
|
||
├── Runtime/
|
||
│ ├── ShrinkSave 存档系统静态门面(保存/加载/删除/事件/查询)
|
||
│ ├── ShrinkSettings 设置系统静态门面(键值对/防抖写入/监听)
|
||
│ ├── ShrinkDataSaverBootstrap 兼容旧入口的初始化组件
|
||
│ ├── ShrinkDataSaverRuntime 可重入运行时初始化入口 + 生命周期驱动
|
||
│ ├── ShrinkDataSaverSettings ScriptableObject 全局配置(支持任意目录)
|
||
│ ├── SaveTypes 核心类型(SaveMeta/ISaveModule/ModuleConfig/EventArgs)
|
||
│ ├── MigrationChain 版本迁移链(注册/链式执行/回滚)
|
||
│ ├── DataSerializer JSON 序列化(Newtonsoft.Json)
|
||
│ ├── SaveEncryptor AES-256-CBC 加密/解密
|
||
│ ├── IStorageProvider 存储后端接口
|
||
│ └── LocalStorageProvider 本地文件系统实现(异步读写 + 原子替换 + 双副本轮换)
|
||
│
|
||
├── Editor/
|
||
│ └── ShrinkDataSaverEditorWindow 中文可视化调试窗口
|
||
│
|
||
└── Tests/
|
||
├── MockStorageProvider 内存模拟存储
|
||
└── *Tests.cs NUnit 单元测试(69 个用例)
|
||
|
||
ShrinkDataSaver.Integration.EventBus/ (可选)
|
||
├── DataSaverEvents 9 个 EventBus 事件类
|
||
└── DataSaverEventBusBridge 自动桥接(RuntimeInitializeOnLoadMethod)
|
||
```
|
||
|
||
**保存流程:**
|
||
|
||
```
|
||
SaveSlotAsync(slotIndex, options)
|
||
├─ ValidateSlotIndex
|
||
├─ OnSaveStarted ← 事件
|
||
├─ 序列化所有模块
|
||
│ ├─ CriticalModule 失败 → OnSaveFailed ← 事件,中止
|
||
│ └─ 普通模块失败 → LogError,跳过继续
|
||
├─ 加密(可选)
|
||
├─ 写入存储
|
||
└─ OnSaveCompleted ← 事件(含 ModuleNames[])
|
||
```
|
||
|
||
**加载流程:**
|
||
|
||
```
|
||
LoadSlotAsync(slotIndex, decryptionKey)
|
||
├─ OnLoadStarted ← 事件
|
||
├─ 读取存储 → 解密(可选)
|
||
├─ 版本迁移(如需)
|
||
│ ├─ DeepClone 备份
|
||
│ ├─ 链式执行迁移
|
||
│ ├─ 失败 → 回滚到备份
|
||
│ └─ OnMigrationCompleted ← 事件
|
||
├─ 反序列化到各模块
|
||
├─ 缓存模块数据(供 QueryModule/HasKey)
|
||
└─ OnLoadCompleted ← 事件(含 ModuleNames[], Version)
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ 最佳实践
|
||
|
||
**设置模块标记 `EnableCloudSync = false`**
|
||
|
||
```csharp
|
||
// ✅ 本地设置不上传云端,避免跨设备覆盖
|
||
ShrinkSave.RegisterModule("settings", () => localPrefs, d => localPrefs = d,
|
||
new ModuleConfig { EnableCloudSync = false });
|
||
```
|
||
|
||
**关键模块标记 `CriticalModule = true`**
|
||
|
||
```csharp
|
||
// ✅ 玩家核心数据序列化失败时中止保存,防止存档损坏
|
||
ShrinkSave.RegisterModule("playerStats", () => stats, d => stats = d,
|
||
new ModuleConfig { CriticalModule = true });
|
||
```
|
||
|
||
**迁移注册必须在加载之前**
|
||
|
||
```csharp
|
||
// ✅ 游戏启动时立即注册所有迁移
|
||
MigrationChain.Register(1, 2, MigrateV1ToV2);
|
||
MigrationChain.Register(2, 3, MigrateV2ToV3);
|
||
ShrinkSave.SetCurrentSaveVersion(3);
|
||
// 之后才能调用 LoadSlotAsync
|
||
```
|
||
|
||
**加密存档先查询元数据**
|
||
|
||
```csharp
|
||
// ✅ 通过 Meta 判断是否需要密钥,避免盲目加载
|
||
var meta = await ShrinkSave.GetMetaAsync(slotIndex);
|
||
string key = meta?.IsEncrypted == true ? AskPlayerForPassword() : null;
|
||
await ShrinkSave.LoadSlotAsync(slotIndex, key);
|
||
```
|
||
|
||
---
|
||
|
||
## ⚠️ 注意事项
|
||
|
||
- **模块注册顺序无关**:`SaveSlotAsync` 序列化所有已注册模块,`LoadSlotAsync` 分发到对应模块,缺失的模块会跳过但不会导致加载失败。
|
||
- **宿主接管兼容**:如果项目接入了 `ShrinkApp`,推荐通过 `ShrinkDataSaver.Integration.App` 让宿主统一初始化,而不是继续依赖场景 bootstrap。
|
||
- **写入原子性**:`LocalStorageProvider` 先异步写入 `.tmp` 临时文件,再通过原子替换提交主文件,并自动轮换 `.bak1` / `.bak2` 两份备份。
|
||
- **读取恢复**:`ShrinkSave` 与 `ShrinkSettings` 读取主文件失败时,会自动回退到 `.bak1`、`.bak2`,并在成功后修复主文件。
|
||
- **Settings 防抖**:`Set()` 调用后不立刻写磁盘,在 300ms(可配置)内连续调用只触发一次写入。退出时强制跳过防抖直接写入。
|
||
- **加密密钥管理**:框架不存储密钥。密钥丢失则对应存档无法解密,建议在 UI 层给玩家明确提示。
|
||
- **截图与 Steam Cloud**:截图压缩为 JPG 并限制最大宽度(默认 256px),仍需注意模块数据体积。Steam Cloud 默认单文件限制 1MB。
|
||
- **配置文件查找顺序**:`Resources.Load` → 编辑器 `AssetDatabase` 全局搜索 → 创建默认实例并输出控制台警告。
|
||
- **自动保存**:仅在有槽位已加载(`LoadedSlot >= 0`)且存在 `AutoSaveIntervalSeconds > 0` 的模块时生效。
|
||
|
||
---
|
||
|
||
## 📄 License
|
||
|
||
[MIT](LICENSE)
|