- PVSM.RU - https://www.pvsm.ru -
В марте 2017 года мы собрали небольшую команду и взялись за разработку нового перспективного проекта. Без особых деталей могу сказать, что задача стояла интересная и соблазнительная — мобильный, синхронный, командный PvP. Спустя 7 месяцев активной разработки мне захотелось рассказать коллегам из других проектов и отделов Pixonic технические детали и я подготовил для них презентацию, которая в дальнейшем превратилась в эту статью.
Как техлид команды, я расскажу, с какими задачами и проблемами мы успели столкнуться, как их решаем и почему. Мы используем итеративный подход добавления функционала в проект и в данный момент у нас реализованы: PvP на iOS и Android (обе платформы играют на одних серверах); набор персонажей, три десятка игровых механик, боты; матчмейкинг; набор мета-фич (кастомизация персонажей, прокачка и другие); решена задача масштабируемости на весь мир.
Итак, поехали.
Disclaimer
Но должен сразу оговориться, что решения, описанные в статье — это уже исторические явления и факты, сложенные из множества обстоятельств: бизнес- и геймдизайн-требований к продукту, поставленных сроков, потенциала команды и неизвестности некоторых проблем на старте. Это не best practice, но опыт, который не бывает лишним.
Еще до начала разработки мы уже представляли некоторые сложности, с которыми однозначно придется столкнуться. А именно:
Дальше я постарался описать нашу ситуацию в виде «проблема — решение».
Как уже было сказано, проект состоит из трех подпроектов:
Я посчитал, что в хранении их всех в одном репозитории git имеется больше минусов чем плюсов, так как все CI процессы становятся дороже и занимают больше времени. В результате у нас три репозитория.
Мы используем protocol buffers для обмена сообщениями между всеми тремя подпроектами. Из этого следует, что мы должны где-то хранить файлы .proto и сгенерированные файлы кода для этих сообщений (к слову, коммитить сгенерированные файлы — не очень хороший тон, но именно для Unity это уменьшает количество компиляций при открытии, что экономит время). Более того, для разных протоколов они должны быть разными, переиспользовать пакеты нет смысла, так как серверу и клиенту нужны разные аргументы. Возникла задача, как эти файлы получать всем проектам. Решили мы ее с помощью git submodules. Между каждой из 3-х пар основных проектов мы завели по дополнительному репозиторию и добавили их как субмодули в основные проекты. Теперь репозиториев уже шесть.
Для ускорения отладки симуляции и возможности запуска игровой симуляции без привязки к серверу мы отделили код симуляции. Это дало нам массу возможностей — профилирование запуска сотни игр как консольного приложения с ускорением времени, или, например, использование симуляции в Unity-клиенте для локальной работы обучения бою. Для самого производства это тоже очень удобно: программист, создавая новую игровую фичу, может поиграть сразу в Unity, ему даже не нужно разворачивать локальный сервер. Чтобы код симуляции мог находиться и в клиенте и в игровом сервере, его тоже пришлось вынести в отдельный сабмодуль.
Через какое-то время нам понадобилось хранить и отслеживать изменения игровых конфигов, необходимые части которых потом расходились по подпроектам, и мы сделали отдельный субмодуль, содержащий proto-схему его десериализации.
О плюсах я уже рассказал, теперь о минусах:
[submodule "Assets/shared-code"]
path = Assets/shared-code
url = ../shared-code.git
Но не факт, что SourceTree вашей версии сможет такое понять, да и хранить придется все репо на одном хосте гита.
Пример: программист делает фичу в своей ветке основного репо и в ветке фичи субмодуля, а в это время в ветку develop субмодуля заливается новый функционал, который не позволит просто обновиться до последней версии. Необходимо менять код основного модуля для поддержки этих изменений. В итоге программист не сможет влить свою готовую фичу в develop, пока не напишет адаптацию под последнюю версию субмодуля, которая с его фичей зачастую и не связана. Это замедляло интеграцию и лишний раз переключало программистов из контекстов их задач. Как было сказано выше, приходится сначала писать изменения субмодуля в ветке фичи, затем писать адаптацию основного репозитория тоже в ветке этой фичи и только после этого, пройдя ревью и тесты, эти ветки вливаются одновременно в develop своих репозиториев.
Теперь перейдем к проблеме намного теснее связанной непосредственно с продуктом. Напомню, между игровым сервером и мобильным клиентом мы используем non-reliable UDP, что не гарантирует доставки сообщений или правильности их порядка. Это, конечно же, накладывает ряд проблем, критичных для самого игрока. Хороший пример — дорогостоящая, мощная ракета и кнопка для её запуска. Игрок ждет подходящей ситуации для использования этой способности и нажимает на кнопку 1 раз, в самый благоприятный, по его мнению, момент. Мы должны гарантированно и максимально быстро доставить на сервер эту информацию, чтобы у игрока этот момент не успел пройти. Но если этот пакет пропадет или придет через 2 секунды, то наша цель не достигнута.
Сначала мы рассматривали стратегию переотправки данных при отсутствии подтверждения о приеме, но нам хотелось максимально приблизить потерю времени к периоду отправки данных клиента. Дополнительной задачей было сделать так, чтобы нажатая во время оглушения кнопка выстрела срабатывала после выхода персонажа из оглушения в симуляции на сервере.
Решение оказалось недорогим, но действенным:
Используя негарантированные способы доставки, мы так же сталкиваемся с проблемой получения состояния мира обратно от сервера к клиенту. Но об этом чуть позже. Для начала я бы хотел описать обязательную проблему, которую решает команда любого проекта-игры, передающего состояния по сети.
Представим игровой сервер — это приложение, которое определенное количество раз в секунду делает одно и то же: получает ввод по сети, принимает решения, и отправляет состояние по сети назад. А теперь представим клиент — это приложение, которое (помимо всего прочего) отображает игровое состояние за определенное количество кадров в секунду (например, 60). Если просто позволить отображать то, что пришло из сети, то каждые 2-3 кадра клиент будет отображать одно и то же пришедшее состояние, и отображение будет происходить рывками, а в случае с неравномерной доставкой — еще и с ускорением/замедлением времени. Для того чтобы сделать отображение плавным, необходимо использовать интерполяцию между двумя состояниями от сервера и отобразить рассчитанные промежуточные значения за несколько необходимых кадров.
Уходим на клиенте в прошлое...
Но у нас есть только одно состояние для данного момента и нет второго, чтобы нарисовать промежуточные кадры. Что делать? Решение: мы смещаем время событий клиента немного в прошлое так, чтобы на момент отрисовки у нас уже была теоретическая возможность для прихода следующего состояния мира.
На практике получается не так радужно: UDP не гарантирует доставку, и если состояние мира не придет на клиент, то вам на несколько кадров не будет данных для отображения — вы получите так называемый «фриз». Балансируя между лагом ввода и процентом потерь пакетов, мы используем отход в прошлое на 2 периода отправки + половина RTT. Таким образом, даже если один пакет потеряется, у вас будет время для приема следующего. В то же время если прием пакетов прервался на 2 и более периодов, то весьма вероятно, что произойдет дальнейший дисконнект, что для игрока намного понятнее, чем спонтанные лаги. Игрок увидит окно реконнекта и оно не так сильно испортит игровой опыт.
На практике схема с интерполяцией и уходом в прошлое работает не всегда хорошо. Игрок мог начать партию, играя по Wi-Fi у себя дома с пингом 10 ms, а затем выйти на улицу, сесть в такси и кататься по городу со включенным мобильным интернетом с пингом уже 100 ms. В этом случае, запомнив на старте игры RTT, у игрока может постоянно не хватать запаса времени для интерполяции, даже если пакеты будут доставляться идеально, равномерно и без потерь.
В нашем случае эту проблему мы решили следующим образом:
<i>2 * Send Rate + RTT/2</i>
Визуально проблема остается в том, что когда мы это обнаружили, клиент уже начал лагать. Мы сдвигаем его в прошлое не мгновенно, а в течении короткого времени (0,5 сек), в этом случае на 1-2 кадра данных у него всё же не будет. В случае перепадов пинга более чем на 1 Send Rate игрок заметит маленькое (130 секунды) единовременное дерганье.
Точно так же и в обратную сторону, если пинг уменьшается, клиент определяет это и приближает отображение ближе к настоящему, чтобы достичь оптимального баланса между плавной картинкой и наименьшей задержкой ввода.
Хотелось бы в заключении ответить на несколько вопросов, которые могли у вас возникнуть.
Почему вместо борьбы с инпут лагом мы не предсказываем локально поведение клиента в симуляции?
Это решение происходит из жанра и механик игры: если вы делаете шутер от первого лица, с мгновенными явлениями и отсутствием отмены влияний игроков, то вам отлично подойдет local prediction + lag compensation. В случае, если у вас геймплей подразумевает большое количество заморозок, толканий и других механик, воздействующих на игроков и изменение их поведений, то проявление сетевых артефактов будет приближаться к 100%. Самыми отчаянными в этом плане я считаю команду проекта Overwatch от Blizzard, которые нашли оптимальный баланс между минимальными артефактами и необходимостью локального предсказания. Но это на PC, где средний пинг игроков это теоретически позволяет. В нашем случае у локального игрока в 100% случаев рывок вперед заканчивался бы «телепортом» на исходное состояние при любом оглушении.
Как будут играть игроки из разных стран с разным пингом?
У кого пинг лучше, тот, естественно, будет иметь преимущество, так как у него есть больше времени на реакцию. Пример: противник хочет бросить снаряд в игрока. Игрок с меньшим пингом чуть раньше заметит начало анимации противника и у него будет больше шансов произвести защитное действие. Более того, защитное действие быстрее дойдет до сервера и вероятность успеть уклониться повысится.
Кто выстрелит первым, если оба нажали одновременно, а пинг одного игрока больше?
Сервер не учитывает время нажатия, лишь время прихода ввода и его порядок, так что работает принцип из ответа выше.
Надеюсь, что написанный мною материал будет полезным другим разработчикам, встающим на схожий путь. Вообще, проблем и решений за это время работы над проектом накопилось так много, что их хватит еще не на одну статью.
Удачи!
Автор: HexGrimm
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/269804
Ссылки в тексте:
[1] Источник: https://habrahabr.ru/post/343820/
Нажмите здесь для печати.