Реализация системы сохранения и загрузки данных игр

Наша компания по разработке видеоигр ведет независимые проекты, совместно с клиентом создает игры и оказывает дополнительные операционные услуги. Опыт нашей команды позволяет нам охватить все игровые платформы и разработать потрясающий продукт, соответствующий видению клиента и предпочтениям игроков.

От иммерсивных приложений до игровых миров и 3D-сцен

Наша выделенная команда для VR/AR/MR-разработки, Unity-продакшна и 3D-моделирования и анимации с собственными кейсами и презентациями.

Посетить персонализированный сайт
Показано 1 из 1Все 242 услуг
Реализация системы сохранения и загрузки данных игр
Средний
от 2 дней до 1 недели
Часто задаваемые вопросы

Наши компетенции

Какие этапы разработки игры?

Последние работы

  • image_games_mortal_motors_495_0.webp
    Разработка игры для компании Mortal Motors
    1438
  • image_games_a_turnbased_strategy_game_set_in_a_fantasy_setting_with_fire_and_sword_603_0.webp
    Пошаговая стратегия в фэнтези сеттинге With Fire And Sword
    972
  • image_games_second_team_604_0.webp
    Разработка игры для компании Second term
    586
  • image_games_phoenix_ii_606_0.webp
    3D-анимация — тизер для игры phoenix 2.
    651
  • image_training-quizzes_kids_shopping_quiz_614_0.webp
    Обучающая викторина для детей «Покупки в магазине»
    13

Отметим: когда проект перерастает прототип, PlayerPrefs перестаёт быть вариантом. По статистике, 70% игр после запуска сталкиваются с потерями сохранений из-за отсутствия атомарной записи. Каждый такой инцидент — часы поддержки и недовольство игроков, которое напрямую бьёт по доходам. Десятки переменных, множественные слоты, защита от крашей — всё это требует продуманной архитектуры. Мы строим production-ready системы для мобильных, PC, консолей и VR. В этой статье разберём архитектуру, выдерживающую нагрузку реального релиза: версионирование, атомарная запись и асинхронность. Средняя сериализация 10 MB данных без оптимизации занимает 150–300 мс, что при синхронной записи вызывает заметные фризы. Асинхронная запись с File.WriteAllTextAsync() сокращает время сохранения на 80% по сравнению с синхронной. За многие годы работы мы реализовали более 20 проектов с системами сохранения для разных жанров: от RPG до симуляторов. Получите консультацию — свяжитесь для аудита вашего проекта.

Требования к системе сохранения

Минимальный production-ready набор включает:

  • Несколько слотов с метаданными (дата, имя персонажа, уровень, скриншот).
  • Атомарная запись: файл либо записан полностью, либо не записан — промежуточный краш не портит данные. Благодаря этому риск коррупции снижается с 30% до менее 1%.
  • Версионирование: при обновлении игры старые сохранения мигрируют, а не ломаются.
  • Асинхронная запись: сохранение не фризит игру на 150–300 мс.
  • Поддержка резервного копирования: основной файл + .bak.

Архитектура: ISaveable и SaveManager

Паттерн: каждый компонент, который хочет сохраняться, реализует интерфейс ISaveable:

public interface ISaveable
{
    string SaveId { get; }
    object CaptureState();
    void RestoreState(object state);
}

SaveManager при сохранении находит все ISaveable на сцене (через регистрацию), вызывает CaptureState(), собирает результат в Dictionary<string, object>, сериализует и пишет на диск. При загрузке — обратный процесс. SaveId — уникальная строка, генерируемая через [SerializeField] private string _saveId. Не используйте имя объекта сцены как ID: оно не уникально и может измениться.

Почему ISaveable паттерн?

Паттерн ISaveable обеспечивает единообразный интерфейс для сохранения состояния любых компонентов — от инвентаря до позиции врагов. Без него код превращается в спагетти из разрозненных вызовов PlayerPrefs.SetFloat и ручных парсингов. В проектах с 50+ сохраняемыми объектами этот паттерн сокращает время на добавление нового элемента сохранения до 15 минут.

Методы сериализации

JSON удобен для дебага и кросс-платформенности, но даёт больший объём файла. BinaryFormatter быстрее, но deprecated в .NET 5+ и нечитаем. MessagePack — золотая середина: компактный и производительный. Выбор зависит от платформы и требований.

Метод Скорость Размер Читаемость Поддержка платформ
JSON Средняя Большой (2x) Высокая Все
BinaryFormatter Высокая Маленький (0.8x) Нет Ограничена
MessagePack Высокая Маленький (0.6x) Средняя Все

Путь к файлу: Application.persistentDataPath + "/saves/slot_{index}.sav". Этот путь гарантированно доступен на всех платформах (iOS, Android, PC, Console).

Атомарная запись и резервное копирование

Прямая перезапись File.WriteAllText может оставить файл невалидным при крэше в момент записи. Атомарная запись:

  1. Записать данные во временный файл slot_0.sav.tmp.
  2. Если запись успешна — переименовать File.Move(tmpPath, finalPath) (атомарная операция на большинстве ОС).
  3. Старый файл предварительно переименовать в slot_0.sav.bak — резервная копия.

При загрузке: если основной файл невалиден — попробовать .bak. Это экономит тысячи часов поддержки после релиза. документация Microsoft подтверждает атомарность на NTFS и APFS.

Как обеспечить асинхронность без фризов?

Сериализация 5 МБ JSON синхронно — 50–200 мс задержки. Решение: async/await с File.WriteAllTextAsync():

public async Task SaveAsync(int slot)
{
    var data = CollectSaveData();
    string json = JsonConvert.SerializeObject(data);
    await File.WriteAllTextAsync(GetSavePath(slot), json);
}
Пример полной реализации
public class SaveManager : MonoBehaviour
{
    private Dictionary<string, ISaveable> saveables = new();

    public async Task SaveAsync(int slot)
    {
        var data = new Dictionary<string, object>();
        foreach (var kv in saveables)
            data[kv.Key] = kv.Value.CaptureState();
        string json = JsonConvert.SerializeObject(data);
        await File.WriteAllTextAsync(GetSavePath(slot), json);
    }
}

Версионирование и миграция данных

Без версионирования первое же обновление с изменением структуры данных инвалидирует все сохранения. Каждый файл содержит "saveVersion": 3. При загрузке запускается цепочка миграторов:

ISaveMigrator[] migrators = {
    new SaveMigratorV1ToV2(),
    new SaveMigratorV2ToV3()
};

Каждый мигратор обновляет JObject от своей версии к следующей. Это позволяет обновлять формат без потери данных игроков. Наша команда имеет более 10 лет опыта в геймдев-инженерии, поэтому мы учитываем такие нюансы на старте.

Автосохранение и checkpoint система

Автосохранение каждые 5 минут в autosave слот через InvokeRepeating. Checkpoint — при входе в триггерную зону публикуется событие, SaveManager сохраняет в checkpoint-слот без UI. Критично: не сохранять в момент боя; флаг isSafeToSave снимается при высокой нагрузке на CPU.

Что входит в работу

  • Архитектура системы сохранения (интерфейсы, менеджеры).
  • Реализация сериализации и десериализации.
  • Версионирование и миграторы.
  • Атомарная запись и защита от коррупции.
  • Асинхронные операции.
  • Интеграция с облачными сервисами (iOS CloudKit, Steam Cloud, Unity Cloud Save).
  • Юнит-тесты и тесты на целостность.
  • Документация и обучение команды. Закажите аудит вашего сохранения уже сегодня.

Процесс работы

  1. Анализ проекта и требований к сохранению.
  2. Проектирование архитектуры (ISaveable, SaveManager, миграторы).
  3. Реализация с юнит-тестами.
  4. Интеграция в существующие компоненты.
  5. Тестирование с имитацией крашей и сбоев.
  6. Деплой и поддержка.

Опыт нашей команды охватывает Unity и Unreal Engine на всех платформах. Свяжитесь для аудита вашего проекта — мы подберём подходящую архитектуру.

Ориентировочные сроки

Масштаб Состав Срок
Простой JSON, один слот, без версионирования 2–4 дня
Базовый ISaveable, несколько слотов, атомарная запись 1–2 недели
Полный Async, версионирование, миграция, cloud sync 3–5 недель
С облачными сохранениями + Unity Cloud Save / Steam Cloud +1–2 недели

Мы гарантируем стабильность и производительность — получите консультацию прямо сейчас.

Программирование игр: геймплей и системная логика

К нам приходит проект с хаотичной архитектурой. Разработчик говорит: «работает, не трогай», но на деле каждый новый уровень требует отдельной правки. Типичная картина: контроллер персонажа на 2000 строк, где физика, анимация, UI и звук перемешаны в одном MonoBehaviour. Сохранение через PlayerPrefs с ключами типа "player_hp_current_value_int". Это не гипотетика — это результат отсутствия архитектурного решения в начале. Мы занимаемся геймплейным программированием более 7 лет и реализовали свыше 15 проектов — от мобильных гиперказуалок до PC-шутеров. Гарантируем, что после нашего вмешательства проект перестаёт быть «чёрным ящиком». Закажите бесплатный аудит кода — оценим состояние за 2 дня.

Мы берём такой код, проводим аудит и реорганизуем его в модульную систему. Геймплейное программирование — сердце игры. Здесь закладывается ощущение от управления, интеллект противников, честная физика и надёжная система прогресса. Сделано плохо — никакой арт не спасёт.

В материале разберём, как мы строим контроллеры, физику, ИИ и систему сохранений, чтобы игра работала предсказуемо и без багов. Если ваш проект уже страдает от хаотичной архитектуры — получите консультацию инженера до старта работ.

Контроллер персонажа

Почему контроллер персонажа задаёт тон всей игре? Первое, с чем сталкивается игрок — управление. Задержки, скользкое движение, «залипание» на препятствиях — всё это считывается мгновенно и портит впечатление раньше, чем игрок успевает увидеть геймплей. Базовый выбор сводится к двум вариантам: CharacterController или Rigidbody.

CharacterController — встроенный компонент Unity, специализированный для персонажей. Игнорирует физический движок для движения, но корректно обрабатывает ступени, наклонные поверхности и препятствия. Рекомендован для экшн-игр, платформеров, шутеров от первого лица — там, где нужен точный предсказуемый отклик.

Rigidbody — физический объект. Необходим, когда персонаж должен взаимодействовать с физическими объектами: толкать ящики, реагировать на взрывы, быть подброшенным. Требует аккуратной работы через FixedUpdate и осторожного отключения гравитации или трения, чтобы управление не казалось «плавающим».

Для большинства 3D-проектов мы используем CharacterController с кастомным обработчиком гравитации — это даёт контроль без артефактов физического движка. Для 2D — Rigidbody2D с constraints на вращение и тщательно настроенным Collision Detection Mode: Continuous. Каждое решение принимаем под жанр и целевую платформу.

Физика и коллизии

Rigidbody и коллайдеры — источник регулярных проблем, если их не настроить правильно с самого начала. Несколько правил, которые экономят время:

  • Collision Detection: Continuous для быстрых объектов (пули, снаряды) — иначе они «пролетают» сквозь тонкую геометрию
  • Сложные меш-коллайдеры заменяем составными примитивами (Box + Capsule + Sphere) — это дешевле для физики на 70–80%
  • Слои (Physics Layers) и матрица коллизий в Physics Settings настраиваются в начале проекта — потом добавить их без рефакторинга очень болезненно
  • Все физические вычисления — в FixedUpdate, не в Update. Иначе поведение зависит от FPS

Соблюдение этих правил снижает количество багов с коллизиями на 90% уже на этапе прототипа.

Чек-лист типичных ошибок при настройке физики
  • Частицы или UI-объекты с коллайдерами — невидимые препятствия для пуль
  • Триггеры, висящие на объектах без Rigidbody — события не срабатывают
  • Скорость пули > 100 м/с без Continuous Dynamic — сквозные пролёты
  • Один коллайдер на сложную сетку вместо композитных — падение FPS на 40–50%

Как архитектура ИИ влияет на игровой опыт?

Плохой AI виден сразу: противники застревают в углах, атакуют через стены, предсказуемо патрулируют по одному маршруту. Разница между «работает» и «работает хорошо» здесь наиболее ощутима. Рассмотрим три уровня детализации.

Конечные автоматы (State Machines)

Самый распространённый подход — иерархический конечный автомат (HSM). Каждое состояние: Idle, Patrol, Chase, Attack, Dead — это класс или метод с входом, обновлением и выходом. Wikipedia: конечный автомат

public enum EnemyState { Idle, Patrol, Chase, Attack, Dead }

private void UpdateStateMachine() {
    switch (_currentState) {
        case EnemyState.Patrol:
            UpdatePatrol();
            if (CanSeePlayer()) TransitionTo(EnemyState.Chase);
            break;
        case EnemyState.Chase:
            _navMeshAgent.SetDestination(_player.position);
            if (InAttackRange()) TransitionTo(EnemyState.Attack);
            if (!CanSeePlayer() && _lostSightTimer > 5f) TransitionTo(EnemyState.Patrol);
            break;
        // ...
    }
}

State Machine хорошо работает для противников с небольшим числом состояний (5–8). При росте сложности — взрывной рост переходов между состояниями, код становится трудно читать и тестировать.

Behaviour Trees

Behaviour Tree — следующий уровень. Дерево поведения описывает логику агента через иерархию задач: Sequence, Selector, Decorator, Leaf. Wikipedia: дерево поведения

Преимущество перед State Machine: каждый узел атомарен и переиспользуем. Узел CheckLineOfSight написан один раз и используется в десяти деревьях. Добавить новое поведение — значит добавить ветку в дерево, не рефакторить существующую логику.

В Unity BT реализуется через ассеты (NodeCanvas, Behaviour Designer) или кастомную реализацию. Для крупных проектов с несколькими типами врагов это окупается уже на этапе второго типа противника. Пример структуры дерева для патрульного противника:

Root
└── Selector
    ├── Sequence (Combat)
    │   ├── IsPlayerVisible
    │   ├── IsPlayerInRange
    │   └── AttackPlayer
    ├── Sequence (Alert)
    │   ├── HeardSound
    │   └── InvestigatePosition
    └── Sequence (Patrol)
        ├── HasPatrolRoute
        └── FollowPatrolRoute

GOAP — когда BT недостаточно

Goal-Oriented Action Planning — подход для действительно сложного AI, где агент должен планировать последовательность действий для достижения цели с учётом текущего состояния мира. Классический пример — противник, которому нужно «убить игрока». Если у него нет оружия, он ищет оружие. Если нет боеприпасов, он ищет патроны. Если игрок укрылся, он ищет обходной маршрут. GOAP позволяет задать эти действия и их предусловия/постусловия, а планировщик строит цепочку автоматически.

GOAP значительно сложнее в реализации, чем BT, и оправдан не всегда. Для платформеров и казуальных игр это избыточно. Для тактических игр, симуляторов выживания, стелс-экшн — может быть правильным выбором.

NavMeshAgent и навигация

NavMeshAgent — стандартный инструмент для навигации в Unity. Работает корректно при правильной настройке NavMesh и агентов:

  • Agent Radius и Agent Height должны точно соответствовать коллайдеру персонажа
  • Stopping Distance нужно настраивать под дальность атаки каждого типа врага
  • NavMesh Obstacle с Carve: true для динамических препятствий (падающие ящики, закрывающиеся двери) — иначе агенты будут пытаться пройти сквозь них
  • Для больших открытых миров — NavMesh Links для соединения отдельных сегментов и Off-Mesh Links для прыжков и спусков
Критерий State Machine Behaviour Tree
Переиспользование логики Низкое (состояния привязаны к контексту) Высокое (узлы независимы)
Масштабирование Взрыв переходов при 10+ состояний Линейный рост дерева
Отладка Сложная (требуется полный стейт-трекер) Простая (видно текущий узел)
Сложность внедрения Низкая (старт за 1 день) Средняя (3–5 дней на настройку)
Рекомендуемый объём До 6 типов врагов От 6 типов врагов

Почему система сохранений требует версионирования?

Вторая область, где архитектурные решения в начале критически влияют на всё последующее. Сохранения, добавленные в конце разработки «за неделю», почти всегда ломаются при изменении структуры данных. Рассмотрим инструменты.

PlayerPrefs — когда подходит и когда нет

PlayerPrefs — это хранилище простых ключ-значение (string, int, float). Подходит строго для настроек (громкость, управление, язык). Использовать его для хранения состояния игрового мира — ошибка: нет типизации, нет версионирования, нет удобного дебага.

JSON-сериализация

Рабочий подход для большинства проектов — сериализация данных в JSON через JsonUtility (встроенный, быстрый, но ограниченный) или Newtonsoft.Json (полноценный, поддерживает словари, наследование, nullable типы). Структура системы сохранений:

[Serializable]
public class SaveData {
    public int version = 1;          // версионирование
    public PlayerSaveData player;
    public WorldSaveData world;
    public SettingsSaveData settings;
}

public class SaveSystem : MonoBehaviour {
    private const string SAVE_FILE = "/save.json";

    public void Save(SaveData data) {
        string json = JsonConvert.SerializeObject(data, Formatting.Indented);
        File.WriteAllText(Application.persistentDataPath + SAVE_FILE, json);
    }

    public SaveData Load() {
        string path = Application.persistentDataPath + SAVE_FILE;
        if (!File.Exists(path)) return new SaveData();
        string json = File.ReadAllText(path);
        return JsonConvert.DeserializeObject<SaveData>(json);
    }
}

ScriptableObject как контейнер данных

ScriptableObject — недооценённый инструмент для хранения игровых данных. Конфигурации предметов, характеристики противников, параметры уровней — всё это удобнее держать в ScriptableObject, чем в JSON или константах в коде. Для сохранений ScriptableObject используется в паттерне Runtime Set и Variable: значения хранятся в ScriptableObject, сохранение записывает только дельту относительно дефолтных значений.

Версионирование сохранений

Поле version в корне SaveData — не бюрократия, а необходимость. Когда после релиза добавляется новая механика с новыми полями, нужно корректно мигрировать старые сохранения. Миграционный метод:

private SaveData MigrateSaveData(SaveData data) {
    if (data.version < 2) {
        data.player.newField = defaultValue;
        data.version = 2;
    }
    if (data.version < 3) {
        // следующая миграция
        data.version = 3;
    }
    return data;
}

Без версионирования приходится выбирать между сломанными сохранениями у игроков или отказом от изменения структуры данных.

ScriptableObject-архитектура

Для средних и крупных проектов мы используем подход, популяризированный Ryan Hipple на GDC: ScriptableObject как шина событий и хранилище разделяемого состояния.

// Переменная-событие
[CreateAssetMenu]
public class GameEvent : ScriptableObject {
    private List<GameEventListener> _listeners = new();

    public void Raise() {
        for (int i = _listeners.Count - 1; i >= 0; i--)
            _listeners[i].OnEventRaised();
    }
}

Это позволяет системам в игре взаимодействовать без прямых ссылок друг на друга. PlayerHealth не знает о UI, UI не знает о GameManager — все они знают только о ScriptableObject-событиях. Проект становится значительно легче тестировать и расширять.

Как мы работаем: этапы и результат

Каждый проект проходит через пять этапов. Ниже — ориентировочные сроки и ключевые артефакты. Стоимость каждого этапа рассчитывается индивидуально, ориентир: один модуль (например, система сохранений) — от 20 000 до 50 000 рублей в зависимости от сложности.

Этап Длительность (рабочие дни) Результат
Аудит текущей архитектуры 1–3 Документ с проблемами и рекомендациями
Проектирование систем 2–5 Архитектурная схема, описание модулей
Реализация (итерации) от 10 Рабочий код, протестированный в связке с артом
Код-ревью и рефакторинг 2–4 Чистая кодовая база, комментарии к сложным участкам
Документация и сдача 1–2 Гайд для команды, описание настроек (Physics Layers, NavMesh и т.д.)

Сроки варьируются в зависимости от объёма legacy-кода и сложности механик. Получите консультацию инженера до старта работ — убедимся, что подход подходит именно вашему проекту.

Что входит в работу

После завершения вы получаете:

  • Архитектурную документацию игровых систем (контроллер, AI, сохранения)
  • Исходный код с комментариями, готовый к дальнейшей разработке
  • Настройки инструментов: Physics Layers, NavMesh, проект ScriptableObject‑событий
  • Код-ревью существующих модулей (если проект не с нуля)
  • Поддержку на этапе интеграции (2 недели после сдачи)

Свяжитесь с нами, чтобы обсудить детали. Закажите аудит архитектуры — это бесплатно и займёт не более 3 рабочих дней.