一、为什么要替换PlayerPrefs?

你有没有遇到过这样的情况?用Unity自带的PlayerPrefs存玩家的金币、等级这些核心数据,玩家随便找个注册表编辑器,就能把自己的金币改成几百万,等级直接拉满,游戏的平衡和体验瞬间就崩了。PlayerPrefs的本质是明文存储,在Windows里存注册表,Android里存XML文件,玩家不用破解游戏,用普通工具就能修改,这对需要保护存档的游戏来说,简直是裸奔。所以我们需要一个安全的替代方案,既兼容Unity的跨平台特性,又能把存档数据加密锁起来。

二、用AES加密替代的核心思路

2.1 通俗理解AES加密

AES是现在游戏开发里常用的对称加密方式,你可以把它想象成一个带锁的盒子:游戏和玩家都知道唯一的钥匙(密钥),要存的存档数据先塞进盒子里,用钥匙锁起来,别人拿到盒子没有钥匙打不开;游戏读取的时候,用同样的钥匙打开盒子,就能拿到原始的存档数据。这个方式简单高效,特别适合游戏本地存档这种不需要多人共享、只需要本地安全的场景。

2.2 序列化+加密的完整流程

整个过程是两步:第一步是序列化,就是把玩家的存档对象(比如包含金币、等级的类)转成一串字节或者字符串,这样才能被加密;第二步是加密,把序列化后的内容用AES加密,变成看不懂的乱码,存到本地文件里。读取的时候反过来,先解密乱码得到原始序列化内容,再反序列化回游戏能用的存档对象。

三、具体实现示例(Unity C#环境)

3.1 技术栈说明

技术栈:Unity 2020+ 版本(兼容.NET Standard 2.1),使用Newtonsoft.Json做序列化(比Unity自带序列化更灵活),无需额外插件,只需要在Unity里导入Newtonsoft.Json包即可。

3.2 完整代码实现

using System;
using System.IO;
using System.Security.Cryptography;
using UnityEngine;
using Newtonsoft.Json;

// 1. 定义存档数据的类:所有要存的游戏数据都放这里,必须支持序列化
[Serializable]
public class PlayerSaveData
{
    public int Gold;       // 玩家当前金币
    public int Level;      // 玩家当前等级
    public float CurrentHp;// 当前血量
    public string PlayerName;// 玩家昵称
    public int UnlockLevel;// 解锁的关卡数
}

// 2. 加密解密工具类:封装加密、解密、存文件、读文件的所有逻辑
public class SaveEncryptHelper
{
    // 注意:这个密钥和IV是测试用的!真实项目里绝对不能硬编码,后面会讲怎么安全存
    private static readonly string AES_KEY = "GameSaveKey_2024_Secure1234"; // 必须是16/24/32位
    private static readonly string AES_IV = "GameSaveIV_2024_Start";         // 必须是16位

    // 加密方法:把存档对象转成加密后的字节数组
    public static byte[] EncryptSave(PlayerSaveData saveData)
    {
        // 第一步:把存档对象转成JSON字符串,再转成字节数组
        string jsonStr = JsonConvert.SerializeObject(saveData);
        byte[] rawBytes = System.Text.Encoding.UTF8.GetBytes(jsonStr);

        // 第二步:用AES加密字节数组
        using (Aes aes = Aes.Create())
        {
            aes.Key = System.Text.Encoding.UTF8.GetBytes(AES_KEY);
            aes.IV = System.Text.Encoding.UTF8.GetBytes(AES_IV);
            ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

            using (MemoryStream ms = new MemoryStream())
            {
                using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                {
                    cs.Write(rawBytes, 0, rawBytes.Length);
                    cs.FlushFinalBlock(); // 必须调用,否则加密不完整
                    return ms.ToArray();
                }
            }
        }
    }

    // 解密方法:把加密的字节数组转成存档对象
    public static PlayerSaveData DecryptSave(byte[] encryptedBytes)
    {
        using (Aes aes = Aes.Create())
        {
            aes.Key = System.Text.Encoding.UTF8.GetBytes(AES_KEY);
            aes.IV = System.Text.Encoding.UTF8.GetBytes(AES_IV);
            ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

            using (MemoryStream ms = new MemoryStream(encryptedBytes))
            {
                using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                {
                    using (StreamReader sr = new StreamReader(cs))
                    {
                        string jsonStr = sr.ReadToEnd();
                        // 转成存档对象,如果格式不对会抛异常,后面处理
                        return JsonConvert.DeserializeObject<PlayerSaveData>(jsonStr);
                    }
                }
            }
        }
    }

    // 存存档到本地文件:Unity跨平台兼容路径
    public static void SaveToFile(PlayerSaveData saveData, string fileName = "MyGameSave.dat")
    {
        // Application.persistentDataPath是Unity专门给游戏的可写路径,不同平台都能用
        string fullPath = Path.Combine(Application.persistentDataPath, fileName);
        byte[] encryptedData = EncryptSave(saveData);
        File.WriteAllBytes(fullPath, encryptedData);
        Debug.Log($"存档已保存,路径:{fullPath}");
    }

    // 从本地文件加载存档,解密后返回
    public static PlayerSaveData LoadFromFile(string fileName = "MyGameSave.dat")
    {
        string fullPath = Path.Combine(Application.persistentDataPath, fileName);
        // 如果文件不存在,返回默认存档
        if (!File.Exists(fullPath))
        {
            Debug.Log("未找到存档文件,返回初始存档");
            return GetDefaultSaveData();
        }

        byte[] encryptedData = File.ReadAllBytes(fullPath);
        try
        {
            return DecryptSave(encryptedData);
        }
        catch (Exception e)
        {
            // 解密失败(密钥错或数据被改),返回默认存档
            Debug.LogError($"存档损坏,错误信息:{e.Message},返回默认存档");
            return GetDefaultSaveData();
        }
    }

    // 生成默认存档:当无存档或存档损坏时用
    private static PlayerSaveData GetDefaultSaveData()
    {
        return new PlayerSaveData
        {
            Gold = 1000,
            Level = 1,
            CurrentHp = 150f,
            PlayerName = "冒险者",
            UnlockLevel = 1
        };
    }
}

// 3. 游戏内调用示例:比如在游戏管理器里用这个工具
public class GameMain : MonoBehaviour
{
    private PlayerSaveData currentSave;

    void Start()
    {
        // 游戏启动时加载存档
        currentSave = SaveEncryptHelper.LoadFromFile();
        Debug.Log($"启动成功,当前金币:{currentSave.Gold}");
    }

    void Update()
    {
        // 按S键保存,按L键加载,测试用
        if (Input.GetKeyDown(KeyCode.S))
        {
            currentSave.Gold += 50; // 模拟杀怪赚金币
            SaveEncryptHelper.SaveToFile(currentSave);
            Debug.Log("存档已更新");
        }
        if (Input.GetKeyDown(KeyCode.L))
        {
            currentSave = SaveEncryptHelper.LoadFromFile();
            Debug.Log($"加载后金币:{currentSave.Gold}");
        }
    }
}

四、应用场景

这个方案特别适合这些游戏类型:比如单机RPG、养成类、经营类,这些游戏的存档数据是玩家的核心资产,玩家修改存档会直接影响游戏的公平性和可玩性;也适合有剧情存档、多周目存档的游戏,防止玩家通过修改存档跳过剧情或者获取额外收益。而像特别简单的休闲游戏(比如只是存个玩家的游戏次数),如果对安全要求不高,也可以用,但还是推荐这个方案,毕竟加密只需要加几行代码,能多一层保护。

五、技术优缺点分析

5.1 优点

第一是安全,AES是国际通用的成熟加密算法,不知道密钥的人根本打不开加密后的存档,破解难度远高于明文的PlayerPrefs;第二是跨平台,Unity的Application.persistentDataPath自动适配Windows、Android、iOS等所有平台,不需要单独处理路径;第三是灵活,序列化用JSON,可以轻松扩展存档类,比如加个新的道具列表,只需要在PlayerSaveData里加字段就行。

5.2 缺点

第一是比PlayerPrefs麻烦一点,需要处理加密解密和序列化,不像PlayerPrefs一条命令就能存;第二是密钥管理的问题,一旦密钥泄露,存档就等于没加密,这个需要特别注意;第三是如果存档类的结构改了(比如改了字段名),旧的存档可能解密失败,需要做兼容处理。

六、注意事项

6.1 密钥的安全存储

刚才的代码里密钥是硬编码在类里的,这是测试用的,绝对不能用在正式项目!因为反编译Unity游戏的工具(比如dnSpy)能直接看到这些密钥,存档就白加密了。正确的做法是:把密钥拆成几段,分别藏在游戏的不同资源里,或者用平台提供的安全存储,比如Windows的DPAPI、Android的Keystore、iOS的Keychain;也可以用设备的唯一标识(比如Android的IMEI、iOS的IDFA)生成密钥,这样同一设备用固定密钥,不同设备的存档不能互通,进一步提升安全。

6.2 序列化的兼容

如果更新游戏时改了PlayerSaveData的结构(比如把Gold改成Coin),旧的存档解密会失败,这时候需要加兼容逻辑,比如在存档类里加版本号,读取的时候先判断版本,再处理旧字段,不然玩家的存档就废了。

6.3 异常处理

解密失败的情况要考虑到,比如玩家修改了加密后的文件,或者换了设备,这时候返回默认存档,而不是让游戏崩溃,提升鲁棒性。

七、总结

用C#的AES加密替代Unity的PlayerPrefs,是保护游戏存档安全的简单有效方案,整个流程就是序列化-加密-存文件,读取时解密-反序列化-用数据,代码量不大但安全提升明显。只要注意密钥的安全存储、序列化的兼容和异常处理,就能在不影响游戏性能的前提下,防止玩家恶意修改存档,保证游戏的公平性和玩家体验。