Реализация синхронизации состояния мобильной игры под ключ
Синхронизация состояния — фундамент любого мультиплеерного опыта. Суть проблемы: два устройства должны видеть одинаковую картину игрового мира в один момент времени, несмотря на задержку сети, потери пакетов и разную вычислительную мощность. Решений несколько, и выбор зависит от жанра. Мы внедряем синхронизацию под ключ: от выбора протокола до тестирования под миллисекундной задержкой. Например, для шутера критичны низкая задержка и плавность, поэтому используем client prediction и delta-сжатие. Для пошаговой стратегии достаточно event sourcing с fixed-point математикой. В любом случае, без правильной архитектуры игроки столкнутся с лагами, рассинхроном и потерей прогресса.
Что такое синхронизация состояния в мобильных играх?
Синхронизация — это механизм, обеспечивающий согласованность данных между сервером и всеми клиентами. Без неё в мультиплеере невозможно: игроки видят разных врагов, пули пролетают сквозь стены, прогресс теряется. Основная дилемма: полная точность требует пропускной способности, которую мобильный интернет редко предоставляет. Поэтому приходится балансировать между детализацией и трафиком.
Snapshot vs State delta vs Event sourcing
Три основных подхода к распределению состояния:
Full snapshot: сервер каждые N мс отправляет полное состояние мира. Просто, но расточительно. При 20 сущностях по 50 байт каждая, 20 Hz — 20 КБ/с на клиента. При 10 клиентах сервер рассылает 200 КБ/с. Подходит для небольших игр.
Delta compression: отправляем только изменения от предыдущего подтверждённого снэпшота. Клиент подтверждает ack: last_received_tick, сервер вычитает delta. Сокращает трафик в 3-10 раз на динамичных сценах. Реализация усложняется: нужно хранить историю состояний для вычисления delta, обрабатывать потерю пакета с delta (без базового снэпшота delta не применить).
Event sourcing: сервер рассылает события (PlayerMoved, BulletFired, EntityDied), клиент воспроизводит их поверх базового состояния. Хорошо для детерминированных игр: шахматы, карточные, стратегии. Плохо для физических симуляций: любой float-погрешности хватает, чтобы состояния разошлись через 30 секунд.
Как детерминизм влияет на синхронизацию?
Для event sourcing критично: обе платформы (iOS ARM64, Android ARM64/x86) должны давать одинаковый результат одних и тех же вычислений. Стандартный float в C# — детерминированный на одной платформе, но может давать разные результаты на разных CPU. Решение — fixed-point математика: вместо float 1.5f используем FixedPoint 15000 (масштаб 1/10000). Сложение и умножение целых чисел детерминированы везде. Библиотека FixedMath.Net для Unity, libfixmath для нативного C++. Godot использует детерминированную физику через _physics_process — все шаги фиксированы. Unity Physics (DOTS) поддерживает детерминизм при одинаковом порядке обработки объектов. Classic Unity PhysX — не детерминирован между платформами.
Почему fixed-point математика лучше float для синхронизации?
Float может давать разные результаты на разных архитектурах из-за различий в округлении. Fixed-point гарантирует детерминизм, что критично для event sourcing. Кроме того, fixed-point операции быстрее на некоторых процессорах, так как не требуют аппаратного FPU. Однако точность ниже: при масштабе 1/10000 максимальная ошибка — 0.0001, что приемлемо для позиций, но может быть недостаточно для физики высоких скоростей. Мы помогаем выбрать оптимальный формат под вашу игру.
Client-side prediction и reconciliation
Детальный разбор паттерна для real-time игр:
Client tick 100: применяю ввод локально, отправляю InputPayload{tick:100, input}
Client tick 101-110: продолжаю предсказывать локально
Server: получает InputPayload{tick:100}, симулирует, отвечает StatePayload{tick:100, pos, vel}
Client tick 112: получаю ответ сервера для тика 100
→ Сравниваю предсказанное состояние на тик 100 с серверным
→ Если расхождение > threshold: откат к серверному состоянию тика 100
→ Повторно применяю буфер вводов 101-112
Буфер вводов — circular array фиксированного размера (обычно 64-128 тиков). Каждый элемент: { tick, inputData, predictedState }. При reconciliation — итерация по буферу с повторным применением. Threshold для reconciliation: не ноль. Если 0.001 юнита расхождения — откат → клиент постоянно подёргивается. Типичный порог: 0.1-0.5 юнита в зависимости от скорости персонажа.
Когда нужна interpolation для других игроков?
Собственный персонаж — client prediction. Остальные игроки — interpolation:
Клиент хранит буфер снэпшотов с серверными метками времени:
[{time: 1000ms, pos: (10,0,5)}, {time: 1050ms, pos: (10.5,0,5)}, ...]
Рендер происходит с задержкой interpolation_delay (обычно 2-3 снэпшота = 100-150 мс при 20 Hz). В момент рендера находим два ближайших снэпшота и линейно интерполируем позицию:
float t = (renderTime - fromState.time) / (toState.time - fromState.time);
renderPosition = Vector3.Lerp(fromState.position, toState.position, t);
Для ротации — Quaternion.Slerp. Для скорости — нужна Hermite interpolation или Catmull-Rom по нескольким точкам — плавнее при изменении направления.
Как client prediction снижает задержку?
Без prediction игрок видит задержку равную RTT/2 (время до сервера и обратно). С prediction ввод применяется мгновенно, а сервер лишь корректирует. Это снижает воспринимаемую задержку в 2-5 раз. Для шутеров это критично: даже 50 мс задержки ощутимы.
Проблема расхождения (desync)
В детерминированных играх расхождение проявляется не сразу. Стандартная диагностика — state hash comparison: каждый N тиков все клиенты отправляют hash текущего состояния на сервер. Если хэши не совпадают — desync, сервер рассылает полный снэпшот для синхронизации. Hash вычисляется от критичных полей состояния (позиции, hp, статусы) — не от всего, чтобы не включать несущественные различия (анимационные веса, UI состояния).
Как мобильный интернет влияет на синхронизацию?
Мобильный интернет нестабилен. Проектируй под худший сценарий: 150 мс RTT, 5% packet loss, бюджет трафика 50 КБ/с на игрока. Практические меры:
- Позиции: int16 вместо float32 с масштабированием (экономия 50%)
- Ротация: quaternion → два угла int8 (экономия 75%)
- Entities вне зоны видимости: не рассылать или снизить частоту обновлений
- Priority-based updates: быстро двигающиеся объекты обновляются чаще
BitPacking библиотеки: NetStack (C#), LiteNetLib BitWriter — упаковка нескольких малых значений в один байт.
Как работает desync detection на практике?
Мы реализуем state hash comparison с частотой 1 раз в секунду. При обнаружении desync сервер отправляет полный снэпшот для всех клиентов, и они синхронизируются. Дополнительно используем checksums для каждого пакета, чтобы выявлять битовые ошибки.Что входит в работу
| Этап | Результат | Длительность (ориентировочно) |
|---|---|---|
| Аналитика | Протокол синхронизации, оценка трафика | 3-7 дней |
| Проектирование | Спецификация архитектуры, выбор стека | 5-10 дней |
| Разработка | Реализация синхронизации, client prediction, reconciliation | 30-60 дней |
| Тестирование | Нагрузочное тестирование, desync-симуляция | 7-14 дней |
| Деплой | Интеграция с CDN/бекендом, мониторинг | 5-10 дней |
Стоимость рассчитывается индивидуально в зависимости от сложности. Закажите консультацию — мы оценим ваш проект за 1-2 дня. Пишите — расскажем, как уменьшить задержки и трафик.
Сроки
Snapshot-синхронизация с interpolation для 4-10 игроков: 2-3 недели. Client prediction, reconciliation, delta compression, desync detection: 1.5-3 месяца. Детерминированная симуляция с fixed-point математикой: добавляет 3-6 недель. Свяжитесь с нами — подберём оптимальное решение под ваш бюджет.
Сравнение подходов:
| Подход | Трафик | Задержка | Сложность |
|---|---|---|---|
| Snapshot | Высокий | Средняя | Низкая |
| Delta compression | Низкий | Низкая | Средняя |
| Event sourcing | Средний | Высокая | Высокая |
Delta compression в 3-10 раз эффективнее snapshot по трафику, а event sourcing позволяет обойтись без серверного авторитета ценой сложности.







