2、PlayerPrefs基础:原理、数据类型与核心API

好,咱们正式开始讲数据持久化。第一个要聊的,就是 PlayerPrefs。

说实话,这个 API 可能是 Unity 里最简单、最“无脑”的存档方式了。我刚入行那会儿,做第一个小游戏,存金币数量、存关卡进度,用的就是它。一行代码搞定,连数据库都不用学。

但简单归简单,它背后有些东西,你最好还是搞清楚。不然哪天线上项目出了 bug,你连怎么排查都不知道。

PlayerPrefs 的原理

PlayerPrefs 本质上是一个键值对存储系统。你给它一个“钥匙”(Key),再给它一个“值”(Value),它帮你存到本地硬盘上。

具体存到哪?不同平台不一样:

平台 存储位置 文件格式
Windows 注册表(HKCU\Software\公司名\产品名) 二进制
macOS ~/Library/Preferences/unity.公司名.产品名.plist plist
Android /data/data/包名/shared_prefs/包名.xml XML
iOS NSUserDefaults(沙盒内) plist
WebGL 浏览器 IndexedDB JSON

嗯,这里要注意:PlayerPrefs 不是加密的。你存进去的数值,玩家用个文本编辑器或者手机文件管理器就能看到。所以千万别存密码、支付凭证这类敏感信息。我见过有人把微信支付密钥存进去,结果被玩家扒出来刷了十几万的道具……那场面,惨不忍睹。

支持的数据类型

PlayerPrefs 支持的数据类型非常有限,就三种:

  • int —— 整数,比如金币数量、关卡编号
  • float —— 浮点数,比如音量大小、玩家坐标
  • string —— 字符串,比如玩家昵称、存档时间

没了?对,就这三种。

你可能会问:“那我存个 bool 值怎么办?”

我个人的习惯是用 int 代替,0 表示 false,1 表示 true。或者用 string 存 "true"/"false",然后自己解析。虽然有点土,但够用。

那存个 Vector3 呢?比如玩家位置。我一般把它序列化成字符串:"1.5,2.3,0.8",然后存成 string。取出来再 Split 一下。说白了,就是自己手动做序列化。

小技巧: 如果你需要存复杂结构,比如一个装备列表、一个任务进度,别硬塞进 PlayerPrefs。它不适合。后面我们会讲 JSON、二进制、SQLite,那才是干这活的。

核心 API 详解

API 不多,我按使用频率给你列一下:

1. 存数据

// 存整数
PlayerPrefs.SetInt("Gold", 100);

// 存浮点数
PlayerPrefs.SetFloat("MusicVolume", 0.75f);

// 存字符串
PlayerPrefs.SetString("PlayerName", "张三");

// 重要!必须调用 Save,否则可能丢失
PlayerPrefs.Save();

这里有个坑,我踩过好几次:Set 之后如果不调用 Save(),数据不会立刻写入硬盘。Unity 会在应用退出时自动保存一次,但如果游戏中途闪退了,那中间 Set 的数据就全丢了。

所以我的习惯是:每次 Set 完关键数据(比如玩家通关、获得道具),立刻调一次 Save()。别偷懒。

2. 读数据

// 读整数,第二个参数是默认值
int gold = PlayerPrefs.GetInt("Gold", 0);

// 读浮点数
float volume = PlayerPrefs.GetFloat("MusicVolume", 1.0f);

// 读字符串
string name = PlayerPrefs.GetString("PlayerName", "默认名字");

第二个参数是默认值。如果这个 Key 不存在,就返回默认值。这个设计很贴心,你不需要先判断 Key 存不存在,直接 Get 就行。

但要注意:默认值类型必须匹配。你 GetInt 的时候传个字符串进去,编译器不会报错,但运行时会出问题。

3. 判断是否存在

if (PlayerPrefs.HasKey("Gold"))
{
    Debug.Log("Gold 存在");
}

这个 API 我用的不多。因为 Get 的时候给个默认值就够了。但有一种场景必须用:你想知道玩家是不是第一次启动游戏。如果 HasKey("FirstLaunch") 返回 false,说明是第一次,你就初始化一些数据。

4. 删除数据

// 删除单个 Key
PlayerPrefs.DeleteKey("Gold");

// 删除所有数据(慎用!)
PlayerPrefs.DeleteAll();

DeleteAll 这个函数,我建议你只在“设置”界面加个隐藏按钮,或者测试的时候用。千万别在游戏逻辑里随便调。我曾经见过一个同事,在“返回主菜单”时调了 DeleteAll,结果玩家所有存档都没了……那用户差评刷的,啧啧。

一个完整的例子

咱们写个简单的金币系统,把上面这些 API 串起来:

public class GoldManager : MonoBehaviour
{
    private const string GOLD_KEY = "PlayerGold";

    void Start()
    {
        // 读取金币,默认给 100
        int gold = PlayerPrefs.GetInt(GOLD_KEY, 100);
        Debug.Log("当前金币: " + gold);
    }

    public void AddGold(int amount)
    {
        int current = PlayerPrefs.GetInt(GOLD_KEY, 100);
        current += amount;
        PlayerPrefs.SetInt(GOLD_KEY, current);
        PlayerPrefs.Save();  // 立刻保存
    }

    public void ResetGold()
    {
        PlayerPrefs.DeleteKey(GOLD_KEY);
        PlayerPrefs.Save();
    }
}

你看,就这么几行代码,一个简单的存档系统就出来了。适合原型开发、小游戏、或者临时存一些设置项。

避坑指南

我曾经踩过的坑:
  • Key 命名冲突: 不同模块用了相同的 Key。比如 UI 模块存了 "Volume",音频模块也存了 "Volume",互相覆盖。我后来强制团队用 "模块名_变量名" 的格式,比如 "Audio_MusicVolume"。
  • 忘记 Save: 测试时一切正常,发布后玩家反馈“金币丢了”。一查,原来是没调 Save(),闪退导致数据丢失。
  • 存了太多数据: 有人把整个游戏配置表用 JSON 序列化后存成 string,结果 PlayerPrefs 文件变得巨大,加载卡顿。记住,PlayerPrefs 只适合存少量简单数据。

什么时候用 PlayerPrefs?

说实话,现在项目里我很少用它了。但有些场景它依然是最佳选择:

  • 游戏设置: 音量、画质、语言、是否显示教程
  • 简单进度: 当前关卡、最高分、解锁了几个角色
  • 原型验证: 快速做个 demo,不想搞数据库

什么时候别用?

  • 存大量数据(比如几百个道具、几千条日志)
  • 存敏感数据(密码、支付信息)
  • 需要跨平台同步(PlayerPrefs 不支持云存档)

好,PlayerPrefs 的基础就这些。下一章咱们聊聊怎么用它做一套完整的存档系统,包括存档版本控制、数据校验这些实战技巧。