Привет всем! Я - Пётр, вы - вы, а это - статья о моих [проблемах и] попытках реализовать нормальный мультиплеер для Android игры.
Недавно я уже делал пару постов о том, как идёт работа над моей игрой "Бункер 2021", она полностью офлайн, если интересно, можете зайти в профиль да почитать.
Сегодня же я хочу рассказать о бедах с реализацией мультиплеера в игре для Android. Хотя всех проблем (в большей степени) можно было бы избежать, если бы я банально не торопился и делал всё с умом.
Решил я, значит, сделать сетевую "гонку" + езду с препятствиями, в стиле Gravity Defied, но в 3D и с флажками. На мой взгляд, это было бы интересно, как с точки зрения реализации, так и с точки зрения "поиграть".
Начал делать.
С режимом "прохождение" в целом возился я не долго, т.к. настроить физику трактора - дело одного вечера. Благо Godot Engine уже имеет всё под капотом.
Далее оставалось наделать уровней, раскидать бонусов, флажков, и вроде как, всё.
А вот режим игры по сети стал для меня целым приключением. И ещё неизвестно, кто в итоге победит.
Работу я вёл поэтапно, изучая основы и плавно повышая сложность реализации.
Ранее я уже работал с сетью, но не в играх, а в приложениях для WEB, в основном это был AJAX и WebSocket.
Всё, что походит на AJAX для сети не подходит, так как отправка данных на сервер и получение ответа в этом случае занимает секунды, даже если пакеты данных совсем мелкие. Во время гонки такие задержки непростительны.
Для сети в Godot есть много всего, что безумно радует, нет необходимости ковыряться в разных библиотеках, подключать, настраивать, компилировать...
Однако, есть одно большое НО - Android устройства не можно подключить друг к другу напрямую по, например, IP-адресу. Просто потому что у них их (в том виде, в котором они нужны) нет.
Оставался только один 100% рабочий вариант - использовать промежуточный сервер.
Условно, при игровом сеансе устройства должны подключаться к единому серверу, который бы раскидывал данные между устройствами.
Попытка № 1. WebSocket
Так как ранее я уже работал в веб-сокетами, я решил, что проще всего будет попробовать сделать сервер на них. Набросал быстренько тестовое приложение "чат" на основе документации, и, вроде как, всё заработало.
В целом алгоритм в моей голове был довольно простой - есть сервер на NodeJS, который регистрирует устройства, запоминает их в специальный список и раскидывает меж ними разные команды и пакеты.
Начал собирать тестовый стенд. Когда всё было готово, я сильно удивился от того, что это работает. Ну знаете: то, что работает с первого раза, скорее всего не работает, как надо. А тут не так. Тут сразу работает.
Обрадованный я начал усложнять. Помимо обычного подключения к игре и учета игроков я начал делать систему движения.
В первоначальном варианте я сделал так, что при движении игроки передают серверу свою позицию, угол вращения и разные мета-данные, такие как угол поворота рулевых колёс. В целом, такой вариант работал пока я не словил баг с сетью.
Случилось это вообще случайно, когда я тестировал игру на кухне и жена включила микроволновку, трактор противника начал "вздрагивать" при движении. То есть его позиции стали меняться не в реальном времени.
Было решено попробовать подкрутить интерполяцию. Микроволновка стала играть роль ухудшенной связи (например, мобильной). Всему виной, вероятно, старенький роутер. Но это не точно.
Интерполяция решила проблему с дерганием, но не решила проблему с отставаниями. Так как я слал позиции в порядке следования, появившаяся задержка шлейфом тянулась через всю цепочку позиций, так как клиент складывал их в массив по мере поступления от сервера.
Пришлось делать раз в секунду коррекцию позиции. Это снова привело к микро-вздрагиваниям. Такой расклад мне не понравился, и я решил сменить подход.
Попутно я избавился от беды с микроволновкой, введя фактор случайности в доставке пакетов на стороне сервера.
Новый принцип был таков: не передавать фактические данные состояния игрока, а передавать на сервер намерения игрока в движении (нажатия клавиш и состояния), переложив всю физику на клиента. Стало заметно лучше, однако, я заметил другую беду - физика на двух разных устройствах отрабатывала иногда по-разному. То есть в одной версии трактор может проехать в миллиметре от камешка и не шелохнуться, а в другой версии - соприкоснуться с ним и начать шататься. Тем самым меняя своё оригинальное направление. Плюсом к этому если включать режим ухудшенной сети, то иногда команды от сервера прилетали с опозданием, из-за чего всё могло совсем сломаться. Снова нужны были компенсации позиции. По факту я вернулся к тому, с чего начал.
Попутно я читал про сеть и про обработки столкновений, и решил написать вторую реализацию.
Попытка № 2. ENet и физический сервер
Моя вторая попытка реализации была достаточно простая. На компьютере был запущен сервер, написанный уже на Godot, а не NodeJS, с использованием ENet, к нему цеплялись устройства (пока что по локальной WiFi сети) и всё это работало.
Общий сервер крутится на компе, к нему присоединяются устройства и регистрируются в специальный список "игроки".
У игроков есть несколько состояний - покой (когда находится в меню), в игре (когда идёт гонка), и пауза (когда идёт гонка но игрок открыл меню паузы).
Физика и обработка объектов теперь на стороне сервера (Godot умеет). Это тут же решило проблемы с обработкой команд и вообще все объекты ведут себя полностью одинаково, даже при задержках сети.
Вся реализация была основана на официальной документации.
Когда основа была составлена, я решил выйти за пределы WiFi, оформил IP у провайдера, выделил порт, и начал пробовать подключаться.
В целом я полностью удовлетворился работой игры по WiFi. Даже когда игра идёт через провайдера (у меня это ДомRу). То есть и в локальной сети и через интернет, но по WiFi, всё работает чётко.
Как только я перешел на мобильное соединение, снова начались проблемы.
Иногда данные просто не прилетают, то есть клиент повисает в ожидании отправки данных, а иногда сервер повисает в ожидании.
Пришлось делать ручной контроль пакетов. Хоть в документации и сказано, что движок сам понимает потери и компенсирует их, почему-то проблему с зависаниями сети он никак не решает. Я же решил её довольно примитивным способом. Пока что он работает, чего мне, в общем-то достаточно.
При отправке и приёме данных я с набором всяких "цифер" шлю абсолютный порядковый номер "транзакции". На сервере и на клиенте при этом слежу, чтобы номера пакетов отличались от предыдущих не более, чем на два.
Как только происходит расхождение, я отправляю параллельный запрос по WebSocket с корректировками. То есть приложение устанавливает сразу два соединения, одно основное, второе - резервное. Пока я не могу точно сказать, в чем именно проблема, так как воюю с ней прямо сейчас, однако WS соединение к NodeJS на мобильном операторе работает фактически без сбоев, благодаря чему можно поддерживать соединение.
Пока я делаю ещё один тестовый стенд для проверки сети, но что-то мне подсказывает, что резервное соединение не требуется и где-то у меня косяки.
Буду думать!
Скоро постараюсь выкатить тестовую сборку игры, если вдруг кому-то будет интересно, пишите в ЛС или в комментарии.
Всем большое спасибо за внимание! Удачных вам проектов!