# PlayerPrefs Advanced (Unity 6) **Created for:** Unity 6 (6000.x). Companion to [[Custom Player Preferences User Manual]], which saves component values to a JSON *file*. This note covers Unity's built-in **`PlayerPrefs`** class properly: what it is good for, a type-safe wrapper around it, storing structs as JSON, why "encrypting" it is mostly theatre, and when to move to file-based saves. Docs: [PlayerPrefs API](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/PlayerPrefs.html) · [JsonUtility](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/JsonUtility.html) · [JSON Serialization manual](https://docs.unity3d.com/6000.0/Documentation/Manual/json-serialization.html) · [Application.persistentDataPath](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Application-persistentDataPath.html) ## What PlayerPrefs actually is - A key/value store with only three types: `int`, `float`, `string` (`SetInt/GetInt`, `SetFloat/GetFloat`, `SetString/GetString`), plus `HasKey`, `DeleteKey`, `DeleteAll`, `Save`. - Values are written to disk automatically on a clean quit (`OnApplicationQuit`). Call `PlayerPrefs.Save()` after important changes so a crash doesn't lose them - but don't call it every frame (it does disk I/O). - Storage location (per Company Name + Product Name in Player Settings): - **Windows standalone:** registry `HKCU\Software\[company]\[product]` (Editor: `HKCU\Software\Unity\UnityEditor\[company]\[product]`). - **macOS:** `~/Library/Preferences/` plist. **Linux:** `~/.config/unity3d/[company]/[product]`. - **WebGL:** browser IndexedDB (size limited). - Changing Company/Product name → a new, empty store. Pick final names early. **Good for:** settings (volume, resolution, key rebinds, language), "seen tutorial" flags, last selected slot. **Not good for:** save games, inventories, anything large or structured. ## Step 1 - Typed keys Raw string keys get misspelled. A typed key couples the name, type, and default in one place. ```csharp public readonly struct PrefKey<T> { public readonly string Name; public readonly T Default; public PrefKey(string name, T defaultValue) { Name = name; Default = defaultValue; } public override string ToString() => Name; } public static class PrefKeys { // Bump the prefix if you ever change a key's meaning/type. const string P = "v1."; public static readonly PrefKey<float> MasterVolume = new(P + "audio.master", 0.8f); public static readonly PrefKey<float> MusicVolume = new(P + "audio.music", 0.6f); public static readonly PrefKey<bool> Fullscreen = new(P + "video.fullscreen", true); public static readonly PrefKey<int> QualityLevel = new(P + "video.quality", 2); public static readonly PrefKey<string> Language = new(P + "ui.language", "en"); public static readonly PrefKey<bool> TutorialSeen = new(P + "flags.tutorialSeen", false); } ``` ## Step 2 - The wrapper Adds `bool`, `enum`, `Vector3`, `Color` and any `[Serializable]` type (via JSON), a change event, and deferred saving. ```csharp using System; using UnityEngine; public static class Prefs { public static event Action<string> Changed; static bool dirty; // ---- primitives ---- public static int Get(PrefKey<int> k) => PlayerPrefs.GetInt(k.Name, k.Default); public static float Get(PrefKey<float> k) => PlayerPrefs.GetFloat(k.Name, k.Default); public static string Get(PrefKey<string> k) => PlayerPrefs.GetString(k.Name, k.Default); public static bool Get(PrefKey<bool> k) => PlayerPrefs.GetInt(k.Name, k.Default ? 1 : 0) != 0; public static void Set(PrefKey<int> k, int v) { PlayerPrefs.SetInt(k.Name, v); Touch(k.Name); } public static void Set(PrefKey<float> k, float v) { PlayerPrefs.SetFloat(k.Name, v); Touch(k.Name); } public static void Set(PrefKey<string> k, string v) { PlayerPrefs.SetString(k.Name, v ?? ""); Touch(k.Name); } public static void Set(PrefKey<bool> k, bool v) { PlayerPrefs.SetInt(k.Name, v ? 1 : 0); Touch(k.Name); } // ---- enums (stored by name so reordering the enum doesn't corrupt data) ---- public static TEnum GetEnum<TEnum>(PrefKey<TEnum> k) where TEnum : struct, Enum { string s = PlayerPrefs.GetString(k.Name, k.Default.ToString()); return Enum.TryParse(s, out TEnum v) ? v : k.Default; } public static void SetEnum<TEnum>(PrefKey<TEnum> k, TEnum v) where TEnum : struct, Enum { PlayerPrefs.SetString(k.Name, v.ToString()); Touch(k.Name); } // ---- anything JsonUtility can serialize (structs/classes marked [Serializable], Vector3, Color...) ---- public static T GetJson<T>(PrefKey<T> k) { if (!PlayerPrefs.HasKey(k.Name)) return k.Default; try { return JsonUtility.FromJson<T>(PlayerPrefs.GetString(k.Name)); } catch (Exception e) { Debug.LogWarning(quot;Pref '{k.Name}' corrupt, using default. {e.Message}"); return k.Default; } } public static void SetJson<T>(PrefKey<T> k, T value) { PlayerPrefs.SetString(k.Name, JsonUtility.ToJson(value)); Touch(k.Name); } // ---- housekeeping ---- public static bool Has<T>(PrefKey<T> k) => PlayerPrefs.HasKey(k.Name); public static void Delete<T>(PrefKey<T> k) { PlayerPrefs.DeleteKey(k.Name); Touch(k.Name); } static void Touch(string key) { dirty = true; Changed?.Invoke(key); } /// Call at sensible checkpoints (settings menu closed, level complete). public static void SaveIfDirty() { if (!dirty) return; PlayerPrefs.Save(); dirty = false; } } ``` **Explanation:** - `JsonUtility.FromJson<T>` / `ToJson` handle public fields and `[SerializeField]` private fields of `[Serializable]` types and Unity structs like `Vector3`/`Color`. They do **not** handle `Dictionary`, properties, or top-level arrays/lists (wrap them in a class). - Enums stored as names survive inserting new enum members. ## Step 3 - Storing a struct ```csharp using System; using UnityEngine; [Serializable] public struct KeyBindings { public string jump; public string interact; public float mouseSensitivity; } public static class MorePrefKeys { public static readonly PrefKey<KeyBindings> Bindings = new("v1.input.bindings", new KeyBindings { jump = "<Keyboard>/space", interact = "<Keyboard>/e", mouseSensitivity = 1f }); public static readonly PrefKey<Vector3> LastCheckpoint = new("v1.world.checkpoint", Vector3.zero); } ``` > `JsonUtility.FromJson<Vector3>` works because `Vector3` is a serializable Unity struct. For a bare `int` or `string` use the primitive overloads instead - JsonUtility needs an object/struct at the top level. ## Step 4 - A settings component that uses it ```csharp using UnityEngine; using UnityEngine.Audio; using UnityEngine.UI; public class SettingsMenu : MonoBehaviour { [SerializeField] AudioMixer mixer; // exposed params "MasterVol", "MusicVol" [SerializeField] Slider masterSlider; [SerializeField] Slider musicSlider; [SerializeField] Toggle fullscreenToggle; void OnEnable() { masterSlider.SetValueWithoutNotify(Prefs.Get(PrefKeys.MasterVolume)); musicSlider.SetValueWithoutNotify(Prefs.Get(PrefKeys.MusicVolume)); fullscreenToggle.SetIsOnWithoutNotify(Prefs.Get(PrefKeys.Fullscreen)); masterSlider.onValueChanged.AddListener(v => { Prefs.Set(PrefKeys.MasterVolume, v); Apply(); }); musicSlider.onValueChanged.AddListener(v => { Prefs.Set(PrefKeys.MusicVolume, v); Apply(); }); fullscreenToggle.onValueChanged.AddListener(v => { Prefs.Set(PrefKeys.Fullscreen, v); Apply(); }); Apply(); } void OnDisable() { masterSlider.onValueChanged.RemoveAllListeners(); musicSlider.onValueChanged.RemoveAllListeners(); fullscreenToggle.onValueChanged.RemoveAllListeners(); Prefs.SaveIfDirty(); // write once when the menu closes } void Apply() { mixer.SetFloat("MasterVol", ToDb(Prefs.Get(PrefKeys.MasterVolume))); mixer.SetFloat("MusicVol", ToDb(Prefs.Get(PrefKeys.MusicVolume))); Screen.fullScreen = Prefs.Get(PrefKeys.Fullscreen); } // Linear 0..1 slider to decibels (mixer range -80..0 dB) static float ToDb(float linear) => linear <= 0.0001f ? -80f : Mathf.Log10(linear) * 20f; } ``` Also save on suspend for mobile, where `OnApplicationQuit` often never runs: ```csharp using UnityEngine; public class PrefsAutoSave : MonoBehaviour { void OnApplicationPause(bool paused) { if (paused) Prefs.SaveIfDirty(); } void OnApplicationQuit() => Prefs.SaveIfDirty(); } ``` ## Step 5 - Editor helper to clear prefs ```csharp #if UNITY_EDITOR using UnityEditor; using UnityEngine; static class PrefsMenu { [MenuItem("Tools/Prefs/Delete All PlayerPrefs")] static void DeleteAll() { if (EditorUtility.DisplayDialog("Delete PlayerPrefs", "Delete all PlayerPrefs for this project?", "Delete", "Cancel")) { PlayerPrefs.DeleteAll(); PlayerPrefs.Save(); Debug.Log("PlayerPrefs cleared."); } } } #endif ``` (Unity also has **Edit → Clear All PlayerPrefs**.) ## Encryption caveats - PlayerPrefs is plain text in the registry/plist. Anyone can edit it with regedit. - You can XOR/AES the string before `SetString`, but the key ships inside your build and is recoverable with a decompiler (IL2CPP raises the bar, it doesn't remove it). Treat client-side encryption as **obfuscation that deters casual edits**, not security. - What actually works: - **Tamper detection:** store an HMAC/hash of the value alongside it; reject or reset on mismatch. Still defeatable, but cheap and catches casual edits. - **Server authority** for anything with real value (currency, leaderboards, purchases). Validate on a server; never trust the client. - Never store secrets (passwords, API tokens) in PlayerPrefs. ## When to use file-based saves instead Switch to a JSON (or binary) file in `Application.persistentDataPath` when you need any of: - Multiple save slots, structured data (inventory, quest state), or data larger than a few KB. - Atomic writes / backups (write `save.tmp`, then replace `save.json`). - Versioning and migration of a schema. - Cloud sync (Steam Cloud and console APIs work with files). - Easy inspection/debugging (open the file). Minimal, crash-safe file save: ```csharp using System.IO; using UnityEngine; public static class FileSave { static string PathFor(string slot) => Path.Combine(Application.persistentDataPath, quot;{slot}.json"); public static void Save<T>(string slot, T data) { string path = PathFor(slot); string tmp = path + ".tmp"; File.WriteAllText(tmp, JsonUtility.ToJson(data, prettyPrint: true)); if (File.Exists(path)) File.Replace(tmp, path, path + ".bak"); else File.Move(tmp, path); } public static bool TryLoad<T>(string slot, out T data) { data = default; string path = PathFor(slot); if (!File.Exists(path)) return false; try { data = JsonUtility.FromJson<T>(File.ReadAllText(path)); return true; } catch { return false; } } } ``` > On WebGL, `File` I/O goes to an in-memory/IndexedDB filesystem and needs extra syncing; PlayerPrefs is the simpler option there. The existing [[Custom Player Preferences User Manual]] system is a file-based approach of this kind. ## Common pitfalls 1. **Typo keys** silently return defaults - solved by typed keys. 2. **`GetFloat` on a key saved with `SetInt`** returns the default, not a converted value. Types don't cross over. 3. **Calling `Save()` every frame** - stalls on disk I/O. 4. **Mobile kills the app** without `OnApplicationQuit` - save on `OnApplicationPause(true)`. 5. **Editor and build share nothing** - the Editor uses a separate registry location; test persistence in a build too. 6. **Changing Company/Product name** orphans all existing prefs. 7. **JsonUtility limitations** - no dictionaries, no properties, no polymorphism (use `[SerializeReference]` or a third-party serializer like Newtonsoft `com.unity.nuget.newtonsoft-json` if you need them). ## Related - [[Custom Player Preferences User Manual]] - [[ScriptableObject Architecture]] - why SOs are not a save system