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

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

В закладки

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

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

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

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

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

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

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

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

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

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

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

{ "items": [{"title":"","image":{"type":"image","data":{"uuid":"709762ce-4aac-53c4-6118-e9833f379439","width":1024,"height":576,"size":29695,"type":"jpg","color":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"01b69d8c-fead-443d-ef01-5dd49016edc4","width":1024,"height":576,"size":55785,"type":"jpg","color":"","external_service":[]}}}] }

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

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

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

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

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

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

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

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

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

#опыт #индидев

{ "author_name": "Андрей Верещагин", "author_type": "editor", "tags": ["\u0438\u043d\u0434\u0438\u0434\u0435\u0432","\u043e\u043f\u044b\u0442"], "comments": 9, "likes": 14, "favorites": 1, "is_advertisement": false, "subsite_label": "gamedev", "id": 8930, "is_wide": false }
{ "id": 8930, "author_id": 22254, "diff_limit": 1000, "urls": {"diff":"\/comments\/8930\/get","add":"\/comments\/8930\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/8930"}, "attach_limit": 2, "max_comment_text_length": 5000 }

9 комментариев 9 комм.

Популярные

По порядку

Написать комментарий...

Pavel Shestakov

1

А в чем суть статьи в разделе Про? По мне, вся статья - это два трюка весьма очевидных (про относительные координаты и желтку стрелку). В остальном какое-то описание UNet-а + три названия самописных класса.
Честно, ожидал большего.

Ответить

Андрей Медведев

0

Не дочитал до конца статью, но суть ясна. Подход не новый и называется в народе "Локстеп". По сути статью можно не читать - сразу идите на gafferongames.com. Там инфа по сетевому коду хорошо сложена

Ответить

Старый игродел

0

за последние 20 лет подходы к синхронизации вообще не изменились. Берете код Quake, читаете статью от Valve и смотрите на архитектуру Tribes. И вы все знаете

Ответить

Scapior

Старый
0

Забавно, подходы не изменились, но дайсы в BF4 свой неткод 2 года чинили)

Ответить

Старый игродел

Scapior
0

тот же gaffer у себя в блоге писал, что делая сетку 10+ лет каждый раз в грабли какие-нибудь упрешься.

Ответить

Александр Болуженков

0

"То есть получая снапшоты, клиент проигрывает их не сразу, а, например, через 100 миллисекунд."

Это ведь получается что в сделали пинг минимум 100 ? Плюс задержки потери пакетов, сети и т.п - будет под 200 ?

Ответить

Старый игродел

Александр
0

Это интерполяция, а не задержка инпута. В том же CS интерполяция 100 мм. Индустриальный стандарт так сказать.

Ответить

Николай Костоправ

0

Хочется хоть каких-нибудь чисел - нагрузки на узлы кластера, отдельные машины, в целом кластер. Как тестировали на нагрузку, объем трафика и т.п.

Ответить
0

Прямой эфир

Подписаться на push-уведомления
[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fizc" } } }, { "id": 4, "label": "240х200_mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "flbq" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjog" } } }, { "id": 10, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-250597-0", "render_to": "inpage_VI-250597-0-1134314964", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=clmf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Плашка на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudo", "p2": "ftjf" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvc" } } } ]