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

Физика Ladder

Опубликовано Kpoluk 27 Янв в 18:53
Движение по лестницам (тем, что на английском называются ladder, а не ступенькам) на первый взгляд не является чем-то изощрённым, однако разобраться с ним мы сможем только сейчас, имея представление о внутреннем устройстве карты. Сам факт нахождения на лестнице в PM_PlayerMove определяется в функции PM_Ladder, которая по сути проверяет, находится ли центр игрока внутри оболочки hull какой-либо брашевой entity с содержимым CONTENTS_LADDER. То есть чтобы двигаться по лестнице, нужно хотя бы на чуть-чуть оказаться внутри неё. Передвижение внутри лестницы ограничено по сути лишь следующей далее функцией PM_LadderMove, которая преобразует желаемую скорость игрока таким образом, чтобы он двигался вдоль лестницы.

Отсюда можно сделать вывод, что то, насколько глубоко игрок окажется внутри лестницы, зависит от его скорости во фрейме перед пересечением оболочки лестницы hull, а также от того, с какой стороны игрок попадает на лестницу. К примеру, если попытаться запрыгнуть на лестницу на kz_longjumps2 (или kz_longjumps3) просто разбежавшись ей навстречу, то мы окажемся внутри лестницы, но не вплотную к стене, а тем ближе, чем больше была скорость (даже при 340 юнитах/с мы продвинемся внутрь не более, чем на 3.4 юнита, в то время как толщина лестницы на этих картах составляет 4 юнита). Если же подойти к лестнице сбоку вдоль стены, то мы окажемся внутри лестницы и при этом вплотную к стене. Получается неплохой лайфхак для тех, кто хочет брать ladder блоки.

Посмотрим теперь подробнее на то, что происходит с вектором скорости при движении по (внутри) лестницы. Как обычно, преобразуем PM_LadderMove к более удобоваримому виду и последовательно разберём её работу:


Сначала, исходя из габаритов лестницы, находится её центр ladderCenter, при этом независимо от наклона лестницы центр будет лежать внутри неё. Далее PM_TraceModel находит пересечение линии, соединяющей центр игрока и центр лестницы, с hull лестницы, причём hull берётся для точечного объекта, то есть по сути повторяет форму брашевой entity. В trace записывается результат пересечения. Если пересечения не произошло, мы не трогаем скорость и выходим из функции, в противном случае двигаемся дальше. Если зажата клавиша прыжка, то скорость приравниваем 270 юнитам/с и направляем по нормали к плоскости лестницы, полученной в trace, и опять же выходим. Если прыжка не было, начинаем расчёт:


Здесь наша старая знакомая AngleVectors по направлению взгляда рассчитывает два единичных вектора: v_forward смотрит по направлению нашего прицела, а v_right вправо относительно нас. В зависимости от нажатых клавиш составляющие forward вдоль v_forward и right вдоль v_right по величине равны MAX_CLIMB_SPEED (т.е. 200) либо нулю, поэтому результирующий вектор скорости будет по модулю либо нулевым, либо равным 200, либо примерно 200 * sqrt(2) = 282.8427. Если при этом была зажата клавиша приседания, то эти величины умножаются на PLAYER_DUCKING_MULTIPLIER (т.е. 0.333), уменьшаясь приблизительно в 3 раза. Теперь остаётся только спроецировать вектор скорости на плоскость лестницы, однако движок CS делает это чуть более хитро, чем можно было ожидать:


Vn находится как проекция вектора скорости на направление нормали trace.plane.normal к плоскости лестницы, а Vl по сути является составляющей скорости V, лежащей в плоскости лестницы. Для получения результирующего вектора скорости pmove->velocity к Vl неожиданным образом прибавляется произведение Vn на единичный вектор tmp. В простейшем случае вертикальной лестницы tmp направлен вверх, а если лестницу наклонять, то он будет наклоняться вместе с ней. Но не будем торопить события, возьмём вертикальную лестницу и попробуем подвигаться по ней:


1) смотрим наверх по +Z, нажимаем W. Желаемый вектор скорости V направлен вертикально вверх, Vl = V, Vn = 0, значит результат pmove->velocity совпадает с V, и мы движемся вверх со скоростью 200 юнитов/с.
2) смотрим вбок по +Y, нажимаем W. Желаемый вектор скорости V направлен вбок, Vl = V, Vn = 0, опять же pmove->velocity совпадает с V, движемся вбок со скоростью 200 юнитов/с.
3) поворачиваемся лицом к лестнице, смотрим по +X, нажимаем W. Желаемый вектор скорости V направлен по +X, Vl = 0, проекция скорости на нормаль к лестнице Vn = –V, и тогда pmove->velocity = 0 - tmp * Vn = tmp * V = (0, 0, V), то есть мы движемся вверх со скоростью 200 юнитов/с, прямо как в первом случае!

Любую скорость можно разбить на три рассмотренных составляющих, а значит проясняется общая картина: всё, что лежит в плоскости лестницы, остаётся неизменным, а направленная в сторону лестницы составляющая становится направленной вверх (соответственно направленная в сторону от лестницы становится направленной вниз). Но куда нужно направить вектор желаемой скорости, чтобы двигаться вверх как можно быстрее? Рассмотрим такой случай: 4) поворачиваемся лицом к лестнице, смотрим по +X, поднимаем взгляд на угол u, нажимаем W.


Желаемый вектор скорости V = (V * cos(u), 0, V * sin(u)), проекция Vn = -V * cos(u), составляющая Vl = (0, 0, V * sin(u)), результирующая скорость pmove->velocity = (0, 0, V * sin(u)) + V * cos(u) * (0, 0, 1) = (0, 0, V * (cos(u) + sin(u))). Максимальная вертикальная скорость получится при u = 45°, составив 200 * sqrt(2). Можно попробовать опускать взгляд вниз, то есть брать отрицательные значения u, там мы обнаружим, что при u = -45° получается нулевая скорость. Итого в целом картина получается такой: смотрим вниз, скорость 200 юнитов/с вниз, начинаем поднимать взгляд, скорость спуска уменьшается и спустя 45° мы замираем, после чего начинаем двигаться вверх. Когда взгляд поднялся до горизонтали, скорость подъёма достигла 200 юнитов/с, смотрим ещё выше и спустя 45° достигаем максимума 282.8427 юнитов/с, после чего замедляемся и вновь выходим на 200 юнитов/с, смотря вверх.

Теперь вспомним, что с помощью зажатия сразу двух клавиш, например W и D, можно создать ещё большую скорость. Если мы продолжим быть повёрнуты лицом к лестнице, то зажатая D даст дополнительное движение вправо. Но мы можем повернуться к лестнице правым боком, и тогда вся составляющая скорости от зажатой D превратится в скорость движения вверх. Чтобы в таком положении составляющая от зажатой W также пошла в вертикальную скорость, достаточно посмотреть вверх. Тогда получим pmove->velocity = 2 * 200 = 400 юнитов/с. Таким образом мы получили максимальную скорость подъёма и спуска по лестнице, хорошо известную всем опытным игрокам.

А что насчёт максимальной горизонтальной скорости? Возьмём для примера лестницу на kzro_nightcamp, по которой нужно двигаться вдоль стены. Здесь воспользоваться превращением горизонтальной составляющей в вертикальную не получится, так что максимумом здесь будет скорость 282.8427 юнита/с, которую можно достичь, зажав W и A и повернувшись на 45° к стене, либо W и D и отвернувшись на 45° от стены.

Рассмотрим теперь лестницу, наклонённую под углом 45° (пример лестницы под наклоном можно найти на kz_alpin). Вектор tmp здесь будет также наклонён на 45°, а значит чтобы достичь скорости 282.8427 юнита/с с зажатой W, достаточно смотреть в горизонт в сторону лестницы. Вариант с зажатием W и D для достижения 400 юнитов/с здесь также работает, в таком случае нужно смотреть в горизонт, повернувшись влево на 45°.


При меньших наклонах сохранение положения желаемой скорости относительно плоскоти лестницы потребует поднятия взгляда наверх и меньшего отворота от лестницы, при больших наклонах наоборот. Ситуация меняется лишь когда лестница полностью ложится в горизонтальную плоскость, тогда вектор tmp вырождается и становится нулевым. Причём неважно, находится лестница под ногами или над головой. Такую лестницу можно встретить, например, на vee_mojave, и здесь, как и на kzro_nightcamp, единственным вариантом будет с двумя зажатыми клавишами смотреть в горизонт, повернувшись вбок на 45° и двигаясь со скоростью 282.8427 юнита/с.

Вернёмся к PM_LadderMove, в которой остаётся проверить, нет ли необходимости сойти с лестницы. Для этого мы проверяем, нет ли пола на расстоянии в 1 юнит под ногами, а также смотрим на знак Vn.


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

Ещё один широко известный факт, связанный с переходом между полом и лестницей, заключается в том, что сидя на полу невозможно начать подниматься по лестнице. Теперь это тоже становится довольно очевидным: максимальная скорость подъёма по лестнице в приседе составит 400 * 0.333 = 133.2 юнита/с, что недостаточно для успешного отрыва от пола. Действительно, как мы помним, каждый фрейм движок с помощью функции PM_CategorizePosition проверяет, нет ли у игрока в 2 юнитах под ногами земли, и если она там оказалась, то игрока телепортирует на неё, т.е. оторваться от земли можно только с вертикальной скоростью больше 200 юнитов/с.


Ladder boost


Отдельно стоит обсудить такое явление, как ladder boost. Игрок неким образом забирается поглубже внутрь лестницы, после чего его выбрасывает в другой конец комнаты настолько быстро, что это больше похоже на телепортацию, чем на boost. Примерами служат сокращения, выполняемые на картах cg_d2block_ez и kz_wild. Разберёмся, в чём же тут дело.
Подойдём к вертикальной лестнице, стоящей на горизонтальном полу, и, зажимая W и смотря вниз, упрёмся в неё. Как мы уже выяснили, желаемая скорость будет полностью перенаправлена вниз, после чего в PM_PlayerMove мы попадём в уже знакомую нам функцию PM_FlyMove. Там вертикальная скорость проецируется на плоскость пола, тем самым обнуляясь. Однако пол ведь может быть и не горизонтальным, а наклонным, там что если после лестницы вектор скорости упирается в пол, наклон которого уходит вниз, то после проецирования скорость становится направленной вдоль пола, т.е. внутрь лестницы.


Если толщина лестницы достаточно большая, то мы будем погружаться в неё всё дальше и дальше. И как только центр игрока оказывается внутри брашевой entity, нахождение пересечения ломается. Чтобы увидеть это, проследим всю цепочку. В PM_LadderMove создаётся экземпляр trace структуры trace_t. В этот момент в нём содержится какой-то мусор. Далее он отдаётся в PM_TraceModel, которая в свою очередь передаёт trace в SV_RecursiveHullCheck, а та уже занимается поиском конкретной пересекаемой плоскости. При этом в отличие от PM_RecursiveHullCheck функция SV_RecursiveHullCheck исходит из того, что пересечение обязательно найдётся (ведь мы ещё в PM_Ladder узнали, что находимся внутри лестницы, а на такое глубокое погружение в лестницу никто не рассчитывал), поэтому, когда после прохода по всем плоскостям SV_RecursiveHullCheck ничего не находит, она просто оставляет trace в том состоянии, в котором он ей был передан, то есть с неким мусором внутри. Таким образом, в PM_LadderMove попадают огромные значения компонент trace.plane.normal, из которых получается огромная скорость pmove->velocity, и в следующий фрейм движок кидает нас через всю комнату, а то и всю карту. Мусорные значения каждый раз отличаются, но в относительном выражении это изменение не особо большое, поэтому бросает игрока примерно в одно и то же место. С другой стороны, нет никаких гарантий того, что после очередного обновления CS значения не станут совершенно другими, так что намеренное использование ladder boost на своих картах мапперам категорически не рекомендуется.