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