๐พ Lesson 5.3: Saving with PlayerPrefs
Everything you've built so far vanishes the moment the game quits. PlayerPrefs is Unity's simplest way to make a few values stick between sessions โ the master volume, the last selected level, a high score. It's a tiny key-value store, dead easy to use, and easy to misuse. This lesson shows exactly what it's for, what it is emphatically not for, and where those saved values actually live.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Store and read values with
SetInt/SetFloat/SetStringand theirGetcounterparts - Understand when writes actually hit disk and why you call
PlayerPrefs.Save() - Provide sensible defaults and check existence with
HasKey - Delete individual keys or wipe everything with
DeleteKey/DeleteAll - Wire a settings menu to PlayerPrefs so choices persist
- Explain what PlayerPrefs is good for (settings, high score) and why it's wrong for complex or secure data
- Know where PlayerPrefs is stored on Windows, macOS, and other platforms
Estimated Time: 35 minutes ยท Prerequisite: Lesson 5.2 (Additive & Multi-Scene Workflows)
In This Lesson
A Tiny Key-Value Store
PlayerPrefs stores named values that survive between play sessions. You write with a key (a string like "MasterVolume") and a value; later you read it back by the same key. It supports exactly three types โ int, float, and string โ and nothing else.
Think of it as a small dictionary Unity saves to disk for you. No file paths, no serialization, no setup. That simplicity is its whole appeal and also the source of every mistake people make with it.
๐ Definition
PlayerPrefs: a static Unity class that persists small key-value pairs (int, float, string) across sessions. Values are stored per user, per application, in a platform-specific location (the Windows registry, a macOS .plist, and so on). It is meant for preferences and other tiny bits of state โ not for a full save game.
The API: Set, Get, Save
The entire everyday API fits on one screen. Writing:
using UnityEngine;
// Store values (key first, value second)
PlayerPrefs.SetFloat("MasterVolume", 0.75f);
PlayerPrefs.SetInt("LastLevel", 3);
PlayerPrefs.SetString("PlayerName", "Ray");
// Flush the writes to disk right now.
PlayerPrefs.Save();
Reading back โ later, or after a restart:
float volume = PlayerPrefs.GetFloat("MasterVolume", 1f); // 1f = default if missing
int level = PlayerPrefs.GetInt("LastLevel", 0);
string name = PlayerPrefs.GetString("PlayerName", "Player");
The Get methods take an optional second argument: the default returned when the key doesn't exist yet (a brand-new player has no saved volume). Always supply it โ it saves you a pile of "why is my volume 0 on first launch?" bugs.
โ ๏ธ Set doesn't always mean saved
The Set calls update an in-memory copy. Unity automatically writes that to disk when the application quits normally (via OnApplicationQuit). But if the game crashes, or you stop Play mode abruptly, un-flushed changes can be lost. Call PlayerPrefs.Save() after important changes to force the write immediately. Don't call it every frame โ it's a disk operation.
Rounding out the API, you can check for and remove keys:
if (PlayerPrefs.HasKey("PlayerName")) { /* it exists */ }
PlayerPrefs.DeleteKey("LastLevel"); // remove one key
PlayerPrefs.DeleteAll(); // wipe EVERYTHING for this app
โ ๏ธ DeleteAll is a sledgehammer
PlayerPrefs.DeleteAll() erases every key your game has ever stored โ settings, high scores, the lot. It's handy for a "Reset to defaults" button during development, but never wire it to something a player can hit by accident. Prefer DeleteKey for surgical removals.
Defaults & HasKey
Because a fresh install has no saved data, robust code always plans for "the key isn't there yet." You have two clean patterns:
- Default argument (preferred for simple values):
PlayerPrefs.GetInt("LastLevel", 0)returns 0 when the key is missing. - HasKey guard (when the very first run needs special handling): check
PlayerPrefs.HasKey(...), and if it's the first launch, seed defaults and maybe show a welcome flow.
void ApplyOrSeedSettings()
{
if (!PlayerPrefs.HasKey("MasterVolume"))
{
// First ever launch โ seed sensible defaults.
PlayerPrefs.SetFloat("MasterVolume", 0.8f);
PlayerPrefs.SetInt("Fullscreen", 1);
PlayerPrefs.Save();
}
AudioListener.volume = PlayerPrefs.GetFloat("MasterVolume");
Screen.fullScreen = PlayerPrefs.GetInt("Fullscreen") == 1;
}
Notice PlayerPrefs has no bool type, so booleans are stored as an int: 1 for true, 0 for false. That's a standard idiom you'll see constantly.
A Settings Menu Bound to PlayerPrefs
The textbook use of PlayerPrefs is an options screen. A volume slider, a fullscreen toggle, and a graphics dropdown โ each reads its saved value on open and writes it back on change. Here's the panel we're wiring, drawn as it appears in-game:
The script behind it is short. On Start it loads saved values into the controls; each control's callback writes back:
using UnityEngine;
using UnityEngine.UI;
public class SettingsMenu : MonoBehaviour
{
[SerializeField] Slider volumeSlider;
[SerializeField] Toggle fullscreenToggle;
[SerializeField] Dropdown qualityDropdown;
void Start()
{
// Load saved prefs into the UI (with first-launch defaults)
volumeSlider.value = PlayerPrefs.GetFloat("MasterVolume", 0.8f);
fullscreenToggle.isOn = PlayerPrefs.GetInt("Fullscreen", 1) == 1;
qualityDropdown.value = PlayerPrefs.GetInt("QualityLevel", 2);
// Apply them to the actual systems
ApplyAll();
}
public void OnVolumeChanged(float v)
{
PlayerPrefs.SetFloat("MasterVolume", v);
AudioListener.volume = v;
}
public void OnFullscreenChanged(bool on)
{
PlayerPrefs.SetInt("Fullscreen", on ? 1 : 0); // bool -> int
Screen.fullScreen = on;
}
public void OnQualityChanged(int index)
{
PlayerPrefs.SetInt("QualityLevel", index);
QualitySettings.SetQualityLevel(index);
}
// Hook to the Apply button to flush everything to disk.
public void Apply()
{
PlayerPrefs.Save();
}
void ApplyAll()
{
AudioListener.volume = volumeSlider.value;
Screen.fullScreen = fullscreenToggle.isOn;
QualitySettings.SetQualityLevel(qualityDropdown.value);
}
}
Hook each control's On Value Changed event to the matching method in the Inspector, and the Apply button to Apply(). That's a complete, persistent options screen.
โ Pro Tip
Define your keys as const string in one place (const string VolumeKey = "MasterVolume";) and reference the constant everywhere. A typo in a raw string like "MastreVolume" silently reads/writes the wrong key with no compiler error โ the exact class of bug we warned about with Animator parameter names in Module 1.
Where Is It Stored?
PlayerPrefs doesn't create a file you choose. Unity picks a per-platform location keyed by your Company Name and Product Name (set in Project Settings โธ Player):
- Windows: the registry, under
HKCU\Software\[CompanyName]\[ProductName]. - macOS: a preferences
.plistat~/Library/Preferences/unity.[CompanyName].[ProductName].plist. - Linux: a file under
~/.config/unity3d/[CompanyName]/[ProductName]/. - Android / iOS: the OS's shared-preferences /
NSUserDefaultsstore for the app. - WebGL: the browser's IndexedDB for that page origin.
Two consequences follow from this. First, because the path uses Company and Product name, renaming your product orphans the old prefs โ the game looks like a fresh install. Second, on desktop the values are stored in plain text / plain registry entries a curious player can open and edit.
๐ก Editor vs. build. In the Editor, PlayerPrefs uses a location tied to the Editor and your project, separate from a built game's store. So a value you saved while testing in Play mode won't magically appear in a standalone build, and vice versa โ that's expected, not a bug.
What PlayerPrefs Is NOT For
Because it's so easy, people reach for PlayerPrefs to store their entire save game โ serializing an inventory into one giant string, cramming level layouts into keys. Resist that. PlayerPrefs is the right tool for a narrow job and the wrong one for everything else.
preference/score?"} B -- "Yes" --> C["โ PlayerPrefs
volume ยท last level ยท high score ยท toggles"] B -- "No" --> D{"Structured game state?
inventory ยท stats ยท position"} D -- "Yes" --> E["๐ JSON save file
(next lesson)"] D -- "Sensitive / anti-cheat?" --> F["๐ Server or
encrypted storage"]
Figure 2: Choosing where data lives. PlayerPrefs handles the small, flat stuff; structured state belongs in a JSON file; anything cheat-sensitive belongs on a server.
Specifically, do not use PlayerPrefs for:
- Complex / structured data โ an inventory, a list of quests, a world layout. There's no nesting, no arrays, no objects. You'd be hand-serializing into strings, which is exactly what a real save system (next lesson) does properly.
- Large data โ it's tuned for small values. Megabytes of game state will be slow and clumsy.
- Secure or anti-cheat data โ it's trivially editable on desktop. Currency, unlocks, or leaderboard scores that matter must be validated server-side, never trusted from PlayerPrefs.
โ ๏ธ "But I could just JSON it into a string keyโฆ"
You can shove a serialized blob into a single string key, and plenty of tutorials do. But you lose every advantage of a real file (readable on disk, versionable, easy to back up) and gain the registry's size limits and awkward tooling. If your data is structured, write a proper JSON save file instead โ which is precisely what Lesson 5.4 builds.
Hands-on Challenge
๐๏ธ Exercise 1: A persistent high score
Objective: Save and display a high score that survives restarts.
- Write a
HighScoreclass with a methodReport(int score). - Inside, read the current best with
PlayerPrefs.GetInt("HighScore", 0). If the new score beats it,SetIntthe new value andSave(). - Display the stored value in a UI Text on start.
- Enter Play mode, report a few scores, stop, and re-enter โ confirm the best persists.
๐ก Hint: it resets every time I press Play
Two likely causes: you're calling DeleteAll or re-seeding somewhere on Start, or you never called Save() and stopped Play mode before Unity auto-flushed. Add a Save() right after a new best is written.
โ Success check
The high score only ever goes up, shows the correct value immediately on launch, and remembers it after fully quitting and reopening a build.
๐๏ธ Exercise 2: Wire the settings panel
Build the Settings menu from Figure 1: a volume Slider, a Fullscreen Toggle, and a Quality Dropdown. Attach the SettingsMenu script and hook each control's On Value Changed event plus the Apply button. Confirm your choices reload correctly after restarting, and add a Reset button that calls DeleteKey on just those three keys (not DeleteAll).
๐ฏ Quick Quiz
Question 1: Which of these is the right job for PlayerPrefs?
Question 2: You called PlayerPrefs.SetInt(...) but the value is lost after a crash. Why?
Question 3: How do you store a bool in PlayerPrefs?
Summary
๐ Key Takeaways
PlayerPrefsis a tiny key-value store for int, float, and string values that persist between sessions.Set*updates memory; callPlayerPrefs.Save()after important changes so a crash can't lose them.- Always pass a default to
Get*, and store bools as an int (1/0). - Use
HasKeyto detect a first launch; useDeleteKeyfor surgical removal and reserveDeleteAllfor dev resets. - Storage is per-platform (Windows registry, macOS
.plist, etc.), keyed by Company/Product name โ and it's editable, so it's not secure. - Great for settings and high scores; wrong for structured, large, or cheat-sensitive data.
๐ What's Next?
PlayerPrefs handles the small, flat stuff beautifully. But a real save game โ the player's position, their stats, an inventory of items โ is structured, and that's precisely what PlayerPrefs can't do. In Lesson 5.4: A JSON Save/Load System we build a proper save file with JsonUtility and write it to disk, tying the whole module together in a mini-project.
๐พ You can persist settings now
A few lines of Set, Get, and Save give you options that stick and scores that stay. Just remember what PlayerPrefs is not for โ that judgment is half the skill.