Half-Life Inside: всё о вселенной Half-Life. Определение ID недостающих устройств. Что такое ID

Многопользовательские игры на движке Source используют архитектуру Клиент/Сервер. Обычно Сервер — это выделенная машина, на которой запущена игра и которая диктует симуляцию игрового мира, правила игры и результаты обработки действий игрока. Клиент — это компьютер игрока, подключенный к игровому серверу. Клиент и сервер общаются между собой путем частой посылки небольших пакетов с данными (обычно 20-30 пакетов в секунду). Клиент получает текущее состояние игрового мира от сервера и на основе этих данных генерирует картинку и звук. Клиент также получает данные с устройств ввода (клавиатура, мышь, микрофон и т.д.) и отправляет эти данные на сервер для последующей обработки. Клиенты общаются только с сервером, но не между собой (в отличие от приложений с архитектурой peer-to-peer). В отличие от однопользовательских игр, многопользовательским требуется решать широкий спектр проблем, связанных с общением на базе передачи пакетов данных.


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

Движок Source использует несколько различных техник для того, чтобы справиться с этими проблемами или по крайней мере сделать их менее видимыми игроку. Эти техники включают сжатие данных, интерполяцию, предсказание и лаго-компенсацию. Все эти техники тесно взаимосвязаны и изменения в одной системе могут серьезно повлиять на остальные. Этот документ описывает общую функциональность этих систем и механизмы их взаимодействия.

Основы сетевого кода

Сервер симулирует игровой мир в дискретные промежутки времени, названные «тик» (tick). По умолчанию используется 66 тиков в секунду, однако моды могут использовать свою собственную частоту тиков. Например в Counter-Strike: Source используется частота 33 тика в секунду, чтобы снизить нагрузку на процессор. Во время каждого тика сервер обрабатывает пользовательские команды, симулирует физику, проверяет правила игры и обновляет состояние объектов игрового мира. После завершения симуляции тика, сервер определяет каким клиентам требуется обновление и делает снимок состояния мира, если это необходимо. Более высокая частота тиков увеличивает точность симуляции, но требует бо льших ресурсов процессора и пропускной способности как на сервере, так и на клиенте. Администратор сервера может установить значение частоты тиков с помощью параметра -tickrate в строке запуска сервера, однако подобное изменение не рекомендуется, так как работа мода может быть нарушена при установке нестандартного значения частоты тиков.

Пропускная способность клиента обычно ограничена. В худших случаях, игрок с модемным соединением может принимать не более 5-7 Кб/сек. Если бы сервер попытался отсылать ему обновления с большей частотой, то потеря пакетов стала бы неизбежной. Поэтому, клиент должен сообщить серверу о доступной входящей пропускной способности с помощью консольной переменной rate (в байтах в секунду). Это наиболее важная сетевая переменная для клиента и она должна быть выставлена правильна для достижения оптимальной производительности. Клиент может также требовать определенную частоту обновлений изменяя значение переменной cl_updaterate (значение по умолчанию: 20), но сервер никогда не пошлет обновлений больше, чем способен производить тиков или же чем установленное клиентом значение rate. К тому же, администратор сервера может ограничивать количество посылаемой информации и частоту обновлений с помощью серверных переменных sv_maxrate , sv_maxupdaterate и sv_maxupdaterate .

Клиент создает пользовательские команды опрашивая устройства ввода с той же частотой, с которой работает сервер. Пользовательская команда — это попросту говоря, снимок текущего состояния клавиатуры и мыши. Но вместо отправки на сервер нового пакета на каждую пользовательскую команду, клиент шлет обновления с определенной частотой в секунду (обычно 30). Это значит, что две или более команд передаются в каждом пакете. Игрок может увеличить частоту отправляемых пакетов с помощью команды cl_cmdrate . Это улучшит то, как реагирует клиент, но в то же время потребует большей пропускной способности.

Для уменьшения нагрузки на сеть используется так называемая дельта-компрессия. Это значит, что сервер посылает не полный снимок, а разницу между текущим и последним подтвержденным клиентом снимками (дельта-снимок). С каждым пакетом, передаваемым между сервером и клиентом, передаются данные о подтверждении приема, чтобы отслеживать непрерывность потока. Обычно полные снимки состояния мира передаются только в начале игры или в случае, когда клиент страдает от сильной потери пакетов в течении нескольких секунд. Клиент может запросить полное обновление вручную с помощью команды cl_fullupdate .

Время реакции, то есть время, проходящее между совершением пользователем определенного действия и отражением этого действия в игровом мире, зависит от многих факторов, включая загрузку процессора на клиенте и сервере, частоте тиков, скорости передачи данных, настройках обновлений, но больше всего от времени прохождения пакета по сети. Время, проходящее между отправкой клиентом пользовательской команды и реакцией на нее окружающего мира, называется задержкой или пингом (latency или ping). Низкий пинг является серьезным преимуществом в онлайновой многопользовательской игре. Такие вещи, как предсказание и лаго-компенсация направлены на то, чтобы минимизировать это преимущество и дать возможность игрокам с медленным соединением играть на равных с остальными. Настройка сетевых переменных также помогает улучшить производительность при условии достаточной производительности процессора и пропускной способности. Мы рекомендуем придерживаться стандартных значений, так как некорректные настройки могут привести с негативным эффектам вместо пользы.

Интерполяция


По умолчанию клиент получает 20 снимков игрового мира в секунду. Если бы объекты окружающего мира отрисовывались только на позициях, продиктованных сервером, любые движущиеся предметы и анимация выглядели бы прерывисто. Потерянные пакеты также порождали бы заметные визуальные проблемы. Для того, чтобы избежать этого, весь рендеринг происходит в прошлом так, что клиент непрерывно вычисляет движение объектов между двумя последними принятыми снимками. Эта техника называется интерполяцией на стороне клиента и включена по умолчанию командой cl_interpolate 1 . При 20 обновлениях в секунду, новый снимок доставляется клиенту в среднем один раз в 50 миллисекунд. Если бы рендеринг на клиенте происходил с задержкой в 50 миллисекунд, клиент мог бы всегда интерполировать между текущим и последним снимками. В Source используется интерполяция с задержкой в 100-милисекунд (cl_interp 0.1). Благодаря этому, даже если один снимок потерян, у клиента всегда есть два корректных обновления для интерполяции. Иллюстрация, демонстрирует время прибытия снимков игрового мира.

Последний снимок был получен клиентом на тике 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 , чтобы видеть разницу в пользовательских и серверных хитбоксах:


Этот скриншот был снят на невыделенном сервере с задержкой 200 миллисекунд (используя команду net_fakelag) сразу после подтверждения попадания сервером. Красный хитбокс отображает позицию цели на клиенте 100 миллисекунд назад. С этого момент цель продолжила движение влево пока пользовательская команда доставлялась на сервер. После получения пользовательской команды сервер восстановил позицию цели (синий хитбокс) на основе подсчитанного времени исполнения команды. Сервер отследил выстрел и подтвердил попадание (клиент видит всплеск крови). Хитбоксы на сервере и на клиенте не полностью совпадают в результате небольших ошибок при подсчете времени. Но для быстродвижущихся объектов даже задержка в несколько миллисекунд может привести к смещению в несколько сантиметров. Регистрация попаданий в многопользовательском режиме не попиксельно точна и имеет свои ограничения в зависимости от скорости объекта и установленного значения частоты тиков. Увеличение частоты тиком улучшает регистрацию попаданий, но в то же время требует большей нагрузки на процессор и большей пропускной способности как на сервере, так и на клиенте.

Возникает закономерный вопрос: почему регистрация попаданий на сервере так сложна? Зачем разбираться с откатами в прошлое для выяснения позиций игроков и проблемами с точностью если все это можно обработать на клиенте без проблем и с точностью до пикселя. Клиент же мог бы просто посылать серверу сообщение о попадании. Этого мы позволить не можем просто потому, что сервер не может доверять клиенту в столь ответственных решениях. Даже если сам клиент чист и защищен VAC (Valve-Anti-Cheat), пакет с данными можно модифицировать с помощью 3-й машины по пути до сервера. Подобные «cheat прокси» могли бы вставлять сообщения о попадании в пакет минуя защиту VAC.

Задержки сети и лаго-компенсация порождают парадоксы, выглядящие алогично в реальном мире. Например, в вас может попасть противник, которого вы уже даже не можете видеть, потому что вы уже скрылись в укрытии. Это случается потому, что сервер передвинул вас в прошлое на позицию, где вы были еще видимы. Подобные несоответствия не могут быть решены из-за относительно невысокой скорости путешествия пакета. В реальном мире вы не замечаете подобных проблем потому, что свет (пакет с информацией) путешествует столь быстро, что все вокруг видят мир также, как и вы в данный момент.

Net Graph

В Source существует несколько инструментов, позволяющих проверить скорость и качество соединения клиента. Самый популярный из них — это net graph, который можно включить командой net_graph 2 . Входящие пакеты отражаются в виде небольших линий, движущихся справа налево. Высота каждой линии отражает размер пакета. Если между линиями возникает разрыв, это значит что пакеты пришли в некорректном порядке или были потеряны. Цвет линий отражает содержащуюся в пакете информацию.

Под графической частью первая строка сообщает количество отрисовываемых кадров в секунду, среднюю задержку и текущее значение cl_updaterate. Вторая строка показывает размер последнего пакета (снимка) в байтах, среднюю используемую пропускную способность и количество полученных пакетов в секунду. В третьей строке отображаются эти же данные, но для исходящих пакетов (пользовательских команд).

Часто при установке новой версии Windows или во время подключения к компьютеру другого оборудования система выдает сообщение о том, что не может отыскать драйвер. Что делать в такой ситуации: обращаться за помощью к «продвинутым» программистам или попробовать решить задачу самостоятельно? Как узнать ID компьютера и его конкретный адрес в интернете?

Что такое ID

Это понятие означает физический адрес сетевой карты в персональном компьютере, через которую он подсоединяется к интернету. Часто пользователи путают понятия IP и ID адреса, отождествляя их. Но это неправильно. IP-адресом является уникальный идентификатор лэптопа, подключенного к всемирной паутине. Если устройство находится в сети, значит, у него есть свой уникальный адрес. Он может быть статическим (провайдер выделяет пользователю 1 постоянный адрес) и динамическим (пользователям выдаются свободные адреса в момент подключения к сети). ID относится к адресу сетевой платы. Через нее происходит соединение ПК с интернетом. Узнать ID компьютера можно, воспользовавшись командной строкой. А его адрес в интернете находят через специальные программы и интернет-порталы.

Определение ID компьютера

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


На экране монитора должно появиться окно, где можно увидеть надпись «Физический адрес». Напротив этого словосочетания находится ID компьютера. Он может быть представлен в виде латинских букв с цифрами. Это является кодом материнской платы. Если на ПК их несколько, значит, и код будет не один. Чтобы получить данные об удаленных компьютерах, можно применять сторонние программы, среди которых особой популярностью пользуется сканер на бесплатной основе сетиLanSpy. Скачивается он через сайт разработчика.

Определение ID недостающих устройств

Есть другой способ, позволяющий узнать коды оборудования, которое требуется установить на ПК. Как узнать ID компьютерас помощью этого способа?


Можно ли изменить ID компьютера

Домашние модели компьютеров состоят из множества отдельных деталей. На каждую деталь можно найти ее ID. Но часто пользователи сталкиваются с другой проблемой: определенный сайт банит адрес их лэптопа. И если задача "как узнать свой ID компьютера" легко решается, то возможность изменения адреса его сетевой платы остается под вопросом. Профессиональные программисты предлагают несколько способов замены сетевой карты, в результате чего меняется ее физический адрес, а значит, и ID. Но за это придется заплатить определенную денежную сумму. Когда интернет привязан к МАК-адресу, можно поменять его.

Как узнать ID «ВКонтакте» и можно ли его изменить

Социальные сети с каждым годом набирают обороты. Среди них - популярная сеть в России «ВКонтакте». Ее владельцы постоянно стремятся усовершенствовать интерфейс и исправить возникающие ошибки, учитывая пожелания пользователей. Каждому участнику социальной сети присвоен ID-адрес. Часто пользователь «ВКонтакте» не знает, где посмотреть его. Это можно сделать, переместив курсор на адрес страницы. Она будет иметь формат vk.com/id****, где звездочки являются ID-адресом. Можно зайти в раздел «Друзья» и посмотреть на адрес странички: vk.com/friends?id=***?section=all. В данном случае звездочки также указывают на ID-адрес пользовательской страницы.

Сменить адрес достаточно просто. Следует открыть «Мои настройки»; во вкладке «Общие» выбрать пункт «Адрес вашей страницы», указать новый адрес и нажать «Изменить».

Как скрыться в сети?

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

  • Privacy (incognito mode) : закрывает возможности идентификации пользователя через кеш, browser storage.
  • NoScript : плагин для FireFox, перекрывающий запуск скриптов, flash и другого активного содержимого.
  • Локальный прокси , который правит заголовки. Лучше использовать типичные заголовки для конкретной версии браузера, а нестандартные - убирать.

При совокупном и аккуратном применении всех мер идентифицировать пользователя будет сложнее, но все-таки возможно, если это будет действительно необходимо.

  • Перевод

Чем больше узнаёшь в своей области знания, тем чётче понимаешь, что никто не может знать всего.

По какой-то причине (за что, господи, за что?) моей областью стала разработка игр. Каждый, кто работает в этой сфере, скажет вам: никогда не добавляй сетевой многопользовательский режим в уже готовую игру, никогда, пьяный ты клоун.

Как бы то ни было, я именно это и сделал, и ненавижу себя за это. На удивление, вышло замечательно. Никто из нас не знает всего.

Проблема №1: ресурсы

Первый вопрос, который у меня возник: как сказать клиенту, что для рендеринга объекта нужно использовать такой-то меш?

Сериализировать весь меш? Не стоит, у клиента он уже есть на диске.

Передавать имя файла? Не-а, малоэффективно и небезопасно.

Ну ладно, может быть, просто строковый идентификатор?

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

Если я переименую текстуру, то не хочу получать от игроков отчеты об ошибках с такими скриншотами:


Я никогда не задумывался, насколько мощны и сложны строки. Половина задач в области компьютерных наук связана со строками и их возможностями. Обычно они требуют динамического выделения памяти или даже чего-то ещё более сложного, типа ropes и пула строк.

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

А тут я использую этих сложных чудовищ для идентификации объектов . Да я использовал строки даже для доступа к свойствам объектов. Какое безумие!

Короче говоря, я выработал в себе стойкую установку: избегать строк там, где это возможно.
Я написал препроцессор, создающий в процессе сборки примерно такие файлы заголовков:

Namespace Asset { namespace Mesh { const int count = 3; const AssetID player = 0; const AssetID enemy = 1; const AssetID projectile = 2; } }
Поэтому я могу ссылаться на меши следующим образом:

Renderer->mesh = Asset::Mesh::player;
Если я переименую меш, то компилятор превратит это в мою проблему, а не в проблему какого-то несчастного игрока. И это хорошо!

Плохие новости заключаются в том, что мне всё равно нужно взаимодействовать с файловой системой, а для этого необходимо использовать строки.

Хорошие новости заключаются в том, что нас снова может спасти препроцессор.

Const char* Asset::Mesh::filenames = { "assets/player.msh", "assets/enemy.msh", "assets/projectile.msh", 0, };
Благодаря всему этому я смог запросто передавать ресурсы по сети.
Это просто числа! Я могу даже проверять их.

If (mesh < 0 || mesh >= Asset::Mesh::count) net_error(); // что ты пытаешься получить, парень?

Проблема №2: ссылки на объекты

Следующим у меня возник такой вопрос: как мне вежливо попросить клиента, чтобы он переместил/удалил/обработал «тот объект, что и раньше, ты знаешь, какой».

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

С самого начала я знал, что мне потребуется куча списков различных видов объектов, примерно вот таких:

Array Turret::list; ArrayProjectile::list; Array Avatar::list;
Допустим, я хочу сослаться на первый объект в списке Avatar, даже без сети, просто на локальной машине. Первой моей идеей стало использование простого указателя:

Avatar* avatar; avatar = &Avatar::list;
При этом появляется гора неочевидных проблем.

Во-первых, я компилирую для 64-битной архитектуры, то есть указатель занимает 8 байт памяти целиком, даже несмотря на то, что бОльшая их часть будет скорее всего заполнена нулями. А память - это основное «бутылочное горлышко» скорости игр.

Во-вторых, если я добавлю в массив достаточно много объектов, он будет переназначен в другую часть памяти, а указатель станет указывать на мусор.

Ну ладно, хорошо. Буду использовать вместо указателя ID.

Template struct Ref { short id; inline Type* ref() { return &Type::list; } // перегруженный оператор "=" опускается }; Ref avatar = &Avatar::list; avatar.ref()->frobnicate();
Вторая проблема: если я удалю этот Avatar из списка, то на его место будет перемещён другой Avatar, а я ничего об этом не узнаю.

Программа продолжит выполнение, величественно и спокойно корёжа всё, пока какой-нибудь игрок не отправит мне отчёт об ошибке «игра ведёт себя странно».

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

Так, ладно. Вместо удаления аватара я буду приписывать ему номер версии:

Struct Avatar { short revision; }; template struct Ref { short id; short revision; inline Type* ref() { Type* t = &Type::list; return t->revision == revision ? t: nullptr; } };
Я не удаляю аватар полностью, а помечаю его как «мёртвый» и увеличиваю номер версии. Теперь всё, что попытается получить к нему доступ, будет получать null pointer exception. А сериализация ссылки по сети - это всего лишь вопрос передачи двух легко проверяемых чисел.

Проблема №3: дельта-компрессия

Если бы мне пришлось сократить свою статью до одной строчки, то это была бы просто ссылка на блог Гленна Фидлера.

Когда я решил реализовать собственную версию сетевого кода Гленна, я изучил ,
в которой подробно рассматривается одна из самых серьёзных проблем многопользовательских игр. А именно следующая: если вы хотите передавать состояние всего мира по сети 60 раз в секунду, то забьёте 17 Мбит/с от ширины канала.

И это только на одного клиента .

Дельта-компрессия - это один из лучших способов снижения объёма передаваемых данных. Если клиент уже знает, где находится объект, и тот не двигался, то нам не нужно отправлять его позицию повторно. Но реализовать это правильно бывает довольно сложно.


Первая часть самая сложная: а знает ли вообще клиент, где находится объект? То, что я отправил позицию, не означает, что клиент её получил. Клиент может отправлять обратно подтверждения типа «я получил пакет 218, но это было 0,5 секунды назад и с тех пор ничего больше не принимал».

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

Кто-то задал на Reddit вопрос: «разве это не огромный объём памяти?»

Нет, не огромный.

Я храню в памяти 255 копий мира в едином огромном массиве. Но это ещё не всё -
в каждой из копий достаточно места для максимального количества объектов (2048) даже если активны только 2.

Если хранить состояние объекта как позицию и поворот, то нужно 7 чисел float: 3 на координаты XYZ и 4 на кватернион. Каждое число float занимает 4 байта. Игра поддерживает до 2048 объектов. 7 float * 4 байта * 2048 объектов * 255 копий =…

14 МБ. То есть примерно половина современной текстуры.

Могу представить, как писал бы эту систему пять лет назад на C#. Я бы сразу начал беспокоиться об используемой памяти, совсем как тот человек с Reddit, даже не задумавшись о действительном объёме задействованных данных. Я бы написал какую-нибудь ненужную, безумно изощрённую, переполненную багами систему компрессии. Когда вы уделяете секунду и задумываетесь над тем, какими будут настоящие данные, то это называется Data-Oriented Design . Когда я рассказываю людям о DOD, многие сразу начинают говорить: «Ого, это очень низкоуровневый подход. Похоже, ты хочешь выжать максимум производительности. У меня нет на это времени. Да и мой код работает нормально». Давайте разобьём эту фразу на утверждения.

Утверждение 1: «Это очень низкоуровневый подход».

Вы же видите - я всего лишь перемножил четыре числа, это не квантовая физика.

Утверждение 2: «Приходится жертвовать читаемостью и простотой ради скорости».

Давайте представим два разных решения этой проблемы с сетевым кодом. Для ясности давайте представим, что нам нужны всего три копии мира, в каждой из которых содержится по два объекта.

Вот решение, которое я только что описал. Всё статически размещено в сегменте.bss . Он никогда не перемещается, всегда одного размера и совершенно не использует указателей:


А вот характерное для C# решение. Всё случайным образом разбросано по динамической памяти. Элементы перераспределяются или перемещаются прямо посередине кадра, массив хаотичен, повсюду 64-битные указатели:


Что проще?

На самом деле вторая схема далеко не исчерпывающа. В реальном мире территория C# гораздо сложнее. В комментариях меня наверняка поправят и расскажут, как на самом деле работает C#.

Но такова моя точка зрения. В моём решении я могу запросто сконструировать «достаточно хорошую» мысленную модель, чтобы понимать, что на самом деле происходит в машине. А в решении на C# я едва начал реализацию. Я понятия не имею, как оно поведёт себя в процессе выполнения.

Предположение 3: «Писать код таким образом стоит только ради производительности».

По моему мнению, скорость - это приятный побочный эффект Data Oriented Design. Главное преимущество - ясность мысли. Пять лет назад, если бы я приступил к решению задачи, то первым делом бы подумал не о самой задаче, а о том, как втиснуть её в классы и интерфейсы.

Недавно я собственными глазами наблюдал такой «паралич анализа» на геймджеме. Мой друг застопорился на создании сетки для игры в стиле 2048 . Он не мог понять, должно ли каждое число быть объектом, или каждая ячейка сетки, или все они. Я сказал: «Сетка - это массив чисел. Каждая операция - это функция, изменяющая сетку». Внезапно ему всё стало кристально ясно.

Предположение 4: «Мой код работает нормально».

Повторюсь: скорость - не основная проблема, но она важна. Именно из-за неё весь мир переключился с Firefox на Chrome.

Попробуйте провести эксперимент: запустите calc.exe. Теперь скопируйте 100-мегабайтный файл из одной папки в другую.

Я не знаю, что делает calc.exe в течение этих бесконечных 300 мс, но вы можете сделать собственные выводы из двухминутного исследования: calc.exe запускает процесс Calculator.exe, и один из аргументов командной строки называется "-ServerName".

Скажите, calc.exe «работает нормально»? Упрощает ли всё добавление сервера, или только замедляет и усложняет?

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

Проблема №4: лаг

Теперь я вкратце расскажу о той части истории, в которой сетевой код хоть как-то работает.

У меня сразу же возникли проблемы с сетевой задержкой. Игры должны отвечать игрокам мгновенно, даже если на получение пакета с сервера уходит 150 мс. Особенно бесполезны в условиях лагающей сети пули и снаряды. Ими невозможно прицелиться.

Я решил повторно использовать эти 14 МБ копий мира. Когда сервер получает команду на выстрел снарядом, он перематывает мир назад на 150 мс к тому моменту, в котором находился игрок, когда нажимал кнопку стрельбы. Затем сервер симулирует снаряд и пошагово перематывает мир вперёд, пока он не будет совпадать с текущим состоянием. И здесь он создаёт снаряд.

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

Вот как это работает на деле. Фальшивый снаряд появляется мгновенно, но проходит прямо сквозь стену. Сервер получает сообщение и перематывает снаряд вперёд, к той части, где тот ударяется в стену. 150 мс спустя клиент получает пакет и видит эффект частиц попадания.

Your browser does not support HTML5 video.


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

Your browser does not support HTML5 video.


Эта способность разваливается в случаях с высоким лагом. К тому времени, когда игрок видит, что снаряд попадает в его персонажа, сервер уже зарегистрировал попадание 100 мс назад. Но пакет даже ещё не добрался до клиента. Это значит, что мы должны предвидеть предстоящий урон и среагировать задолго до попадания. Посмотрите представленное выше видео и заметьте, насколько рано мне пришлось нажать на кнопку.

Чтобы исправить это, на сервере реализовано то, что я называю «буферизацией урона». Вместо мгновенного применения урона сервер на 100 мс (или на любое время, необходимое на полный путь до клиента и обратно) записывает урон в буфер. В конце этого времени он или применяет урон, или, если игрок среагировал, отражает его обратно.

Вот, как это выглядит в действии. Можно заметить задержку в 200 мс между попаданием в меня снаряда и применением урона.

Your browser does not support HTML5 video.


Вот ещё один пример. В моей игре игроки могут швырять себя во врагов. При идеальном попадании враги умирают мгновенно, но скользящие удары они отражают и отправляют вас в полёт таким образом:

Your browser does not support HTML5 video.


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

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

В результате я реализовал ещё одну систему буферизации. И клиент, и сервер при распознавании попадания переходят в «буферное» состояние, в котором игрок стоит и ждёт, пока удалённый хост подтвердит попадание. Чтобы минимизировать рывки, сервер всегда полагается на клиента при определении направления отскока. Если клиент так и не подтверждает попадания, то сервер действует так, как будто ничего не произошло, и продолжает перемещать игрока по его исходной траектории, перематывая его вперёд для компенсации времени, в течение которого он ожидал подтверждения.

Проблема №5: джиттер

Мой сервер отправляет пакеты 60 раз в секунду. Но что произойдёт у игроков, компьютеры которых выдают бОльшую частоту кадров? Они увидят дёрганую анимацию.

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

В моей предыдущей попытке реализации сетевого мультиплеера я попробовал сделать так, чтобы каждый объект самостоятельно отслеживал данные о своём положении и сглаживал себя сам. В результате я запутался, а система так и не заработала, как надо.

На этот раз, поскольку я уже могу запросто хранить всё состояние мира в struct, мне оказалось достаточно написать всего две функции, чтобы всё получилось. Одна функция получает два состояния мира и смешивает их. Другая функция получает состояние мира и применяет его к игре.

Насколько большой должна быть задержка буфера? Изначально я использовал константу, но потом посмотрел видео разработчиков Overwatch , в котором они упоминают адаптивную задержку интерполяции. Буферная задержка должна сглаживать не только частоту получаемых от сервера кадров, но все колебания времени доставки пакетов.

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

Разумеется, подобные автоматизированные системы часто действуют против желания игрока, поэтому в алгоритм обязательно нужно добавить возможности настройки!

Проблема №6: подключение к серверу посередине матча

Постойте, у меня уже есть способ сериализации всего игрового состояния. Так в чём же проблема?

Оказывается, для сериализации нового игрового состояния с нуля требуется не один, а несколько пакетов. А для передачи пакета клиенту может понадобиться несколько попыток. Чтобы получить полное состояние, может потребоваться несколько сотен миллисекунд, а как мы уже видели, это целая вечность. Если игра уже идёт, этого времени достаточно для передачи 20 пакетов с новыми сообщениями, которые клиент ещё не готов обработать, потому что он ещё не загрузился.

Решение, как вы уже догадались, заключается в ещё одном буфере.

Я изменил систему сообщений так, чтобы она поддерживала два отдельных потока сообщений в одном пакете. Первый поток содержит данные карты, которые обрабатываются сразу же после получения.

Второй поток - это обычная лавина игровых сообщений, получаемых, пока клиент загружается. Клиент буферизирует эти сообщения, пока не завершит загрузку, а затем обрабатывает их все, пока не догонит состояние сервера.

Проблема №7: вопросы разбиения

Эта часть будет самой противоречивой.

Помните житейскую мудрость разработчиков игр, упомянутую в начале статьи? «Никогда не добавляй сетевой многопользовательский режим в уже готовую игру».

Так получилось, что бОльшая часть сетевого кода в буквальном смысле приклеена к этой игре скотчем. Она находится в собственном исходном файле на 5000 строк. Код встраивается в игру, записывает какие-то данные в память, а потом игра их рендерит.

Прежде чем распинать меня за это, подождите секунду. Что лучше: сгруппировать весь сетевой код в одном месте, или разбросать внутри каждого игрового объекта?

Думаю, у обоих подходов есть свои достоинства и недостатки. На самом деле в разных частях игры я использовал и первый, и второй, по разным причинам, от личных до технических.

Но некоторые парадигмы проектирования (*кхм* ООП) не даёт мне принимать такие решения. Разумеется, нужно вставлять сетевой код внутрь объекта! Его данные приватны, поэтому для доступа к ним всё равно придётся писать интерфейс. Возможно, также придётся использовать всевозможные интеллектуальные преобразования. Добавить метки