Втрата прогресу — найгірше, що може статися з гравцем. Пройшов 30 рівнів, купив бустери, розблокував персонажів — і після перевстановлення або зміни пристрою нічого нема. Рейтинг у магазині падає, користувач йде. Нещодавно до нас звернувся клієнт: після оновлення всі збереження виявилися битими, 15% активної аудиторії пішло за тиждень. Після впровадження нашої системи відтік скоротився до 2%, а рейтинг у магазині виріс з 3.2 до 4.5 зірок. Ми знаємо, як цього уникнути.
Ми розробляємо систему збереження під ключ для ігор на Unity, Android, iOS та кроссплатформенних проєктах. За 5 років реалізували понад 40 ігор, гарантуючи збереженість даних та безшовний користувацький досвід. Оцінимо ваш проєкт — зв'яжіться з нами, щоб обговорити деталі.
Які дані потрібно зберігати і де?
Прогрес гри включає кілька типів даних з різними вимогами до надійності та швидкості доступу:
- Критичні дані: рівень, валюта, покупки. Повинні синхронізуватися з сервером — ніколи не втрачатися. Зберігаються локально + хмара.
- Ігровий прогрес: пройдені рівні, досягнення, розблоковані предмети. Локально + опціональна синхронізація.
- Користувацькі налаштування: гучність, керування, графіка. Тільки локально — втрата некритична.
- Сесійні дані: поточний рівень, позиція, тимчасові бафи. Тільки в пам'яті — не потребують персистентності.
| Тип даних |
Місце зберігання |
Частота збереження |
Ризик втрати |
| Критичні |
Локально + хмара |
Після кожної зміни |
Мінімальний |
| Ігровий прогрес |
Локально + опціонально хмара |
При проходженні рівнів |
Низький |
| Налаштування |
Локально |
За запитом користувача |
Приймається |
| Сесійні |
Пам'ять |
Не зберігаються |
Високий (але допустимий) |
Локальне зберігання
Для простих ігор — JSON-файл або SharedPreferences/UserDefaults. Для складного прогресу з безліччю сутностей — SQLite через Room.
@Entity(tableName = "save_data")
data class SaveDataEntity(
@PrimaryKey val playerId: String,
val level: Int,
val experience: Long,
val coins: Long,
val gems: Int,
val unlockedLevels: String, // JSON array
val inventory: String, // JSON array
val achievements: String, // JSON array
val settings: String, // JSON object
val lastSavedAt: Long,
val version: Int = 1 // для міграцій формату
)
Для Unity — PlayerPrefs для простих значень, Application.persistentDataPath + бінарний файл для складних структур. BinaryFormatter застарів у сучасних версіях Unity — використовуємо JsonUtility або Newtonsoft.Json + File.WriteAllBytes.
// Unity: збереження через JsonUtility
[Serializable]
public class SaveData {
public int level;
public long coins;
public List<string> unlockedItems = new List<string>();
public int[] levelStars; // 3 зірки на кожен рівень
public long savedAt;
}
public class SaveSystem : MonoBehaviour {
private const string SAVE_FILE = "save.json";
public static void Save(SaveData data) {
data.savedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
string json = JsonUtility.ToJson(data);
string path = Path.Combine(Application.persistentDataPath, SAVE_FILE);
// Спочатку пишемо в temp, потім перейменовуємо — атомарна операція
string tempPath = path + ".tmp";
File.WriteAllText(tempPath, json);
File.Move(tempPath, path, overwrite: true);
}
}
Запис через temp-файл з подальшим перейменуванням — захист від корумпованого файлу при креші під час запису.
Хмарна синхронізація
Для кроссплатформенних ігор — власний бекенд. Для iOS-only — Game Center + iCloud. Для Android — Google Play Games Services. Для Unity — обгортки над обома.
// Android: Google Play Games Services
GamesSignInClient.signIn().addOnCompleteListener { task ->
if (task.isSuccessful) {
PlayGames.getSnapshotsClient(activity)
.open(SNAPSHOT_NAME, true, SnapshotsClient.RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED)
.addOnSuccessListener { dataOrConflict ->
if (dataOrConflict.isConflict) {
resolveConflict(dataOrConflict.conflict)
} else {
loadFromSnapshot(dataOrConflict.data)
}
}
}
}
Google Play Games Snapshots API автоматично керує конфліктами при RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED — перемагає більш свіже збереження. Для більшості ігор цього достатньо.
Порівняйте хмарні рішення:
| Платформа |
Сервіс |
Автоматичне вирішення конфліктів |
Ліміт розміру збереження |
| iOS |
CloudKit |
Так (останній запис) |
1 MB на запис, 10 MB загальний |
| Android |
Google Play Games Snapshots |
Так (останній запис або ручне) |
3 MB |
| Кроссплатформа |
Власний бекенд |
Налаштовується |
Необмежений |
Згідно з документацією Apple, CloudKit забезпечує автоматичне вирішення конфліктів на основі часових міток. В одному проєкті впровадження серверної валідації дозволило заощадити понад $10,000 на місяць на поверненнях від чітерів.
Чому важлива міграція формату?
Формат даних змінюється з оновленнями гри. Потрібна міграційна система:
class SaveMigrator {
fun migrate(data: SaveDataEntity): SaveDataEntity {
var current = data
while (current.version < CURRENT_VERSION) {
current = when (current.version) {
1 -> migrateV1toV2(current)
2 -> migrateV2toV3(current)
else -> throw IllegalStateException("Unknown version: ${current.version}")
}
}
return current
}
private fun migrateV1toV2(data: SaveDataEntity): SaveDataEntity {
// У версії 2 додали daily challenges progress
return data.copy(
dailyChallenges = "{}",
version = 2
)
}
}
Перевіряємо версію при завантаженні — якщо стара, мігруємо до актуальної та зберігаємо заново.
Як забезпечити захист від чітерства?
Для ігор з монетизацією — хеш для виявлення модифікації файлу:
fun computeChecksum(data: SaveDataEntity): String {
val content = "${data.playerId}|${data.coins}|${data.gems}|${data.level}|${SECRET_SALT}"
return MessageDigest.getInstance("SHA-256")
.digest(content.toByteArray())
.fold("") { str, byte -> str + "%02x".format(byte) }
}
Серверна валідація — надійніше. Критичні операції (покупка за gems) проходять через сервер, клієнт не може просто записати потрібне значення у файл. В одному проєкті ми знизили кількість чітерів на 95% всього за місяць після впровадження серверної перевірки кожної покупки.
Як вибрати між CloudKit та Google Play Games?
Вибір залежить від платформи та вимог. Якщо гра тільки на iOS — CloudKit та Game Center. Для Android — Google Play Games Snapshots. Для кроссплатформенного проєкту частіше обирають власний бекенд для єдності. Ми допомагаємо визначитися на етапі аудиту — замовте консультацію, щоб отримати оптимальне рішення.
Що входить у розробку системи збереження
Ми надаємо:
- Аналіз вимог до даних та вибір архітектури
- Реалізацію локального сховища (Room, DataStore, CoreData, PlayerPrefs)
- Підключення хмарної синхронізації (iCloud, Google Play Games, власний сервер)
- Механізм міграції формату при оновленнях
- Захист від злому (хеші, серверна валідація)
- Тестування на різних пристроях та версіях ОС
- Документацію та навчання команди
Терміни — від 2 до 4 тижнів залежно від складності. Ми проводимо навантажувальне тестування: симулюємо 1000 одночасних збережень, щоб переконатися у відмовостійкості. Середня окупність інвестицій — 3 місяці за рахунок лояльності користувачів. Зв'яжіться з нами, щоб отримати консультацію та оцінку вашого проєкту.
Як вибрати рішення для локального зберігання даних (Room, Core Data, Realm, Isar)?
Ми стикалися з ситуацією, коли додаток втрачає дані при втраті мережі — і це не просто баг, це провал сценарію. Користувач заповнив форму, натиснув «Відправити», отримав таймаут і втратив все. Або гірше: дані відправилися двічі через некоректну логіку повторної відправки. Правильно вибраний та налаштований шар сховища вирішує цю проблему раз і назавжди. Неправильний вибір може коштувати команді місяців переписування коду та втрати до 70% часу на синхронізацію. Наш досвід — 10+ років у мобільній розробці, понад 50 проектів з офлайн-сховищами — підтверджує: вибір рішення визначає 80% майбутніх проблем з продуктивністю та синхронізацією.
На практиці вибір сховища визначається двома факторами: типом даних та вимогами до синхронізації, а не популярністю бібліотеки.
Room (Android) — обгортка над SQLite з compile-time верифікацією SQL-запитів. Якщо запит невалідний, збірка падає — це краще, ніж SQLiteException в рантаймі. Room добре інтегрується з Kotlin Flow та LiveData, що робить реактивні UI-оновлення прямолінійними. Основна складність — міграції схеми. @Database(version = N, exportSchema = true) з файлами міграцій в assets/databases/ — обов'язкова практика, інакше при оновленні додатка fallbackToDestructiveMigration() просто зітре дані користувача.
Core Data (iOS) — не база даних, а фреймворк управління графом об'єктів поверх SQLite (або XML, або in-memory). NSPersistentContainer з viewContext для читання на main thread та newBackgroundContext() для запису — базова схема. Проблема починається, коли розробник робить save() в viewContext з фонового потоку: EXC_BAD_ACCESS в рандомний момент, відтворюється раз на тиждень, в крешлозі майже нічого корисного. Потрібно використовувати performAndWait або perform для кожного контексту строго в своєму потоці. Apple Core Data Programming Guide рекомендує саме такий підхід.
Realm виграє там, де потрібна швидкість роботи з великими наборами об'єктів та вбудована реактивність через Results + observe(). Realm зберігає об'єкти напряму, без маппінгу ORM, тому читання не потребує десеріалізації. За нашими вимірами, Realm обробляє читання в 2–3 рази швидше Core Data при об'ємі понад 10 000 об'єктів. На Flutter Realm SDK (ex-MongoDB Realm) підтримує Device Sync — але це вже managed-сервіс з окремою інфраструктурою.
Hive та Isar — Flutter-специфічні рішення. Hive — key-value сховище, швидко, просто, підходить для налаштувань та кешів. Isar — повноцінна документо-орієнтована БД з індексами, написана на Rust, компілюється в нативний код. Для Flutter-додатків з офлайн-функціональністю Isar зараз переважніше: вбудований query builder з типобезпечними фільтрами, транзакції, watchObject/watchQuery для реактивності.
| Платформа |
Рішення |
Реактивність |
Синхронізація |
| Android |
Room + Flow |
LiveData/Flow |
WorkManager |
| iOS |
Core Data |
NSFetchedResultsController |
CloudKit |
| Flutter |
Isar |
Streams |
Custom / Realm Sync |
| Cross-platform |
Realm |
RealmResults.observe |
Device Sync |
| Flutter (простий) |
Hive |
ValueListenable |
Немає |
Зв'яжіться з нами, щоб отримати безкоштовний аудит вашого поточного сховища та рекомендації з оптимізації — це зекономить вам сотні годин розробки та до 60% трафіку на серверні запити.
Чому офлайн-синхронізація — найскладніша частина?
Локальне сховище саме по собі нескладне. Складність — у синхронізації з сервером при наявності конфліктів.
Найчастіший патерн — optimistic updates з rollback. Користувач редагує запис, UI відображає зміну миттєво, фоновий запит йде на сервер. Якщо сервер повертає помилку — відкочуємо локальний стейт. Виглядає просто. На практиці: якщо користувач встиг піти з екрану і повернутися, а відкат відбувся через 3 секунди — UX зламаний. Потрібна явна черга операцій зі станом (PENDING, SYNCED, FAILED) в окремій таблиці.
На Android для фонової синхронізації використовуємо WorkManager з Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED). Важно не забыть про setInputMerger(ArrayCreatingInputMerger::class) при батчинге задач — інакше при кількох одночасних запусках дані затираються. Типова реалізація черги операцій:
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val pendingOps = syncDao.getPendingOperations()
for (op in pendingOps) {
try {
apiClient.send(op.payload)
syncDao.markSynced(op.id)
} catch (e: Exception) {
syncDao.markFailed(op.id, e.message)
return Result.retry()
}
}
return Result.success()
}
}
На iOS аналог — BGTaskScheduler з BGProcessingTaskRequest. Обмеження iOS на фоновий час виконання (~30 секунд для refresh tasks) означають, що синхронізація повинна бути інкрементальною: не «синхронізувати все», а «синхронізувати наступні N записів, зберегти курсор».
Конфлікти при мультипристроєвій роботі вирішуються одним із трьох підходів:
- Last-write-wins по
updated_at (найпростіший, втрачає дані при одночасному редагуванні)
- Server-wins (клієнт завжди приймає серверну версію)
- Three-way merge (складно, потрібен спільний предок — підходить для документів)
У більшості B2C-додатків достатньо last-write-wins з вектором часу на рівні користувача, але при спільному редагуванні потрібен CRDTs-підхід — тоді дивимося на Automerge або Yjs з мобільними біндингами.
Як ми будуємо шар сховища
Репозиторний патерн — не опціональний, а обов'язковий. UserRepository не знає, звідки дані: з Room, Realm чи мережі. ViewModel викликає repository.getUser(id), отримує Flow/Stream, відображає дані. Логіка кешування — всередині репозиторія.
Для Flutter типова архітектура: Isar для персистентності, Riverpod для управління стейтом, ConnectivityPlus для визначення стану мережі, кастомний SyncService з чергою операцій. Riverpod AsyncNotifier зручно покриває логіку «показати кеш, оновити з мережі, показати нові дані». Приклад репозиторія з кешуванням:
class UserRepository {
final Isar isar;
final ApiClient api;
Future<User> getUser(String id) async {
// 1. спробувати з локального сховища
final cached = await isar.user.where().idEqualTo(id).findFirst();
if (cached != null) return cached;
// 2. інакше з мережі
final remote = await api.fetchUser(id);
// 3. зберегти локально
await isar.writeTxn(() => isar.user.put(remote));
return remote;
}
}
Окрема тема — шифрування. Якщо додаток зберігає медичні дані, платіжні картки або корпоративні документи, SQLCipher (Android) та NSFileProtection (iOS) — не опція. Realm підтримує шифрування нативно через ключ у 64 байти, який потрібно зберігати в Keychain/Keystore, а не в SharedPreferences. Економія на безпеці може обійтися у витік даних з гучними наслідками.
Що входить в роботу
Ми гарантуємо прозорий процес і фіксуємо кожен етап:
| Етап |
Результат |
| Аудит вимог |
Документ з аналізом типів даних, обсягів, сценаріїв синхронізації |
| Проектування схеми |
ER-діаграма, файли міграцій, план конфлікт-резолюції |
| Розробка репозиторного шару |
Код з юніт-тестами (in-memory БД + моки мережі) |
| Інтеграція синхронізації |
Черга операцій, обробка помилок, fallback-логіка |
| Профілювання та оптимізація |
Звіт Android Profiler / Core Data SQLDebug, рекомендації |
| Деплой та документування |
Інструкція з розгортання, API-опис, доступ до репозиторію |
Бажаєте уникнути типових помилок при проектуванні сховища? Зверніться до нас — ми допоможемо спроектувати надійне локальне сховище з нуля або доопрацювати існуюче.
Які етапи роботи?
Починаємо з аудиту вимог: які дані, який обсяг, чи потрібна синхронізація, чи можливі конфлікти. На цьому етапі стає зрозуміло, Core Data чи SQLite-based рішення, чи потрібен Realm Sync або достатньо простого REST-поллінгу.
Далі — проектування схеми з урахуванням міграцій. Схему змінюють у будь-якому проекті — питання не «чи будуть міграції», а «наскільки болісно вони пройдуть». Експортуємо схему в JSON, зберігаємо в репозиторії, пишемо тести на міграцію кожної версії.
Розробка йде з покриттям репозиторного шару юніт-тестами: моки мережевого шару, реальна in-memory база для тестування запитів. Перед релізом — профілювання запитів через Android Profiler (вкладка Database Inspector) або Core Data debug флаги (-com.apple.CoreData.SQLDebug 1).
Термін реалізації шару сховища з базовою офлайн-синхронізацією — від 2 до 6 тижнів залежно від складності схеми та вимог до конфлікт-резолюції. Вартість розраховується індивідуально після аудиту вашого проекту. Замовте розробку під ключ — отримайте консультацію з вибору оптимального стеку та міграціям.