Последние рекорды серверов
Pro Nub

Физика Bhop

Опубликовано Kpoluk 13 Мая 2023 в 11:58
На этот раз мы столкнёмся с параметром fuser2, который был введён в CS 1.6, а значит нам придётся вместо pm_shared.cpp из HLSDK использовать pm_shared.cpp из проекта ReGameDLL.

Рассмотрим подробнее функцию PM_PlayerMove, упомянутую в статье о физике стрейфов:


Как видим, есть несколько типов движения, и рассмотренные нами функции PM_Friction, PM_WalkMove и PM_AirMove относятся к типу MOVETYPE_WALK. С PM_Duck, отвечающей за процесс приседания, мы познакомимся в следующей статье, а пока нас будет интересовать вызов PM_Jump перед PM_Friction. Обратите внимание, что этот вызов происходит в том случае, если в переменной cmd.buttons, хранящей состояния кнопок, выставлен бит IN_JUMP. Когда мы нажимаем кнопку прыжка, то движку посылается команда +jump, и бит IN_JUMP становится равным 1. При отпускании кнопки посылается команда –jump, и в этот же фрейм IN_JUMP обнуляется. Подробнее об этом и о том, почему мы можем прыгать с помощью скролла, можете почитать под спойлером

Заглянем в input.cpp и найдём функцию InitInput. В ней мы обнаружим, что команда +jump инициирует вызов функции IN_JumpDown, которая изменяет переменную state (состояние) кнопки прыжка: выставляются первый и второй биты state. Первый бит значит, что кнопка зажата вообще, а второй – что кнопка зажата только что. Команда –jump инициирует вызов IN_JumpUp, которая обнуляет у state первый бит (кнопка отжата) и выставляет третий (кнопка отпущена только что).

Далее в CL_ButtonBits находим, что бит IN_JUMP в cmd.buttons выставляется в том случае, если в state стоят первый либо второй биты. Таким образом, если мы прыгнули с помощью, например, пробела, то IN_JUMP будет выставлен начиная с фрейма, в который мы нажали пробел. Во фрейм, когда пробел был отпущен, бит IN_JUMP уже обнулён. Если же мы прыгнули при помощи скролла, то команды +jump и –jump были отправлены в одном фрейме прямо друг за другом. Тогда в этом фрейме первый бит у state нулевой, то есть кнопка прыжка считается отжатой, однако поскольку второй бит у state выставлен, то IN_JUMP в cmd.buttons будет также выставлен, а значит PM_Jump будет вызвана. Вот почему мы можем прыгать на скролл.


Теперь посмотрим на саму функцию PM_Jump:


Заметьте, что если в предыдущем фрейме имелся бит IN_JUMP, то в текущем фрейме прыжка уже не будет. В противном случае мы могли бы делать bhop, просто зажав пробел. Кроме того, такое условие означает, что отправленная при помощи скролла команда +jump;-jump позволит оттолкнуться от земли только в том случае, если команды прыжка не было в предыдущем фрейме. Как это влияет на bhop?

Введём термин FOG (frames on the ground) – количество фреймов, которые игрок проводит на земле во время одного bhop’а.

Теоретически bhop можно делать и с помощью пробела, однако человеческой реакции не хватает, чтобы вовремя отправить команду прыжка. Прокруткой скролла мы отправляем сразу несколько команд прыжка, что позволяет нам минимизировать FOG, а значит потерять меньше скорости. Однако слишком резкая прокрутка скролла приведёт к тому, что несколько команд прыжка могут идти подряд, а значит, вызвать прыжок сможет только первая из них, остальные окажутся бесполезны. Хорошее распределение команд прыжка – это когда мы чередуем фреймы с +jump и фреймы без +jump. В таком случае мы гарантируем себе 1 или 2 FOG. На практике распределения у всех игроков разные, но, тем не менее, с опытом всем удаётся минимизировать количество bhop’ов с FOG больше 2.

Горизонтальная скорость


Однако всегда ли уменьшение FOG означает минимальные потери скорости? Прежде всего, следует уточнить, что нас интересует сейчас горизонтальная скорость, ведь именно горизонтальную скорость перед попаданием на землю показывает нам статистика прыжков в качестве престрейфа при bhop’е. Для простоты пока что не будем обращать внимания на переменную fuser2, а вертикальная скорость на земле пусть будет равна нулю. Рассмотрим два случая:

1) bhop в 1 FOG. Успешный прыжок произошёл в тот же фрейм, когда мы оказались на земле. Внутри PM_Jump переменная pmove->onground стала равной -1, поэтому PM_Friction в PM_PlayerMove вызвана не будет. Единственная функция, способная повлиять на горизонтальную скорость, это PM_PreventMegaBunnyJumping в PM_Jump. Вот как она выглядит:


Здесь pmove->maxspeed (с usp или ножом) равен 250 юнитов/с, значит maxscaledspeed = 300 юнитов/с. Если скорость больше, чем 300, то она становится равной maxscaledspeed * 0.8 = 240 юнитов/с (сохраняя направление). В противном случае скорость вообще не поменяется!

2) bhop в 2 FOG. Успешный прыжок произошёл спустя один фрейм после приземления. В первый фрейм на земле на нас подействовала только PM_Friction, отняв 4 процента от скорости, а во второй – только PM_PreventMegaBunnyJumping. То есть в первый фрейм скорость в любом случае потеряется, но если после этого она окажется не больше 300 юнитов/с, то второй фрейм пройдёт уже без потерь.

Таким образом, мы смогли бы обойтись без потерь скорости, если бы совершали bhop в 1 FOG при скорости не больше 300 юнитов/с. Однако на практике большие расстояния между блоками и необходимость менять направление во время bhop’а вынуждают нас набирать скорость больше 300. И тогда оказывается выгоднее сделать bhop в 2 FOG! К примеру, пусть наша скорость перед приземлением равна 310 юнитов/с. Тогда bhop в 1 FOG урежет её до 240, а в 2 FOG до 297.6 юнитов/с. Ниже мы уточним эти вычисления с учётом вертикальной скорости и переменной fuser2, однако качественно результат не изменится.

Если контролировать скорость с помощью стрейфов мы ещё как-то можем, то контролировать количество FOG физиологически невозможно. Прокрутка скролла может дать нам высокий шанс сделать FOG меньше 3, однако будет это 1 или 2 FOG – отчасти опыт игрока, отчасти элемент случайности. С одной стороны такая зависимость от удачи не очень приятна, когда речь идёт о прохождении на время. С другой стороны непредсказуемость FOG учит игрока до каждого bhop’а предполагать возможность худшего варианта, а после bhop’а ориентироваться по ситуации. И в этом заключается ещё одна изюминка kreedz, из-за которой он стал интересен такому большому количеству людей.

Вертикальная скорость


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


Здесь ent_gravity можно считать равным 1, pmove->movevars->gravity как раз и есть значение sv_gravity (по умолчанию 800), а длительность фрейма в секундах pmove->frametime при 100 fps равна 0.01. Итого, внутри функции вертикальная скорость уменьшится на 4 юнита/с. Если внутри PM_PlayerMove не вызывается PM_Jump, то вызовы PM_AddCorrectGravity и PM_FixupGravityVelocity уменьшат вертикальную скорость на 8 юнитов/с, после чего, в случае если мы находимся на земле, она обнулится. Если же вызывается PM_Jump, то в ней вертикальная скорость становится равной sqrt(2 * 800 * 45) = 268.33 юнита/с, после чего уменьшается на 8 юнитов/с за счёт двух вызовов PM_FixupGravityVelocity (внутри самой PM_Jump и позже в PM_PlayerMove).

Отсюда легко получить высоту прыжка. Можно считать, что на земле начальная скорость составляет V0 = sqrt(2 * 800 * 45) юнита/с, ускорение направлено вниз и равно a = 800 юнитов/с^2. Тогда высота составит H = V0^2 / (2 * a) = 2 * 800 * 45 / (2 * 800) = 45 юнитов.

Обратите внимание, что если fps уменьшается, то на прыжок придётся меньшее количество фреймов, однако за счёт того, что в формулах используется pmove->frametime, высота прыжка останется прежней. Если вы ещё раз заглянете в статью про физику стрейфов, то обнаружите, что такой же принцип заложен в функциях PM_Accelerate, PM_AirAccelerate и PM_Friction.

И ещё один важный момент: когда мы совершаем прыжок, то перед PM_PreventMegaBunnyJumping вызывается PM_AddCorrectGravity, поэтому вертикальная скорость в этот момент будет не нулевая, а -4 юнита/с. Следовательно, модуль общей скорости окажется больше 300, когда горизонтальная составляющая будет больше sqrt(300^2 – 4^2) = 299.973 юнита/с. Так что если ваша статистика прыжков пишет, что хороший престрейф должен быть меньше 300 юнитов/с, она вас немножко обманывает.

fuser2


А теперь самое интересное. В статье про физику hj мы подробнее познакомились с трением в движке GoldSource. В ходе эволюции Counter-Strike к этому трению добавился параметр fuser2, который влияет на все три компоненты скорости. В момент прыжка в функции PM_Jump переменная fuser2 становится равной 1315.789429 (выглядит как магическое число, но предположу, что это 100 * 1000 / 4.0 / 19.0). Затем каждый фрейм в PM_PlayerMove вызывается PM_ReduceTimers, которая помимо прочего уменьшает fuser2 на длительность фрейма в миллисекундах (то есть при 100 fps из fuser2 вычитается 10). Если в течение 1.31 секунды нового прыжка не происходит, то fuser2 доходит до нуля и больше не влияет на физику.

Однако даже на ровной поверхности fuser2 не успевает обнулиться между bhop’ами. Из-за этого в PM_Jump ещё до вызова PM_FixupGravityVelocity обрезается вертикальная скорость. Таким же образом меняется и горизонтальная скорость в начале функции PM_WalkMove:


PM_WalkMove вызывается только на земле, и мы хорошо это чувствуем, когда делаем обычные прыжки на какой-нибудь climb секции. Чем меньше времени мы провели в воздухе, тем дольше нам приходится выдерживать паузу после приземления перед следующим прыжком. Теперь вы понимаете, что в этом виновата переменная fuser2.

Вернёмся на ровную поверхность. Сначала просто разбежимся и прыгнем. В этот момент fuser2 равен нулю, так что при 100 fps мы точно знаем, что вертикальная скорость равна 268.33 юнита/с, что в воздухе мы пробудем 66 фреймов (stand-up мы пока не рассматриваем), и что максимальная высота прыжка составит 45 юнитов. Пусть к концу полёта мы достигли скорости 310 юнитов/с. Как было выяснено ранее, нам выгоднее было бы сделать bhop в 2 FOG. Проверим, изменится ли этот результат с учётом fuser2.

В первый фрейм на земле PM_Friction отнимет от горизонтальной скорости 4 процента, останется 297.6 юнитов/с. После 66 фреймов в воздухе и 1 фрейма на земле fuser2 = 1315.789429 – 67 * 10 = 645.789429, поэтому после PM_WalkMove горизонтальная скорость станет равной 297.6 * (100.0 - 645.789429 * 0.001 * 19.0) * 0.01 = 261.08 юнита/с. Во втором фрейме внутри PM_PreventMegaBunnyJumping мы обнаружим, что 261.08 < 299.973, поэтому горизонтальная скорость не изменится. В случае 1 FOG скорость обрезалась бы до 299.973 * 0.8 = 239.98 юнитов/с, так что 2 FOG в данном случае действительно остался более выгодным. Кстати, если бы мы делали DropBhop, в ходе которого нам пришлось набрать достаточно большую горизонтальную скорость, то выгодным мог бы оказаться bhop в 3 FOG, а то и больше.

Что касается вертикальной скорости, то во втором фрейме на земле fuser2 уменьшится ещё на 10, и вертикальная скорость до вызова PM_FixupGravityVelocity составит 268.33 * (100.0 - 635.789429 * 0.001 * 19.0) * 0.01 = 235.92 юнита/с. Тогда после bhop’а в воздухе мы проведём 58 фреймов, а высота прыжка составит лишь 34.78 юнита. В последующих bhop’ах в зависимости от fuser2 и количества FOG мы будем находиться в воздухе порядка 56-58 фреймов.

Если бы на ровной поверхности мы использовали stand-up bhop вместо обычного, то за счёт приседания мы провели бы в воздухе между прыжками больше фреймов (порядка 64-65). За счёт этого fuser2 успевал бы сильнее уменьшиться, и прыжки были бы выше примерно на 1 юнит, а скорость в случае bhop’ов в 2 FOG уменьшалась бы не так сильно. Однако это не значит, что stand-up bhop всегда лучше обычного, ведь он занимает больше времени. Именно поэтому опытные игроки при прохождении на время стараются везде, где только возможно, сделать обычный bhop вместо stand-up.

Мы вернёмся к теме stand-up bhop’а в следующей статье, где исследуем работу duck.