Реалізація системи збереження та завантаження даних ігор
PlayerPrefs.SetFloat("health", 100) працює для прототипу. Для продакшену — це тупик. Коли обсяг збереження даних виростає до десятків змінних, PlayerPrefs перетворюється на неструктуровану свалку без версіонування, без можливості кількох слотів та без захисту від корупції. Перехід з PlayerPrefs на нормальну систему в середині розробки — болісна задача.
Що повинна вміти система збереження
Мінімальний production-ready набір:
- Кілька слотів збереження з метаданими (дата, ім'я персонажа, рівень, скріншот)
- Атомарна запис: файл або записаний повністю, або не записаний взагалі (проміжний краш не корруптує дані)
- Версіонування: при оновленні гри старі збереження повинні мігрувати, а не ламатися
- Асинхронна запис: збереження не повинне фризити гру на 200ms
Архітектура: ISaveable та SaveManager
Паттерн: кожен компонент, який хоче зберігатися, реалізує інтерфейс ISaveable:
public interface ISaveable
{
string SaveId { get; }
object CaptureState();
void RestoreState(object state);
}
SaveManager при збереженні знаходить усі ISaveable на сцені (через FindObjectsOfType або регістрацію), викликає CaptureState() у кожного, збирає результат у Dictionary<string, object>, серіалізує та пише на диск. При завантаженні — зворотний процес.
SaveId — унікальна рядок для кожного компонента. Використовується GUID, генерований в інспекторі через [SerializeField] private string _saveId. Важливо не використовувати ім'я об'єкту сцени як ID: це не унікально і може змінитися.
Серіалізація: JSON vs Binary
JSON (Newtonsoft.Json) — читаємий, легко дебажиться, сумісний із різними платформами. Мінуси: більший обсяг файлу, трохи повільніше, потрібні кастомні конвертери для типів Unity (Vector3, Quaternion, Color). JsonConvert.SerializeObject(data, Formatting.None) з кастомним UnityTypeConverter — робочий підхід.
BinaryFormatter — Unity-вбудований, швидкий, компактний. Але: deprecated в .NET 5+, має вразливості безпеки (не критично для offline ігор). Для нових проектів не рекомендується.
MessagePack-CSharp — бінарний формат з продуктивністю кращою за JSON та без проблем BinaryFormatter. Хороший вибір для мобільних ігор з великими обсягами даних.
Шлях до файлу збереження: Application.persistentDataPath + "/saves/slot_{index}.sav". persistentDataPath гарантовано доступний для запису на всіх платформах (iOS, Android, PC, Console).
Атомарна запис та захист від корупції
Пряма перезапис файлу File.WriteAllText(path, json) може залишити файл у невалідному стані при крашуванні під час запису. Атомарна запис:
- Записати дані у тимчасовий файл
slot_0.sav.tmp - Якщо запис успішний — переіменувати
File.Move(tmpPath, finalPath)(атомарна операція на більшості ОС) - Старий файл попередньо переіменувати в
slot_0.sav.bak— резервна копія
При завантаженні: якщо основний файл не знайдений або невалідний — спробувати .bak. Це елементарний захист, який економить тисячі годин поддержки після релізу.
Асинхронне збереження
Серіалізація 5 МБ JSON синхронно — це 50–200ms затримки на середньому PC, на мобільних ще гірше. Рішення: async/await з File.WriteAllTextAsync():
public async Task SaveAsync(int slot)
{
var data = CollectSaveData();
string json = JsonConvert.SerializeObject(data);
await File.WriteAllTextAsync(GetSavePath(slot), json);
}
У Unity async Task методи працюють коректно з ConfigureAwait(false) для фонових потоків. UI індикатор збереження показується до виклику, приховується у finally блоці.
Версіонування та міграція
Кожен файл збереження містить "saveVersion": 3. При завантаженні версія порівнюється з currentSaveVersion. Якщо версії не збігаються — запускається ланцюжок мігреторів:
ISaveMigrator[] migrators = {
new SaveMigratorV1ToV2(),
new SaveMigratorV2ToV3()
};
Кожен мігратор знає як оновити JObject від своєї версії до наступної. Це дозволяє оновлювати формат збережень без втрати даних гравців. Без версіонування перше ж оновлення гри зі зміною структури даних інвалідує всі існуючі збереження.
Автозбереження та checkpoint система
Автозбереження через InvokeRepeating("AutoSave", 300f, 300f) — кожні 5 хвилин у спеціальний autosave слот. Checkpoint-збереження: при вході в trigger-зону публікується подія OnCheckpointReached, SaveManager зберігає у checkpoint-слот без UI.
Критично: не зберігати у момент бою або завантаженої сцени — вибирати момент збереження так, щоб фоновий потік запису не конкурував з піком CPU геймплею. Прапор isSafeToSave знімається на час інтенсивних сцен.
Орієнтовні строки
| Масштаб | Склад | Строк |
|---|---|---|
| Простий | JSON, один слот, без версіонування | 2–4 дня |
| Базовий | ISaveable паттерн, кілька слотів, атомарна запис | 1–2 тижні |
| Повний | Async, версіонування, міграція, cloud sync | 3–5 тижнів |
| З хмарними збереженнями | + Unity Cloud Save / Steam Cloud інтеграція | +1–2 тижні |
Процес
Спочатку пишемо SaveManager з тестом в ізоляції (Unity Test Runner): зберегти дані, завантажити, перевірити ідентичність. Потім інтегруємо ISaveable у існуючі компоненти по одному. Імітуємо краш запису під час тестування — спеціально переривємо процес збереження і перевіряємо, що дані не корруптувалися. Cloud sync реалізується останнім — тільки після стабільної роботи локального збереження.





