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

Физика Pixelwalk

Опубликовано Kpoluk 5 Авг 2023 в 16:04
Познакомившись с основными понятиями, связанными со структурой карты, мы можем приступить к разбору конкретных явлений, первым из которых станет pixelwalk. Из практики нам известно, что эта техника чем-то напоминает слайд, выполняемый на стыке обычного браша и брашевой entity (либо на стыке двух брашевых entity). Однако мало того, что не каждый такой стык подойдёт для pixelwalk, так ещё и упасть на такой стык нужно с определённой высоты. Проще всего это сделать, выполнив doubleduck с последующим приседанием с той же высоты, на которой находится нужный стык. В таком случае нас сначала подбросит на 18 юнитов вверх, а затем моделька игрока в приседе опустится на 36 юнитов, как раз чтобы коснуться стыка. Для статьи я создал проект карты hull_pixelcheck.jmf (архив с файлами можно скачать здесь), из которого компиляцией получил карту hull_pixelwalk.bsp. Она представляет собой комнату, создаваемую по умолчанию редактором карт J.A.C.K., в которую я поместил два браша, превратив верхний их них в func_wall. С нижнего браша можно сделать doubleduck с приседом на стык слева и "проскользить" с зажатой D вдоль стыка:


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

Как мы видели ранее, каждый фрейм движок CS рассчитывает новое положение игрока и вызывает функцию PM_PlayerTrace, которая фактически проверяет, не пересекает ли линия, соединяющая старое и новое положение, какую-либо из плоскостей окружающих брашей. Для этого ей нужно перебрать все модели на карте (в коде модели обозначены как physents):


Хорошо видно, как результирующий трейс меняет конечную точку в том случае, если произошло пересечение более близкой плоскости. Поиск этой точки для конкретной модели происходит более изощрённо, с рекурсивным проходом по clip-нодам. Разберём этот процесс поэтапно, заглянув внутрь PM_RecursiveHullCheck (код сокращён исходя из того, что мы будем работать с плоскостями, чьи нормали направлены по базисным осям):


Точки p1 и p2 (начало и конец трейса) могут лежать как по одну сторону от плоскости соответствующего clip-нода, так и по разные. Это мы выясняем с помощью знаков величин t1 и t2, модули которых фактически равны расстоянию от p1 и p2 до плоскости. Если обе точки оказались в полупространстве с положительным направлением нормали, то мы берём левый дочерний clip-нод и идём на следующий шаг цикла. Если обе точки лежат по другую сторону плоскости, то на следующий шаг берём правый дочерний clip-нод. Если знаки t1 и t2 отличаются, то необходимо найти координаты точки mid пересечения с плоскостью:


Заметьте, что эта точка лежит не на самой плоскости. Если p1 отстоит от плоскости больше, чем на DIST_EPSILON = 0.03125 (далее для краткости будем называть эту величину eps), то соответствующая координата точки mid выбирается ровно на расстоянии eps от плоскости. Если же p1 оказалась ближе, то в качестве координаты mid выбирается координата самой p1:


Зачем вообще нужен этот отступ на eps? Ответ кроется в следующем этапе:


Допустим точка p1 лежала в полупространстве с положительным направлением нормали (side = 0), тогда PM_RecursiveHullCheck вызывается для левого дочернего clip-нода, причём трейс идёт от p1 до mid. Другими словами, мы пытаемся найти в этом полупространстве более близкую к p1 точку пересечения, рассматривая всё что ниже дочернего clip-нода как отдельное поддерево. Если в левом поддереве ничего интересного не нашли, переходим к правому дочернему clip-ноду. Однако прежде чем лезть в правое поддерево, мы вызываем для точки mid функцию PM_HullPointContents, которая проходит по этому поддереву с целью выяснить, не лежит ли точка mid для какой-либо его плоскости снаружи по отношению к модели. Вот тут-то нам и нужен отступ на eps, без него это выяснить просто не получилось бы. Если точка mid хотя бы для одной плоскости правого поддерева лежит снаружи модели, то мы заходим на следующий шаг цикла, выбрав правый дочерний clip-нод и заменив p1 на mid (если в правом поддереве пересечений с плоскостями не будет, то точка mid не станет конечной точкой трейса, то есть столкновения с моделью не произошло). Если же mid лежит внутри, то заходить в правое поддерево смысла нет, и мы наконец переходим к заполнению информации по трейсу, записав в него последнюю пересечённую плоскость и точку mid в качестве конечной точки (при этом предварительно с помощью вызова PM_HullPointContents для всего clip-дерева проверяется, что точка mid не лежит внутри модели).

На первый взгляд подобный рекурсивный проход по clip-дереву даёт вполне корректные результаты, однако в отступе на eps скрыто несколько коварных моментов, один из которых и делает возможным pixelwalk. Вернёмся к нашей карте hull_pixelwalk.bsp. Схематично изобразим в разрезе func_wall и нижний браш вместе с его clip-нодами для сидящего игрока (на рисунке плоскости clip-нодов ограничивают оранжевую область, а розовым показана eps-область):


Когда мы стоим на нижнем браше, благодаря PM_RecursiveHullCheck между моделькой игрока и брашем остаётся ненулевой зазор величиной в eps или меньше. Сделав doubleduck, мы в приседе упадём ровно до той же высоты над уровнем верхней плоскости браша (а не ровно до уровня стыка браша и func_wall, как мы думали в начале этой статьи). При этом прижавшись во время падения к боковой плоскости func_wall, мы также сохраним зазор в eps или меньше между моделькой игрока и func_wall. В момент пролёта стыка центр игрока окажется на пересечении eps-областей, то есть в красном квадрате:


Здесь мы взяли точку p1 ровно на расстоянии eps от обеих плоскостей (это наиболее распространённый случай), а точка p2, в которой мы желаем оказаться, лежит ниже и правее по другую сторону плоскостей, поскольку мы падаем и при этом зажимаем клавишу D. На основании выгруженных данных карты (см. hull_pixelwalk.txt в архиве с файлами проекта) строим clip-дерево:


Трейс из p1 в p2 пересекает плоскости clip-нодов #24 и #27, причём PM_RecursiveHullCheck при прохождении по clip-дереву сначала встретит clip-нод #24, получит точку mid (она совпадает с p1), а затем дойдёт до clip-нода #27 (в качестве mid снова будет взята точка p1). В итоге в качестве плоскости столкновения выступит именно горизонтальная плоскость clip-нода #27, причём исключительно потому, что при обходе clip-дерева она встретилась позже. Вертикальная скорость обнулится, и останется лишь составляющая вдоль оси X (по Y нам не даёт перемещаться func_wall), что и будет выглядеть как скольжение вдоль стыка. Если мы попытаемся сделать pixelwalk с правой стороны, то там трейс затронет clip-ноды #27 и #29, причём до #29 функция доберётся позже, а значит здесь результатом трейса станет его вертикальная плоскость, и никакого pixelwalk у нас не получится.

Ещё одним важным условием для pixelwalk здесь было то, что в func_wall был превращён верхний браш, а не нижний. В PM_PlayerTrace первой моделью в цикле была сама комната вместе с нижним брашем, и трейс total дал для неё fraction, равный нулю. Трейс с func_wall, последовавший за ним, также дал нулевой fraction, однако результирующий trace обновлён не был, и плоскость столкновения осталась горизонтальной. Таким образом, pixelwalk возможен только в том случае, когда индекс нижней модели у стыка меньше, чем верхней. То есть снизу может быть обычный браш, а сверху брашевая entity, либо обе модели могут быть брашевыми entity, но у нижней индекс должен быть меньше.

Казалось бы, секрет раскрыт, но это ещё не всё. Мало того, что порядок соответствующих clip-нодов в дереве для стоячей и сидячей моделек игрока не обязательно совпадает, так ещё и порядок внутри каждого из деревьев может изменяться при каждой компиляции карты. Например, ещё раз скомпилировав тот же самый проект hull_pixelcheck.jmf, я получил ещё одну карту hull_nopixelwalk.bsp (см. архив с файлами), на которой pixelwalk нельзя сделать ни с правой, ни с левой стороны.


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


Определим высоты, падение с которых позволит выполнить pixelwalk (речь идёт в том числе и о падении после doubleduck или некого специфического bhop'а). Мы уже производили похожий расчёт высот для JumpBug, однако на этот раз следует учесть, что в момент трейса скорость уже уменьшилась на 4 юнита/с в функции PM_AddCorrectGravity:

h = 0.01 * 4 + 0.01 * 12 + 0.01 * 20 + ... + 0.01 * (4 + (N - 1) * 8) = 0.04 * N^2 = (N / 5)^2

Чтобы попасть в eps-область, нужно получить целую высоту h, для чего достаточно отсчитать число фреймов N, кратное 5. Например, спустя 30 фреймов мы опустимся на h = 36 юнитов, что как раз подходит под doubleduck с приседанием, который мы и делали на нашей тестовой карте.

Напоследок отметим ещё один важный нюанс. Возможно вы замечали, что на некоторых картах pixelwalk получается выполнять очень нестабильно. Дело в том, что при расчёте конечной точки трейса в функции PM_FlyMove используется тип переменной float, а его точность равна примерно 7 значащим цифрам. Это значит, что если, к примеру, в процессе падения мы хотели опуститься до высоты 123.03125 юнита, то из-за погрешности мы можем оказаться на высоте 123.031265 юнита. Казалось бы, небольшое отличие, но в eps-область мы уже не попадаем, и pixelwalk сделать не получается. А спустя минуту попыток расчёт может оказаться корректным, и pixelwalk заработает. Вот такой рандом в и без того неочевидной технике.

С pixelwalk разобрались, но в следующей статье отступ eps продолжит преподносить сюрпризы.