独立游戏存档系统设计:自动存档、云存档与存档数据结构完全指南

深入讲解独立游戏存档系统设计的完整实战指南:涵盖手动存档、自动存档、检查点系统设计,存档数据结构与序列化方案,版本兼容与迁移策略,Steam云存档集成,防作弊与存档加密,以及跨平台存档同步,附代码示例与最佳实践。

独立游戏存档系统设计:自动存档、云存档与存档数据结构完全指南

存档系统是游戏中最容易被低估、却最容易引发玩家愤怒的系统。一个设计糟糕的存档系统可以让玩家丢失数十小时的游戏进度,直接导致差评和退款。根据 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-128128 位大多数独立游戏
AES-256256 位有内购/竞技的游戏

注意事项

  • 密钥不要硬编码在代码中(容易被反编译)
  • 使用设备特定信息生成密钥(如机器码)
  • 定期更换密钥(大版本更新时)

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

集成方法

  1. Steamworks 配置

    • 登录 Steamworks 后台
    • 进入 “Steam Cloud” 设置
    • 启用 Cloud
    • 设置字节配额(推荐:1GB)
    • 设置文件数配额(推荐:100 个)
  2. 代码集成(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);
        }
    }
}
  1. 同步策略
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 Cloud00Steam 游戏
EOS00跨平台游戏
AWS S30$5-20自建平台
Azure Blob0$5-20自建平台

6.4 冲突解决

冲突类型

  1. 时间戳冲突:本地和云端都有更新,但时间不同
  2. 内容冲突:本地和云端数据不一致
  3. 删除冲突:一端删除,另一端更新

解决策略

策略描述适用场景
最新优先使用时间戳最新的版本大多数游戏
玩家选择显示两个版本,让玩家选择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 后台

步骤 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);
    }
}

存档系统是游戏体验的基石。一个设计良好的存档系统可以让玩家安心投入游戏,而一个糟糕的存档系统会让玩家永远离开。从项目开始就重视存档系统设计,遵循本文的最佳实践,你的玩家会感谢你的。祝你的存档系统稳定可靠!

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页