Unity Saving Systems
Introduction to Game Data Persistence
Saving and loading game data is an essential feature for almost every game. Whether it's saving player progress, high scores, settings, or the entire game state, a robust saving system enhances the player experience and allows for continuous gameplay across sessions.
In this guide, you'll learn:
- Different data persistence approaches in Unity
- Working with PlayerPrefs for simple data
- Serialization techniques for complex data
- Creating save/load systems with JSON
- Binary serialization for better security
- Building a complete save system architecture
- Cross-platform saving considerations
Using PlayerPrefs for Simple Data
PlayerPrefs is Unity's built-in solution for storing small amounts of data persistently between game sessions. It's essentially a key-value store that saves data to the registry (on Windows), plist files (on macOS), or specific locations on other platforms.
Basic PlayerPrefs Usage:
using UnityEngine;
using UnityEngine.UI;
public class PlayerPrefsExample : MonoBehaviour
{
// References to UI elements
public InputField playerNameInput;
public Slider volumeSlider;
public Toggle fullscreenToggle;
public Text highScoreText;
// Current score and high score
private int currentScore = 0;
private int highScore = 0;
void Start()
{
// Load saved settings when the game starts
LoadSettings();
// Set up UI callbacks
volumeSlider.onValueChanged.AddListener(OnVolumeChanged);
fullscreenToggle.onValueChanged.AddListener(OnFullscreenToggled);
// Initialize high score display
UpdateHighScoreDisplay();
}
// Load all settings from PlayerPrefs
void LoadSettings()
{
// Load player name (with a default if not found)
string playerName = PlayerPrefs.GetString("PlayerName", "New Player");
playerNameInput.text = playerName;
// Load volume setting (default to 0.75 if not found)
float volume = PlayerPrefs.GetFloat("MasterVolume", 0.75f);
volumeSlider.value = volume;
// Set the actual volume
AudioListener.volume = volume;
// Load fullscreen preference (default to true if not found)
bool fullscreen = PlayerPrefs.GetInt("Fullscreen", 1) == 1;
fullscreenToggle.isOn = fullscreen;
// Apply fullscreen setting
Screen.fullScreen = fullscreen;
// Load high score
highScore = PlayerPrefs.GetInt("HighScore", 0);
}
// Save the player name when the input field changes
public void OnPlayerNameChanged()
{
string newName = playerNameInput.text;
// Save to PlayerPrefs
PlayerPrefs.SetString("PlayerName", newName);
// Call Save to write to disk immediately (optional)
PlayerPrefs.Save();
Debug.Log("Player name saved: " + newName);
}
// Handle volume change from slider
void OnVolumeChanged(float newVolume)
{
// Set actual volume
AudioListener.volume = newVolume;
// Save to PlayerPrefs
PlayerPrefs.SetFloat("MasterVolume", newVolume);
PlayerPrefs.Save();
Debug.Log("Volume saved: " + newVolume);
}
// Handle fullscreen toggle
void OnFullscreenToggled(bool isFullscreen)
{
// Apply fullscreen
Screen.fullScreen = isFullscreen;
// Save to PlayerPrefs (convert bool to int since PlayerPrefs doesn't support bool)
PlayerPrefs.SetInt("Fullscreen", isFullscreen ? 1 : 0);
PlayerPrefs.Save();
Debug.Log("Fullscreen saved: " + isFullscreen);
}
// When the player scores points
public void AddScore(int points)
{
currentScore += points;
// Check if this is a new high score
if (currentScore > highScore)
{
highScore = currentScore;
// Save the new high score
PlayerPrefs.SetInt("HighScore", highScore);
PlayerPrefs.Save();
// Update the display
UpdateHighScoreDisplay();
Debug.Log("New high score saved: " + highScore);
}
}
// Reset the high score (might be triggered by a button)
public void ResetHighScore()
{
highScore = 0;
// Save to PlayerPrefs
PlayerPrefs.SetInt("HighScore", 0);
PlayerPrefs.Save();
// Update the display
UpdateHighScoreDisplay();
Debug.Log("High score reset");
}
// Delete all saved PlayerPrefs
public void DeleteAllSavedData()
{
// This will erase ALL PlayerPrefs data
PlayerPrefs.DeleteAll();
// Reload default settings
LoadSettings();
Debug.Log("All saved data deleted");
}
// Update the high score text display
void UpdateHighScoreDisplay()
{
if (highScoreText != null)
{
highScoreText.text = "High Score: " + highScore;
}
}
Creating a PlayerPrefs Manager:
A centralized PlayerPrefs manager can help manage PlayerPrefs more efficiently and reduce code duplication. Here's an example implementation:
PlayerPrefs Manager Code:
using UnityEngine;
using System;
// Static class for centralized PlayerPrefs access
public static class PlayerPrefsManager
{
// Key constants to avoid typos and ensure consistency
private static class Keys
{
// Player settings
public const string PLAYER_NAME = "PlayerName";
public const string PLAYER_LEVEL = "PlayerLevel";
public const string PLAYER_EXPERIENCE = "PlayerExperience";
public const string PLAYER_HEALTH = "PlayerHealth";
// Game settings
public const string MASTER_VOLUME = "MasterVolume";
public const string MUSIC_VOLUME = "MusicVolume";
public const string SFX_VOLUME = "SFXVolume";
public const string FULLSCREEN = "Fullscreen";
public const string GRAPHICS_QUALITY = "GraphicsQuality";
// Game progress
public const string HIGHEST_LEVEL_COMPLETED = "HighestLevelCompleted";
public const string TOTAL_PLAYTIME = "TotalPlaytime";
public const string HIGH_SCORE = "HighScore";
// Tutorial flags
public const string TUTORIAL_COMPLETED = "TutorialCompleted";
// Custom settings - create a key with a prefix
private const string CUSTOM_SETTING_PREFIX = "CustomSetting_";
public static string GetCustomSettingKey(string settingName)
{
return CUSTOM_SETTING_PREFIX + settingName;
}
}
#region Player Data Methods
// Player name
public static string GetPlayerName()
{
return PlayerPrefs.GetString(Keys.PLAYER_NAME, "New Player");
}
public static void SetPlayerName(string name)
{
PlayerPrefs.SetString(Keys.PLAYER_NAME, name);
PlayerPrefs.Save();
}
// Player level
public static int GetPlayerLevel()
{
return PlayerPrefs.GetInt(Keys.PLAYER_LEVEL, 1);
}
public static void SetPlayerLevel(int level)
{
PlayerPrefs.SetInt(Keys.PLAYER_LEVEL, level);
PlayerPrefs.Save();
}
// Player experience
public static int GetPlayerExperience()
{
return PlayerPrefs.GetInt(Keys.PLAYER_EXPERIENCE, 0);
}
public static void SetPlayerExperience(int exp)
{
PlayerPrefs.SetInt(Keys.PLAYER_EXPERIENCE, exp);
PlayerPrefs.Save();
}
// Player health
public static float GetPlayerHealth()
{
return PlayerPrefs.GetFloat(Keys.PLAYER_HEALTH, 100f);
}
public static void SetPlayerHealth(float health)
{
PlayerPrefs.SetFloat(Keys.PLAYER_HEALTH, health);
PlayerPrefs.Save();
}
#endregion
#region Game Settings Methods
// Master volume
public static float GetMasterVolume()
{
return PlayerPrefs.GetFloat(Keys.MASTER_VOLUME, 0.75f);
}
public static void SetMasterVolume(float volume)
{
// Clamp the value between 0 and 1
volume = Mathf.Clamp01(volume);
PlayerPrefs.SetFloat(Keys.MASTER_VOLUME, volume);
PlayerPrefs.Save();
}
// Music volume
public static float GetMusicVolume()
{
return PlayerPrefs.GetFloat(Keys.MUSIC_VOLUME, 0.5f);
}
public static void SetMusicVolume(float volume)
{
volume = Mathf.Clamp01(volume);
PlayerPrefs.SetFloat(Keys.MUSIC_VOLUME, volume);
PlayerPrefs.Save();
}
// SFX volume
public static float GetSFXVolume()
{
return PlayerPrefs.GetFloat(Keys.SFX_VOLUME, 0.8f);
}
public static void SetSFXVolume(float volume)
{
volume = Mathf.Clamp01(volume);
PlayerPrefs.SetFloat(Keys.SFX_VOLUME, volume);
PlayerPrefs.Save();
}
// Fullscreen setting
public static bool GetFullscreenEnabled()
{
return PlayerPrefs.GetInt(Keys.FULLSCREEN, 1) == 1;
}
public static void SetFullscreenEnabled(bool enabled)
{
PlayerPrefs.SetInt(Keys.FULLSCREEN, enabled ? 1 : 0);
Screen.fullScreen = enabled; // Apply right away
PlayerPrefs.Save();
}
// Graphics quality setting
public static int GetGraphicsQuality()
{
return PlayerPrefs.GetInt(Keys.GRAPHICS_QUALITY, QualitySettings.GetQualityLevel());
}
public static void SetGraphicsQuality(int qualityIndex)
{
// Ensure the index is valid
int maxQuality = QualitySettings.names.Length - 1;
qualityIndex = Mathf.Clamp(qualityIndex, 0, maxQuality);
// Save the setting
PlayerPrefs.SetInt(Keys.GRAPHICS_QUALITY, qualityIndex);
// Apply right away
QualitySettings.SetQualityLevel(qualityIndex);
PlayerPrefs.Save();
}
#endregion
#region Game Progress Methods
// Highest level completed
public static int GetHighestLevelCompleted()
{
return PlayerPrefs.GetInt(Keys.HIGHEST_LEVEL_COMPLETED, 0);
}
public static void SetHighestLevelCompleted(int level)
{
// Only update if it's a higher level than before
int currentHighest = GetHighestLevelCompleted();
if (level > currentHighest)
{
PlayerPrefs.SetInt(Keys.HIGHEST_LEVEL_COMPLETED, level);
PlayerPrefs.Save();
}
}
// Total playtime
public static float GetTotalPlaytime()
{
return PlayerPrefs.GetFloat(Keys.TOTAL_PLAYTIME, 0f);
}
public static void UpdateTotalPlaytime(float sessionTime)
{
float currentTotal = GetTotalPlaytime();
PlayerPrefs.SetFloat(Keys.TOTAL_PLAYTIME, currentTotal + sessionTime);
PlayerPrefs.Save();
}
// High score
public static int GetHighScore()
{
return PlayerPrefs.GetInt(Keys.HIGH_SCORE, 0);
}
public static bool TrySetHighScore(int score)
{
int currentHighScore = GetHighScore();
if (score > currentHighScore)
{
PlayerPrefs.SetInt(Keys.HIGH_SCORE, score);
PlayerPrefs.Save();
return true; // New high score
}
return false; // Not a new high score
}
#endregion
#region Tutorial Flags Methods
public static bool IsTutorialCompleted()
{
return PlayerPrefs.GetInt(Keys.TUTORIAL_COMPLETED, 0) == 1;
}
public static void SetTutorialCompleted(bool completed)
{
PlayerPrefs.SetInt(Keys.TUTORIAL_COMPLETED, completed ? 1 : 0);
PlayerPrefs.Save();
}
#endregion
#region Custom Settings Methods
// Get a custom string setting
public static string GetCustomSetting(string settingName, string defaultValue = "")
{
string key = Keys.GetCustomSettingKey(settingName);
return PlayerPrefs.GetString(key, defaultValue);
}
// Set a custom string setting
public static void SetCustomSetting(string settingName, string value)
{
string key = Keys.GetCustomSettingKey(settingName);
PlayerPrefs.SetString(key, value);
PlayerPrefs.Save();
}
// Get a custom int setting
public static int GetCustomSettingInt(string settingName, int defaultValue = 0)
{
string key = Keys.GetCustomSettingKey(settingName);
return PlayerPrefs.GetInt(key, defaultValue);
}
// Set a custom int setting
public static void SetCustomSettingInt(string settingName, int value)
{
string key = Keys.GetCustomSettingKey(settingName);
PlayerPrefs.SetInt(key, value);
PlayerPrefs.Save();
}
// Get a custom float setting
public static float GetCustomSettingFloat(string settingName, float defaultValue = 0f)
{
string key = Keys.GetCustomSettingKey(settingName);
return PlayerPrefs.GetFloat(key, defaultValue);
}
// Set a custom float setting
public static void SetCustomSettingFloat(string settingName, float value)
{
string key = Keys.GetCustomSettingKey(settingName);
PlayerPrefs.SetFloat(key, value);
PlayerPrefs.Save();
}
// Get a custom bool setting
public static bool GetCustomSettingBool(string settingName, bool defaultValue = false)
{
string key = Keys.GetCustomSettingKey(settingName);
return PlayerPrefs.GetInt(key, defaultValue ? 1 : 0) == 1;
}
// Set a custom bool setting
public static void SetCustomSettingBool(string settingName, bool value)
{
string key = Keys.GetCustomSettingKey(settingName);
PlayerPrefs.SetInt(key, value ? 1 : 0);
PlayerPrefs.Save();
}
// Check if a custom setting exists
public static bool HasCustomSetting(string settingName)
{
string key = Keys.GetCustomSettingKey(settingName);
return PlayerPrefs.HasKey(key);
}
// Delete a custom setting
public static void DeleteCustomSetting(string settingName)
{
string key = Keys.GetCustomSettingKey(settingName);
if (PlayerPrefs.HasKey(key))
{
PlayerPrefs.DeleteKey(key);
PlayerPrefs.Save();
}
}
#endregion
#region Global Methods
// Save all settings immediately
public static void SaveAll()
{
PlayerPrefs.Save();
}
// Clear all settings
public static void DeleteAll()
{
PlayerPrefs.DeleteAll();
}
// Check if this is the first time running the game
public static bool IsFirstRun()
{
const string FIRST_RUN_KEY = "HasGameRunBefore";
bool isFirst = !PlayerPrefs.HasKey(FIRST_RUN_KEY);
if (isFirst)
{
// Mark that the game has run before
PlayerPrefs.SetInt(FIRST_RUN_KEY, 1);
PlayerPrefs.Save();
}
return isFirst;
}
#endregion
}
Benefits of a centralized PlayerPrefs manager:
- Type safety: Prevent errors by using strongly typed methods
- Key consistency: Avoid typos in key names with constants
- Default values: Centralized defaults for consistent fallbacks
- Validation: Add validation logic in one place (e.g., clamping values)
- Organization: Group related settings for better code organization
- Extensibility: Easy to add new settings or change implementation
PlayerPrefs Limitations
- Limited data types: Only supports int, float, and string
- No complex data: Can't directly store objects, arrays, or collections
- Not secure: Data is stored in easily accessible locations and not encrypted
- Size limitations: Not suitable for large amounts of data
- Platform differences: Storage locations and behavior vary by platform
- No version control: No built-in way to handle data format changes