Save / Load — сохранение прогресса
PlayerPrefs, JSON, бинарная сериализация, шифрование, версионирование сейвов.
Save/Load — стандартная задача любой игры. В Unity есть 3 основных пути, и выбор зависит от размера и сложности сохраняемых данных.
Три пути
| Подход | Когда | Платформы |
|---|---|---|
| PlayerPrefs | Маленькие primitive-значения (настройки, высокий счёт) | Все |
| JSON + File.WriteAllText | Структурированные сейвы, человекочитаемые | Все, кроме WebGL без plugin |
| Binary Serialization | Большие/сложные сейвы, защита от tampering | Все |
localStorage для маленького (5MB лимит), IndexedDB для большого. В Node — обычная файловая
система.
PlayerPrefs ≈ localStorage. JSON + File — это уже “пользовательский файл на диске” — в
Application.persistentDataPath (varies per platform).
PlayerPrefs — для настроек
// Сохранение
PlayerPrefs.SetInt("HighScore", 1500);
PlayerPrefs.SetFloat("MasterVolume", 0.8f);
PlayerPrefs.SetString("PlayerName", "Konstantin");
PlayerPrefs.Save(); // явно записать на диск (autoSave при выходе)
// Чтение
int score = PlayerPrefs.GetInt("HighScore", 0); // 0 — default если ключ не найден
float volume = PlayerPrefs.GetFloat("MasterVolume", 1.0f);
// Удаление
PlayerPrefs.DeleteKey("HighScore");
PlayerPrefs.DeleteAll(); // осторожно — стирает все ключи
Где хранится:
- Windows:
HKCU\Software\[Company]\[Product]в Registry. - macOS:
~/Library/Preferences/[bundle identifier].plist. - Linux:
~/.config/unity3d/[Company]/[Product]/prefs.
PlayerPrefs не зашифрован. Пользователь может открыть и поправить — для настроек это OK, для прогресса игры — небезопасно.
Запись каждого вызова Save синхронно сериализует ВСЕ preferences. Если у вас 1000 ключей с
массивами строк — производительность пострадает. PlayerPrefs — для десятков ключей, не
тысяч.
JSON-сохранения
Для структурированных данных используйте JSON. Unity имеет встроенный JsonUtility (без внешних
зависимостей).
using UnityEngine;
using System.IO;
[System.Serializable]
public class SaveData
{
public string playerName;
public int level;
public float playTimeSeconds;
public Vector3 position;
public InventoryData inventory;
}
[System.Serializable]
public class InventoryData
{
public int gold;
public string[] items;
}
public class SaveSystem : MonoBehaviour
{
private const string FILENAME = "save.json";
private string GetSavePath() {
return Path.Combine(Application.persistentDataPath, FILENAME);
}
public void SaveGame(SaveData data) {
string json = JsonUtility.ToJson(data, prettyPrint: true);
File.WriteAllText(GetSavePath(), json);
Debug.Log($"Saved to {GetSavePath()}");
}
public SaveData LoadGame() {
string path = GetSavePath();
if (!File.Exists(path)) {
return new SaveData(); // default values
}
string json = File.ReadAllText(path);
return JsonUtility.FromJson<SaveData>(json);
}
}
Application.persistentDataPath:
- Windows:
C:\Users\[user]\AppData\LocalLow\[Company]\[Product] - macOS:
~/Library/Application Support/[Company]/[Product] - iOS:
Documents/ - Android:
/data/data/[bundle]/files/(sandboxed)
Встроенный JsonUtility НЕ поддерживает: словари (Dictionary<TKey, TValue>), nullable types,
абстрактные классы (полиморфизм). Для них берите Newtonsoft.Json (отдельный package
com.unity.nuget.newtonsoft-json) или System.Text.Json для .NET-style.
Сериализация Dictionary с Newtonsoft
using Newtonsoft.Json;
// ...
public Dictionary<string, int> inventory = new() {
{ "sword", 1 },
{ "potion", 5 },
};
string json = JsonConvert.SerializeObject(inventory, Formatting.Indented);
File.WriteAllText(path, json);
var loaded = JsonConvert.DeserializeObject<Dictionary<string, int>>(File.ReadAllText(path));
Шифрование (или просто obfuscation)
JSON-файл — это plain text. Игрок откроет блокнотом и поменяет "gold": 100 на "gold": 999999.
Решения от простого к сильному:
Уровень 1 — Base64 (не защита, просто “не глаз режет”)
string encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
Уровень 2 — XOR с константой (легко взломать, но скрывает текст)
const byte KEY = 0x5A;
byte[] data = Encoding.UTF8.GetBytes(json);
for (int i = 0; i < data.Length; i++) data[i] ^= KEY;
File.WriteAllBytes(path, data);
Уровень 3 — AES-шифрование (для серьёзных проектов с прогрессом)
using System.Security.Cryptography;
using System.Text;
public static byte[] Encrypt(string text, string password) {
using var aes = Aes.Create();
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(password));
aes.IV = new byte[16];
using var encryptor = aes.CreateEncryptor();
var plain = Encoding.UTF8.GetBytes(text);
return encryptor.TransformFinalBlock(plain, 0, plain.Length);
}
public static string Decrypt(byte[] data, string password) {
using var aes = Aes.Create();
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(password));
aes.IV = new byte[16];
using var decryptor = aes.CreateDecryptor();
var plain = decryptor.TransformFinalBlock(data, 0, data.Length);
return Encoding.UTF8.GetString(plain);
}
⚠️ Важно: пароль хранится в вашем коде → reverse-engineering найдёт его. AES — защита от казуальных читеров, не от serious crackers. Для multiplayer game-state валидация — на сервере, не у клиента.
Версионирование сейвов
Когда вы релизите v2.0 игры с новыми полями, старые сейвы должны работать. Стратегия:
[System.Serializable]
public class SaveData
{
public int version = 2; // <-- ВЕРСИЯ
public string playerName;
public int level;
public InventoryData inventory;
public List<QuestProgress> activeQuests; // <-- НОВОЕ в v2
}
public SaveData LoadGame() {
var data = JsonUtility.FromJson<SaveData>(File.ReadAllText(path));
if (data.version < 2) {
// Миграция v1 → v2
data.activeQuests = new List<QuestProgress>(); // дефолт
data.version = 2;
}
return data;
}
Версионируйте С САМОГО НАЧАЛА. Добавить версию позже, когда у пользователей уже есть сейвы, сильно сложнее.
Async-сохранение для крупных файлов
Если сейв большой (несколько мегабайт):
public async Awaitable SaveGameAsync(SaveData data) {
string json = JsonUtility.ToJson(data);
// File.WriteAllText блокирует main thread → используем async
await File.WriteAllTextAsync(GetSavePath(), json);
}
Это спасает от 100ms stutter при автосохранении.
Auto-save паттерн
public class AutoSave : MonoBehaviour
{
[SerializeField] private float intervalSeconds = 60f;
[SerializeField] private SaveSystem saveSystem;
[SerializeField] private GameState gameState;
private void Start() {
InvokeRepeating(nameof(DoAutoSave), intervalSeconds, intervalSeconds);
}
private async void DoAutoSave() {
var data = gameState.CaptureState();
await saveSystem.SaveGameAsync(data);
// показать "💾 Сохранено" toast
}
private void OnApplicationPause(bool paused) {
if (paused) DoAutoSave(); // на mobile при сворачивании
}
private void OnApplicationQuit() {
DoAutoSave();
}
}
Cloud Saves — Steam, Unity Cloud Save
Для cross-device синхронизации:
- Steam Cloud — через Steamworks API; работает прозрачно, кладёт ваш сейв в их облако.
- Unity Cloud Save (UGS пакет) — официальный, привязан к Unity Authentication.
- Google Play Games Services — для Android.
Все три — отдельная тема, требуют backend-настройки. Для single-player без cross-device локальный сейв обычно достаточен.
Подводные камни
- Не использовать
Application.dataPathдля записи — это путь к асету проекта, в билде read-only. ТолькоpersistentDataPath. - Не сохранять Transform напрямую —
Transformне сериализуется. ИзвлекайтеVector3 position,Quaternion rotation,Vector3 scaleруками. - Не забывать про async — синхронный
File.WriteAllTextблокирует main thread → stutter. - Тестируйте миграцию сейвов — добавили поле → запустите со старым сейвом → не падает?