Трудности синхронизации: создание многопользовательского мобильного шутера

Непрямое управление как способ борьбы с задержкой.

Технический директор Pnzerdog Дмитрий Коблык выступил на конференции DevGAMM 2017 с рассказом о реализации мультиплеера в реальном времени на мобильной платформе на примере игры Tacticool. Редакция DTF расшифровала доклад.

Tacticool — это мобильный мультиплеерный шутер с видом сверху и очень короткими матчами. В игре персонаж всё время бежит вперёд. Герой поворачивает влево или вправо в зависимости от того, на какую часть экрана нажимает игрок.

Изначально мы хотели сделать игру об автомобилях. Позднее появились новые идеи. Мы стали делать прототипы. В итоге мы пришли к тому, что игрок управляет напрямую персонажем и может садиться в машины на манер GTA.

Мы не знали, с чего начать писать сетевую часть кода. Решить проблему помог сайт gafferongames.com, на котором есть куча статей по этой теме. Как мы синхронизируем данные между клиентом и сервером? Последний шлёт клиенту состояние игрового мира фиксирование количество раз в секунду, а клиент проигрывает эти снапшоты и выступает своего рода видеоплеером. Кроме того клиент отправляет на сервер пользовательский ввод.

В Tacticool данные отправляются 30 раз в секунду. Этот показатель влияет на объем трафика, который генерирует игра, и на задержу пользовательского отклика.

Клиент может сразу воспроизводить снапшоты, полученные от сервера, но в таком случае картинка получается дёрганной. Чтобы исправить мы отправили клиент «в прошлое», относительно сервера. То есть получая снапшоты, клиент проигрывает их не сразу, а, например, через 100 миллисекунд. Изображение становится плавным.

Для игр в реальном времени не подходит протокол TCP. Он гарантирует, что данные будут доставлены адресату в порядке их отправления. По сути — это поток, в который мы можем записывать информацию и считывать её. Больше возможностей он не предоставляет.

На более низком уровне поток разбивается на пакеты для отправления, и когда они теряются появляются задержки. То есть пока TCP не подтвердит потерю одного пакета, мы не сможем получить доступ к следующему. Поэтому мы выбрали UDP, который позволяет нам просто слать пакеты. Если что-то потерялось или пришло не в том порядке — не страшно.

Приблизительно в тот момент, когда мы приступили к разработке игры, Unity выпустила сетевую библиотеку uNet. Её делят на две части: API низкого и высокого уровней. Первый предоставляет функции поддержки соединения между клиентом и сервером, отправки и приёма данных. Так как нам нужем максимальный контроль, только этот API мы и используем в игре.

Есть множество стандартных форматов для сериализации данных, но нам они не подходят. Например, мы хотим отправить на сервер позицию. С помощью BSON эти данные «весили» бы около 96 бит.

Но в нашей игре точность не так важна — достаточно шести бит после запятой. Кроме того, мы знаем, что объект никогда не покинет границы мира и будет находится в пределах от -100 до 100 по координатным осям. Используя эти знания мы уменьшаем объём данных практически в два раза.

Как бороться с задержками. Во-первых, можно использовать «предсказание». То есть клиент обрабатывает пользовательский ввод и проигрывает его, а сервер просто подправляет это «предсказание». Проблема в том, что такой метод не работает с другими игроками — клиент не может обработать их ввод. Мы выбрали иной путь.

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

Во втором случае нам на помощь пришла «жёлтая стрелка», которая показывает направление движения героя. Когда игрок нажимает влево или вправо, стрелка мгновенно реагирует на эти действия, а персонаж начинает поворачивать только после того, как будет получен ответ от сервера.

Непрямое управление хорошо работает в сетевых играх. Такой метод использован и в Dota 2. То есть игрок кликает по карте, а его персонаж начинает бежать с задержкой. При этом пользователь и не ожидает мгновенного отклика. Однако мгновенный отклик необходим в играх с прямым управлением.

Мы используем Unity и на сервере. Это ускоряет разработку. В основе всего в Tacticool лежит Network Identity, который «привязан» к каждому объекту, имеющему отношение к сетевому коду.

Он используется, например, в компоненте AbstractSyncComponent, который синхронизирует абстратные данные между клиентом и сервером. Ещё есть SyncTransformComponent, отвечающий за позицию и вращение объектов. Этот компонент использует практически каждый предмет в нашей игре, поэтому он расходует 80% трафика.

В Tacticool у персонажей очень много анимаций, которые нужно синхронизировать. Для этого мы используем SyncAnimatorComponent. Однако мы синхронизируем не текущее состояние, а набор параметров: какое у персонажа оружие, перекатывается ли он и так далее.

Мы стараемся как можно меньше использовать триггеры, потому что они могут потеряться или дойти до адресата позже, чем мы хотим. Кроме того, мы отказались от появления новых объектов во время игры — «спавн» происходит только при старте. Это сильно упрощает сетевой код. Например, если игрок кидает гранату, а мы «спавним» её в этот момент, то серверу нужно будет отправить клиенту данные об этом.

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

 
Источник: DTF

Читайте также