Разработка мультиплеера мобильной игры (асинхронный)
Асинхронный мультиплеер — вариант пошагового, где оба игрока не онлайн одновременно. Классические примеры: Words with Friends, Clash Royale Trophy Road с replay-атаками, любые игры формата «ход за сутки». Игрок делает ход, данные уходят на сервер, противник заходит через несколько часов и видит обновлённое состояние.
Хранение состояния и версионирование
Каждый ход — это транзакция в БД. Структура записи: game_id, move_number, player_id, action_payload (JSON), timestamp, resulting_state_hash. Hash состояния после каждого хода позволяет обнаружить расхождение: клиент пересчитывает hash по своей логике и сравнивает с серверным. Если расходятся — запрашивает полный снэпшот.
Event sourcing здесь работает лучше хранения только текущего состояния: можно восстановить любой момент игры, реализовать replay, аудит и откат при обнаружении бага в игровой логике.
Синхронизация через polling и WebSocket
При старте сессии клиент делает GET /game/{id}/state и получает текущее состояние с version числом. Дальше два варианта уведомлений о ходе противника: polling каждые 30-60 секунд или WebSocket/SSE при открытом приложении + push при закрытом.
Polling проще, но нагружает сервер. Лучше: при открытом приложении — WebSocket с game_state_updated событием. При закрытом — FCM/APNs push. При получении push — клиент открывает игру и делает GET /game/{id}/state?since_version={lastKnown} — получает только изменения с последней известной версии.
Offline-first UX
Пользователь делает ход без интернета — ход должен сохраниться локально и отправиться при восстановлении соединения. На Android — WorkManager с NetworkType.CONNECTED: задача будет выполнена, как только появится интернет, даже если приложение закрыто.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val submitMoveRequest = OneTimeWorkRequestBuilder<SubmitMoveWorker>()
.setConstraints(constraints)
.setInputData(workDataOf("move_payload" to moveJson))
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(submitMoveRequest)
На iOS — BGProcessingTask с requiresNetworkConnectivity = true. Ход сохраняется в Core Data, задача отправляет при ближайшей возможности.
Конфликты: если оба игрока каким-то образом отправили ход одновременно (баг клиента), сервер принимает только первый по timestamp и возвращает ошибку второму с актуальным состоянием.
Сроки
Базовая асинхронная система для 2 игроков с event sourcing, offline-first и push-уведомлениями: 2-4 недели. Стоимость рассчитывается индивидуально после анализа игровой механики.







