Розробка системи сохранення прогресу мобільної гри
Втрата прогресу — найгірше, що може трапитися гравцю. Пройшов 30 рівнів, купив бустери, розблокував персонажів — і після переустановлення або смены пристрою нічого нема. Рейтинг у магазині падає, користувач йде. Система сохранень — критичний компонент, який не можна додати «потім».
Що та де зберігати
Прогрес гри складається з кількох типів даних з різними вимогами:
Критичні дані (рівень, валюта, покупки) — повинні синхронізуватися з сервером і ніколи не теряться. Зберігаються локально + хмара.
Ігровий прогрес (пройдені рівні, досягнення, розблоковані предмети) — локально + опціональна синхронізація.
Користувальницькі налаштування (гучність, управління, графіка) — тільки локально, втрата некритична.
Сесійні дані (поточний рівень, позиція, тимчасові баффи) — тільки в пам'яті, не потрібно персистувати між сесіями.
Локальне сховище
Для простих ігор — 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 2022 — використовуємо 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 — побеждає більш свіжое сохранення. Для більшості ігор цього достатньо.
Версіонування формату сохранення
Формат даних змінюється з оновленнями гри. Потрібна система міграції:
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) проходять через сервер, клієнт не може просто записати потрібне значення у файл.
Система сохранень з локальним сховищем, хмарною синхронізацією та міграцією формату для Unity/Android/iOS: 2–3 тижні. Вартість рассчитывается індивідуально.







