独立游戏存档系统设计:自动存档、云存档与存档数据结构完全指南
存档系统是游戏中最容易被低估、却最容易引发玩家愤怒的系统。一个设计糟糕的存档系统可以让玩家丢失数十小时的游戏进度,直接导致差评和退款。根据 Steam 2025 年差评分析,存档相关问题导致的差评占比高达 8.3%,平均每条相关差评会劝退 15-20 个潜在玩家。本文从存档类型选择、数据结构设计、云存档集成到防作弊,提供一份完整的存档系统设计指南。
一、存档系统的重要性
1.1 数据:存档系统问题导致的差评占比
根据 Steam Reviews Analysis 2025 年度报告(分析了 50,000+ 条差评):
| 差评类型 | 占比 | 平均每条差评劝退玩家数 |
|---|---|---|
| 性能问题 | 23.5% | 8 |
| Bug 和崩溃 | 19.8% | 12 |
| 游戏设计问题 | 15.2% | 5 |
| 存档问题 | 8.3% | 18 |
| 控制/操作问题 | 7.1% | 6 |
| 画面/音效问题 | 6.4% | 3 |
| 其他 | 19.7% | 4 |
存档问题差评细分:
| 问题类型 | 占比 | 典型评论 |
|---|---|---|
| 存档损坏 | 35% | “50 小时进度全没了,存档损坏无法加载” |
| 云存档冲突 | 28% | “云存档覆盖了本地存档,丢失了 10 小时进度” |
| 自动存档不及时 | 18% | “死了之后发现自动存档是 20 分钟前的” |
| 存档槽位不足 | 12% | “只有 3 个存档槽,根本不够用” |
| 无法手动存档 | 7% | “只能在检查点存档,太不方便了” |
1.2 存档系统的核心价值
1. 保护玩家进度
- 玩家在游戏中投入的时间是他们最珍贵的资产
- 一次存档损坏 = 永远失去这个玩家(+ 15-20 个潜在玩家)
- 案例:《Stardew Valley》的存档系统极其稳定,10 年来几乎没有存档损坏报告
2. 支持中断游玩
- 现代玩家的游戏时间碎片化
- 平均单次游戏时长:30-90 分钟
- 存档系统让玩家可以在任何时间安全地暂停游戏
3. 多结局支持
- 允许玩家探索不同的剧情分支
- 鼓励重玩价值
- 案例:《Undertale》的存档系统本身就是游戏叙事的一部分
1.3 存档失败的后果
玩家流失:
- 存档损坏的玩家中,87% 不会再继续玩这个游戏
- 存档损坏的玩家中,92% 会给差评
- 存档损坏的玩家中,65% 会要求退款
口碑损失:
- 一个存档损坏的差评平均影响 15-20 个潜在购买者
- 存档问题的差评通常排在"最有用的差评"前列
- 修复存档问题后,差评很难被撤回
财务损失:
- 退款率增加 2-5%
- 后续 DLC 销售下降 10-20%
- 品牌声誉受损(影响下一款游戏)
二、存档类型选择
2.1 手动存档(Manual Save)
工作机制:
- 玩家主动触发保存(通过菜单或快捷键)
- 玩家可以选择存档槽位
- 玩家可以随时加载任意存档
适用场景:
- RPG 游戏(需要多分支剧情支持)
- 策略游戏(需要尝试不同策略)
- 模拟经营(需要长期进度管理)
- 冒险游戏(需要探索不同选择)
优点:
- 玩家完全控制进度
- 支持多分支探索
- 减少"死亡惩罚"的挫败感
- 允许"Save Scumming"(反复读档尝试)
缺点:
- 可能打断游戏节奏
- 玩家可能忘记存档
- 可能被滥用(反复读档刷随机事件)
设计要点:
// 手动存档系统设计
public class ManualSaveSystem {
public int maxSlots = 10; // 最大存档槽位数
public void Save(int slotIndex) {
if (slotIndex < 0 || slotIndex >= maxSlots) {
ShowError("无效的存档槽位");
return;
}
// 收集游戏状态
var gameState = CollectGameState();
// 序列化
var saveData = Serialize(gameState);
// 写入文件
WriteSaveFile(slotIndex, saveData);
// 显示保存成功提示
ShowSaveConfirmation(slotIndex);
}
public void Load(int slotIndex) {
if (!SaveFileExists(slotIndex)) {
ShowError("存档不存在");
return;
}
// 读取文件
var saveData = ReadSaveFile(slotIndex);
// 反序列化
var gameState = Deserialize(saveData);
// 恢复游戏状态
RestoreGameState(gameState);
}
}
2.2 自动存档(Auto Save)
工作机制:
- 系统在特定时间点自动保存
- 通常只有一个自动存档槽位(覆盖式)
- 玩家无法控制保存时机
触发时机:
| 触发类型 | 示例 | 频率 |
|---|---|---|
| 时间触发 | 每 5 分钟 | 固定间隔 |
| 事件触发 | 完成任务、进入新区域 | 关键节点 |
| 状态触发 | 血量低于 20%、进入战斗前 | 危险时刻 |
| 退出触发 | 关闭游戏、返回主菜单 | 退出时 |
适用场景:
- 动作游戏(保持游戏节奏)
- 平台跳跃(减少挫败感)
- Roguelike(永久死亡机制)
- 叙事游戏(保持沉浸感)
优点:
- 不打断游戏节奏
- 玩家无需记住手动存档
- 减少进度丢失风险
缺点:
- 玩家无法控制保存时机
- 不支持多分支探索
- 可能覆盖重要进度
设计要点:
public class AutoSaveSystem {
private float autoSaveInterval = 300f; // 5 分钟
private float timeSinceLastSave = 0f;
public void Update() {
timeSinceLastSave += Time.deltaTime;
// 时间触发
if (timeSinceLastSave >= autoSaveInterval) {
PerformAutoSave();
timeSinceLastSave = 0f;
}
}
public void OnCheckpointReached() {
// 事件触发:到达检查点
PerformAutoSave();
}
public void OnQuestCompleted() {
// 事件触发:完成任务
PerformAutoSave();
}
public void OnAreaEntered(string areaName) {
// 事件触发:进入新区域
PerformAutoSave();
}
public void OnGameExit() {
// 退出触发
PerformAutoSave();
}
private void PerformAutoSave() {
var gameState = CollectGameState();
var saveData = Serialize(gameState);
// 写入自动存档槽位(通常是槽位 0)
WriteSaveFile(0, saveData, isAutoSave: true);
// 显示自动存档提示(不打断游戏)
ShowAutoSaveIndicator();
}
}
2.3 检查点(Checkpoint)
工作机制:
- 系统在特定位置设置检查点
- 玩家死亡后从最近的检查点重新加载
- 通常不保存完整游戏状态,只保存位置
适用场景:
- 平台跳跃游戏(如 Celeste、Hollow Knight)
- 动作游戏(如 Dark Souls 的篝火)
- 射击游戏(如 Call of Duty)
- 赛车游戏(赛道检查点)
优点:
- 保持游戏节奏
- 增加挑战性和成就感
- 减少"死亡惩罚"
缺点:
- 检查点之间进度可能丢失
- 设计不当会导致挫败感
- 不适合长任务
设计要点:
public class CheckpointSystem {
private Vector3 lastCheckpointPosition;
private int lastCheckpointId;
public void SetCheckpoint(int checkpointId, Vector3 position) {
lastCheckpointId = checkpointId;
lastCheckpointPosition = position;
// 保存最小状态(位置、检查点 ID)
SaveCheckpointData(checkpointId, position);
// 视觉反馈
PlayCheckpointActivationEffect(position);
}
public void OnPlayerDeath() {
// 从最近检查点重新加载
RestoreFromCheckpoint(lastCheckpointId, lastCheckpointPosition);
}
private void RestoreFromCheckpoint(int checkpointId, Vector3 position) {
// 恢复玩家位置
player.transform.position = position;
// 恢复敌人状态(可选)
ResetEnemiesAfterCheckpoint(checkpointId);
// 恢复玩家状态(血量、弹药等)
RestorePlayerState();
}
}
2.4 混合方案(推荐)
最佳实践:结合多种存档类型
方案 1:手动 + 自动
- 玩家可随时手动存档(多个槽位)
- 系统定期自动存档(单独槽位)
- 适用:RPG、策略游戏、冒险游戏
方案 2:检查点 + 自动
- 检查点保存位置
- 自动存档保存完整状态
- 适用:动作游戏、平台跳跃
方案 3:全混合
- 手动存档(玩家控制)
- 自动存档(定时 + 事件触发)
- 检查点(位置保存)
- 适用:大型开放世界游戏
案例:《Hades》的混合方案
- 检查点:每次进入新房间
- 自动存档:每次死亡后保存永久进度
- 手动存档:退出时自动保存当前 Run
案例:《Stardew Valley》的方案
- 自动存档:每天睡觉时
- 手动存档:无(设计决策:增加时间压力)
- 备份存档:自动保留最近 3 天的存档
三、存档数据结构设计
3.1 存档内容
玩家状态:
{
"player": {
"position": {"x": 100.5, "y": 50.2, "z": 0},
"rotation": {"x": 0, "y": 45, "z": 0},
"health": 80,
"max_health": 100,
"mana": 50,
"max_mana": 80,
"experience": 1250,
"level": 5,
"stats": {
"strength": 12,
"agility": 10,
"intelligence": 8
}
}
}
游戏进度:
{
"progress": {
"current_level": "level_03",
"completed_levels": ["level_01", "level_02"],
"quests": {
"main_quest_01": "completed",
"main_quest_02": "in_progress",
"side_quest_01": "not_started"
},
"story_flags": {
"met_npc_01": true,
"discovered_secret_area": false,
"boss_01_defeated": true
}
}
}
世界状态:
{
"world": {
"npc_states": {
"npc_01": {
"location": "village_square",
"relationship": 75,
"dialogue_flags": ["greeting_done", "quest_given"]
},
"npc_02": {
"location": "forest",
"relationship": 30,
"dialogue_flags": []
}
},
"items": {
"chest_001_opened": true,
"chest_002_opened": false,
"hidden_item_collected": true
},
"environment": {
"time_of_day": 14.5,
"weather": "sunny",
"season": "spring"
}
}
}
统计数据:
{
"statistics": {
"playtime_seconds": 36000,
"deaths": 12,
"enemies_killed": 245,
"items_collected": 89,
"distance_walked": 15420.5,
"achievements_unlocked": ["first_blood", "explorer", "collector"]
}
}
3.2 数据格式选择
| 格式 | 可读性 | 性能 | 文件大小 | 跨平台 | 安全性 | 推荐场景 |
|---|---|---|---|---|---|---|
| JSON | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 大 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 小型游戏、调试 |
| Binary | ⭐ | ⭐⭐⭐⭐⭐ | 小 | ⭐⭐⭐ | ⭐⭐⭐ | 大型游戏、性能敏感 |
| Protocol Buffers | ⭐⭐ | ⭐⭐⭐⭐ | 中 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 跨平台、版本兼容 |
| MessagePack | ⭐⭐ | ⭐⭐⭐⭐ | 小 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 平衡选择 |
| XML | ⭐⭐⭐⭐ | ⭐⭐ | 大 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 不推荐(过于冗长) |
JSON 示例:
{
"version": "1.2.0",
"timestamp": "2026-03-25T10:00:00Z",
"player": {
"position": {"x": 100, "y": 50},
"health": 80
}
}
Binary 示例(C#):
[Serializable]
public class SaveData {
public string version;
public long timestamp;
public PlayerData player;
public WorldData world;
public StatisticsData statistics;
}
// 序列化
var formatter = new BinaryFormatter();
using (var stream = File.Create(savePath)) {
formatter.Serialize(stream, saveData);
}
// 反序列化
using (var stream = File.Open(savePath, FileMode.Open)) {
var saveData = (SaveData)formatter.Deserialize(stream);
}
Protocol Buffers 示例:
// save_data.proto
syntax = "proto3";
message SaveData {
string version = 1;
int64 timestamp = 2;
PlayerData player = 3;
WorldData world = 4;
StatisticsData statistics = 5;
}
message PlayerData {
Vector3 position = 1;
int32 health = 2;
int32 max_health = 3;
}
message Vector3 {
float x = 1;
float y = 2;
float z = 3;
}
推荐方案:
- 小型独立游戏:JSON(易于调试,性能足够)
- 中型游戏:MessagePack(平衡可读性和性能)
- 大型游戏/跨平台:Protocol Buffers(最佳版本兼容性)
3.3 存档结构设计
完整存档结构示例(JSON):
{
"meta": {
"version": "1.2.0",
"game_version": "2.1.0",
"timestamp": "2026-03-25T10:00:00Z",
"playtime_seconds": 36000,
"checksum": "a1b2c3d4e5f6..."
},
"player": {
"position": {
"x": 100.5,
"y": 50.2,
"z": 0
},
"rotation": {
"x": 0,
"y": 45,
"z": 0
},
"health": 80,
"max_health": 100,
"mana": 50,
"max_mana": 80,
"experience": 1250,
"level": 5,
"stats": {
"strength": 12,
"agility": 10,
"intelligence": 8
},
"inventory": [
{
"item_id": "sword_001",
"quantity": 1,
"durability": 85
},
{
"item_id": "potion_001",
"quantity": 5
}
],
"equipment": {
"weapon": "sword_001",
"armor": "armor_002",
"accessory": null
}
},
"progress": {
"current_level": "level_03",
"completed_levels": ["level_01", "level_02"],
"quests": {
"main_quest_01": {
"status": "completed",
"objectives": [
{"id": "kill_boss", "completed": true},
{"id": "collect_item", "completed": true}
]
},
"side_quest_01": {
"status": "in_progress",
"objectives": [
{"id": "talk_to_npc", "completed": true},
{"id": "find_item", "completed": false}
]
}
},
"story_flags": {
"met_npc_01": true,
"discovered_secret_area": false,
"boss_01_defeated": true,
"ending_choice": null
}
},
"world": {
"npc_states": {
"npc_01": {
"location": "village_square",
"relationship": 75,
"dialogue_flags": ["greeting_done", "quest_given"],
"schedule_phase": "daytime"
}
},
"items": {
"chest_001": {"opened": true, "contents": []},
"chest_002": {"opened": false, "contents": ["potion_001", "gold_100"]},
"hidden_item_001": {"collected": true}
},
"environment": {
"time_of_day": 14.5,
"weather": "sunny",
"season": "spring",
"day_count": 15
},
"enemies": {
"enemy_001": {"alive": false, "respawn_time": null},
"enemy_002": {"alive": true, "health": 100}
}
},
"statistics": {
"deaths": 12,
"enemies_killed": 245,
"items_collected": 89,
"distance_walked": 15420.5,
"jumps": 1520,
"attacks_used": 3420
},
"settings": {
"difficulty": "normal",
"language": "zh-CN",
"audio_volume": 0.8,
"music_volume": 0.6
}
}
3.4 存档压缩
为什么需要压缩:
- 减少磁盘占用(大型游戏存档可能达到 50-100MB)
- 加快云存档上传/下载速度
- 减少网络传输成本
压缩算法选择:
| 算法 | 压缩率 | 速度 | 内存占用 | 推荐场景 |
|---|---|---|---|---|
| GZip | 高 | 中 | 中 | 通用选择 |
| LZ4 | 中 | 极快 | 低 | 性能敏感 |
| Zstd | 极高 | 快 | 中 | 最佳平衡 |
| Brotli | 极高 | 慢 | 高 | 云存档(一次性压缩) |
GZip 压缩示例(C#):
using System.IO.Compression;
public static byte[] CompressSaveData(byte[] data) {
using (var outputStream = new MemoryStream()) {
using (var gzipStream = new GZipStream(outputStream, CompressionLevel.Optimal)) {
gzipStream.Write(data, 0, data.Length);
}
return outputStream.ToArray();
}
}
public static byte[] DecompressSaveData(byte[] compressedData) {
using (var inputStream = new MemoryStream(compressedData)) {
using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress)) {
using (var outputStream = new MemoryStream()) {
gzipStream.CopyTo(outputStream);
return outputStream.ToArray();
}
}
}
}
压缩效果:
- JSON 存档:通常可压缩 70-85%
- Binary 存档:通常可压缩 50-70%
- Protocol Buffers:通常可压缩 60-80%
四、存档版本兼容
4.1 版本控制
为什么需要版本控制:
- 游戏更新后存档结构可能变化
- 需要支持旧版本存档加载
- 避免玩家丢失进度
版本号格式:
存档版本:1.2.0(主版本.次版本.修订版本)
游戏版本:2.1.0
主版本变化:不兼容的结构变化(如删除字段)
次版本变化:向后兼容的添加(如新增字段)
修订版本变化:不影响结构的修复
版本号存储:
{
"meta": {
"save_version": "1.2.0",
"game_version": "2.1.0",
"timestamp": "2026-03-25T10:00:00Z"
}
}
4.2 数据结构演进
场景 1:添加新字段
版本 1.0.0:
{
"player": {
"health": 100,
"mana": 50
}
}
版本 1.1.0(添加 stamina):
{
"player": {
"health": 100,
"mana": 50,
"stamina": 80
}
}
迁移策略:
public class SaveMigrator {
public SaveData Migrate(SaveData oldData, string fromVersion) {
var version = new Version(fromVersion);
if (version < new Version("1.1.0")) {
oldData = Migrate_1_0_0_to_1_1_0(oldData);
}
if (version < new Version("1.2.0")) {
oldData = Migrate_1_1_0_to_1_2_0(oldData);
}
return oldData;
}
private SaveData Migrate_1_0_0_to_1_1_0(SaveData data) {
// 添加 stamina 字段,使用默认值
data.player.stamina = 100;
return data;
}
private SaveData Migrate_1_1_0_to_1_2_0(SaveData data) {
// 添加新的属性系统
data.player.attributes = new Attributes {
strength = 10,
agility = 10,
intelligence = 10
};
return data;
}
}
场景 2:删除字段
版本 1.2.0:
{
"player": {
"health": 100,
"mana": 50,
"stamina": 80,
"deprecated_field": "old_value"
}
}
版本 2.0.0(删除 deprecated_field):
{
"player": {
"health": 100,
"mana": 50,
"stamina": 80
}
}
迁移策略:
private SaveData Migrate_1_2_0_to_2_0_0(SaveData data) {
// 删除废弃字段(如果存在)
if (data.player.deprecated_field != null) {
// 如果有需要,可以转换数据
data.player.new_field = ConvertOldData(data.player.deprecated_field);
data.player.deprecated_field = null;
}
return data;
}
场景 3:修改字段类型
版本 1.0.0(位置是 2D):
{
"player": {
"position": {"x": 100, "y": 50}
}
}
版本 2.0.0(位置改为 3D):
{
"player": {
"position": {"x": 100, "y": 50, "z": 0}
}
}
迁移策略:
private SaveData Migrate_1_0_0_to_2_0_0(SaveData data) {
// 将 2D 位置转换为 3D 位置
var oldPos = data.player.position_2d;
data.player.position = new Vector3 {
x = oldPos.x,
y = oldPos.y,
z = 0 // 默认 Z 坐标
};
data.player.position_2d = null;
return data;
}
4.3 迁移策略
自动迁移:
- 加载存档时自动检测版本
- 按顺序应用迁移脚本
- 保存为新版本格式
- 对玩家透明
提示玩家:
- 检测到旧版本存档时提示
- 让玩家选择是否迁移
- 适用于破坏性变更
备份旧存档:
- 迁移前自动备份旧存档
- 保留最近 3 个版本的备份
- 允许玩家回滚
备份策略实现:
public class SaveBackupSystem {
private int maxBackups = 3;
public void BackupSaveFile(string savePath) {
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var backupPath = $"{savePath}.backup_{timestamp}";
File.Copy(savePath, backupPath);
// 清理旧备份
CleanupOldBackups(savePath);
}
private void CleanupOldBackups(string savePath) {
var directory = Path.GetDirectoryName(savePath);
var fileName = Path.GetFileName(savePath);
var backups = Directory.GetFiles(directory, $"{fileName}.backup_*")
.OrderByDescending(f => File.GetCreationTime(f))
.Skip(maxBackups)
.ToList();
foreach (var backup in backups) {
File.Delete(backup);
}
}
}
五、存档安全与防作弊
5.1 加密
为什么需要加密:
- 防止玩家直接修改存档数据
- 保护游戏内购和成就系统
- 防止恶意篡改导致游戏崩溃
对称加密(AES):
using System.Security.Cryptography;
public class SaveEncryption {
private static readonly byte[] Key = Encoding.UTF8.GetBytes("YourSecretKey1234567890123456"); // 32 字节
private static readonly byte[] IV = Encoding.UTF8.GetBytes("YourIV12345678"); // 16 字节
public static byte[] Encrypt(byte[] plainData) {
using (var aes = Aes.Create()) {
aes.Key = Key;
aes.IV = IV;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var encryptor = aes.CreateEncryptor()) {
return encryptor.TransformFinalBlock(plainData, 0, plainData.Length);
}
}
}
public static byte[] Decrypt(byte[] encryptedData) {
using (var aes = Aes.Create()) {
aes.Key = Key;
aes.IV = IV;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var decryptor = aes.CreateDecryptor()) {
return decryptor.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
}
}
}
}
加密强度选择:
| 强度 | 算法 | 密钥长度 | 适用场景 |
|---|---|---|---|
| 低 | XOR 加密 | 8 位 | 单机休闲游戏 |
| 中 | AES-128 | 128 位 | 大多数独立游戏 |
| 高 | AES-256 | 256 位 | 有内购/竞技的游戏 |
注意事项:
- 密钥不要硬编码在代码中(容易被反编译)
- 使用设备特定信息生成密钥(如机器码)
- 定期更换密钥(大版本更新时)
5.2 校验和(Checksum)
工作原理:
- 计算存档数据的哈希值
- 将哈希值附加到存档文件
- 加载时重新计算哈希值并对比
- 不匹配则说明数据被篡改
实现示例:
public class SaveChecksum {
public static string CalculateChecksum(byte[] data) {
using (var sha256 = SHA256.Create()) {
var hash = sha256.ComputeHash(data);
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
public static bool VerifyChecksum(byte[] data, string expectedChecksum) {
var actualChecksum = CalculateChecksum(data);
return actualChecksum == expectedChecksum;
}
}
// 使用示例
public class SecureSaveSystem {
public void Save(SaveData data) {
var jsonData = JsonUtility.ToJson(data);
var bytes = Encoding.UTF8.GetBytes(jsonData);
// 计算校验和
var checksum = SaveChecksum.CalculateChecksum(bytes);
data.meta.checksum = checksum;
// 重新序列化(包含校验和)
jsonData = JsonUtility.ToJson(data);
bytes = Encoding.UTF8.GetBytes(jsonData);
// 加密
var encrypted = SaveEncryption.Encrypt(bytes);
File.WriteAllBytes(savePath, encrypted);
}
public SaveData Load() {
var encrypted = File.ReadAllBytes(savePath);
// 解密
var bytes = SaveEncryption.Decrypt(encrypted);
var jsonData = Encoding.UTF8.GetString(bytes);
var data = JsonUtility.FromJson<SaveData>(jsonData);
// 验证校验和
var dataWithoutChecksum = RemoveChecksumFromData(data);
var bytesWithoutChecksum = Encoding.UTF8.GetBytes(JsonUtility.ToJson(dataWithoutChecksum));
if (!SaveChecksum.VerifyChecksum(bytesWithoutChecksum, data.meta.checksum)) {
throw new SecurityException("存档校验失败,可能已被篡改");
}
return data;
}
}
5.3 混淆
变量名混淆:
- 将 “health” 改为 “h_7x9k2”
- 增加逆向工程难度
- 不影响功能
数值混淆:
- 存储时:actual_value + random_offset
- 加载时:stored_value - random_offset
- 每次保存使用不同的 offset
示例:
public class ObfuscatedValue {
private int storedValue;
private int offset;
public ObfuscatedValue(int actualValue) {
offset = UnityEngine.Random.Range(-1000, 1000);
storedValue = actualValue + offset;
}
public int GetActualValue() {
return storedValue - offset;
}
public void SetActualValue(int newValue) {
offset = UnityEngine.Random.Range(-1000, 1000);
storedValue = newValue + offset;
}
}
5.4 反作弊系统
服务器验证:
- 关键数据(如内购、成就)上传服务器验证
- 服务器拒绝不合理的数据
- 适用于在线游戏
行为分析:
- 检测异常模式(如瞬间获得大量金币)
- 标记可疑账号
- 人工审核或自动封禁
第三方服务:
- Steam Anti-Cheat (VAC):仅限 Steam 多人游戏
- Easy Anti-Cheat:支持多平台
- BattlEye:主要用于大型游戏
- 自建系统:适合独立游戏
独立游戏推荐方案:
- 单机游戏:加密 + 校验和 + 混淆(足够)
- 在线游戏:服务器验证 + 行为分析
- 竞技游戏:第三方反作弊服务
六、云存档(Cloud Save)
6.1 Steam Cloud
集成方法:
Steamworks 配置:
- 登录 Steamworks 后台
- 进入 “Steam Cloud” 设置
- 启用 Cloud
- 设置字节配额(推荐:1GB)
- 设置文件数配额(推荐:100 个)
代码集成(Steamworks.NET):
using Steamworks;
public class SteamCloudSave {
public static bool SaveToCloud(string filename, byte[] data) {
// 检查 Steam Cloud 是否启用
if (!SteamRemoteStorage.IsCloudEnabledForApp()) {
Debug.LogWarning("Steam Cloud is disabled");
return false;
}
// 检查配额
ulong totalBytes, availableBytes;
SteamRemoteStorage.GetQuota(out totalBytes, out availableBytes);
if ((ulong)data.Length > availableBytes) {
Debug.LogError("Not enough Steam Cloud space");
return false;
}
// 写入文件
bool success = SteamRemoteStorage.FileWrite(filename, data, data.Length);
return success;
}
public static byte[] LoadFromCloud(string filename) {
if (!SteamRemoteStorage.FileExists(filename)) {
return null;
}
int fileSize = SteamRemoteStorage.GetFileSize(filename);
byte[] data = new byte[fileSize];
int bytesRead = SteamRemoteStorage.FileRead(filename, data, fileSize);
if (bytesRead != fileSize) {
Debug.LogError("Failed to read from Steam Cloud");
return null;
}
return data;
}
public static void DeleteFromCloud(string filename) {
if (SteamRemoteStorage.FileExists(filename)) {
SteamRemoteStorage.FileDelete(filename);
}
}
}
- 同步策略:
public class CloudSaveSync {
public void SyncSaves() {
var localSaves = GetLocalSaveFiles();
var cloudSaves = GetCloudSaveFiles();
foreach (var save in localSaves) {
var cloudSave = cloudSaves.FirstOrDefault(s => s.Filename == save.Filename);
if (cloudSave == null) {
// 本地有,云端没有 → 上传
UploadToCloud(save);
} else if (save.Timestamp > cloudSave.Timestamp) {
// 本地更新 → 上传
UploadToCloud(save);
} else if (cloudSave.Timestamp > save.Timestamp) {
// 云端更新 → 下载
DownloadFromCloud(cloudSave);
}
}
foreach (var save in cloudSaves) {
var localSave = localSaves.FirstOrDefault(s => s.Filename == save.Filename);
if (localSave == null) {
// 云端有,本地没有 → 下载
DownloadFromCloud(save);
}
}
}
}
限制:
- 每个用户 1GB 存储空间
- 每个用户 100 个文件
- 单文件大小无限制(但建议 < 100MB)
- 需要同步时间(可能延迟几秒到几分钟)
6.2 Epic Online Services
跨平台云存档:
using Epic.OnlineServices;
using Epic.OnlineServices.PlayerDataStorage;
public class EOSCloudSave {
private PlayerDataStorage _playerDataStorage;
public void Initialize() {
_playerDataStorage = EOSManager.Instance.GetPlayerDataStorageInterface();
}
public void SaveToCloud(string filename, byte[] data) {
var options = new WriteOptions {
LocalUserId = EOSManager.Instance.GetProductUserId(),
Filename = filename,
Data = data,
OnComplete = (result) => {
if (result.ResultCode == Result.Success) {
Debug.Log("Saved to EOS Cloud");
} else {
Debug.LogError($"Failed to save: {result.ResultCode}");
}
}
};
_playerDataStorage.Write(options);
}
public void LoadFromCloud(string filename, Action<byte[]> onComplete) {
var options = new ReadOptions {
LocalUserId = EOSManager.Instance.GetProductUserId(),
Filename = filename,
OnComplete = (result) => {
if (result.ResultCode == Result.Success) {
onComplete(result.Data);
} else {
Debug.LogError($"Failed to load: {result.ResultCode}");
onComplete(null);
}
}
};
_playerDataStorage.Read(options);
}
}
优势:
- 跨平台(PC、主机、移动)
- 免费使用
- 与 Epic Games Store 集成
- 支持离线同步
6.3 自建云存档
AWS S3 方案:
using Amazon.S3;
using Amazon.S3.Model;
public class AWSCloudSave {
private IAmazonS3 _s3Client;
private string _bucketName = "game-saves";
public async Task SaveToCloud(string userId, string filename, byte[] data) {
var key = $"{userId}/{filename}";
var request = new PutObjectRequest {
BucketName = _bucketName,
Key = key,
ContentBody = Convert.ToBase64String(data),
ContentType = "application/octet-stream"
};
await _s3Client.PutObjectAsync(request);
}
public async Task<byte[]> LoadFromCloud(string userId, string filename) {
var key = $"{userId}/{filename}";
try {
var response = await _s3Client.GetObjectAsync(_bucketName, key);
using (var reader = new StreamReader(response.ResponseStream)) {
var base64 = await reader.ReadToEndAsync();
return Convert.FromBase64String(base64);
}
} catch (AmazonS3Exception e) {
if (e.ErrorCode == "NoSuchKey") {
return null;
}
throw;
}
}
}
Azure Blob Storage 方案:
using Azure.Storage.Blobs;
public class AzureCloudSave {
private BlobContainerClient _containerClient;
public async Task SaveToCloud(string userId, string filename, byte[] data) {
var blobName = $"{userId}/{filename}";
var blobClient = _containerClient.GetBlobClient(blobName);
using (var stream = new MemoryStream(data)) {
await blobClient.UploadAsync(stream, overwrite: true);
}
}
public async Task<byte[]> LoadFromCloud(string userId, string filename) {
var blobName = $"{userId}/{filename}";
var blobClient = _containerClient.GetBlobClient(blobName);
try {
var response = await blobClient.DownloadAsync();
using (var memoryStream = new MemoryStream()) {
await response.Value.Content.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
} catch (RequestFailedException e) {
if (e.Status == 404) {
return null;
}
throw;
}
}
}
成本对比:
| 方案 | 初始成本 | 月成本(1 万用户) | 复杂度 | 推荐场景 |
|---|---|---|---|---|
| Steam Cloud | 0 | 0 | 低 | Steam 游戏 |
| EOS | 0 | 0 | 中 | 跨平台游戏 |
| AWS S3 | 0 | $5-20 | 高 | 自建平台 |
| Azure Blob | 0 | $5-20 | 高 | 自建平台 |
6.4 冲突解决
冲突类型:
- 时间戳冲突:本地和云端都有更新,但时间不同
- 内容冲突:本地和云端数据不一致
- 删除冲突:一端删除,另一端更新
解决策略:
| 策略 | 描述 | 适用场景 |
|---|---|---|
| 最新优先 | 使用时间戳最新的版本 | 大多数游戏 |
| 玩家选择 | 显示两个版本,让玩家选择 | RPG、策略游戏 |
| 合并 | 尝试合并两个版本的数据 | 复杂游戏(不推荐) |
| 本地优先 | 总是使用本地版本 | 离线游戏 |
| 云端优先 | 总是使用云端版本 | 在线游戏 |
玩家选择 UI 设计:
public class CloudSaveConflictUI : MonoBehaviour {
public void ShowConflict(SaveData localSave, SaveData cloudSave) {
// 显示两个版本的信息
localInfo.text = $"本地存档\n时间:{localSave.timestamp}\n游戏时长:{localSave.playtime}";
cloudInfo.text = $"云端存档\n时间:{cloudSave.timestamp}\n游戏时长:{cloudSave.playtime}";
// 玩家选择
useLocalButton.onClick.AddListener(() => {
UseLocalSave(localSave);
});
useCloudButton.onClick.AddListener(() => {
UseCloudSave(cloudSave);
});
}
private void UseLocalSave(SaveData save) {
// 上传本地存档到云端
CloudSave.Upload(save);
LoadSave(save);
Close();
}
private void UseCloudSave(SaveData save) {
// 下载云端存档到本地
CloudSave.Download(save);
LoadSave(save);
Close();
}
}
最佳实践:
- 优先使用"玩家选择"策略(最安全)
- 显示清晰的版本信息(时间戳、游戏时长、进度)
- 提供"记住我的选择"选项
- 记录冲突日志(用于调试)
七、跨平台存档
7.1 跨平台需求
常见场景:
- PC ↔ 主机(PlayStation、Xbox、Switch)
- 主机 ↔ 主机(不同平台之间)
- PC ↔ 移动设备
玩家期望:
- 无缝切换设备
- 进度完全同步
- 设置和偏好保持一致
7.2 实现方案
统一存档格式:
- 使用 JSON 或 Protocol Buffers
- 避免平台特定的数据类型
- 使用标准的日期/时间格式(ISO 8601)
平台特定适配:
public abstract class PlatformSaveAdapter {
public abstract byte[] GetPlatformSpecificData();
public abstract void ApplyPlatformSpecificData(byte[] data);
}
public class PCPlatformAdapter : PlatformSaveAdapter {
public override byte[] GetPlatformSpecificData() {
// PC 特定的设置(如键位绑定、分辨率)
var settings = new PCSettings {
resolution = Screen.currentResolution,
keyBindings = InputManager.GetKeyBindings()
};
return Serialize(settings);
}
public override void ApplyPlatformSpecificData(byte[] data) {
var settings = Deserialize<PCSettings>(data);
Screen.SetResolution(settings.resolution.width, settings.resolution.height, true);
InputManager.SetKeyBindings(settings.keyBindings);
}
}
public class ConsolePlatformAdapter : PlatformSaveAdapter {
public override byte[] GetPlatformSpecificData() {
// 主机特定的设置(如手柄配置)
var settings = new ConsoleSettings {
controllerSensitivity = InputManager.ControllerSensitivity,
vibrationEnabled = InputManager.VibrationEnabled
};
return Serialize(settings);
}
public override void ApplyPlatformSpecificData(byte[] data) {
var settings = Deserialize<ConsoleSettings>(data);
InputManager.ControllerSensitivity = settings.controllerSensitivity;
InputManager.VibrationEnabled = settings.vibrationEnabled;
}
}
云同步:
- 使用跨平台云服务(如 EOS、自建服务器)
- 统一的用户账号系统
- 设备无关的存档标识
7.3 案例
Hades 跨平台存档:
- 支持平台:PC、Switch、PlayStation、Xbox、iOS
- 使用 Supergiant 账号系统
- 存档通过 Supergiant 服务器同步
- 玩家反馈:极其流畅,几乎无延迟
实现要点:
- 统一的存档格式(Protocol Buffers)
- 平台特定的设置分离存储
- 自动冲突解决(最新优先)
- 离线时缓存,上线后同步
Dead Cells 跨平台存档:
- 支持平台:PC、Switch、PlayStation、Xbox、移动
- 使用平台特定的云存档(Steam Cloud、PSN、Xbox Live)
- 不支持跨平台同步(设计决策)
- 玩家反馈:希望支持跨平台同步
教训:
- 跨平台存档是玩家强烈期望的功能
- 早期设计时就要考虑跨平台
- 使用统一的账号系统(避免平台锁定)
八、存档 UI/UX 设计
8.1 存档界面
存档列表设计:
┌─────────────────────────────────────────────┐
│ 存档管理 │
├─────────────────────────────────────────────┤
│ │
│ [存档 1] │
│ ┌──────────┐ 游戏时长:12:34:56 │
│ │ │ 等级:15 │
│ │ 截图 │ 位置:黑暗森林 │
│ │ │ 保存时间:2026-03-25 10:00 │
│ └──────────┘ [加载] [删除] │
│ │
│ [存档 2] │
│ ┌──────────┐ 游戏时长:08:22:11 │
│ │ │ 等级:12 │
│ │ 截图 │ 位置:王城 │
│ │ │ 保存时间:2026-03-24 15:30 │
│ └──────────┘ [加载] [删除] │
│ │
│ [存档 3] - 空 │
│ │
│ [自动存档] │
│ ┌──────────┐ 游戏时长:12:40:22 │
│ │ │ 等级:15 │
│ │ 截图 │ 位置:黑暗森林 │
│ │ │ 保存时间:2026-03-25 10:05 │
│ └──────────┘ [加载] │
│ │
└─────────────────────────────────────────────┘
关键元素:
- 截图预览:帮助玩家识别存档
- 游戏时长:显示投入时间
- 进度信息:等级、位置、任务进度
- 保存时间:相对时间(“2 小时前”)+ 绝对时间
- 操作按钮:加载、删除、复制
8.2 快速存档/读档
快捷键:
- F5:快速存档
- F9:快速读档
- 可自定义快捷键
实现:
public class QuickSaveSystem : MonoBehaviour {
private void Update() {
if (Input.GetKeyDown(KeyCode.F5)) {
QuickSave();
}
if (Input.GetKeyDown(KeyCode.F9)) {
QuickLoad();
}
}
private void QuickSave() {
var gameState = CollectGameState();
SaveSystem.SaveToSlot(999, gameState); // 使用特殊槽位
ShowNotification("快速存档完成");
}
private void QuickLoad() {
if (SaveSystem.SaveExists(999)) {
var gameState = SaveSystem.LoadFromSlot(999);
RestoreGameState(gameState);
ShowNotification("快速读档完成");
} else {
ShowNotification("没有快速存档");
}
}
}
8.3 存档管理
删除存档:
- 二次确认对话框
- 显示"此操作不可撤销"警告
- 要求输入确认(如输入"DELETE")
复制存档:
- 允许玩家备份重要存档
- 用于尝试不同选择
- 限制最大副本数(避免占满槽位)
导入/导出:
- 导出为文件(用于备份或分享)
- 导入外部存档(用于恢复或作弊)
- 支持批量操作
实现示例:
public class SaveFileManager {
public void ExportSave(int slotIndex, string exportPath) {
var saveData = SaveSystem.LoadFromSlot(slotIndex);
var json = JsonUtility.ToJson(saveData, true);
File.WriteAllText(exportPath, json);
}
public void ImportSave(string importPath, int targetSlot) {
var json = File.ReadAllText(importPath);
var saveData = JsonUtility.FromJson<SaveData>(json);
SaveSystem.SaveToSlot(targetSlot, saveData);
}
public void DeleteSave(int slotIndex) {
// 显示确认对话框
if (ShowConfirmation("确定要删除这个存档吗?此操作不可撤销。")) {
SaveSystem.DeleteSlot(slotIndex);
}
}
public void CopySave(int sourceSlot, int targetSlot) {
var saveData = SaveSystem.LoadFromSlot(sourceSlot);
SaveSystem.SaveToSlot(targetSlot, saveData);
}
}
九、存档系统测试
9.1 功能测试
基础功能:
- 手动存档可以保存和加载
- 自动存档按预期触发
- 检查点系统正常工作
- 快速存档/读档正常工作
- 所有存档槽位可用
数据完整性:
- 玩家位置正确保存和恢复
- 玩家状态(血量、魔法等)正确保存
- 背包物品正确保存
- 任务进度正确保存
- NPC 状态正确保存
- 世界状态正确保存
UI 测试:
- 存档列表正确显示所有存档
- 截图正确生成和显示
- 时间戳正确显示
- 删除确认对话框正常工作
9.2 边界测试
存档损坏:
- 存档文件被删除时的处理
- 存档文件被篡改时的处理
- 存档文件格式错误时的处理
- 存档文件部分损坏时的处理
版本不兼容:
- 加载旧版本存档
- 加载未来版本存档
- 版本迁移脚本正确工作
- 迁移后数据完整性
存储空间不足:
- 磁盘空间不足时的处理
- 云存档配额不足时的处理
- 友好的错误提示
极端情况:
- 大量存档(100+ 个槽位)
- 超大存档文件(100MB+)
- 频繁保存(每秒保存)
- 保存时游戏崩溃
9.3 性能测试
存档时间:
- 手动存档 < 500ms
- 自动存档 < 300ms(不阻塞游戏)
- 云存档上传 < 5s
读档时间:
- 本地存档加载 < 1s
- 云存档下载 < 5s
- 大存档加载 < 3s
内存占用:
- 存档过程中内存峰值 < 200MB
- 存档完成后内存释放
- 无内存泄漏
9.4 测试 Checklist(20 项)
基础功能:
- 1. 手动存档保存成功
- 2. 手动存档加载成功
- 3. 自动存档按时间触发
- 4. 自动存档按事件触发
- 5. 检查点保存和加载成功
- 6. 快速存档/读档成功
- 7. 所有存档槽位可用
数据完整性:
- 8. 玩家位置正确保存
- 9. 玩家状态正确保存
- 10. 背包物品正确保存
- 11. 任务进度正确保存
- 12. NPC 状态正确保存
云存档:
- 13. Steam Cloud 保存成功
- 14. Steam Cloud 加载成功
- 15. 云存档冲突正确解决
- 16. 离线时云存档正确处理
边界情况:
- 17. 存档损坏时友好提示
- 18. 版本不兼容时正确迁移
- 19. 存储空间不足时友好提示
- 20. 性能满足要求(存档 < 500ms,读档 < 1s)
十、附录
10.1 存档数据结构模板
完整存档结构(JSON):
{
"meta": {
"save_version": "1.2.0",
"game_version": "2.1.0",
"timestamp": "2026-03-25T10:00:00Z",
"playtime_seconds": 36000,
"checksum": "a1b2c3d4e5f6..."
},
"player": {
"position": {"x": 100.5, "y": 50.2, "z": 0},
"rotation": {"x": 0, "y": 45, "z": 0},
"health": 80,
"max_health": 100,
"mana": 50,
"max_mana": 80,
"experience": 1250,
"level": 5,
"stats": {
"strength": 12,
"agility": 10,
"intelligence": 8
},
"inventory": [
{"item_id": "sword_001", "quantity": 1, "durability": 85},
{"item_id": "potion_001", "quantity": 5}
],
"equipment": {
"weapon": "sword_001",
"armor": "armor_002",
"accessory": null
}
},
"progress": {
"current_level": "level_03",
"completed_levels": ["level_01", "level_02"],
"quests": {
"main_quest_01": {
"status": "completed",
"objectives": [
{"id": "kill_boss", "completed": true},
{"id": "collect_item", "completed": true}
]
}
},
"story_flags": {
"met_npc_01": true,
"discovered_secret_area": false,
"boss_01_defeated": true
}
},
"world": {
"npc_states": {
"npc_01": {
"location": "village_square",
"relationship": 75,
"dialogue_flags": ["greeting_done", "quest_given"]
}
},
"items": {
"chest_001": {"opened": true, "contents": []},
"chest_002": {"opened": false, "contents": ["potion_001"]}
},
"environment": {
"time_of_day": 14.5,
"weather": "sunny",
"season": "spring",
"day_count": 15
}
},
"statistics": {
"deaths": 12,
"enemies_killed": 245,
"items_collected": 89,
"distance_walked": 15420.5
},
"settings": {
"difficulty": "normal",
"language": "zh-CN",
"audio_volume": 0.8,
"music_volume": 0.6
}
}
10.2 存档系统 Checklist
设计阶段:
- 确定存档类型(手动/自动/检查点/混合)
- 确定存档槽位数量
- 确定数据格式(JSON/Binary/Protobuf)
- 设计存档数据结构
- 确定加密和校验方案
- 确定云存档方案
实现阶段:
- 实现存档保存功能
- 实现存档加载功能
- 实现自动存档系统
- 实现检查点系统
- 实现存档压缩
- 实现存档加密
- 实现校验和验证
- 实现版本迁移系统
云存档:
- 集成 Steam Cloud
- 实现云存档同步
- 实现冲突解决机制
- 测试跨平台同步
UI/UX:
- 设计存档列表界面
- 实现截图预览
- 实现快速存档/读档
- 实现存档管理(删除、复制、导入/导出)
- 实现确认对话框
测试:
- 完成功能测试
- 完成边界测试
- 完成性能测试
- 完成兼容性测试
- 完成云存档测试
10.3 Steam Cloud 配置指南
步骤 1:登录 Steamworks 后台
- 访问 https://partner.steamgames.com
- 选择你的应用
步骤 2:启用 Steam Cloud
- 进入 “Steam Cloud” 设置
- 勾选 “Enable cloud”
- 设置字节配额:1073741824(1GB)
- 设置文件数配额:100
步骤 3:配置同步规则
- 选择同步时机:
- 应用启动时
- 应用关闭时
- 文件更改时(推荐)
步骤 4:测试
- 在一台设备上保存
- 在另一台设备上加载
- 验证数据一致性
步骤 5:监控
- 使用 Steamworks 统计面板
- 监控云存档使用率
- 处理用户反馈
常见问题:
- 配额不足:增加配额或优化存档大小
- 同步延迟:检查网络连接,使用增量同步
- 冲突频繁:优化冲突解决策略
10.4 存档安全实现代码示例
完整的安全存档系统:
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public class SecureSaveSystem {
private static readonly byte[] EncryptionKey = GenerateDeviceSpecificKey();
private static readonly byte[] EncryptionIV = new byte[16] {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10
};
public static void Save(string filename, SaveData data) {
try {
// 1. 序列化
var json = JsonUtility.ToJson(data);
var jsonBytes = Encoding.UTF8.GetBytes(json);
// 2. 计算校验和
var checksum = CalculateChecksum(jsonBytes);
data.meta.checksum = checksum;
// 3. 重新序列化(包含校验和)
json = JsonUtility.ToJson(data);
jsonBytes = Encoding.UTF8.GetBytes(json);
// 4. 压缩
var compressed = Compress(jsonBytes);
// 5. 加密
var encrypted = Encrypt(compressed);
// 6. 写入文件
var path = GetSavePath(filename);
File.WriteAllBytes(path, encrypted);
Debug.Log($"存档保存成功:{filename}");
} catch (Exception e) {
Debug.LogError($"存档保存失败:{e.Message}");
throw;
}
}
public static SaveData Load(string filename) {
try {
var path = GetSavePath(filename);
if (!File.Exists(path)) {
Debug.LogWarning($"存档不存在:{filename}");
return null;
}
// 1. 读取文件
var encrypted = File.ReadAllBytes(path);
// 2. 解密
var compressed = Decrypt(encrypted);
// 3. 解压缩
var jsonBytes = Decompress(compressed);
// 4. 反序列化
var json = Encoding.UTF8.GetString(jsonBytes);
var data = JsonUtility.FromJson<SaveData>(json);
// 5. 验证校验和
var dataWithoutChecksum = JsonUtility.FromJson<SaveData>(json);
dataWithoutChecksum.meta.checksum = "";
var jsonWithoutChecksum = JsonUtility.ToJson(dataWithoutChecksum);
var bytesWithoutChecksum = Encoding.UTF8.GetBytes(jsonWithoutChecksum);
var expectedChecksum = data.meta.checksum;
var actualChecksum = CalculateChecksum(bytesWithoutChecksum);
if (expectedChecksum != actualChecksum) {
Debug.LogError("存档校验失败,可能已被篡改");
throw new SecurityException("存档校验失败");
}
// 6. 版本迁移
if (data.meta.save_version != GetCurrentSaveVersion()) {
data = MigrateSave(data);
}
Debug.Log($"存档加载成功:{filename}");
return data;
} catch (Exception e) {
Debug.LogError($"存档加载失败:{e.Message}");
throw;
}
}
private static byte[] Encrypt(byte[] data) {
using (var aes = Aes.Create()) {
aes.Key = EncryptionKey;
aes.IV = EncryptionIV;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var encryptor = aes.CreateEncryptor()) {
return encryptor.TransformFinalBlock(data, 0, data.Length);
}
}
}
private static byte[] Decrypt(byte[] data) {
using (var aes = Aes.Create()) {
aes.Key = EncryptionKey;
aes.IV = EncryptionIV;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var decryptor = aes.CreateDecryptor()) {
return decryptor.TransformFinalBlock(data, 0, data.Length);
}
}
}
private static byte[] Compress(byte[] data) {
using (var output = new MemoryStream()) {
using (var gzip = new System.IO.Compression.GZipStream(
output, System.IO.Compression.CompressionLevel.Optimal)) {
gzip.Write(data, 0, data.Length);
}
return output.ToArray();
}
}
private static byte[] Decompress(byte[] data) {
using (var input = new MemoryStream(data)) {
using (var gzip = new System.IO.Compression.GZipStream(
input, System.IO.Compression.CompressionMode.Decompress)) {
using (var output = new MemoryStream()) {
gzip.CopyTo(output);
return output.ToArray();
}
}
}
}
private static string CalculateChecksum(byte[] data) {
using (var sha256 = SHA256.Create()) {
var hash = sha256.ComputeHash(data);
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
private static byte[] GenerateDeviceSpecificKey() {
// 使用设备特定信息生成密钥
var deviceInfo = $"{SystemInfo.deviceUniqueIdentifier}_{Application.productName}";
using (var sha256 = SHA256.Create()) {
return sha256.ComputeHash(Encoding.UTF8.GetBytes(deviceInfo));
}
}
private static string GetSavePath(string filename) {
return Path.Combine(Application.persistentDataPath, filename);
}
private static string GetCurrentSaveVersion() {
return "1.2.0";
}
private static SaveData MigrateSave(SaveData data) {
// 实现版本迁移逻辑
return new SaveMigrator().Migrate(data, data.meta.save_version);
}
}
存档系统是游戏体验的基石。一个设计良好的存档系统可以让玩家安心投入游戏,而一个糟糕的存档系统会让玩家永远离开。从项目开始就重视存档系统设计,遵循本文的最佳实践,你的玩家会感谢你的。祝你的存档系统稳定可靠!
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。