Сетевое программирование в Source
Contents
Обзор
Многопользовательские игры на движке Source используют архитектуру Клиент/Сервер. Обычно Сервер - это выделенная машина, на которой запущена игра и которая диктует симуляцию игрового мира, правила игры и результаты обработки действий игрока. Клиент - это компьютер игрока, подключенный к игровому серверу. Клиент и сервер общаются между собой путем частой посылки небольших пакетов с данными (обычно 20-30 пакетов в секунду). Клиент получает текущее состояние игрового мира от сервера и на основе этих данных генерирует картинку и звук. Клиент также получает данные с устройств ввода (клавиатура, мышь, микрофон и т.д.) и отправляет эти данные на сервер для последующей обработки. Клиенты общаются только с сервером, но не между собой (в отличие от приложений с архитектурой peer-to-peer). В отличие от однопользовательских игр, многопользовательским требуется решать широкий спектр проблем, связанных с общением на базе передачи пакетов данных.
В силу того, что пропускная способность сети ограничена, сервер не может посылать пакет с обновлением всем клиентам каждый раз, когда в игровом мире происходит изменение. Вместо этого, сервер делает моментальные снимки состояния игрового мира через равные промежутки времени и передает эти снимки клиентам. На доставку пакета с данными от сервера к клиенту и обратно требуется определенное время (ping). Это означает, что время на клиенте всегда немного отстает от времени сервера. Более того, команды ввода с клиента тоже должны дойти до сервера, так что сервер тоже обрабатывает пользовательские действия с задержкой. В добавок, время прохождения пакета у каждого клиента отличается в зависимости от типа соединения, фонового трафика и частоты обновлений. Эти разницы во времени между сервером и клиентом порождают различные логические проблемы, которые становятся еще серьезнее при возрастании пинга. В боевиках с быстрым геймплеем даже миллисекундные задержки могут вызвать ощущение лага и значительно затруднить взаимодействие и попадание по движущимся объектам. Помимо ограничений, накладываемых пропускной способностью и пингом, проблемы может вызывать еще и потеря пакетов.
![Source Networking Highres.png](/w/images/thumb/6/68/Source_Networking_Highres.png/570px-Source_Networking_Highres.png)
Движок Source использует несколько различных техник для того, чтобы справиться с этими проблемами или по крайней мере сделать их менее видимыми игроку. Эти техники включают сжатие данных, интерполяцию, предсказание и лаго-компенсацию. Все эти техники тесно взаимосвязаны и изменения в одной системе могут серьезно повлиять на остальные. Этот документ описывает общею функциональность этих систем и механизмы их взаимодействия.
Основы сетевого кода
Сервер симулирует игровой мир в дискретные промежутки времени, названные "тик" (tick). По умолчанию используется 66 тиков в секунду, однако моды могут использовать свою собственную частоту тиков. Например в Counter-Strike: Source используется частота 66 тиков в секунду. Во время каждого тика сервер обрабатывает пользовательские команды, симулирует физику, проверяет правила игры и обновляет состояние объектов игрового мира. После завершения симуляции тика, сервер определяет каким клиентам требуется обновление и делает снимок состояния мира, если это необходимо. Более высокая частота тиков увеличивает точность симуляции, но требует больших ресурсов процессора и пропускной способности как на сервере, так и на клиенте. Администратор сервера может установить значение частоты тиков с помощью параметра -tickrate
в строке запуска сервера, однако подобное изменение не рекомендуется, так как работа мода может быть нарушена при установке нестандартного значения частоты тиков.
Пропускная способность клиента обычно ограничена. В худших случаях, игрок с модемным соединением может принимать не более 5-7 Кб/сек. Если бы сервер попытался отсылать ему обновления с большей частотой, то потеря пакетов стала бы неизбежной. Поэтому, клиент должен сообщить серверу о доступной входящей пропускной способности с помощью консольной переменной rate
(в байтах в секунду). Это наиболее важная сетевая переменная для клиента и она должна быть выставлена правильна для достижения оптимальной производительности. Клиент может также требовать определенную частоту обновлений изменяя значение переменной cl_updaterate
(значение по умолчанию: 20), но сервер никогда не пошлет обновлений больше, чем способен производить тиков или же чем установленное клиентом значение rate
. К тому же, администратор сервера может ограничивать количество посылаемой информации переменными sv_minrate
и sv_maxrate
, а частоту обновлений с помощью серверных переменных sv_minupdaterate
и sv_maxupdaterate
.
Клиент создает пользовательские команды опрашивая устройства ввода с той же частотой, с которой работает сервер. Пользовательская команда - это попросту говоря, снимок текущего состояния клавиатуры и мыши. Но вместо отправки на сервер нового пакета на каждую пользовательскую команду, клиент шлет обновления с определенной частотой в секунду (обычно 30). Это значит, что две или более команд передаются в каждом пакете. Игрок может увеличить частоту отправляемых пакетов с помощью команды cl_cmdrate
. Это улучшит то, как реагирует клиент, но в то же время потребует большей пропускной способности.
Для уменьшения нагрузки на сеть используется так называемая дельта-компрессия. Это значит, что сервер посылает не полный снимок, а разницу между текущим и последним подтвержденным клиентом снимками (дельта-снимок). С каждым пакетом, передаваемым между сервером и клиентом, передаются данные о подтверждении приема, чтобы отслеживать непрерывность потока. Обычно полные снимки состояния мира передаются только в начале игры или в случае, когда клиент страдает от сильной потери пакетов в течении нескольких секунд. Клиент может запросить полное обновление вручную с помощью команды cl_fullupdate
.
Время реакции, то есть время, проходящее между совершением пользователем определенного действия и отражением этого действия в игровом мире, зависит от многих факторов, включая загрузку процессора на клиенте и сервере, частоте тиков, скорости передачи данных, настройках обновлений, но больше всего от времени прохождения пакета по сети. Время, проходящее между отправкой клиентом пользовательской команды и реакцией на нее окружающего мира, называется задержкой или пингом (latency или ping). Низкий пинг является серьезным преимуществом в онлайновой многопользовательской игре. Такие вещи, как предсказание и лаго-компенсация направлены на то, чтобы минимизировать это преимущество и дать возможность игрокам с медленным соединением играть на равных с остальными. Настройка сетевых переменных также помогает улучшить производительность при условии достаточной производительности процессора и пропускной способности. Мы рекомендуем придерживаться стандартных значений, так как некорректные настройки могут привести с негативным эффектам вместо пользы.
Интерполяция
По умолчанию клиент получает 20 снимков игрового мира в секунду. Если бы объекты окружающего мира отрисовывались только на позициях, продиктованных сервером, любые движущиеся предметы и анимация выглядели бы прерывисто. Потерянные пакеты также порождали бы заметные визуальные проблемы. Для того, чтобы избежать этого, весь рендеринг происходит в прошлом так, что клиент непрерывно вычисляет движение объектов между двумя последними принятыми снимками. Эта техника называется интерполяцией на стороне клиента и включена по умолчанию командой cl_interpolate 1
. При 20 обновлениях в секунду, новый снимок доставляется клиенту в среднем один раз в 50 миллисекунд. Если бы рендеринг на клиенте происходил с задержкой в 50 миллисекунд, клиент мог бы всегда интерполировать между текущим и последним снимками. В Source используется интерполяция с задержкой в 100-милисекунд (cl_interp 0.1
). Благодаря этому, даже если один снимок потерян, у клиента всегда есть два корректных обновления для интерполяции. Иллюстрация, демонстрирует время прибытия снимков игрового мира.
![Interpolation.gif](/w/images/4/49/Interpolation.gif)
Последний снимок был получен клиентом на тике 344 или на позиции 10.30 секунд. Клиент продолжает отсчитывать время основываясь на этом снимке. Новый видео кадр же рендерится во времени, равном текущему времени клиента минус задержка интерполяции (10.32 - 0.1). В нашем примере это время составит 10.22 секунды и все объекты и анимация интерполируются между корректными снимками 340 и 342.
Учитывая, что задержка интерполяции у нас составляет 100 миллисекунд, мы получили бы реальное отражение мира даже в случае, если бы снимок 342 был недоступен из-за потери пакетов. В этом случае интерполяция использовала бы снимки 340 и 344. При потере более чем одного снимка подряд интерполяция будет работать некорректно по причине отсутствия достаточного количества снимков в буфере. В этом случае используется экстраполяция (cl_extrapolate 1
) - простое линейное предсказание позиций объектов на основе истории их поведения ранее. Экстраполяция совершается только для потери пакетов до 0.25 секунды (cl_extrapolate_amount
), так как дальше ошибки предсказания становятся слишком велики.
Интерполяция вызывает непрерывную задержку отображения в 100 миллисекунд даже, если вы играете на невыделенном сервере (сервер и клиент расположены на одном и том же компьютере и сервер запущен прямо из игры). Так что если вы включите команду sv_showhitboxes
, то хитбоксы игроков будут отрисовываться по времени сервера, то есть будут на 100 миллисекунд опережать модель игрока. Это однако не означает, что вам необходимо целиться впереди противника, так как лаго-компенсация на стороне сервер знает об интерполяции клиента и учитывает ее при подсчете попаданий. Если вы отключите интерполяцию на невыделенном сервере (cl_interpolate 0
), то хитбоксы снова совпадут с моделью игрока, но анимация и движение станут дерганными и прерывистыми.
Предсказание ввода
Давайте предположим, что пинг игрока составляет 100 миллисекунд и игрок начинает движение вперед. Информация о нажатии кнопки +FORWARD
сохраняется в пользовательской команде и отправляется на сервер. Код обработки движения обрабатывает команду и игрок начинает двигаться вперед в игровом мире. Это состояние игрового мира передается всем клиентам вместе со следующим снимком. В результате игрок увидит собственное движение с задержкой 100 миллисекунд с момента как он нажал на кнопку. Эта задержка накладывается на любые действия игрока, будь то движение, стрельба или что либо еще, и ситуация становится еще хуже при увеличении пинга.
Задержка между нажатием кнопки и его визуальным отображением создает странное, неестественное чувство, в результате которого очень сложно двигаться и стрелять точно. Для устранения этой задержки и обеспечения игроку возможности ощущать изменения мгновенно используется предсказание ввода (cl_predict 1
). Вместо того, чтобы дожидаться от сервера информации об изменении собственной позиции, клиент предсказывает результаты собственных пользовательских команд с помощью точно такого же кода, который использует для этой цели сервер. После окончания предсказания, игрок мгновенно перемещается на новую позицию, хотя сервер все еще "видит" его на старом месте.
Через 100 миллисекунд клиент получит от сервера снимок игрового мира, содержащий изменения, основанные на пользовательской команде, предсказанной ранее. Клиент сравнивает данные сервера с результатами предсказания. Если они различаются, происходит ошибка предсказания. Это означает, что у клиента не было корректной информации об окружающих объектах, чтобы произвести корректное предсказание, или же что пользовательская команда не была доставлена в результате потери пакетов. В этом случает клиент корректирует позицию игрока, так как информация сервера является решающей по отношению к клиенту. Если включена команда cl_showerror 1
, клиент может видеть ошибки предсказания. Исправление ошибок предсказания может быть весьма заметным и может привести к скачку изображения. Для смягчения этого эффекта исправление ошибок предсказания происходит не мгновенно, а растягивается в течение короткого времени (cl_smoothtime
). Смягчение ошибок предсказания может быть выключено командой cl_smooth 0
.
Предсказание поведения объектов может работать только если клиент знает те же правила и состояния объектов, что и сервер. Обычно это не так, т.к. серверу известна полная информация о всех клиентах и объектах, в то время как отдельный клиент видит только ту часть игрового мира, которая необходима для корректного рендеринга. В результате, предсказание ввода действует только для самого игрока и оружия, которое он использует. Точное предсказание других игроков и интерактивных объектов на клиенте невозможно.
Лаго-компенсация
Давайте предположим, что игрок стреляет в цель во время 10.5. Информация о выстреле упаковывается в пользовательскую команду и отправляется на сервер. Пока пакет находится в пути, сервер продолжает симулировать игровой мир и мишень могла сдвинуться на другую позицию. Пользовательская команда прибывает на сервер в момент 10.6 и сервер не засчитывает попадание, хотя игрок целился точно в цель. Эта проблема исправляется серверной лаго-компенсацией (sv_unlag 1
).
Система лаго-компенсации хранит историю всех недавних позиций игроков на промежутке времени примерно в одну секунду (это можно изменить переменной sv_maxunlag
). При выполнении пользовательской команды, сервер подсчитывает время, когда она была создана по формуле:
Время исполнения команды = Текущее время сервера - Пинг клиента - Задержка интерполяции клиента
Дальше сервер перемещает всех игроков на позиции, которые они занимали в момент исполнения команды. Пользовательская команда исполняется и попадание засчитывается. После обработки пользовательской команды игроки возвращаются обратно на свои позиции. На невыделенном сервере вы можете использовать команду sv_showimpacts 1
, чтобы видеть разницу в пользовательских и серверных хитбоксах:
![Lag compensation.jpg](/w/images/c/ca/Lag_compensation.jpg)
Этот скриншот был снят на невыделенном сервере с задержкой 200 миллисекунд (используя команду net_fakelag
) сразу после подтверждения попадания сервером. Красный хитбокс отображает позицию цели на клиенте 100 миллисекунд назад. С этого момент цель продолжила движение влево пока пользовательская команда доставлялась на сервер. После получения пользовательской команды сервер восстановил позицию цели (синий хитбокс) на основе подсчитанного времени исполнения команды. Сервер отследил выстрел и подтвердил попадание (клиент видит всплеск крови). Хитбоксы на сервере и на клиенте не полностью совпадают в результате небольших ошибок при подсчете времени. Но для быстродвижущихся объектов даже задержка в несколько миллисекунд может привести к смещению в несколько сантиметров. Регистрация попаданий в многопользовательском режиме не попиксельно точна и имеет свои ограничения в зависимости от скорости объекта и установленного значения частоты тиков. Увеличение частоты тиком улучшает регистрацию попаданий, но в то же время требует большей нагрузки на процессор и большей пропускной способности как на сервере, так и на клиенте.
Возникает закономерный вопрос: почему регистрация попаданий на сервере так сложна? Зачем разбираться с откатами в прошлое для выяснения позиций игроков и проблемами с точностью если все это можно обработать на клиенте без проблем и с точностью до пикселя. Клиент же мог бы просто посылать серверу сообщение о попадании. Этого мы позволить не можем просто потому, что сервер не может доверять клиенту в столь ответственных решениях. Даже если сам клиент чист и защищен VAC (Valve Anti-Cheat), пакет с данными можно модифицировать с помощью 3-й машины по пути до сервера. Подобные "cheat прокси" могли бы вставлять сообщения о попадании в пакет минуя защиту VAC.
Задержки сети и лаго-компенсация порождают парадоксы, выглядящие алогично в реальном мире. Например, в вас может попасть противник, которого вы уже даже не можете видеть, потому что вы уже скрылись в укрытии. Это случается потому, что сервер передвинул вас в прошлое на позицию, где вы были еще видимы. Подобные несоответствия не могут быть решены из-за относительно невысокой скорости передачи пакетов. В реальном мире вы не замечаете подобных проблем потому, что свет ("пакет с информацией") путешествует столь быстро, что все вокруг видят мир таким же, как и вы в данный момент.
Net Graph
В Source существует несколько инструментов, позволяющих проверить скорость и качество соединения клиента. Самый популярный из них - это net graph, который можно включить командой net_graph 2
. Входящие пакеты отражаются в виде небольших линий, движущихся справа налево. Высота каждой линии отражает размер пакета. Если между линиями возникает разрыв, это значит что пакеты пришли в некорректном порядке или были потеряны. Цвет линий отражает содержащуюся в пакете информацию.
Под графической частью первая строка сообщает количество отрисовываемых кадров в секунду, среднюю задержку и текущее значение cl_updaterate
. Вторая строка показывает размер последнего пакета (снимка) в байтах, среднюю используемую пропускную способность и количество полученных пакетов в секунду. В третьей строке отображаются эти же данные, но для исходящих пакетов (пользовательских команд).
Other References
- Networking Entities
- Source engine hitbox demonstration (contains strong & misspelled language)