Как стать автором
Обновить

Как мы писали сетевой код мобильного PvP шутера: синхронизация игрока на клиенте

Время на прочтение13 мин
Количество просмотров33K
Всего голосов 50: ↑49 и ↓1+48
Комментарии31

Комментарии 31

Если время валидно, сервер оставляет игрока в текущем времени, а весь остальной мир откатывает в прошлое к тому состоянию, которое видел игрок, и просчитывает результат выстрела.

Текущее время это время когда был сделан выстрел? Или это время на сервере? Просто создается впечатление что берется угол поворота который «сейчас», а расположение остальных в момент выстрела). А вроде как состояние игрока (патроны например) надо брать которое сейчас, угол поворота в момент выстрела, а расстановку ту которую видит игрок в момент выстрела(вроде бы у вас 100 мс).
Перемотка времени на сервере работает следующим образом: на севере хранится история мира (в ECS) и история физики (поддерживается движком Volatile Physics).

И еще вопрос, у вас 2d мобильный шутер?
Текущее время это время когда был сделан выстрел?
Да, все верно:
текущее время — это время игрока сделавшего выстрел. Состояние игрока берется на момент выстрела. А весь остальной мир (противники, окружение) берутся из истории, так как их видел игрок. Клиент отсылает на сервер номер тика, в котором находилось все окружение, когда был произведен выстрел.

И еще вопрос, у вас 2d мобильный шутер?

Покажем после релиза :)
Клиент отсылает на сервер номер тика, в котором находилось все окружение, когда был произведен выстрел.

Я так понимаю клиент отсылает номер тика с учетом того на сколько он впереди сервера?
Клиент в данных инпута отсылает два времени:
1) Время тика в котором находится он сам (с учетом на сколько он впереди сервера) На диаграмме в статье это тик №20.
2) Время тика в котором клиент видит остальной мир, на диаграмме это тик №10.

На диаграмме, сервер в момент отсылки находится в тике 15.
Когда пакет с данными дойдет до сервера, сервер будет находится в тике №20. Для сервера это станет «настоящим» временем. И сервер применит инпут от клиента подписанный 20м тиком. Т.е. клиент будет совершать выстрел в настоящем времени сервера.
Но для того что бы правильно смоделировать ситуацию того что видел клиент, будет использоваться второе время, пришедшее с клиента. (тик №10)

Резюмируя:
1) Время клиента в инпуте необходимо для определения в какой тик нужно применить инпут.
2) Время остального мира которое видит клиент, необходимо для того что бы правильно рассчитать результаты выстрела.
Вы используете udp протокол как я понимаю. Скажите как вы добились «reliable UDP». У вас какая то своя реализация? Или вы пользовались готовыми решениями?
Да мы используем UPD. В данный момент мы используем Photon SDK (не PUN) для транспортного слоя. Но хотим уйти от этого решения. Оно нас не устраивает по ряду причин.
По-поводу reliable — в Photon SDK поддерживает reliable из коробки. Но по-факту reliable у нас используется только в момент авторизации на гейм. сервере, когда передается большой объем статических данных (конфигурация матча).

Основной гейм.плей использует unreliable unordered udp. Причина в том, что нам просто не нужно гарантировать доставку данных. Т.к. сервер каждый тик шлет обновленное состояние мира. И если если какой-либо пакет потеряется, мы просто пропустим один кадр.
Так же стоит учитывать что на клиенте реализован механизм интерполяции, на случай потерь единичных пакетов.
В быстрых мультиплеерных играх более важна скорость доставки пакетов, чем потери.
В быстрых мультиплеерных играх более важна скорость доставки пакетов, чем потери.

А во время боя у вас разве нету каких нибудь событий которые гарантированно пусть и с задержкой должны доставиться серверу от клиента или доставиться от сервера игрокам? Допустим что будет если пакет с выстрелом не дошел до сервера?
Тут дело в том что, если пакет с выстрелом не дошел до сервера вовремя — выстрел на сервере уже не применится. Сервер обрабатывает инпут только для текущего тика. Если на сервере уже наступил тик N, и в этот момент с клиента дошел инпут для тика N-1, сервер просто проигнорирует эти данные.
Для того что бы уменьшить количество потерь при отправке инпутов игрока мы используем механизм скользящего окна, и буфер инпутов на сервере.

Если же потеря пакета все же произошла, произойдет расхождение состояний мира клиента и сервера, и на клиенте отработает механизм согласования (reconciliation).
В быстрых мультиплеерных играх более важна скорость доставки пакетов, чем потери.

Главное не увлекаться этим постулатом. Яркий пример современности Quake Champions, где огромные проблемы с регистрацией попаданий, которые по-ощущением чинят, но не столь успешно.


Из менее быстрого PUBG, который тоже грешит кривой регистрацией.


Впечатление может испортить сильнее, чем задержки. Хоть и это стороны одной медали по-сути.

habr.com/company/pixonic/blog/415959/#comment_18840585 вот этот пост очень грамотно описывает, почему такой проблемы, скорее всего, не будет.
Соглашусь про регистрацию, у «новых» шутеров с этим невероятные проблемы. Что у Quake Champions, что у Overwatch. Разработчики, видимо, считают, что пинга больше 20 не бывает.
У Overwatch до смешного доходит, можно наблюдать, как projectile атаки проходят сквозь модель/голову игрока, но не регистрируются, т.к. на сервере игрок уже имеет другие координаты. :)

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

Раз мир синхронизируется целиком, то невозможно фильтровать данные, например как это сделали в Battlefield 4 или 1? Или есть еще куда уменьшать трафик?
Потенциально, игрок может получить преимущество за счет искусственного повышения пинга. Т.к. чем больше у игрока пинг, тем дальше в прошлом он производит выстрел.

Эту проблему долго решали в Rainbow6 Siege. Игроки специально увеличивали свой пинг до 150-200 мс и получали игровое преимущество. Ubisoft тогда разделил обработку попаданий по задержкам пользователей — пинг <100 обрабатываем просто, пинг > 100 и < 500 по более сложному алгоритму, пинг >500 отключаем игрока.
Ubisoft тогда разделил обработку попаданий по задержкам пользователей — пинг <100 обрабатываем просто, пинг > 100 и < 500 по более сложному алгоритму, пинг >500 отключаем игрока.

Спасибо, поресерчим этот вопрос. У нас грубо говоря сейчас только два кейса пинг <= 500, и пинг >500. Если пинг выше, отключаем игрока, если ниже считаем по алгоритму из статьи

Раз мир синхронизируется целиком, то невозможно фильтровать данные, например как это сделали в Battlefield 4 или 1? Или есть еще куда уменьшать трафик?

Да у нас не используется конкретно этот метод, однако каждому игроку отправляются только те данные, которые необходимы ему для визуализации картинки, и расчета предикшена. Так же у нас применяется дельта-компрессия, если какая-то сущность не изменилась между тиками, данные о ней не отправляются на клиент.
Мы выпустим отдельную статью о всех оптимизациях трафика которые использовали на этом проекте.
А вы замеряли какой пинг на каком интернете? Типа 4G, WiFi, 3G. Я к тому что пинг меньше 500 это реальная цифра для мобильного сетевого шутера?
500 — это абстрактная цифра с вашего примера. Но да, мы замеряли пинг на различных сетях: в целом ситуация по пингу зависит от того как далеко находятся пользователи от гейм. серверов. Тут нужно найти золотую середину между количеством серверов в регионах и качеством геймплея у пользователей
в целом ситуация по пингу зависит от того как далеко находятся пользователи от гейм. серверов.


А влияние загрузки сети? Случай из личной практики — стабильный пинг до 8.8.8.8 10000-50000мс (нет, я не ошибся с кол-вом нолей). Это в дневное время через дорогу от второго по размеру вуза в регионе (в вечернее такого треша небыло). Правда было это в 2014-2015 году.

К стати, если не секрет, поделитесь в каких городах (и их районах) и на каких операторах вы проводили замеры и какие (хотя бы примерно) результаты.
Проводили замеры из Москвы до серверов в Амстердаме, на офисной сети WiFi, 4G и 3G.
Из операторов MTC и Билайн.
На 4g в среднем пинг 50 — 60 ms.
3g — ближе к 60-65 ms.

Переводили промтом?

Очень интересен ваш подход. А разумно ли сделать так, на ваш взгляд?
Игрок отправляет текущее состояние и управление своего персонажа на сервер. Сервер верит ему и тут же (или с минимальными проверками на достоверность) пересылает всем остальным. Но сервер записывает историю сообщений от каждого клиента.
Когда пришли сообщения от всех клиентов, или вышло предельное время ожидания, происходит симуляция с последнего достоверного состояния мира до момента, на который нам известны управления от всех клиентов, и создается истинная картина мира на новый момент времени.
Новая картина мира сверяется с сообщениями клиентов на тот момент времени и отправляется им в случае сильного расхождения.
Когда клиенты получают истинную историю, они производят ресимуляцию от нее до настоящего времени на основании истории своего управления.
В таком случае, как мне кажется, будет достигнута максимальная отзывчивость за счет доверия клиентам, и надежность за счет перепроверки их состояний.
Доверять клиенту можно в кооперативных играх, где нет соперника-игрока (и то не всегда), а в играх где игроки играют против друг-друга доверять можно только если вы (или другие играющие с вами) можете сами повлиять как-то на подбор игроков (чтобы не допускать уличеных в читерстве), например играя с друзьями и знакомыми. А в случае игры со случайным подбором соперников доверять игроку неприемлимо. Есть доверие — есть возможность читерить. А если такая возможность есть, то обязательно кто-нибудь ее будет использовать.
К стати, на счет доверия клиенту вспомнился забавный случай из коопа, когда легкий рассинхрон в купе с доверием к клиенту приводил к непреднамеренному читерству с моей стороны. Играли мы как-то с друзьями в Dungeon of Endless. У разрабов есть какие-то косяки с периодическими рассинхронами во всех «Endless» играх, и тут в частности. Причем игра этого не обнаруживала. По механике там персонаж мог стрелять только во врагов в своей комнате. Из-за рассинхрона у меня враги шли сквозь стены кратчайшим путем до цели. и проходили мимо меня, где я спокойно в них стрелял. Другие же игроки видели как я, стоя в пустой комнате, убиваю врагов на другом конце карты (чего механика игры вроде как не допускает)
p.s.
После выхода игры такое веселье у меня было каждую вторую партию, но сейчас вроде поправили.
Я же предлагаю перепроверять позже. Доверять только на то время пока управление от всех игроков не пришло и сервер не имеет возможности произвести достоверную симуляцию с учетом управления от всех клиентов.
По-мимо того что отметили в комментарии habr.com/company/pixonic/blog/415959/#comment_18845211
Хочу добавить что:
1) Пересылка помимо инпута еще и состояния мира с клиента на сервер влечет за собой значительное увеличение трафика. На мобильных сетях это критично.
2) Что делать когда два клиента шлют противоречивую информацию о мире (например оба стоят в одной позиции)
Основные проблемы с реализацией сетевого кода с которыми мы столкнулись это был объем трафика, потери в сети и производительность симуляции мира на клиенте.
Ваше решение снижает нагрузку на сервер (не нужно симулировать мир с большой частотой), но практика реализации нашего проекта показывает что это не самое узкое место в мультиплеерных играх
Я предлагаю же отсылать не состояние мира, а состояние персонажа которым управляет клиент, это экономно. И его управление. Пока управление от всех игроков еще не пришло, сервер не может проверить правдивость состояния от клиента, но может ему довериться до тех пор, пока не произведет симуляцию.
Чаще всего перепроверка не будет приводить к поправкам. Но если придет противоречивая информация (а такое случится когда игроки например сталкиваются друг с другом — каждый будет видеть себя немного в будущем по сравнению с соперником, поэтому место столкновения будет отличаться), и тогда поправка после прихода истинной истории и ресимуляция откинет игроков на корректные позиции. Выглядеть у клиентов это будет так, будто игроки столкнулись в одном месте, а потом отъехали на другое место. Так сейчас и происходит при столкновении техники в батлфилде, как мне кажется.
Какой-то оверинжиниринг для такой просто в общем-то концепции.
Вы считаете простой задачу синхронного пвп по сети в игре с быстрым геймплеем с компенсацией задержки? Обоснуйте, пожалуйста.
Мой комментарий в основном относится к ECS
Спасибо за статью.
Если среднее заполнение буфера было больше верхнего граничного условия (т.е. буфер бы заполнен больше, чем требуется) — клиент «уменьшает» размер буфера путем совершения дополнительного тика симуляции.
Если же среднее заполнение буфера было меньше нижнего граничного условия (т.е. буфер не успевал заполнятся, прежде чем клиент начинал чтение из него) — в этом случае клиент «увеличивает» размер буфера путем пропуска одного тика симуляции.

Как это (пропуск или добавление тика) отражается на поведении объектов, видны ли какие-то артефакты?

Еще один вопрос. Решали ли вы вопрос с шарингом общего сетевого кода? Насколько я понимаю, сервер — это standalone решение, тогда может возникнуть желание работать с common частью в и вне юнити. Например, видел такое решение в целом для shared кода.
Как это (пропуск или добавление тика) отражается на поведении объектов, видны ли какие-то артефакты?


Артефакты игроку не видны, если дополнительные тики происходят редко (один дополнительный тик раз в несколько секунд).
Если выполнять ускорение/замедление симуляции чаще, то это проявляется для игрока как ускорение/замедление объектов в игре. Например персонаж начинает двигаться не с постоянной скоростью, а то ускоряясь, то замедляясь.

Еще один вопрос. Решали ли вы вопрос с шарингом общего сетевого кода? Насколько я понимаю, сервер — это standalone решение, тогда может возникнуть желание работать с common частью в и вне юнити.

У нас шарится весь код симуляции (ECS). Изначально у нас было три репозитория, клиент, сервер и сабмодуль шареного кода. Но позже пришли к выводу что команде удобнее работать в одном репозитории, где просто существует папка с шаренным кодом.
Я правильно понимаю, что сервер и клиент были отдельные солюшны, а теперь это один репозиторий и один солюшн с клиентскими, серверными и коммон проектами? (Иначе нужно как-то референсить в сервер солюшн общую часть и обновлять файл проекта). Тогда, на первый взгляд, видится менее удобной работа с сервером — дебаг, билд/CI и т.д.
Хотелось бы прояснить этот момент, и узнать какие в итоге плюсы и минусы перехода на такую модель работы.
Репозиторий один, солюшина два. Первый — тот, который генерирует Unity, второй — с серверным и общим кодом. Общий код лежит внутри проекта юнити, в папке Assets.
Добавление общего кода происходит через серверный солюшн, а Unity из коробки автоматически подхватывает все новые файлы в свой солюшн.
Для нас основным плюсом в переходе на такую систему было упрощение вливание фичей в develop. (Мы работаем по стандартному gitflow) В системе с 3 репозиториями, когда делается фича нужно было синхронизировать время вливание фичи во всех репозиториях. А так же создавать отдельные pull-requests в каждом репозитории. Для нашей команды это было неудобно.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий