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直接罢工,返回空字符串。嗯,这个坑踩过一次就记住了。
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()会卡顿 | 批量操作,减少写入频率 |
4.4 性能考量:别让存档拖慢游戏
说到性能,咱们得聊聊PlayerPrefs的读写机制。你每次调用 SetString 或 SetInt,数据其实先写到了内存缓存里。只有调用了 Save(),才会真正写入磁盘。这个设计本身没问题,但很多人会犯一个错误——频繁调用Save()。
为什么会这样?因为有些人觉得“不Save不放心”,于是在Update里每帧都Save一次。你想想看,每帧都写一次磁盘,游戏不卡才怪。
我个人习惯的做法是:
- 批量修改,一次保存:把所有数据改完后,最后调一次Save()。
- 定时保存:比如每30秒自动保存一次,或者切场景时保存。
- 增量保存:只保存变化的部分,而不是全量覆盖。
// 不好的做法:频繁保存
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;
}
}
4.5 避坑指南:我踩过的那些坑
最后,分享几个我亲身经历过的坑,希望能帮你省点时间。
- Key冲突:不同模块用了相同的Key,互相覆盖。我曾经在UI模块和战斗模块都用了"Level"这个Key,结果UI改完,战斗模块的数据就丢了。解决方案:用命名空间前缀,比如"UI_Level"和"Battle_Level"。
- JSON字段名变更:项目迭代中,把类里的字段名改了,结果旧存档加载出来全是默认值。因为JsonUtility是按字段名匹配的。解决方案:用
[FormerlySerializedAs]特性,或者写一个版本迁移逻辑。 - 特殊字符:存字符串时,如果包含换行符或引号,JSON序列化会自动转义。但如果你手动拼接字符串,很容易出问题。所以,永远用JsonUtility,别自己拼JSON。
嗯,关于PlayerPrefs的进阶用法,咱们就聊到这儿。下一章我会讲怎么用文件系统来存更复杂的数据,到时候你会看到,PlayerPrefs其实只是冰山一角。