- PVSM.RU - https://www.pvsm.ru -
В мае этого года я участвовал в качестве игрока в MMO-мероприятии KatherineOfSky [1]. Я заметил, что когда количество игроков достигает определённого числа, через каждые несколько минут часть из них «отваливается». К счастью для вас (но не для меня), я был одним из тех игроков, которые отключались каждый раз, даже при наличии хорошего подключения. Я воспринял это как личный вызов и начал искать причины проблемы. Спустя три недели отладки, тестирования и исправлений ошибка наконец устранена, но это путешествие было не таким уж простым.
Проблемы многопользовательских игр очень трудно отследить. Обычно они возникают в очень конкретных условиях параметров сетей и при очень специфичных состояниях игры (в данном случае — наличие более 200 игроков). И даже когда удаётся воспроизвести проблему, её невозможно должным образом отлаживать, потому что вставка контрольных точек останавливает игру, путает таймеры и обычно приводит к завершению соединения из-за превышения срока ожидания. Но благодаря упорности и замечательному инструменту под названием clumsy [2] мне удалось выяснить, что же происходит.
Если вкратце: из-за ошибки и неполной реализации симуляции состояния задержки клиент иногда оказывался в ситуации, когда ему приходится за один такт отправлять сетевой пакет, состоящий из вводимых игроком действий выбора примерно 400 игровых сущностей (мы называем его «мегапакетом»). После этого сервер не только должен правильно получить все эти действия ввода, но и отправить их всем остальным клиентам. Если у тебя 200 клиентов, это быстро становится проблемой. Канал к серверу быстро забивается, что приводит к утере пакетов и каскаду повторно запрошенных пакетов. Откладывание действий ввода затем приводит к тому, что ещё больше клиентов начинает отправлять мегапакеты, и их лавина становится ещё сильнее. Удачливым клиентам удаётся восстановиться, все остальные «отваливаются».
Проблема была достаточно фундаментальной, и у меня ушло 2 недели на её устранение. Она довольно техническая, поэтому ниже я объясню сочные технические подробности. Но для начала вам нужно знать, что с версии 0.17.54, выпущенной 4 июня, в условиях временных проблем с подключением мультиплеер стал более стабильным, а сокрытие задержек — гораздо менее глючным (меньше торможений и телепортирования). Кроме того, я изменил способ сокрытия задержек в бою и надеюсь, что благодаря этому они будут немного более плавными.
Если объяснять упрощённо, то мультиплеер в игре работает следующим образом: все клиенты симулируют состояние игры, получая и отправляя только ввод игрока (называемый «действиями ввода», Input Actions). Основная задача сервера — передача Input Actions и контроль того, что все клиенты выполняют одинаковые действия в одном такте. Подробнее об этом можно прочитать в посте FFF-149 [3].
Так как сервер должен принимать решения о том, какие действия нужно выполнять, действия игрока движутся примерно по такому пути: действие игрока -> клиент игры -> сеть -> сервер -> сеть -> клиент игры. Это значит, что каждое действие игрока выполняется только после того, как совершит путь туда-обратно по сети. Из-за этого игра бы казалась ужасно тормозной, поэтому почти сразу же после появления в игре мультиплеера был введён механизм сокрытия задержек. Сокрытие задержке имитирует ввод игрока без учёта действий других игроков и принятия решений сервером.
В Factorio есть игровое состояние Game State — это полное состояние карты, игрока, сущностей и всего остального. Оно детерминированно симулируется во всех клиентах на основании действий, полученных от сервера. Игровое состояние священно, и если оно когда-нибудь начинает отличаться от сервера или любого другого клиента, то возникает рассинхронизация.
Кроме Game State у нас есть состояние задержек Latency State. Оно содержит небольшое подмножество основного состояния. Latency State не священно и просто представляет картину того, как будет выглядеть состояние игры в будущем на основании введённых игроком Input Actions.
Для этого мы храним копию создаваемых Input Actions в очереди задержек.
То есть в конце процесса на стороне клиента картина выглядит примерно так:
Всё это повторяется в каждом такте.
Слишком сложно? Не расслабляйтесь, это ещё не всё. Чтобы компенсировать ненадёжность Интернет-соединений, мы создали два механизма:
Сами по себе эти механизмы довольно просты, но когда они используются совместно (что часто случается при проблемах с соединением), логика кода становится трудноуправляемой и с кучей пограничных случаев. Кроме того, когда в дело вступают эти механизмы, сервер и очередь задержек должны правильно внедрять особое Input Action под названием StopMovementInTheNextTick. Благодаря этому при проблемах с соединением персонаж не будет бежать сам по себе (например, под поезд).
Теперь нужно объяснить вам, как работает выбор сущностей. Один из передаваемых типов Input Action — это изменение состояния выбора сущности. Оно сообщает всем, на какую сущность игрок навёл курсор мыши. Как можно понять, это одно из самых частых действий ввода, отправляемых клиентами, поэтому для экономии пропускной способности канала мы оптимизировали его так, чтобы оно занимало как можно меньше места. Это реализовано так: при выборе каждой сущности вместо сохранения абсолютных, высокоточных координат карты игра сохраняет низкоточное относительное смещение от предыдущего выбора. Это хорошо работает, потому что выделение мышью обычно происходит очень близко к предыдущему выделению. Из-за этого возникают два важных требования: Input Actions никогда нельзя пропускать и необходимо выполнять их в верном порядке. Эти требования удовлетворяются для Game State. Но поскольку задача Latency state в том, чтобы «выглядеть достаточно хорошо» для игрока, в состоянии задержек они не удовлетворяются. Latency State не учитывает многие пограничные случаи [4], связанные с пропуском тактов и изменением задержек передачи туда-обратно.
Вы уже можете догадаться, к чему всё идёт. Наконец мы начинаем видеть причины проблемы мегапакета. Корень проблемы заключается в том, что в принятии решения о том, нужно ли передавать действие изменения выбора, логика выбора сущностей полагается на Latency State, а это состояние не всегда содержит верную информацию. Поэтому мегапакет генерируется примерно так:
Ирония заключается в том, что механизм, предназначенный для экономии пропускной способности канала, в результате создавал огромные сетевые пакеты.
Мы решили эту проблему, исправив все пограничные случаи обновления и поддержки очереди задержек. Хоть это и заняло довольно много времени, в конечном итоге стоило реализовать всё правильно, а не полагаться на быстрые хаки.
Автор: PatientZero
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/setevy-e-tehnologii/329071
Ссылки в тексте:
[1] MMO-мероприятии KatherineOfSky: https://youtu.be/d_DlvkTK9hU
[2] clumsy: https://github.com/jagt/clumsy
[3] FFF-149: https://www.factorio.com/blog/post/fff-149
[4] многие пограничные случаи: https://cdn.factorio.com/assets/img/blog/fff-302-megapacket-commit.jpg
[5] Источник: https://habr.com/ru/post/466135/?utm_source=habrahabr&utm_medium=rss&utm_campaign=466135
Нажмите здесь для печати.