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

Физика EdgeBug и JumpBug

Опубликовано Kpoluk 13 Мая 2023 в 12:07
Когда я вставляю в статью фрагменты кода, я стараюсь показать только то, что относится в рассматриваемому вопросу. Поэтому в статьях про физику Bhop и CountJump мы для упрощения проигнорировали функцию PM_CategorizePosition, однако в этой статье она сыграет очень важную роль. Для начала уточним, где именно она вызывается:

  • в PM_PlayerMove до PM_Duck;
  • в PM_PlayerMove после PM_WalkMove и PM_AirMove;
  • в PM_Duck в случае успешного приседания;
  • в PM_UnDuck в случае успешного вставания.
Вот что в ней происходит:


Как видим, PM_CategorizePosition делает трейс на 2 юнита вниз и проверяет, не задел ли он что-нибудь. Если пересечений нет, то все компоненты tr.plane.normal равны нулю, и мы оказываемся в воздухе. Если же трейс задел какой-то объект, то в tr.plane.normal запишется нормаль к поверхности этого объекта. Пусть a – это угол между нормалью и вертикальной осью, тогда условие tr.plane.normal[2] < 0.7 можно переписать следующим образом: cos(a) < 0.7, откуда a > 45.573°. То есть если наклон поверхности составляет больше 45.573°, то мы имеем дело со слайдом, что с точки зрения переменной pmove->onground равносильно нахождению в воздухе.


Далее, если задетая поверхность не является слайдом, PM_CategorizePosition телепортирует нас на неё (если конечно мы уже не стоим на ней). Благодаря этому при спуске с обычной скоростью по не слишком крутому склону мы не оказываемся постоянно в воздухе, а спокойно идём по земле. Чуть позже мы вернёмся к этой безобидной на первый взгляд функции.

Изменение координат


В статье про физику стрейфов мы рассматривали в функциях PM_AirMove и PM_WalkMove изменение скорости, при этом обделив вниманием изменение координат. Настало время исправить это.

1) В конце PM_AirMove сразу после получения новой скорости вызывается функция PM_FlyMove. Она не только получает новые координаты при движении в воздухе, но ещё и может обработать столкновения с поверхностями, в том числе одновременно с несколькими. Однако в этой статье нам будет интересно взаимодействие только с одной поверхностью, так что вид функции можно значительно упростить:



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

Всего PM_FlyMove может выполнить до 4 таких итераций, то есть в течение фрейма она способна обработать до 4 последовательных столкновений с различными поверхностями. Остаётся только понять, как PM_ClipVelocity меняет вектор скорости при столкновении:


Обозначим V – исходный вектор скорости in, Vx – итоговый вектор скорости out, N – нормаль к поверхности normal. Тогда формулы из PM_ClipVelocity сводятся к следующему виду:


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

2) при движении по земле в PM_WalkMove мы также получаем новые координаты:


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

Итак, мы находимся на земле и пытаемся двигаться в направлении горизонтальной скорости, но во что-то упираемся. Здесь возможны две принципиально разных ситуации: когда мы поднимаемся по наклонной плоскости и когда перед нами ступенька. В первом случае достаточно вызвать ту самую PM_FlyMove, а вот во втором всё происходит более хитро – игрока поднимает на высоту, равную значению квара sv_stepsize (по умолчанию 18 юнитов), вызывается PM_FlyMove, а затем игрока опускает на то же значение sv_stepsize обратно. Конечно, поднимание и опускание производятся аккуратно, с предварительным трейсом. Заранее неизвестно, какой из случаев нам попадётся, поэтому разработчики решили эту проблему следующим образом – делается прогноз положения для обеих ситуаций, а затем из получившихся точек выбирается та, которая находится дальше от текущего положения. Благодаря такому подходу мы можем не только подниматься по склонам, но и без дополнительных действий забираться на ступеньки высотой до 18 юнитов, о чём мы уже говорили в статье про физику CountJump.

Приземление


Теперь мы готовы разобраться в том, как происходит приземление. Причём нас интересует не только тот фрейм, в который мы из воздуха попали на землю, а ещё и следующий сразу за ним. На рисунках мы будем изображать их отдельно, чтобы было видно, какой из фреймов ответственен за процессы, происходящие на их стыке. Условно разделим приземление на 4 типа:

Тип 1. мы падаем на горизонтальную поверхность, и после очередного вызова PM_FlyMove в PM_AirMove оказывается, что расстояние от модельки до земли меньше 2 юнитов, а значит после PM_AirMove вызовется PM_CategorizePosition, которая телепортирует нас на землю. В следующем фрейме за изменение координат будет отвечать уже PM_WalkMove. Она переместит нас по земле вдоль горизонтальной составляющей скорости (вертикальная просто обнулится).


Тип 2. мы падаем на горизонтальную поверхность, и очередной вызов PM_FlyMove обрабатывает столкновение с поверхностью земли, проецируя скорость на горизонтальное направление. Далее вызывается PM_CategorizePosition, которая с помощью переменной pmove->onground подтверждает, что мы оказались на земле, и поэтому в следующем фрейме вызовется PM_WalkMove. Итоговая скорость получится такой же, как и в случае первого типа.


Тип 3. мы падаем на наклонную поверхность, не являющуюся слайдом, при этом нас, прямо как в первом типе, телепортирует на поверхность, а вызов PM_WalkMove в следующем фрейме обнуляет вертикальную скорость. Если горизонтальная составляющая при этом была ненулевая, то после PM_WalkMove мы оказываемся в воздухе. Если же падение происходило строго вертикально, то после столкновения мы бы полностью остановились!


Тип 4. мы падаем на наклонную поверхность, не являющуюся слайдом, и в какой-то момент, как и во втором типе, PM_FlyMove обрабатывает столкновение, оставляя от скорости только ту часть, которая параллельна поверхности. Далее PM_WalkMove обнулит вертикальную составляющую новой скорости, и нас отбросит от поверхности.


Заметьте, что здесь скорость ведёт себя совершенно иначе, чем в случае третьего типа. Если бы мы падали вертикально со скоростью V на плоскость с наклоном в 45°, то после столкновения скорость равнялась бы V * cos(45°) * cos(45°) = V / 2, плюс PM_Friction забрала бы свои 4%. К примеру, при максимальной скорости падения 2000 юнитов/с (задаётся кваром sv_maxvelocity) мы отскочили бы со скоростью 960 юнитов/с. Столкновение такого рода можно назвать термином bounce.


Примером использования bounce для набора большой скорости до нажатия на кнопку старта могут послужить демки kz_42_amazon_surRendi_0228.26 и kz_j2s_darktower_shooting-star_0102.77.

Падение без потери HP


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

1. EdgeBug. Приземление происходит на край блока.


Чем больше горизонтальная скорость, тем больше шансов, что в конце фрейма мы не останемся на земле. При этом приземление здесь именно второго типа, с изменением направления движения за счёт вызова PM_FlyMove.

Если вы хотите увидеть применение EdgeBug при прохождении карт, то можете посмотреть демку kz_giantbean_b15[1337trees]_spr1n_0103.97. Также хорошим примером будет fof_32_shooting-star_0125.92, где EdgeBug выполнен с небольшой горизонтальной скоростью на движущемся объекте.

2. JumpBug. Как мы выяснили, функция PM_CategorizePosition телепортирует нас на землю, если расстояние от ног до земли меньше 2 юнитов или, что то же, если центр стоячей модельки оказывается на высоте 36-38 юнитов от земли. Идея JumpBug заключается в том, что в конце первого фрейма мы оказываемся в этом зазоре, но в сидячем положении, так что телепорта не происходит, а в следующем фрейме мы одновременно встаём и прыгаем. При этом внутри PM_UnDuck происходит вызов PM_CategorizePosition, то есть во втором фрейме нас всё-таки телепортирует вниз, однако затем функция PM_Jump вновь помещает нас в воздух. Если бы PM_Jump вызывалась в коде не после, а до PM_UnDuck, то никакого JumpBug’а у нас бы уже не получилось.


Таким образом, здесь от нас требуется во-первых выполнить падение первого типа, а во-вторых одновременно встать и прыгнуть в определённом фрейме. Прыжок можно осуществить как с помощью нажатия кнопки, так и скроллом. Вставание обычно выполняют, отпуская кнопку приседания, которую заранее зажали в полёте. Тем не менее, как мы помним из статьи про физику CountJump, вызов PM_UnDuck происходит также спустя один фрейм после scroll duck’а. Так что в теории можно сделать JumpBug с помощью скролла: в первый фрейм сделать scroll duck, а в следующий прыгнуть, прокрутив скролл в другую сторону.

Горизонтальная скорость до прыжка роли не играет. Нужно лишь понимать, что раз вызывается PM_Jump, то достаточно большая скорость будет урезана функцией PM_PreventMegaBunnyJumping, которую мы рассматривали в статье про физику bhop. Однако здесь её влияние будет сильнее, поскольку скорость в неё попадет с ненулевой вертикальной составляющей, и в итоге горизонтальную скорость урежет тем сильнее, чем больше вертикальная. Именно поэтому словить нежелательный JumpBug во время stand-up bhop даже на ровной поверхности оказывается опаснее, чем 3+ FOG.

В качестве примера можно посмотреть демки kz_ep_gigablock_b01_kayne_0206.57 и kz_man_redrock_shooting-star_0527.35, а также знаменитую kz_cg_wigbl0ck_spr1n_0211.50 с тремя JumpBug'ами в одном ране. Кроме того, в той же kz_giantbean_b15[1337trees]_spr1n_0103.97 сразу после EdgeBug'а spr1n делает JumpBug на невидимом блоке!

3. DuckBug. В случае падения третьего типа мы также могли бы сделать JumpBug, но поскольку за счёт отскока от наклонной поверхности мы и так оказываемся в воздухе, то прыжок в данном случае можно и не делать. Получается, что нужно лишь в нужный момент встать, однако при падении с заданной высоты этот момент может изменяться в зависимости от того, в какую точку наклонной плоскости мы приземляемся. В связи с этим для DuckBug обычно используют прокручивание скролла (например, как в демке kz_6fd_volcano_kzz1lla_0103.42), хотя наряду со scroll duck’ом здесь сработает и обычное отпускание кнопки приседания.


4. SlideBug. Как мы видели ранее, с точки зрения переменной pmove->onground нет разницы между пребыванием в воздухе и на слайде, так что EdgeBug можно немного модифицировать, заменив падение на край блока приземлением в основание слайда. Например, SlideBug можно применить на той же kz_cg_wigblock (скачать демо).


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

Высота падения


Падения типов 1 и 3 принципиально отличаются от типов 2 и 4 тем, что в один из фреймов вертикальная координата игрока в стоячем положении находится на расстоянии 36-38 юнитов от земли. Если известна высота падения, то при стабильном значении FPS мы можем точно сказать, выполнится ли это условие, а значит и возможно ли выполнить одну из описанных выше техник. Для этого мы могли бы последовательно получить значения высот, используя формулы для нахождения координат из PM_FlyMove и изменения скорости из PM_AddCorrectGravity. Однако ещё лучше будет получить универсальную формулу, которая сразу давала бы ответ.

Пусть в процессе падения мы имеем стабильные 100 FPS, а начальная вертикальная скорость равняется нулю. Тогда длительность фрейма pmove->frametime = 0.01 секунды, а скорость каждый фрейм уменьшается на 8 юнитов/с. Спустя фрейм мы пролетим 0.01 * 8 юнитов, спустя два фрейма 0.01 * 8 + 0.01 * 8 * 2, спустя три фрейма 0.01 * 8 + 0.01 * 8 * 2 + 0.01 * 8 * 3 и так далее. После N фреймов высота падения составит

h = 0.01 * 8 * (1 + 2 + ... + N) = 0.04 * (N + 1) * N

Если же нам известна высота H в каком-то конкретном месте карты, то для нахождения количества фреймов достаточно решить квадратное уравнение:
H = 0.04 * (N + 1) * N
N^2 + N - 25 * H = 0
N = (sqrt(1 + 100 * H) - 1) / 2

Так как N должно быть целым, то берём ближайшее целое число, меньшее N, которое обозначим как [N]. За [N] фреймов мы преодолеем расстояние h = 0.04 * ([N] + 1) * [N]. Остаётся только найти разницу H – h и проверить, меньше ли она 2 юнитов.

Пусть мы хотим сделать JumpBug, упав с определённой высоты, но расчёт по полученной формуле показал нам, что это невозможно. В таком случае мы можем попробовать изменить высоту, предварительно сделав какой-нибудь прыжок. Например, прыжок с места прибавит к высоте 45 юнитов, dd даст дополнительные 18 юнитов, а прыжок из сидячего положения 45 – 18 = 27 юнитов. Так как dd не влияет на fuser2, то cj и dcj также прибавят 45 юнитов, а duckbhop после dd 27 юнитов. Можно воспользоваться и обычным bhop’ом, но тогда высота будет зависеть от FOG и fuser2, о чём мы уже говорили в статье о физике bhop. Причём после первого bhop’а высота будет около 34.5-34.8 юнита, а после второго и далее 33.1-33.4. Для stand-up bhop’а получим соответственно 35.5-35.8 юнита после первого bhop’а и 34.5-34.8 после остальных. При duckbhop’е, если мы начинаем из стоячего положения, то перед первым bhop’ом мы проводим в воздухе столько же времени, как при stand-up’е, поэтому числа будут на 18 юнитов меньше, то есть 17.5-17.8 юнита. Между последующими bhop’ами мы находимся в воздухе столько же фреймов, как при обычном bhop’е, поэтому получаем 15.1-15.4 юнита. Хорошим примером варьирования высоты служит уже упоминавшаяся демка kz_cg_wigbl0ck_spr1n_0211.50: первый JumpBug сделан после bhop'а, второй после обычного прыжка, третий после dd.

При всём этом следует понимать, что выбор другой техники вместо обычного падения приведёт к тому, что вертикальная скорость будет отсчитываться не от нуля, и приведённые выше расчёты высот работать уже не будут. К примеру, если падать с высоты 45 юнитов, то JumpBug возможен, а вот если прыгнуть на месте и попытаться сделать JumpBug, то ничего не выйдет, хотя казалось бы высота прыжка как раз 45 юнитов. Из-за всех этих сложностей проще воспользоваться нашим плагином lj статистики, который при попытке сделать JumpBug подскажет, возможно ли это на данной высоте (о его работе можно почитать здесь). А пока вернёмся к обычным падениям и прикинем, насколько часто встречаются высоты, с которых можно сделать JumpBug.

Пока скорость падения меньше 200 юнитов/с, за один фрейм мы пролетаем меньше 2 юнитов, следовательно, мы можем сделать JumpBug в любой из фреймов вплоть до достижения этой скорости. А достигнем мы её спустя N = V / 8 = 200 / 8 = 25 фреймов, пролетев h = 0.04 * (25 + 1) * 25 = 26 юнитов. На скоростях, больших 200 юнитов/с, искомое соотношение P можно определить, поделив 2 юнита на расстояние, пролетаемое за один фрейм. Например, на скорости 400 юнитов/с, достигаемой на высоте h = 0.04 * (50 + 1) * 50 = 102 юнита, получим P = 2 / 4 * 100% = 50%. А при максимальной скорости 2000 юнитов/с, достигаемой на высоте h = 0.04 * (250 + 1) * 250 = 2510 юнитов, имеем P = 2 / 20 * 100% = 10%. То есть начиная с 2510 юнитов на одну высоту, с которой возможен JumpBug, будет приходиться 9 высот, с которых он невозможен. В промежутке 25 < h < 2510 зависимость P от h находится как P = 2 / (8 * N * 0.01) * 100% = 50 / (sqrt(1 + 100 * h) - 1) * 100%. На графике это можно условно изобразить следующим образом:


Тут стоит вспомнить, что вообще говоря мы делаем JumpBug, чтобы не потерять HP. А начинаются потери с высоты 164 юнита (P = 50 /(sqrt(1 + 100 * 164) - 1) * 100% = 39.3%), причём разбиться, имея 100 HP, можно при падении с высоты 603 юнита (P = 50 / (sqrt(1 + 100 * 603) - 1) * 100% = 20.4%).

Engine FPS


По идее даже небольшие отклонения в FPS должны сильно влиять на то, с каких высот можно сделать JumpBug. Однако на самом деле это не совсем так. Посмотрим, как переменная pmove->frametime, используемая при получении новых координат, вычисляется в PM_PlayerMove прямо перед PM_ReduceTimers:


Здесь pmove->cmd.msec - длительность фрейма в миллисекундах (целое число). Если мы попробуем получить значение FPS (назовём его engine FPS) как 1.0 / pmove->frametime = 1000.0 / pmove->cmd.msec, то при длительности фрейма 10 миллисекунд FPS будет равно 100, а соседние значения 9 и 11 миллисекунд дадут соответственно 111.11 и 90.90 FPS. Это значит, что пока наше реальное FPS больше 90.90 и меньше или равно 100, engine FPS будет ровно 100.

По этой же причине во вступлении к данной серии статей отдельное внимание было уделено квару fps_max. Если превысить легальное значение на 0.5, то реальное FPS будет отличаться не так сильно, однако engine FPS станет равным 111, что в свою очередь повлияет не только на столкновение с поверхностями, но и на то, как набирается скорость в функциях PM_Accelerate и PM_AirAccelerate (ведь там тоже используется pmove->frametime).

В следующей части PM_FlyMove также сыграет важную роль.