Истребители багов: безумие частоты опроса

Выпуск от 28 марта 2018г.

Марк Эйбент не ожидает, что эти баги заговорят. Он ожидает, что они умрут. Так и будет, ведь на этой неделе наш местный мастер-истребитель берется устранить клиент-серверную ошибку, которая вызывает проблемы при взлете.

Всем привет. Мы находимся на карте Постоянной Вселенной. Знаю, вам нравится мой забавный тестовый уровень, но, к сожалению, для воспроизведения этого бага он не подойдет. Я нахожусь в своем Gladius, и у нас тут такой веселый баг: когда показатели FPS на клиенте и на сервере начинают слишком сильно различаться (на сервере слишком низкий, тогда как на клиенте слишком высокий – из-за большого количества вносимых нами оптимизаций), существует шанс, что при попытке взлететь вы сначала немного подергаетесь на месте, а затем уже оторветесь от земли. Посмотрим, удастся ли нам повторить эту ошибку, и для этого прикажем серверу принудительно работать с частотой 5 FPS. Да, вот так, потому что я монстр. Итак, серверы работают при 5 FPS, тогда как у меня на клиенте порядка 30 FPS.  Теперь я попытаюсь воспроизвести баг и для этого подготовлю корабль к полету. Теперь я получил контроль над кораблем и пробую взлететь. Как видите, я действительно не могу оторваться от земли – корабль дергает и трясет.

Поставим в коде точку остановки. В общем, это код полетного контроллера IFCS или сетевой системы корабля. Класс ServerPlayer здесь – это, по сути, серверное представление корабля. У меня также есть клиентская версия этого класса, и они делятся друг с другом информацией – поэтому сервер знает, что пытается сделать клиент, и наоборот. В данной ситуации у нас получается, что сервер пытается обработать какие-то пакеты, поступающие от клиента, но не может этого сделать, потому что частота опроса у клиента составляет 4000 тактов, а последнее полученное им от сервера значение частоты опроса превышает 5000 тактов. Из-за этого сервер никогда не обрабатывает состояние клиента. Довольно необычно.

Вот что происходит в действительности. Когда я впервые сажусь в корабль, сервер контролирует практически всю систему. А когда я провожу предполетную подготовку, я говорю серверу, что готов взять управление этим кораблем на себя. Теперь я локальный клиент, и я должен указывать серверу, что я собираюсь делать с кораблем: двигаться влево, вправо, вперед, назад и т.д. В этот момент клиент как бы обладает полномочиями для управления кораблем, но сервер может вмешаться и сказать: «мне не нравится то, что ты делаешь», и скорректировать текущее состояние корабля по своему усмотрению. В этот момент сервер пытается перехватить контроль у клиента, однако из-за низкого показателя FPS он выполняет обновления медленнее клиента. Получается, что когда мы передаем полномочия серверу, клиент корректирует свое положение согласно данным с сервера, и это задает ему частоту опроса в 4000 тактов. Однако же время, когда сервер последний раз получал обновления от клиента, составляет 5000 тактов. В общем, мы попадаем в такую странную ситуацию, что при передаче полномочий и захвате частоты обновлений сервер… портится. Причина кроется в том, что мы привязались к состоянию клиента, но последний раз сервер слышал о клиенте еще когда тот обладал возможностью управлять кораблем. В конечном счете частота опроса клиента должна подняться до 5000 перед тем, как я смогу взлететь. Это большая проблема, потому что я не могу ждать целый день. На самом деле, если вы сядете в кабину, просидите там 10 минут, а затем решите взлететь, вам придется прождать еще 10 минут прежде чем вы действительно сможете оторваться от земли. Не очень весело.

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

Нам нужно убедиться, что клиент реагирует на обрыв управления и передачу полномочий серверу для совершения коррекции. Мы спросим клиента, какой по его мнению должна быть частота опроса. Итак, когда сервер захочет совершить коррекцию, он изменит значение булевой переменной на «true», и это будет означать, что мы ждем, пока клиент повторно обработает полученные данные. Вдобавок мы избавимся от странного решения, когда мы отправляем -1 в качестве текущей частоты опроса.  -1 тут значит, что мы не хотим совершать коррекцию. Любое другое значение, напротив, будет означать, что мы хотим ее совершить. Перепишем эту часть. Вместо -1 нам нужно отправлять клиенту время совершения сервером последней коррекции. И если текущая частота опроса не согласуется с полученным значением, значит мы привязываемся к той частоте, которую выдал нам сервер. Так клиент будет знать, что необходимо передать полномочия серверу для коррекции положения корабля.

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

Мы определили процесс совершения коррекции и теперь будем постоянно получать от сервера актуальные значения частоты опроса. Сама коррекция, в общем случае, телепортирует нас туда, куда укажет сервер: клиент получает от сервера данные о новом положении и перескакивает туда. Теперь черед за сервером.  Клиент ответил: «я совершил коррекцию в этот момент времени». Если проведенная им коррекция согласуется с ожиданиями сервера, и сервер в данный момент ждет ответа о проведении коррекции, мы обработаем результат и продолжим дальше.

Получается, что сервер отправляет частоту опроса и указание на совершение коррекции. Клиент их получает и обнаруживает, что они отличаются от его текущих значений. Это значит, клиенту нужно скорректировать свое состояние в соответствии с параметрами, полученными от сервера, а затем сообщить серверу об этом. Сервер посмотрит и подтвердит: «да, клиент действительно совершил коррекцию», после чего начнет обрабатывать новые состояния как и положено. Перекомпилируем код и посмотрим, что получится.

Мы вернулись в корабль. Давайте попробуем взлететь. Успех, мы можем взлететь! Интерфейс немного дрожит, но я тем не менее могу оторваться от земли и полететь. Похоже, мы устранили проблему. Когда мы заставляем игру работать быстрее, мы сталкиваемся вот с такими вот забавными проблемами. Надеюсь, вам понравились эти сетевые уловки, и увидимся в следующий раз.

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