Методы компенсации латентности в разработке и оптимизации протоколов Клиент-Серверной игры.
от Askalite
Оригинал:
https://developer.valvesoftwar...ign_and_Optimization
Yahn W. Bernier ([email protected]), 2001
Software Development Engineer
Содержание
1. Обзор
2. Базовая архитектура клиент-серверной игры
3. Содержание сообщений пользователя
4. Прогнозирование на стороне клиента
5. Прогнозирование на стороне клиента стрельбы из оружия
6. Умм, много работы
7. Отображение целей
8. Компенсация задержки
9. Особенности игры с компенсацией задержек
10. Заключение
11. Сноски
Обзор
Разработка онлайн игр от первого лица – сложный процесс. Однако наличие онлайн-геймплея становится надёжной гарантией успеха вашего экшена. Кроме того, ПК пространство хорошо известно тем, что разработчикам требуется поддержка широкого спектра клиентских устройств. Часто клиенты имеют не самое лучшее оборудование. То же самое справедливо и для их сетевых соединений.
Широкополосная связь была создана как панацея от всех текущих проблем онлайн-игр, однако это не то решение, которое позволяет разработчикам игнорировать латентность и другие сетевые факторы в игровых проектах. Потребуется некоторое время, прежде чем широкополосная связь действительно будет хорошо распространена в Соединенных Штатах и намного больше, чем предполагается, когда она появится для ваших клиентов в остальном мире. Кроме того, существует множество плохих широкополосных решений, где пользователи могут иногда иметь высокую пропускную способность, но также имея значительную задержку и потерю пакетов в своих соединениях.
Ваша игра должна быть хорошо приспособлена к реальному миру. Эта статья-рассуждение даст вам понимание о некоторых компромиссах, нужных для предоставления передового опыта действий в Интернете. Мы расскажем о том, как архитектура клиент-сервер работает во многих онлайн-играх. Кроме того, будет показано, как интеллектуальное моделирование можно использовать для маскировки эффектов латентности. Наконец, будет описан конкретный механизм компенсации латентности, позволяющий игре компенсировать качество соединения.
Базовая архитектура клиент-серверной игры
Большинство онлайн экшн игр, это модификации клиент-серверных игр. Такие игры, как Half-Life, включая его моды, такие как Counter-Strike и Team Fortress Classic, работают на той же системе, что и игры на движке Quake3 и Unreal Tournament. В них есть один, авторитарный сервер, который отвечает за работу основной логики игры. К нему подключены один или несколько "глупых" клиентов. Изначально эти клиенты были лишь способом ввода команд пользователями и отправки их на сервер. Сервер выполнял входящие команды, обновлял состояния всех других объектов, а затем отправлял клиенту список объектов для рендеринга. Разумеется, реальные системы имеют больше компонентов, но упрощенное определение полезно при последующих размышлениях о предсказании и компенсации отставания.
Учитывая это, типичная архитектура движка клиент-серверной игры выглядит примерно так:
Для этого рассуждения-статьи все сообщения и координации, необходимые для запуска соединения между клиентом и сервером, опущены. Игровой цикл клиента выглядит примерно так:
1. Сохранение времени старта
2. Обработка пользовательского ввода
3. Обновление и отправка пакета с командой перемещения используя время моделирования прошлого кадра
4. Чтение пакетов пришедших от сервера по сети
5. Использование пакетов для определения видимых объектов и их состояния
6. Рендеринг сцены
7. Сохранение времени завершения
8. Время завершения минус время старта – это время выполнения для следующего кадра
Каждый раз, когда клиент завершает очередной шаг, "время цикла" за который был выполнен шаг цикла, используется для определения того, сколько времени потребуется для моделирования на следующем шаге. Если ваша частота кадров полностью постоянна, то время цикла будет правильной мерой. В противном случае временные рамки будут неправильными, и не существует решения этой проблемы (вы не можете определить, сколько времени потребуется, для следующей итерации игрового цикла, прежде чем запускать его ...).
Сервер имеет схожий цикл:
1. Сохранение времени старта
2. Чтение пользовательского ввода пришедшего по сети
3. Выполнение сообщений пользовательского ввода клиента
4. Моделирование серверных объектов с использованием времени моделирования от последнего полного прохода
5. Для каждого подключенного клиента упаковать видимые объекты/состояния мира и отправить клиенту
6. Сохранить время завершения
7. Время завершения минус время старта – это время моделирования для следующего кадра
В этой модели объекты, не связанные с игроком, выполняются исключительно на сервере, в то время как объекты игрока управляются движениями на основе входящих пакетов. Конечно, это не единственный возможный способ выполнить эту задачу, но наиболее разумный.
Содержание сообщений пользователя
В игровом движке Half-Life формат сообщений пользовательских команд довольно прост и инкапсулирован в структуру данных, содержащую только несколько важных полей:
Важными полями здесь являются msec, viewangles, forward, side, upmove и buttons. Поле msec соответствует времени выполнения команды (это время цикла). Поле viewangles – это вектор направления в котором игрок смотрел во время кадра. Forward, side и upmove поля – это импульсы, определяемые путем изучения клавиатуры, мыши и джойстика, считываемые, если удерживались какие-либо клавиши перемещения. Наконец, поле buttons – это просто поле с одним или несколькими битами, установленными для каждой удерживаемой кнопки.
Учитывая приведенные выше структуры данных и архитектуру клиент-сервера, ядро моделирования выглядит следующим образом. Во-первых, клиент создает и отправляет пользовательскую команду на сервер. Затем сервер выполняет её и отправляет обновленные позиции всего обратно клиенту. Наконец, клиент отображает сцену со всеми текущими объектами. Это ядро хотя и довольно простое, не очень хорошо реагирует на ситуации в реальности, где пользователи могут испытывать значительную задержку в своих интернет-соединениях. Основная проблема заключается в том, что клиент поистине "глупый", и все, что он делает, – это простая задача чтения ввода и ожидание результатов ответа сервера. Если у клиента есть 500 миллисекунд задержек при его подключении к серверу, то для любых действий клиента, которые должны быть подтверждены сервером, потребуется 500 миллисекунд, чтобы результаты были заметны на клиенте. Хотя эта задержка в оба конца может быть приемлемой для локальной сети (LAN), она неприемлема в Интернете.
Прогнозирование на стороне клиента
Одним из способов улучшения этой проблемы является локальное выполнение команд на клиенте предполагая, что сервер немедленно примет и подтвердит команды клиента. Этот метод помечен как предсказание на стороне клиента.
Предсказание движений требует от нас отказаться от "глупого" или правила минималистичного клиента. Это не означает, что клиент полностью контролирует свое моделирование, как в одиночной игре. По-прежнему существует авторитарный сервер, на котором выполняется симуляция, как указано выше. Наличие авторитарного сервера означает, что даже если клиент имитирует результаты отличные от серверных, он исправит свои ошибки получив ответы от сервера. Из-за латентности в соединении коррекция может не произойти до тех пор, пока не закончится время очередного шага игрового цикла. Недостатком является то, что это может вызвать очень заметный сдвиг в позиции игрока из-за накопления ошибки прогнозирования, произошедшей ранее.
Чтобы реализовать предсказание движения у клиента, используется следующая процедура. Как и раньше, обрабатывается пользовательский ввод и формируются команды клиента. Как и ранее, команда отправляется на сервер. Однако каждая команда и точное время её создания хранится на клиенте. Их потом использует алгоритм прогнозирования.
Для прогнозирования в качестве отправной точки использует последнее подтверждение с сервера. Оно указывает, какая команда клиента была последний раз использована сервером, а также указывает точное местоположение (и другие данные состояния) игрока после того, как эта команда движения была смоделирована на сервере. Последняя подтвержденная команда будет где-то в прошлом, если есть какое-либо отставание в соединении. Например, если клиент работает со скоростью 50 кадров в секунду (fps) и имеет 100 миллисекунд задержки (roundtrip), тогда клиент будет хранить пять пользовательских команд перед последним, подтвержденным сервером. Эти пять пользовательских команд моделируются на клиенте как часть прогнозирования на стороне клиента. Предполагая полное предсказание [1], клиент захочет начать с последних данных с сервера, а затем запустить пять пользовательских команд с помощью "аналогичной логики" на то, что использует сервер для моделирования движения клиента. Выполнение этих команд должно давать точное конечное состояние на клиенте (наиболее важна позиция конечного игрока), которая может использоваться для определения какую позицию использовать для рендеринга сцены во время текущего кадра.
В Half-Life минимизация расхождений между клиентом и сервером в логике прогнозирования достигается путем совместного использования идентичного кода движения для игроков как в коде на стороне сервера, так и в коде на стороне клиента. Это подпрограммы в pm_shared/ (имя папки "совместное перемещение игрока") HL SDK. Входные данные для общих процедур инкапсулируются командой пользователя и состоянием "из" игрока. Результатом является новое состояние игрока после выдачи команды пользователя. Общий алгоритм у клиента выглядит следующим образом:
Исходная и другая информация о состоянии в окончательном состоянии "to state" (состояние) является результатом прогнозирования и используется для рендеринга сцены в каждом кадре. Часть, где выполняется команда, – это просто часть, где все данные состояния игрока копируются в общую структуру данных, обрабатывают пользовательские команды (путем выполнения общего кода в подпрограммах pm_shared в случае Half-Life), и полученные данные копируют обратно в "to state".
Есть несколько важных замечаний к этой системе. Во-первых, вы заметите, что от клиента зависит задержка и насколько быстро клиент генерирует команды пользователя (т.е. клиентский игровой цикл), клиент чаще всего оказываются под управлением той же команды снова и снова, пока они, наконец, не подтверждены на сервере и удалены из списка (скользящие окна в случае Half-life) команд, которых только предстоит подтвердить. Первое предостережение заключается в том, как обрабатывать любые звуковые эффекты и визуальные эффекты, созданные в общем коде. Поскольку команды могут выполняться снова и снова, важно не создавать звуки шагов и т.д. несколько раз при повторном запуске старых команд для обновления прогнозируемой позиции. Кроме того, важно, чтобы сервер не отправлял клиентские эффекты, которые уже предсказываются на клиенте. Однако, клиент должен повторно запустить старые команды, иначе не будет никакого способа для сервера, чтобы исправить ошибочное предсказание клиентом. Решение этой проблемы очень просто: клиент просто отмечает те команды, которые еще не были предсказаны и играет только если команда пользователя выполняется в первый раз.
Другое предостережение касается данных состояния, которые существуют исключительно на клиенте и не являются частью достоверных данных обновлённых с сервера. Если у вас нет такого типа данных, вы можете просто использовать последнее подтвержденное состояние сервера в качестве отправной точки и выполнить предсказанные команды пользователя "на месте" для этих данных, чтобы прийти к конечному состоянию (которое включает вашу позицию для рендеринга). В этом случае не требуется сохранять все промежуточные результаты вдоль маршрута для прогнозирования от последнего подтвержденного состояния до текущего времени. Однако, если вы делаете какую-либо логику полностью на стороне клиента (эта логика может включать такие функции, как определение положения глаз, это когда вы находитесь на корточках — и это на самом деле не полностью на стороне клиента, так как сервер тоже имитирует эти данные), что влияет на поля, которые не копируются с сервера на клиент на сетевой уровень обработки информации о состоянии игрока, то вам нужно будет сохранить промежуточные результаты прогнозирования. Это можно сделать с помощью скользящего окна, где "to state" находится в начале, а затем каждый раз, когда вы запускаете пользовательскую команду через предсказание, вы заполняете следующее состояние в окне. Когда сервер, наконец, признает получение одной или нескольких команд, которые были предсказаны, то это уже простой вопрос поиска, какое состояние сервер подтверждает, и простое копирование данных, которые полностью на стороне клиента в новое начало или "to state".
Выше мы расписали процедуру которая описывает, как выполнить прогноз движений на стороне клиента. Эта система аналогична системе, в QuakeWorld [2].
Прогнозирование на стороне клиента стрельбы из оружия
Добавление предсказания к стрельбе из оружия к вышеупомянутой системе предсказаний движений происходит просто. Разумеется, для локального игрока необходима дополнительная информация о состоянии клиента, в том числе о том, какое оружие хранится, какое из них активно, и сколько боеприпасов осталось у каждого из этих оружия. С помощью этой информации, логика стрельбы может быть наложена поверх логики движений, поскольку, ещё раз, состояние кнопок стрельбы включаются в структуру данных пользовательских команд, которая совместно используется клиентом и сервером. Конечно, сложность возрастёт, если фактическая логика оружия отличается между клиентом и сервером. В Half-Life мы решили избежать этого осложнения, переместив реализацию логики стрельбы оружия в "общий код", как и код движения игрока. Все переменные, которые способствуют определению состояния оружия (например, боеприпасы, когда следующий выстрел оружия может произойти, какая анимация у оружия и т.д. ), также являются частью главного состояния сервера и копируются на стороне клиента, поэтому они могут использоваться для прогнозирования состояния оружия.
Прогнозирование стрельбы на стороне клиента, вероятно, приведет к решению и прогнозировать переключение оружия, развертывание и убирание. Таким образом, пользователь будет чувствовать, что игра на 100% реагирует на его движения и активацию оружия. Это имеет большое значение для уменьшения ощущения зввисания и тормозов, которое пережили многие игроки используя интернет.
Хм, много лишней работы?
Репликация необходимых полей клиенту и обработка всех промежуточных состояний – это большая работа. На этом этапе вы можете спросить, почему бы не убрать все связанное с сервером и просто сообщить клиенту, где он/она находится после каждого движения? Другими словами, почему бы не бросить серверную хрень и просто запустить движение и оружие исключительно на стороне клиента? Затем клиент просто отправил бы результаты на сервер примерно так: "я сейчас нахожусь в позиции x, и, кстати, я просто выстрелил игроку 2 в голову." Это нормально, если вы можете доверять клиенту. Именно так работают многие системы военного моделирования (т. е. они являются закрытой системой и доверяют всем клиентам). Так обычно работают одноранговые игры. Для Half-Life этот механизм неработоспособен из-за реальных опасений обмана. Если бы мы инкапсулировали абсолютные данные состояния таким образом, мы бы подняли мотивацию для взлома клиента даже выше, чем это уже есть [3]. Для наших игр этот риск слишком высок, и мы возвращаемся к требованию авторитарного сервера.
Система, в которой движения и эффекты оружия предсказываются на стороне клиента, является очень работоспособной системой. Например, это система, которую поддерживает Quake3 engine. Одна из проблем с этой системой заключается в том, что вам все еще нужно ощущать задержку, чтобы определить, как вести свои цели (для мгновенного попадания оружия). Другими словами, хотя вы сразу слышите выстрелы из оружия, и ваша позиция полностью обновлена, результаты ваших выстрелов по-прежнему подвержены задержкам. Например, если вы нацелены на игрока, Бегущего перпендикулярно вашему виду, и у вас есть задержка 100 миллисекунд, а игрок бежит со скоростью 500 единиц в секунду, вам нужно будет нацелить 50 единиц перед целью, чтобы поразить цель мгновенным оружием. Чем больше задержка, тем больше необходимо целится наперёд. Получить "чувство" для вашей латентности трудно. Quake3 попытался смягчить это, играя короткий звук, когда ваши попадания подтверждены. Таким образом, вы могли бы выяснить, как далеко продвинутся, стреляя очередями и регулируя прицел, пока не услышите стабильный поток звуков. Очевидно, что с достаточной задержкой и противником, который активно уклоняется, довольно сложно получить достаточную обратную связь, чтобы последовательно сосредоточиться на противнике. Если ваш пинг колеблется, это может быть еще сложнее.
Отображение целей
Еще одним важным аспектом, влияющим на то, как пользователь воспринимает отзывчивость мира, является механизм определения на клиенте, где показывать других игроков. Два основных механизма для определения места отображения объектов – экстраполяция и интерполяция [4].
Для экстраполяции другой игрок/объект моделируется вперед во времени от последнего известного мечта, направления и скорости более или менее баллистическим способом. Таким образом, если вы отстаете на 100 миллисекунд, и последнее обновление, которое вы получили, состояло в том, что (как указано выше) другой игрок бежал 500 единиц в секунду перпендикулярно вашему взгляду, тогда клиент может предположить, что в "реальном времени" игрок переместился на 50 единиц прямо вперед от этой последней известной позиции. Затем клиент может просто нарисовать игрока в этой экстраполированной позиции, и локальный игрок все равно может более или менее нацелиться прямо на другого игрока.
Самый большой недостаток экстраполяции заключается в том, что движения игрока очень не баллистические, а очень недетерминированные и с большими рывками [5]. Уровнем выше нереалистичные модели физики игрока, которые используют большинство игр FPS, где игрок может мгновенно поворачиваться и применять нереалистичные силы для создания огромных ускорений под произвольными углами, и вы увидите, что экстраполяция довольно часто неверна. Разработчик может смягчить ошибку, ограничив время экстраполяции разумным значением (например, QuakeWorld ограничил экстраполяцию до 100 миллисекунд). Это ограничение помогает, потому что, как только истинная позиция игрока, наконец, получена, будет ограниченное количество корректирующих деформаций. В мире, где большинство игроков все еще имеют задержку более 150 миллисекунд, игрок все равно должен вести прицел предугадывая движения других игроков, чтобы поразить их. Если эти игроки "прыгают" на новые места из-за ошибок экстраполяции, то процесс игры становится ужасным.
Другой метод определения места отображения объектов и игроков — интерполяция. Интерполяция может быть рассмотрена как всегда движущиеся объекты несколько в прошлом по отношению к последней допустимой позиции, полученной для объекта. Например, если сервер отправляет 10 обновлений в секунду (точно) состояния world, то мы можем наложить задержку интерполяции в 100 миллисекунд в нашем рендеринге. Затем, когда мы отрисовываем кадры, мы интерполируем положение объекта между последней обновленной позицией и позицией, обновленной до этого (или последняя позиция отрисовки) за эти 100 миллисекунд. По мере того, как объект только попадает в последнюю обновленную позицию, мы получаем новое обновление с сервера (так как 10 обновлений в секунду означает, что обновления приходят каждые 100 миллисекунд), мы можем начать двигаться к этой новой позиции в течение следующих 100 миллисекунд.
Если один из пакетов обновления не приходит, то есть два варианта: мы можем начать экстраполировать позицию игрока, как указано выше (с большими потенциальными ошибками), или мы можем просто оставить игрока на позиции в последнем обновлении, пока не придет новое обновление (в результате чего движение игрока заикаться).
Общий алгоритм для этого типа интерполяции выглядит следующим образом:
1. Каждое обновление содержит отметку времени сервера на момент его создания [6]
2. От текущего времени клиента клиент вычисляет конечное время путем вычитания дельты времени интерполяции (100 мс)
3. Если конечное время находится между отметкой времени последнего обновления и предыдущей, то эти метки времени определяют, какая часть промежутка времени прошла.
4. Это отношение используется для интерполяции любых значений (например, положения и углов).
По сути, в приведенном выше примере интерполяцию можно представить как буферизацию дополнительных 100 миллисекунд данных на клиенте. Таким образом, другие игроки рисуются там, где они были в прошлом, что равно вашей точной задержке плюс количество времени, в течение которого вы интерполируете. Чтобы иметь дело со случайным отбрасыванием пакета, мы могли бы установить время интерполяции в 200 миллисекунд вместо 100 миллисекунд. Это (опять же, предполагая 10 обновлений в секунду с сервера) позволит нам полностью пропустить одно обновление и по-прежнему интерполировать игрока к действительной позиции, часто проходя через эту интерполяцию без сучка и задоринки. Конечно, интерполяция с помощью большего времени компромисс, потому что он торгует дополнительные задержки (сделав интерполяцию игрока усложним атаку), для визуальной гладкости.
Кроме того, описанный выше тип интерполяции (когда клиент отслеживает только последние два обновления и всегда движется непосредственно к последнему обновлению) требует фиксированного интервала времени между обновлениями сервера. Метод также страдает от визуальных проблем качества, которые трудно решить. Проблема визуального качества заключается в следующем. Представьте, что интерполируемым объектом является прыгающий мяч (который на самом деле точно описывает некоторых наших игроков). В крайних случаях мяч либо находится высоко в воздухе, либо ударяется о тротуар. Однако в среднем мяч находится где-то посередине. Если мы интерполируем только до последней позиции, то очень вероятно, что эта позиция находится не на земле или в верхней точке. Прыжок шарика "сглажен" и он кажется никогда не ударяет землю. Это классическая проблема дискретизации и может быть облегчена путем повышения частоты измерения состояния мира. Однако, мы всё ещё вероятно, никогда не имеем актуальную интерполяцию состояния на земле или в высшей точке, хотя это всё равно сгладит позиции.
Кроме того, поскольку разные пользователи имеют разные подключения, принудительное обновление на шаге, например 10 обновлений в секунду, приводит к неоправданному снижению общего знаменателя скорости чоединения для пользователей. В Half-Life мы позволяем пользователю запрашивать столько обновлений в секунду, сколько он или она хочет (в пределах лимита). Таким образом, пользователь с быстрым подключением мог получать 50 обновлений в секунду, если желает. По умолчанию Half-Life отправляет 20 обновлений в секунду каждому игроку, клиент Half-Life интерполирует игроков (и многие другие объекты) в течение 100 миллисекунд. [7]
Чтобы избежать проблемы сглаживания прыгающего шара, мы используем другой алгоритм интерполяции. В этом методе мы сохраняем более полную "историю позиций" для каждого объекта, который может быть интерполирован.
История позиций – это метка времени, начало координат и углы (и может включать любые другие данные, которые мы хотим интерполировать) для объекта. Каждое обновление, которое мы получаем от сервера, создает новую запись истории позиций, включая метку времени и начало/углы для этой метки времени. Чтобы интерполировать, мы вычисляем конечное время, как указано выше, на затем мы ищем назад по истории позиций, ища пару обновлений, которые охватывают конечное время. Затем мы используем их для интерполяции и вычисления конечного положения для этого кадра. Это позволяет нам плавно следовать кривой, которая полностью включает все наши опорные точки. Если мы работаем на более высокой частоте кадров, чем скорость входящего обновления, мы почти уверены в плавном перемещении по точкам выборки, тем самым сводя к минимуму (но не устраняя, конечно, так как полная частота дискретизации мировых обновлений является ограничивающим фактором) проблему сглаживания, описанную выше.
Одна проблема, мы должны добавить поверх любой интерполяционной схемы какой-то способ определить, что объект был принудительно телепортирован, а не просто двигался очень быстро. В противном случае мы могли бы "плавно " перемещать объект на большие расстояния, заставляя объект выглядеть слишком быстрым. Мы можем либо установить флаг в обновлении, который говорит: "не интерполировать" или "очистить историю позиций", или мы можем определить, является ли расстояние между источником и одним обновлением и другим слишком большим, и, таким образом, предполагая телепортацию/деформацию. В этом случае решение, вероятно, состоит в том, чтобы просто переместить объект в последнюю известную позицию и начать интерполяцию оттуда.
Компенсация задержки
Понимание интерполяции важно при разработке компенсации запаздывания, поскольку интерполяция является другим типом задержки в работе пользователя. В той степени, в которой игрок смотрит на другие объекты, то количество интерполяции, которое должны быть принято во внимание при вычислении на сервере верности цели игрока.
Компенсация лагов – это метод нормализации на стороне сервера состояния мира для каждого игрока по мере выполнения пользовательских команд этого игрока. Вы можете думать о компенсации задержки как о том, чтобы сделать шаг назад во времени, на сервере, и посмотреть на состояние мира в тот момент, когда пользователь выполнил какое-то действие. Алгоритм работы следующий:
1. Перед выполнением команды текущего пользователя сервера:
1.1. Вычисляет довольно точную задержку для игрока
1.2. Ищет в истории сервера (для текущего игрока) обновление мира, которое было отправлено игроку и получено игроком непосредственно перед тем, как игрок выдал бы команду движения
1.3. Из этого обновления (и следующего за ним на основе точного конечного времени) для каждого игрока в обновлении переместите других игроков назад во времени точно туда, где они были, когда была создана пользовательская команда текущего игрока. Это перемещение назад должно учитывать как задержку подключения, так и сумму интерполяции [8], которую использовал клиент.
2. Разрешить выполнение команды пользователя (включая любые команды стрельбы из оружия и т. д., который будет запускать броски лучей против всех других игроков в их "старых" позициях).
3. Переместить всех двигавшихся/застрявших игроков назад на их корректные/текущие позиции.
Обратите внимание, что на шаге, на котором мы перемещаем игрока назад по времени, это может фактически потребовать принудительного получения дополнительной информации о прошлом состоянии (например, был ли игрок жив или мертв, или игрок уклонялся). Конечный результат компенсации отставания заключается в том, что каждый местный клиент может напрямую нацелиться на других игроков, не беспокоясь о том, чтобы возглавить свою цель, чтобы забить удар. Конечно, такое поведение является компромиссом игрового дизайна.
Особенности игры с компенсацией задержек
Введение компенсации задержки позволяет каждому игроку работать на по их собственному времени без видимой задержки. В этой связи важно понимать, что могут возникать определенные парадоксы или несоответствия. Конечно, старая система с авторитарным сервером и "глупыми" или простыми клиентами имела свои парадоксы. В конце концов, выбор компромисса – это компромисс в разработке игр. Для Half-Life мы считаем, что компромисс в пользу компенсации отставания было оправданным решением для игрового дизайна.
Первая проблема старой системы заключалась в том, что вы должны были вести свою цель на некотором смещении, связанным с вашей задержкой на сервере. Прицеливание непосредственно в другого игрока и нажатие кнопки огня почти гарантировало пропуск этого игрока. Непорядок здесь заключается в том, что прицеливание просто нереалистично и что элементы управления игрока имеют непредсказуемую отзывчивость.
С компенсацией запаздывания, несоответствия бывают разные. Для большинства игроков всё, что им нужно сделать, это приобрести некоторые навыки прицеливания, и они могут стать опытными (вы все равно должны уметь целиться). Компенсация отставания позволяет игроку прицелиться прямо в свою цель и нажать кнопку огня (для мгновенного попадания оружия [9]). Однако иногда возникают несоответствия с точки зрения обстреливаемых игроков.
Например, если сильно запаздывающий игрок стреляет в менее запаздывающего игрока и забивает хит, менее запаздывающему игроку может показаться, что запаздывающий игрок каким-то образом "выстрелил за угол" [10]. В этом случае игрок с более низким лагом, возможно, бросился за угол. Но отстающий игрок видит все в прошлом. Для отстающего игрока, он имеет прямую линию видимости к другому игроку. Игрок выровнял прицел и нажимает кнопку огня. Тем временем игрок с низким лагом пробежал угол и, возможно, даже присел за ящик. Если игрок с высоким лагом достаточно запаздывает, скажем, на 500 миллисекунд или около того, этот сценарий вполне возможен. Затем, когда пользовательская команда отстающего игрока поступает на сервер, скрытый игрок переносится назад во времени и попадает. Это крайний случай, и в этом случае игроку с низким пингом говорится, что тот был застрелен из-за угла. Однако, с точки зрения отстающего игрока, он установил прицел на другого игрока и произвёл прямое попадание. С точки зрения геймдизайна, решение для нас было простым: пусть каждый отдельный игрок имеет полностью отзывчивое взаимодействие с миром и своим оружием.
Кроме того, описанная выше непоследовательность гораздо менее выражена в обычных боевых ситуациях. Для шутеров от первого лица, есть еще два типичных случая. Во-первых, рассмотрим двух игроков, работающих прямо друг на друга, нажав кнопку огня. В этом случае вполне вероятно, что компенсация задержки просто переместит другого игрока назад по той же линии, что и его движение. Атакованный человек будет глядеть прямо на своего убийцу и не будет чувства "пули, огибающей углы".
Следующий пример: два игрока, один целится в другого, а другой бросается вперед перпендикулярно первому игроку. В этом случае парадокс минимизируется по совершенно иной причине. Игрок, который бросается через линию прицела стрелка, вероятно, имеет (по крайней мере, в шутерах от первого лица) поле зрения 90 градусов или меньше. По сути, бегун не видит, куда целится другой игрок. Поэтому получение выстрела не будет удивительным или неправильным (вы получаете то, что заслуживаете, чтобы бегать в открытую, как маньяк). Конечно, если у вас есть танковая игра или игра, где игрок может бежать в одном направлении и смотреть в другом, то этот сценарий менее ясен, так как вы можете увидеть другого игрока, нацеленного в немного неправильном направлении.
Заключение
Компенсация лага является инструментом, чтобы исправить влияние латентности в современных экшн играх. Решение о том, следует ли внедрять эту систему, остается за разработчиком игры, поскольку решение напрямую меняет ощущение игры. Для Half-Life, Team Fortress и Counter-Strike преимущества компенсации задержек легко перевешивали отмеченные выше несоответствия.
Сноски
1. В движке Half-Life можно попросить алгоритм прогнозирования клиента учесть некоторые, но не все задержки при выполнении прогнозирования. Пользователь может управлять величиной прогноза, изменяя значение консольной переменной "pushlateny" на движке. Эта переменная является отрицательным числом, указывающим максимальное число миллисекунд для выполнения прогноза. Если число больше (по модулю), чем текущая задержка пользователя, то происходит полный прогноз до текущего времени. В этом случае пользователь чувствует нулевую задержку в своих движениях. Основываясь на некоторых ошибочных суевериях в сообществе, многие пользователи настаивали на том, что установка pushlatency в минус половину текущей средней задержки является правильной настройкой. Конечно, это все равно оставит движения игрока запаздывающими (часто описывается так, как если бы вы перемещались на коньках) на половину задержки пользователя. Вся эта путаница привела нас к выводу, что полное предсказание должно происходить все время и что переменная pushlatency должна быть удалена из движка Half-life. Назад
2. http://www.quakeforge.net/files/q1source.zip Назад
3. Обсуждение вопроса об обмане и о том, что разработчики могут сделать для его предотвращения, выходит за рамки настоящего документа. Назад
4. Хотя возможны гибриды и корректирующие методы. Назад
5. "Рывок" – это мера того, как быстро меняются силы ускорения. Назад
6. В этой статье предполагается, что часы клиента непосредственно синхронизируются с часами сервера по модулю задержки соединения. Другими словами, сервер отправляет клиенту в каждом обновлении значение часов сервера, и клиент принимает это значение в качестве своих часов. Таким образом, часы сервера и клиента всегда будут совпадать, с клиентом, выполняющим ту же самую синхронизацию несколько в прошлом (велечина в прошлом равна текущей задержке клиента). Сглаживание расхождений в часах клиента можно решить различными способами. Назад
7. Интервал между обновлениями не обязательно фиксирован. Причина в том, что во время периодов высокой активности игры (особенно для пользователей с более низкой пропускной способностью), вполне возможно, что игра захочет отправить вам больше данных, чем может вместить ваше соединение. Если бы мы были на фиксированном интервале обновления, то вам, возможно, придется ждать целый дополнительный интервал, прежде чем следующий пакет будет отправлен клиенту. Однако это не соответствует эффективной доступной пропускной способности. Вместо этого сервер после отправки каждого пакета игроку определяет, когда может быть отправлен следующий пакет. Это зависит от полосы пропускания пользователя или параметра "скорость" и количества обновлений, запрашиваемых в секунду. Если пользователь запрашивает 20 обновлений в секунду, то до отправки следующего пакета обновлений пройдет не менее 50 миллисекунд. Если ограничение пропускной способности активно (и сервер имеет достаточно высокую чачтоту обновления), это может быть 61 и т. д., за миллисекунды до отправки следующего пакета. Таким образом, пакеты Half-Life несколько разнесены между собой. Простой переход к последним целевым схемам интерполяции не ведет себя так же хорошо (подумайте о старой опорной точке для движения как о переменной) в этих условиях, как метод интерполяции истории позиций (описанный ниже). Назад
8. Что Half-Life кодирует в поле lerp_msec структуры usercmd_t, описанной ранее. Назад
9. Для оружия, стреляющего снарядами, компенсация отставания более проблематична. Например, если снаряд живет автономно на сервере, то в каком временном пространстве должен находиться снаряд? Должен ли каждый другой игрок быть "перемещен назад" каждый раз, когда снаряд готов к моделированию и перемещению сервером? Если да, то как далеко назад во времени должны быть перемещены другие игроки? Это интересные вопросы для рассмотрения. В Half-Life мы избегали их; мы просто не компенсировали лаги для объектов снарядов (это не значит, что мы не предсказывали звук вашей стрельбы снаряда клиенту, только то фактически отставание снаряда никак компенсируется). Назад
10. Фраза использованная для описания несостыковки принадлежит сообществу наших пользователей. Назад
https://developer.valvesoftwar...ign_and_Optimization
Yahn W. Bernier ([email protected]), 2001
Software Development Engineer
Содержание
1. Обзор
2. Базовая архитектура клиент-серверной игры
3. Содержание сообщений пользователя
4. Прогнозирование на стороне клиента
5. Прогнозирование на стороне клиента стрельбы из оружия
6. Умм, много работы
7. Отображение целей
8. Компенсация задержки
9. Особенности игры с компенсацией задержек
10. Заключение
11. Сноски
Обзор
Разработка онлайн игр от первого лица – сложный процесс. Однако наличие онлайн-геймплея становится надёжной гарантией успеха вашего экшена. Кроме того, ПК пространство хорошо известно тем, что разработчикам требуется поддержка широкого спектра клиентских устройств. Часто клиенты имеют не самое лучшее оборудование. То же самое справедливо и для их сетевых соединений.
Широкополосная связь была создана как панацея от всех текущих проблем онлайн-игр, однако это не то решение, которое позволяет разработчикам игнорировать латентность и другие сетевые факторы в игровых проектах. Потребуется некоторое время, прежде чем широкополосная связь действительно будет хорошо распространена в Соединенных Штатах и намного больше, чем предполагается, когда она появится для ваших клиентов в остальном мире. Кроме того, существует множество плохих широкополосных решений, где пользователи могут иногда иметь высокую пропускную способность, но также имея значительную задержку и потерю пакетов в своих соединениях.
Ваша игра должна быть хорошо приспособлена к реальному миру. Эта статья-рассуждение даст вам понимание о некоторых компромиссах, нужных для предоставления передового опыта действий в Интернете. Мы расскажем о том, как архитектура клиент-сервер работает во многих онлайн-играх. Кроме того, будет показано, как интеллектуальное моделирование можно использовать для маскировки эффектов латентности. Наконец, будет описан конкретный механизм компенсации латентности, позволяющий игре компенсировать качество соединения.
Базовая архитектура клиент-серверной игры
Большинство онлайн экшн игр, это модификации клиент-серверных игр. Такие игры, как Half-Life, включая его моды, такие как Counter-Strike и Team Fortress Classic, работают на той же системе, что и игры на движке Quake3 и Unreal Tournament. В них есть один, авторитарный сервер, который отвечает за работу основной логики игры. К нему подключены один или несколько "глупых" клиентов. Изначально эти клиенты были лишь способом ввода команд пользователями и отправки их на сервер. Сервер выполнял входящие команды, обновлял состояния всех других объектов, а затем отправлял клиенту список объектов для рендеринга. Разумеется, реальные системы имеют больше компонентов, но упрощенное определение полезно при последующих размышлениях о предсказании и компенсации отставания.
Учитывая это, типичная архитектура движка клиент-серверной игры выглядит примерно так:
Для этого рассуждения-статьи все сообщения и координации, необходимые для запуска соединения между клиентом и сервером, опущены. Игровой цикл клиента выглядит примерно так:
1. Сохранение времени старта
2. Обработка пользовательского ввода
3. Обновление и отправка пакета с командой перемещения используя время моделирования прошлого кадра
4. Чтение пакетов пришедших от сервера по сети
5. Использование пакетов для определения видимых объектов и их состояния
6. Рендеринг сцены
7. Сохранение времени завершения
8. Время завершения минус время старта – это время выполнения для следующего кадра
Каждый раз, когда клиент завершает очередной шаг, "время цикла" за который был выполнен шаг цикла, используется для определения того, сколько времени потребуется для моделирования на следующем шаге. Если ваша частота кадров полностью постоянна, то время цикла будет правильной мерой. В противном случае временные рамки будут неправильными, и не существует решения этой проблемы (вы не можете определить, сколько времени потребуется, для следующей итерации игрового цикла, прежде чем запускать его ...).
Сервер имеет схожий цикл:
1. Сохранение времени старта
2. Чтение пользовательского ввода пришедшего по сети
3. Выполнение сообщений пользовательского ввода клиента
4. Моделирование серверных объектов с использованием времени моделирования от последнего полного прохода
5. Для каждого подключенного клиента упаковать видимые объекты/состояния мира и отправить клиенту
6. Сохранить время завершения
7. Время завершения минус время старта – это время моделирования для следующего кадра
В этой модели объекты, не связанные с игроком, выполняются исключительно на сервере, в то время как объекты игрока управляются движениями на основе входящих пакетов. Конечно, это не единственный возможный способ выполнить эту задачу, но наиболее разумный.
Содержание сообщений пользователя
В игровом движке Half-Life формат сообщений пользовательских команд довольно прост и инкапсулирован в структуру данных, содержащую только несколько важных полей:
- typedef struct usercmd_s
- {
- // Интерполяция времени относительно клиента
- short lerp_msec;
- // Длительность в мс команды
- byte msec;
- // Command view angles.
- // Угол обзора
- vec3_t viewangles;
- // intended velocities
- // предполагаемые скорости
- // Forward velocity.
- float forwardmove;
- // Sideways velocity.
- float sidemove;
- // Upward velocity.
- float upmove;
- // Attack buttons
- unsigned short buttons;
- //
- // Additional fields omitted...
- //
- } usercmd_t;
Важными полями здесь являются msec, viewangles, forward, side, upmove и buttons. Поле msec соответствует времени выполнения команды (это время цикла). Поле viewangles – это вектор направления в котором игрок смотрел во время кадра. Forward, side и upmove поля – это импульсы, определяемые путем изучения клавиатуры, мыши и джойстика, считываемые, если удерживались какие-либо клавиши перемещения. Наконец, поле buttons – это просто поле с одним или несколькими битами, установленными для каждой удерживаемой кнопки.
Учитывая приведенные выше структуры данных и архитектуру клиент-сервера, ядро моделирования выглядит следующим образом. Во-первых, клиент создает и отправляет пользовательскую команду на сервер. Затем сервер выполняет её и отправляет обновленные позиции всего обратно клиенту. Наконец, клиент отображает сцену со всеми текущими объектами. Это ядро хотя и довольно простое, не очень хорошо реагирует на ситуации в реальности, где пользователи могут испытывать значительную задержку в своих интернет-соединениях. Основная проблема заключается в том, что клиент поистине "глупый", и все, что он делает, – это простая задача чтения ввода и ожидание результатов ответа сервера. Если у клиента есть 500 миллисекунд задержек при его подключении к серверу, то для любых действий клиента, которые должны быть подтверждены сервером, потребуется 500 миллисекунд, чтобы результаты были заметны на клиенте. Хотя эта задержка в оба конца может быть приемлемой для локальной сети (LAN), она неприемлема в Интернете.
Прогнозирование на стороне клиента
Одним из способов улучшения этой проблемы является локальное выполнение команд на клиенте предполагая, что сервер немедленно примет и подтвердит команды клиента. Этот метод помечен как предсказание на стороне клиента.
Предсказание движений требует от нас отказаться от "глупого" или правила минималистичного клиента. Это не означает, что клиент полностью контролирует свое моделирование, как в одиночной игре. По-прежнему существует авторитарный сервер, на котором выполняется симуляция, как указано выше. Наличие авторитарного сервера означает, что даже если клиент имитирует результаты отличные от серверных, он исправит свои ошибки получив ответы от сервера. Из-за латентности в соединении коррекция может не произойти до тех пор, пока не закончится время очередного шага игрового цикла. Недостатком является то, что это может вызвать очень заметный сдвиг в позиции игрока из-за накопления ошибки прогнозирования, произошедшей ранее.
Чтобы реализовать предсказание движения у клиента, используется следующая процедура. Как и раньше, обрабатывается пользовательский ввод и формируются команды клиента. Как и ранее, команда отправляется на сервер. Однако каждая команда и точное время её создания хранится на клиенте. Их потом использует алгоритм прогнозирования.
Для прогнозирования в качестве отправной точки использует последнее подтверждение с сервера. Оно указывает, какая команда клиента была последний раз использована сервером, а также указывает точное местоположение (и другие данные состояния) игрока после того, как эта команда движения была смоделирована на сервере. Последняя подтвержденная команда будет где-то в прошлом, если есть какое-либо отставание в соединении. Например, если клиент работает со скоростью 50 кадров в секунду (fps) и имеет 100 миллисекунд задержки (roundtrip), тогда клиент будет хранить пять пользовательских команд перед последним, подтвержденным сервером. Эти пять пользовательских команд моделируются на клиенте как часть прогнозирования на стороне клиента. Предполагая полное предсказание [1], клиент захочет начать с последних данных с сервера, а затем запустить пять пользовательских команд с помощью "аналогичной логики" на то, что использует сервер для моделирования движения клиента. Выполнение этих команд должно давать точное конечное состояние на клиенте (наиболее важна позиция конечного игрока), которая может использоваться для определения какую позицию использовать для рендеринга сцены во время текущего кадра.
В Half-Life минимизация расхождений между клиентом и сервером в логике прогнозирования достигается путем совместного использования идентичного кода движения для игроков как в коде на стороне сервера, так и в коде на стороне клиента. Это подпрограммы в pm_shared/ (имя папки "совместное перемещение игрока") HL SDK. Входные данные для общих процедур инкапсулируются командой пользователя и состоянием "из" игрока. Результатом является новое состояние игрока после выдачи команды пользователя. Общий алгоритм у клиента выглядит следующим образом:
- "from state" <- state after last user command acknowledged by the server; состояние после последней пользовательской команды, подтвержденной сервером;
- "command" <- first command after last user command acknowledged by server; первая команда после последней команды пользователя, подтвержденной сервером;
- while (true)
- {
- run "command" on "from state" to generate "to state"; <- to state – состояние)
- if (this was the most up to date "command"
- break;
- "from state" = "to state";
- "command" = next "command";
- };
Исходная и другая информация о состоянии в окончательном состоянии "to state" (состояние) является результатом прогнозирования и используется для рендеринга сцены в каждом кадре. Часть, где выполняется команда, – это просто часть, где все данные состояния игрока копируются в общую структуру данных, обрабатывают пользовательские команды (путем выполнения общего кода в подпрограммах pm_shared в случае Half-Life), и полученные данные копируют обратно в "to state".
Есть несколько важных замечаний к этой системе. Во-первых, вы заметите, что от клиента зависит задержка и насколько быстро клиент генерирует команды пользователя (т.е. клиентский игровой цикл), клиент чаще всего оказываются под управлением той же команды снова и снова, пока они, наконец, не подтверждены на сервере и удалены из списка (скользящие окна в случае Half-life) команд, которых только предстоит подтвердить. Первое предостережение заключается в том, как обрабатывать любые звуковые эффекты и визуальные эффекты, созданные в общем коде. Поскольку команды могут выполняться снова и снова, важно не создавать звуки шагов и т.д. несколько раз при повторном запуске старых команд для обновления прогнозируемой позиции. Кроме того, важно, чтобы сервер не отправлял клиентские эффекты, которые уже предсказываются на клиенте. Однако, клиент должен повторно запустить старые команды, иначе не будет никакого способа для сервера, чтобы исправить ошибочное предсказание клиентом. Решение этой проблемы очень просто: клиент просто отмечает те команды, которые еще не были предсказаны и играет только если команда пользователя выполняется в первый раз.
Другое предостережение касается данных состояния, которые существуют исключительно на клиенте и не являются частью достоверных данных обновлённых с сервера. Если у вас нет такого типа данных, вы можете просто использовать последнее подтвержденное состояние сервера в качестве отправной точки и выполнить предсказанные команды пользователя "на месте" для этих данных, чтобы прийти к конечному состоянию (которое включает вашу позицию для рендеринга). В этом случае не требуется сохранять все промежуточные результаты вдоль маршрута для прогнозирования от последнего подтвержденного состояния до текущего времени. Однако, если вы делаете какую-либо логику полностью на стороне клиента (эта логика может включать такие функции, как определение положения глаз, это когда вы находитесь на корточках — и это на самом деле не полностью на стороне клиента, так как сервер тоже имитирует эти данные), что влияет на поля, которые не копируются с сервера на клиент на сетевой уровень обработки информации о состоянии игрока, то вам нужно будет сохранить промежуточные результаты прогнозирования. Это можно сделать с помощью скользящего окна, где "to state" находится в начале, а затем каждый раз, когда вы запускаете пользовательскую команду через предсказание, вы заполняете следующее состояние в окне. Когда сервер, наконец, признает получение одной или нескольких команд, которые были предсказаны, то это уже простой вопрос поиска, какое состояние сервер подтверждает, и простое копирование данных, которые полностью на стороне клиента в новое начало или "to state".
Выше мы расписали процедуру которая описывает, как выполнить прогноз движений на стороне клиента. Эта система аналогична системе, в QuakeWorld [2].
Прогнозирование на стороне клиента стрельбы из оружия
Добавление предсказания к стрельбе из оружия к вышеупомянутой системе предсказаний движений происходит просто. Разумеется, для локального игрока необходима дополнительная информация о состоянии клиента, в том числе о том, какое оружие хранится, какое из них активно, и сколько боеприпасов осталось у каждого из этих оружия. С помощью этой информации, логика стрельбы может быть наложена поверх логики движений, поскольку, ещё раз, состояние кнопок стрельбы включаются в структуру данных пользовательских команд, которая совместно используется клиентом и сервером. Конечно, сложность возрастёт, если фактическая логика оружия отличается между клиентом и сервером. В Half-Life мы решили избежать этого осложнения, переместив реализацию логики стрельбы оружия в "общий код", как и код движения игрока. Все переменные, которые способствуют определению состояния оружия (например, боеприпасы, когда следующий выстрел оружия может произойти, какая анимация у оружия и т.д. ), также являются частью главного состояния сервера и копируются на стороне клиента, поэтому они могут использоваться для прогнозирования состояния оружия.
Прогнозирование стрельбы на стороне клиента, вероятно, приведет к решению и прогнозировать переключение оружия, развертывание и убирание. Таким образом, пользователь будет чувствовать, что игра на 100% реагирует на его движения и активацию оружия. Это имеет большое значение для уменьшения ощущения зввисания и тормозов, которое пережили многие игроки используя интернет.
Хм, много лишней работы?
Репликация необходимых полей клиенту и обработка всех промежуточных состояний – это большая работа. На этом этапе вы можете спросить, почему бы не убрать все связанное с сервером и просто сообщить клиенту, где он/она находится после каждого движения? Другими словами, почему бы не бросить серверную хрень и просто запустить движение и оружие исключительно на стороне клиента? Затем клиент просто отправил бы результаты на сервер примерно так: "я сейчас нахожусь в позиции x, и, кстати, я просто выстрелил игроку 2 в голову." Это нормально, если вы можете доверять клиенту. Именно так работают многие системы военного моделирования (т. е. они являются закрытой системой и доверяют всем клиентам). Так обычно работают одноранговые игры. Для Half-Life этот механизм неработоспособен из-за реальных опасений обмана. Если бы мы инкапсулировали абсолютные данные состояния таким образом, мы бы подняли мотивацию для взлома клиента даже выше, чем это уже есть [3]. Для наших игр этот риск слишком высок, и мы возвращаемся к требованию авторитарного сервера.
Система, в которой движения и эффекты оружия предсказываются на стороне клиента, является очень работоспособной системой. Например, это система, которую поддерживает Quake3 engine. Одна из проблем с этой системой заключается в том, что вам все еще нужно ощущать задержку, чтобы определить, как вести свои цели (для мгновенного попадания оружия). Другими словами, хотя вы сразу слышите выстрелы из оружия, и ваша позиция полностью обновлена, результаты ваших выстрелов по-прежнему подвержены задержкам. Например, если вы нацелены на игрока, Бегущего перпендикулярно вашему виду, и у вас есть задержка 100 миллисекунд, а игрок бежит со скоростью 500 единиц в секунду, вам нужно будет нацелить 50 единиц перед целью, чтобы поразить цель мгновенным оружием. Чем больше задержка, тем больше необходимо целится наперёд. Получить "чувство" для вашей латентности трудно. Quake3 попытался смягчить это, играя короткий звук, когда ваши попадания подтверждены. Таким образом, вы могли бы выяснить, как далеко продвинутся, стреляя очередями и регулируя прицел, пока не услышите стабильный поток звуков. Очевидно, что с достаточной задержкой и противником, который активно уклоняется, довольно сложно получить достаточную обратную связь, чтобы последовательно сосредоточиться на противнике. Если ваш пинг колеблется, это может быть еще сложнее.
Отображение целей
Еще одним важным аспектом, влияющим на то, как пользователь воспринимает отзывчивость мира, является механизм определения на клиенте, где показывать других игроков. Два основных механизма для определения места отображения объектов – экстраполяция и интерполяция [4].
Для экстраполяции другой игрок/объект моделируется вперед во времени от последнего известного мечта, направления и скорости более или менее баллистическим способом. Таким образом, если вы отстаете на 100 миллисекунд, и последнее обновление, которое вы получили, состояло в том, что (как указано выше) другой игрок бежал 500 единиц в секунду перпендикулярно вашему взгляду, тогда клиент может предположить, что в "реальном времени" игрок переместился на 50 единиц прямо вперед от этой последней известной позиции. Затем клиент может просто нарисовать игрока в этой экстраполированной позиции, и локальный игрок все равно может более или менее нацелиться прямо на другого игрока.
Самый большой недостаток экстраполяции заключается в том, что движения игрока очень не баллистические, а очень недетерминированные и с большими рывками [5]. Уровнем выше нереалистичные модели физики игрока, которые используют большинство игр FPS, где игрок может мгновенно поворачиваться и применять нереалистичные силы для создания огромных ускорений под произвольными углами, и вы увидите, что экстраполяция довольно часто неверна. Разработчик может смягчить ошибку, ограничив время экстраполяции разумным значением (например, QuakeWorld ограничил экстраполяцию до 100 миллисекунд). Это ограничение помогает, потому что, как только истинная позиция игрока, наконец, получена, будет ограниченное количество корректирующих деформаций. В мире, где большинство игроков все еще имеют задержку более 150 миллисекунд, игрок все равно должен вести прицел предугадывая движения других игроков, чтобы поразить их. Если эти игроки "прыгают" на новые места из-за ошибок экстраполяции, то процесс игры становится ужасным.
Другой метод определения места отображения объектов и игроков — интерполяция. Интерполяция может быть рассмотрена как всегда движущиеся объекты несколько в прошлом по отношению к последней допустимой позиции, полученной для объекта. Например, если сервер отправляет 10 обновлений в секунду (точно) состояния world, то мы можем наложить задержку интерполяции в 100 миллисекунд в нашем рендеринге. Затем, когда мы отрисовываем кадры, мы интерполируем положение объекта между последней обновленной позицией и позицией, обновленной до этого (или последняя позиция отрисовки) за эти 100 миллисекунд. По мере того, как объект только попадает в последнюю обновленную позицию, мы получаем новое обновление с сервера (так как 10 обновлений в секунду означает, что обновления приходят каждые 100 миллисекунд), мы можем начать двигаться к этой новой позиции в течение следующих 100 миллисекунд.
Если один из пакетов обновления не приходит, то есть два варианта: мы можем начать экстраполировать позицию игрока, как указано выше (с большими потенциальными ошибками), или мы можем просто оставить игрока на позиции в последнем обновлении, пока не придет новое обновление (в результате чего движение игрока заикаться).
Общий алгоритм для этого типа интерполяции выглядит следующим образом:
1. Каждое обновление содержит отметку времени сервера на момент его создания [6]
2. От текущего времени клиента клиент вычисляет конечное время путем вычитания дельты времени интерполяции (100 мс)
3. Если конечное время находится между отметкой времени последнего обновления и предыдущей, то эти метки времени определяют, какая часть промежутка времени прошла.
4. Это отношение используется для интерполяции любых значений (например, положения и углов).
По сути, в приведенном выше примере интерполяцию можно представить как буферизацию дополнительных 100 миллисекунд данных на клиенте. Таким образом, другие игроки рисуются там, где они были в прошлом, что равно вашей точной задержке плюс количество времени, в течение которого вы интерполируете. Чтобы иметь дело со случайным отбрасыванием пакета, мы могли бы установить время интерполяции в 200 миллисекунд вместо 100 миллисекунд. Это (опять же, предполагая 10 обновлений в секунду с сервера) позволит нам полностью пропустить одно обновление и по-прежнему интерполировать игрока к действительной позиции, часто проходя через эту интерполяцию без сучка и задоринки. Конечно, интерполяция с помощью большего времени компромисс, потому что он торгует дополнительные задержки (сделав интерполяцию игрока усложним атаку), для визуальной гладкости.
Кроме того, описанный выше тип интерполяции (когда клиент отслеживает только последние два обновления и всегда движется непосредственно к последнему обновлению) требует фиксированного интервала времени между обновлениями сервера. Метод также страдает от визуальных проблем качества, которые трудно решить. Проблема визуального качества заключается в следующем. Представьте, что интерполируемым объектом является прыгающий мяч (который на самом деле точно описывает некоторых наших игроков). В крайних случаях мяч либо находится высоко в воздухе, либо ударяется о тротуар. Однако в среднем мяч находится где-то посередине. Если мы интерполируем только до последней позиции, то очень вероятно, что эта позиция находится не на земле или в верхней точке. Прыжок шарика "сглажен" и он кажется никогда не ударяет землю. Это классическая проблема дискретизации и может быть облегчена путем повышения частоты измерения состояния мира. Однако, мы всё ещё вероятно, никогда не имеем актуальную интерполяцию состояния на земле или в высшей точке, хотя это всё равно сгладит позиции.
Кроме того, поскольку разные пользователи имеют разные подключения, принудительное обновление на шаге, например 10 обновлений в секунду, приводит к неоправданному снижению общего знаменателя скорости чоединения для пользователей. В Half-Life мы позволяем пользователю запрашивать столько обновлений в секунду, сколько он или она хочет (в пределах лимита). Таким образом, пользователь с быстрым подключением мог получать 50 обновлений в секунду, если желает. По умолчанию Half-Life отправляет 20 обновлений в секунду каждому игроку, клиент Half-Life интерполирует игроков (и многие другие объекты) в течение 100 миллисекунд. [7]
Чтобы избежать проблемы сглаживания прыгающего шара, мы используем другой алгоритм интерполяции. В этом методе мы сохраняем более полную "историю позиций" для каждого объекта, который может быть интерполирован.
История позиций – это метка времени, начало координат и углы (и может включать любые другие данные, которые мы хотим интерполировать) для объекта. Каждое обновление, которое мы получаем от сервера, создает новую запись истории позиций, включая метку времени и начало/углы для этой метки времени. Чтобы интерполировать, мы вычисляем конечное время, как указано выше, на затем мы ищем назад по истории позиций, ища пару обновлений, которые охватывают конечное время. Затем мы используем их для интерполяции и вычисления конечного положения для этого кадра. Это позволяет нам плавно следовать кривой, которая полностью включает все наши опорные точки. Если мы работаем на более высокой частоте кадров, чем скорость входящего обновления, мы почти уверены в плавном перемещении по точкам выборки, тем самым сводя к минимуму (но не устраняя, конечно, так как полная частота дискретизации мировых обновлений является ограничивающим фактором) проблему сглаживания, описанную выше.
Одна проблема, мы должны добавить поверх любой интерполяционной схемы какой-то способ определить, что объект был принудительно телепортирован, а не просто двигался очень быстро. В противном случае мы могли бы "плавно " перемещать объект на большие расстояния, заставляя объект выглядеть слишком быстрым. Мы можем либо установить флаг в обновлении, который говорит: "не интерполировать" или "очистить историю позиций", или мы можем определить, является ли расстояние между источником и одним обновлением и другим слишком большим, и, таким образом, предполагая телепортацию/деформацию. В этом случае решение, вероятно, состоит в том, чтобы просто переместить объект в последнюю известную позицию и начать интерполяцию оттуда.
Компенсация задержки
Понимание интерполяции важно при разработке компенсации запаздывания, поскольку интерполяция является другим типом задержки в работе пользователя. В той степени, в которой игрок смотрит на другие объекты, то количество интерполяции, которое должны быть принято во внимание при вычислении на сервере верности цели игрока.
Компенсация лагов – это метод нормализации на стороне сервера состояния мира для каждого игрока по мере выполнения пользовательских команд этого игрока. Вы можете думать о компенсации задержки как о том, чтобы сделать шаг назад во времени, на сервере, и посмотреть на состояние мира в тот момент, когда пользователь выполнил какое-то действие. Алгоритм работы следующий:
1. Перед выполнением команды текущего пользователя сервера:
1.1. Вычисляет довольно точную задержку для игрока
1.2. Ищет в истории сервера (для текущего игрока) обновление мира, которое было отправлено игроку и получено игроком непосредственно перед тем, как игрок выдал бы команду движения
1.3. Из этого обновления (и следующего за ним на основе точного конечного времени) для каждого игрока в обновлении переместите других игроков назад во времени точно туда, где они были, когда была создана пользовательская команда текущего игрока. Это перемещение назад должно учитывать как задержку подключения, так и сумму интерполяции [8], которую использовал клиент.
2. Разрешить выполнение команды пользователя (включая любые команды стрельбы из оружия и т. д., который будет запускать броски лучей против всех других игроков в их "старых" позициях).
3. Переместить всех двигавшихся/застрявших игроков назад на их корректные/текущие позиции.
Обратите внимание, что на шаге, на котором мы перемещаем игрока назад по времени, это может фактически потребовать принудительного получения дополнительной информации о прошлом состоянии (например, был ли игрок жив или мертв, или игрок уклонялся). Конечный результат компенсации отставания заключается в том, что каждый местный клиент может напрямую нацелиться на других игроков, не беспокоясь о том, чтобы возглавить свою цель, чтобы забить удар. Конечно, такое поведение является компромиссом игрового дизайна.
Особенности игры с компенсацией задержек
Введение компенсации задержки позволяет каждому игроку работать на по их собственному времени без видимой задержки. В этой связи важно понимать, что могут возникать определенные парадоксы или несоответствия. Конечно, старая система с авторитарным сервером и "глупыми" или простыми клиентами имела свои парадоксы. В конце концов, выбор компромисса – это компромисс в разработке игр. Для Half-Life мы считаем, что компромисс в пользу компенсации отставания было оправданным решением для игрового дизайна.
Первая проблема старой системы заключалась в том, что вы должны были вести свою цель на некотором смещении, связанным с вашей задержкой на сервере. Прицеливание непосредственно в другого игрока и нажатие кнопки огня почти гарантировало пропуск этого игрока. Непорядок здесь заключается в том, что прицеливание просто нереалистично и что элементы управления игрока имеют непредсказуемую отзывчивость.
С компенсацией запаздывания, несоответствия бывают разные. Для большинства игроков всё, что им нужно сделать, это приобрести некоторые навыки прицеливания, и они могут стать опытными (вы все равно должны уметь целиться). Компенсация отставания позволяет игроку прицелиться прямо в свою цель и нажать кнопку огня (для мгновенного попадания оружия [9]). Однако иногда возникают несоответствия с точки зрения обстреливаемых игроков.
Например, если сильно запаздывающий игрок стреляет в менее запаздывающего игрока и забивает хит, менее запаздывающему игроку может показаться, что запаздывающий игрок каким-то образом "выстрелил за угол" [10]. В этом случае игрок с более низким лагом, возможно, бросился за угол. Но отстающий игрок видит все в прошлом. Для отстающего игрока, он имеет прямую линию видимости к другому игроку. Игрок выровнял прицел и нажимает кнопку огня. Тем временем игрок с низким лагом пробежал угол и, возможно, даже присел за ящик. Если игрок с высоким лагом достаточно запаздывает, скажем, на 500 миллисекунд или около того, этот сценарий вполне возможен. Затем, когда пользовательская команда отстающего игрока поступает на сервер, скрытый игрок переносится назад во времени и попадает. Это крайний случай, и в этом случае игроку с низким пингом говорится, что тот был застрелен из-за угла. Однако, с точки зрения отстающего игрока, он установил прицел на другого игрока и произвёл прямое попадание. С точки зрения геймдизайна, решение для нас было простым: пусть каждый отдельный игрок имеет полностью отзывчивое взаимодействие с миром и своим оружием.
Кроме того, описанная выше непоследовательность гораздо менее выражена в обычных боевых ситуациях. Для шутеров от первого лица, есть еще два типичных случая. Во-первых, рассмотрим двух игроков, работающих прямо друг на друга, нажав кнопку огня. В этом случае вполне вероятно, что компенсация задержки просто переместит другого игрока назад по той же линии, что и его движение. Атакованный человек будет глядеть прямо на своего убийцу и не будет чувства "пули, огибающей углы".
Следующий пример: два игрока, один целится в другого, а другой бросается вперед перпендикулярно первому игроку. В этом случае парадокс минимизируется по совершенно иной причине. Игрок, который бросается через линию прицела стрелка, вероятно, имеет (по крайней мере, в шутерах от первого лица) поле зрения 90 градусов или меньше. По сути, бегун не видит, куда целится другой игрок. Поэтому получение выстрела не будет удивительным или неправильным (вы получаете то, что заслуживаете, чтобы бегать в открытую, как маньяк). Конечно, если у вас есть танковая игра или игра, где игрок может бежать в одном направлении и смотреть в другом, то этот сценарий менее ясен, так как вы можете увидеть другого игрока, нацеленного в немного неправильном направлении.
Заключение
Компенсация лага является инструментом, чтобы исправить влияние латентности в современных экшн играх. Решение о том, следует ли внедрять эту систему, остается за разработчиком игры, поскольку решение напрямую меняет ощущение игры. Для Half-Life, Team Fortress и Counter-Strike преимущества компенсации задержек легко перевешивали отмеченные выше несоответствия.
Сноски
1. В движке Half-Life можно попросить алгоритм прогнозирования клиента учесть некоторые, но не все задержки при выполнении прогнозирования. Пользователь может управлять величиной прогноза, изменяя значение консольной переменной "pushlateny" на движке. Эта переменная является отрицательным числом, указывающим максимальное число миллисекунд для выполнения прогноза. Если число больше (по модулю), чем текущая задержка пользователя, то происходит полный прогноз до текущего времени. В этом случае пользователь чувствует нулевую задержку в своих движениях. Основываясь на некоторых ошибочных суевериях в сообществе, многие пользователи настаивали на том, что установка pushlatency в минус половину текущей средней задержки является правильной настройкой. Конечно, это все равно оставит движения игрока запаздывающими (часто описывается так, как если бы вы перемещались на коньках) на половину задержки пользователя. Вся эта путаница привела нас к выводу, что полное предсказание должно происходить все время и что переменная pushlatency должна быть удалена из движка Half-life. Назад
2. http://www.quakeforge.net/files/q1source.zip Назад
3. Обсуждение вопроса об обмане и о том, что разработчики могут сделать для его предотвращения, выходит за рамки настоящего документа. Назад
4. Хотя возможны гибриды и корректирующие методы. Назад
5. "Рывок" – это мера того, как быстро меняются силы ускорения. Назад
6. В этой статье предполагается, что часы клиента непосредственно синхронизируются с часами сервера по модулю задержки соединения. Другими словами, сервер отправляет клиенту в каждом обновлении значение часов сервера, и клиент принимает это значение в качестве своих часов. Таким образом, часы сервера и клиента всегда будут совпадать, с клиентом, выполняющим ту же самую синхронизацию несколько в прошлом (велечина в прошлом равна текущей задержке клиента). Сглаживание расхождений в часах клиента можно решить различными способами. Назад
7. Интервал между обновлениями не обязательно фиксирован. Причина в том, что во время периодов высокой активности игры (особенно для пользователей с более низкой пропускной способностью), вполне возможно, что игра захочет отправить вам больше данных, чем может вместить ваше соединение. Если бы мы были на фиксированном интервале обновления, то вам, возможно, придется ждать целый дополнительный интервал, прежде чем следующий пакет будет отправлен клиенту. Однако это не соответствует эффективной доступной пропускной способности. Вместо этого сервер после отправки каждого пакета игроку определяет, когда может быть отправлен следующий пакет. Это зависит от полосы пропускания пользователя или параметра "скорость" и количества обновлений, запрашиваемых в секунду. Если пользователь запрашивает 20 обновлений в секунду, то до отправки следующего пакета обновлений пройдет не менее 50 миллисекунд. Если ограничение пропускной способности активно (и сервер имеет достаточно высокую чачтоту обновления), это может быть 61 и т. д., за миллисекунды до отправки следующего пакета. Таким образом, пакеты Half-Life несколько разнесены между собой. Простой переход к последним целевым схемам интерполяции не ведет себя так же хорошо (подумайте о старой опорной точке для движения как о переменной) в этих условиях, как метод интерполяции истории позиций (описанный ниже). Назад
8. Что Half-Life кодирует в поле lerp_msec структуры usercmd_t, описанной ранее. Назад
9. Для оружия, стреляющего снарядами, компенсация отставания более проблематична. Например, если снаряд живет автономно на сервере, то в каком временном пространстве должен находиться снаряд? Должен ли каждый другой игрок быть "перемещен назад" каждый раз, когда снаряд готов к моделированию и перемещению сервером? Если да, то как далеко назад во времени должны быть перемещены другие игроки? Это интересные вопросы для рассмотрения. В Half-Life мы избегали их; мы просто не компенсировали лаги для объектов снарядов (это не значит, что мы не предсказывали звук вашей стрельбы снаряда клиенту, только то фактически отставание снаряда никак компенсируется). Назад
10. Фраза использованная для описания несостыковки принадлежит сообществу наших пользователей. Назад