4. PlayerPrefs进阶:保存复杂数据(JSON序列化)、PlayerPrefs的局限性分析、性能考量

好,咱们接着聊PlayerPrefs。上一章咱们用它存了些简单的键值对,比如玩家姓名、音量设置。但问题来了——你想存一个背包里的道具列表,或者一个角色的完整属性,怎么办?PlayerPrefs本身只支持int、float、string这三种类型。嗯,这时候就得请出JSON了。

4.1 用JSON把复杂数据“塞进”字符串

说白了,JSON就是一种轻量级的数据交换格式。它能把一个对象或数组,序列化成一个字符串。然后这个字符串,就可以用PlayerPrefs的SetString来保存了。

我个人习惯用Unity自带的JsonUtility。它用起来简单,而且性能也不错。来看个例子:

// 定义一个可序列化的类
[System.Serializable]
public class PlayerData
{
    public string playerName;
    public int level;
    public float health;
    public List<string> inventory; // 道具列表
}

// 保存数据
public void SavePlayerData(PlayerData data)
{
    string json = JsonUtility.ToJson(data);
    PlayerPrefs.SetString("PlayerDataKey", json);
    PlayerPrefs.Save();
    Debug.Log("数据已保存为JSON: " + json);
}

// 加载数据
public PlayerData LoadPlayerData()
{
    if (PlayerPrefs.HasKey("PlayerDataKey"))
    {
        string json = PlayerPrefs.GetString("PlayerDataKey");
        PlayerData data = JsonUtility.FromJson<PlayerData>(json);
        return data;
    }
    return null;
}

你看,核心就两步:ToJson 把对象变字符串,FromJson 把字符串变回对象。我在项目中遇到过一个问题——忘了给类加 [System.Serializable] 特性,结果JsonUtility直接罢工,返回空字符串。嗯,这个坑踩过一次就记住了。

小提示:JsonUtility不支持Dictionary的直接序列化。如果你非要存字典,可以自己写一个包装类,或者用第三方库如Newtonsoft.Json。我个人建议,能用List就用List,省心。

4.2 保存数组和嵌套对象

你想想看,游戏里哪有那么简单的一个平面数据?角色有装备,装备有属性,属性还有加成。这种嵌套结构,JSON处理起来就游刃有余了。

[System.Serializable]
public class Equipment
{
    public string name;
    public int attackBonus;
    public int defenseBonus;
}

[System.Serializable]
public class GameSaveData
{
    public int gold;
    public List<Equipment> equipmentList;
    public Vector3 playerPosition; // 注意:Vector3也可以序列化
}

// 使用示例
GameSaveData saveData = new GameSaveData();
saveData.gold = 1000;
saveData.equipmentList = new List<Equipment>();
saveData.equipmentList.Add(new Equipment { name = "铁剑", attackBonus = 5, defenseBonus = 0 });
saveData.playerPosition = new Vector3(10.5f, 0f, -3.2f);

string json = JsonUtility.ToJson(saveData);
PlayerPrefs.SetString("GameSave", json);

这里要注意一点:Vector3、Quaternion这些Unity常用类型,JsonUtility是原生支持的。但如果你用了自定义的结构体,记得也要加上 [System.Serializable]

4.3 PlayerPrefs的局限性分析

讲到这里,我得泼点冷水了。PlayerPrefs虽然方便,但它不是万能的。我在好几个项目里都吃过它的亏,咱们一条条说。

局限性 具体表现 我的建议
存储位置固定 Windows在注册表,Mac在plist,Android在SharedPreferences 跨平台移植时要注意清理旧数据
数据不安全 明文存储,玩家可以轻易修改 不要存敏感信息,如金币数量、充值记录
大小限制 不同平台限制不同,一般1MB左右 只存配置类数据,别存大文本或二进制
不支持复杂查询 只能通过Key取值,没法遍历或搜索 数据量大了就换SQLite或文件存储
性能问题 频繁调用Save()会卡顿 批量操作,减少写入频率
警告:千万不要用PlayerPrefs存游戏存档!我曾经在一个RPG项目里,把整个地图的NPC状态都塞进PlayerPrefs,结果存档文件超过2MB,每次保存都卡半秒。玩家骂声一片。后来乖乖换成了二进制文件存储。

4.4 性能考量:别让存档拖慢游戏

说到性能,咱们得聊聊PlayerPrefs的读写机制。你每次调用 SetStringSetInt,数据其实先写到了内存缓存里。只有调用了 Save(),才会真正写入磁盘。这个设计本身没问题,但很多人会犯一个错误——频繁调用Save()。

为什么会这样?因为有些人觉得“不Save不放心”,于是在Update里每帧都Save一次。你想想看,每帧都写一次磁盘,游戏不卡才怪。

我个人习惯的做法是:

  1. 批量修改,一次保存:把所有数据改完后,最后调一次Save()。
  2. 定时保存:比如每30秒自动保存一次,或者切场景时保存。
  3. 增量保存:只保存变化的部分,而不是全量覆盖。
// 不好的做法:频繁保存
void Update()
{
    if (Input.GetKeyDown(KeyCode.S))
    {
        PlayerPrefs.SetInt("Score", score);
        PlayerPrefs.Save(); // 每按一次S就写磁盘
    }
}

// 好的做法:延迟批量保存
private bool needSave = false;

public void SetScore(int newScore)
{
    score = newScore;
    needSave = true;
}

void LateUpdate()
{
    if (needSave)
    {
        PlayerPrefs.SetInt("Score", score);
        PlayerPrefs.Save();
        needSave = false;
    }
}
核心要点:PlayerPrefs适合存少量、不频繁变更的配置数据。比如音量、画质、语言设置。对于游戏存档、排行榜、玩家属性这类复杂且频繁变动的数据,建议使用文件存储或数据库。

4.5 避坑指南:我踩过的那些坑

最后,分享几个我亲身经历过的坑,希望能帮你省点时间。

  • Key冲突:不同模块用了相同的Key,互相覆盖。我曾经在UI模块和战斗模块都用了"Level"这个Key,结果UI改完,战斗模块的数据就丢了。解决方案:用命名空间前缀,比如"UI_Level"和"Battle_Level"。
  • JSON字段名变更:项目迭代中,把类里的字段名改了,结果旧存档加载出来全是默认值。因为JsonUtility是按字段名匹配的。解决方案:用 [FormerlySerializedAs] 特性,或者写一个版本迁移逻辑。
  • 特殊字符:存字符串时,如果包含换行符或引号,JSON序列化会自动转义。但如果你手动拼接字符串,很容易出问题。所以,永远用JsonUtility,别自己拼JSON。

嗯,关于PlayerPrefs的进阶用法,咱们就聊到这儿。下一章我会讲怎么用文件系统来存更复杂的数据,到时候你会看到,PlayerPrefs其实只是冰山一角。