Физика в Quake

Материал из CSM Wiki
Перейти к навигации Перейти к поиску

В 90-е годы, когда, собственно и были написаны все три квейка общие тенденции сводились к тому, что для каждого игрового движка писалась простенькая самописная физика, а специализированных физических движков, как вы сами понимаете, тогда уже не было. Да и в этих отдельных физических движках тоже мало кто разбирается, достаточно умения его грамотно приладить к игровому движку. А что уж там внутри - это всем пофигу. Но с Quake несколько иная ситуация. Физика там простенькая, во многом ограниченная, и понимать как она работает жизненно необходимо, если вы вдруг надумаете сделать на ней машинки или, скажем, склеить вместе несколько лифтов и платформ (movewith). А уж если вы захотите сделать коллоизацию с рандомным англированием и последующей нуллизацией хулла, то вам тем более необходимо понимать, что и как взаимодействует.

Обзор физики

Любая физика, как известно, состоит из трёх частей: коллижен детектора (коллоизации), эвалюатора (итератора) и математической библиотеки. В Quake все эти части присутствуют. В качестве коллижен детектора у нас SV_Move и SV_PointContents, в качестве итератора - функция SV_Physics, ну а математические функции богато представлены в mathlib.c. Начнём с коллоизации.

Коллоизация

Почти все мапперы под Half-Life наверняка хоть раз слышали выражение про "четыре хулла". Мол, первый хулл видимый, второй размером с игрока, третий размером с присевшего игрока, а четвертый вообще не пойми, зачем нужен. Человеку, далёкому от внутренней реализации этого механизма, может быть попросту непонятно - ну как это так? Если хуллы для коллоизации, то почему же первый видимый? И если он видимый, тогда почему же я его никогда не видел? На самом же деле данное выражение, судя по всему, является каким-то очередным переводом надмозгов документации Valve. Да хуллов, действительно четыре, и первый из них не видимый, а точечный, то есть имеет нулевой размер. Поскольку трасса с точечным хуллом идеально точно трейсит видимую геометрию, и возникло, видимо, это выражение про видимый хулл.

С этим разобрались, теперь определимся, что это вообще за хуллы такие и почему их надо нуллизировать. Как вы помните, Quake вышел в 1995 года. Среднестатистический компьютер того времени - Pentium 80, а в России еще и 386-е, и 486-е были в ходу. То есть, по сегодняшним меркам, проц и так жутко слабый(у вас в телефонах быстрее), а квейку кроме обсчётов физики еще надо было рисовать геометрию программно. Потому что видеоускорителей тогда практически не было. Точнее говоря, они только-только начинали зарождаться в виде отдельной платы (3dfx) и широкой популярности в народе получить не успели. Так вот. Тут понимаешь надо каждый кадр нарисовать разные полигоны, а нам еще и физику считать.

Выход был найден следуюшим образом: было введено несколько копий геометрии уровня для разных размеров бб-оксов. BBox - это Bounding Box, такая абстрактная коробка, которую можно описать при помощи двух векторов. Первый вектор это mins - то есть размеры по X, Y, Z от центра объекта до его нижней части. А второй вектор - это maxs, размеры объекта по X, Y, Z до его верхней части. Для примера max( 16, 16, 72 ), mins( -16, -16, 0 ) даст нам коробку, у которой центр находится в полу, а её размеры по X, Y равны шестнадцати. На практике обычно используют симметричные bboxы по X и Y, меняется только Z. Так для монстров оригин находится у самого пола, а для игрока - по центру, то есть в пузе. Следует отметить, что оригин для ббокса игрока в пузе вовсе не является нововведением Valve, которая ввела в игру возможность приседания игрока (а значит и изменения размеров его bbox). На самом деле, он такой еще с первой кваки. Возможно это было сделано для удобства, чтобы проще считать позицию, из которой вылетают пули и лучи, когда игрок стреляет, возможно еще по каким-то причинам, я не углублялся.

Итак, возвращаясь к теме, компилятор делает несколько копий двоичных деревьев, с учётом разных размеров хулла. Поскольку двоичные деревья и так сами по себе занимают достаточно места, а тем более нам еще надо собрать четыре таких дерева, то были проделаны следующие оптимизации. Во-первых, плоскости (об которые, собственно и коллидит трасса) берутся из общего массива плоскостей, компилятор подбирает наиболее подходящие. Это не так сложно, как может показаться на первый взгляд - ведь хуллы-то строятся не в сферическом вакууме, а на основе исходной геометрии. Впрочем компилятор умеет делить плоскости пополам, если это вдруг потребуется. Таким образом, теоретически в массиве плоскостей вполне может присутствовать некая их часть, которая относится только к коллоизации (для видимой геометрии плоскости нужны, как вы помните, для их отсечения пирамидой фруструма). Сама же структурка clipnode_t имеет два указателя на своих детей и номер плоскости в массиве. А где же clipleaf_t, спросите вы? Дело в том, что для физики конечные листья не несут практически никакой полезной информации. В листьях, как вы помните из предыдущей статьи, хранится тип содержания - contents. Ну это, допустим вода, твёрдая поверхность или наоборот пустота. Вот эта-то информация и является единственной, которая нам требуется от clipleaf_t. Но зачем же заводить отдельную структурку под одну переменную? Незачем. Поэтому тип содержания хранится прямо там же - в указателях на детей ветви. А чтобы как-то отличить лист от ветви все контентсы были попросту сделаны отрицательным числом.

Таким образом, перемещаясь по дереву, мы всегда знаем: положительное число - номер следующей ветви. Отрицательное число - стоп-машина, приехали. Не забудьте сохранить контентс, он нам еще понадобится. Поскольку деревья хуллов частично друг-друга перекрывают (ну например хулл меньшего размера пройдет сквозь одно дерево, но застопорится об другое), то нам вовсе необязательно юзать прямо совсем фиксированные размеры хуллов. Мы можем примерно прикидывать какое именно дерево выбрать для того или иного хулла. Этим у нас занимается функция SV_HullForBSP. Функция абсолютно не генерична, в ней жестко прописаны константы, поэтому выбор хулла работает правильно лишь на тех размерах, под которые строилась игра. С другими размерами, а тем более сильно отличающимися, возможны разные мерзкие баги, в частности застревания на ровных местах, невозможность прохода в достаточно широкий проём, невидимые препятствия посреди дороги (впрочем этим грешит даже игра с оригинальными хуллами).

Это - расплата за сверхбыструю реализацию физики. Кроме того подобный код потенциально склонен к застреванию в сложных брашевых конструкциях. В Quake для этих целей даже ввели специальную функцию SV_CheckStuck. Эта функция каждый кадр проверяет игрока на предмет застревания и пытается вызволить из плена. Кроме того, она сохраняет каждый кадр для игрока последнюю правильную позицию в pev->oldorigin. Физика Quake1 считает внешний лист вокруг карты солидным, вследствие чего полёт вокруг карты и попытка отключения noclip в консоли, в тот момент, когда игрок находится за картой, перенесет вас (о чудо!) на то самое место, где вы впервые включили noclip. На самом же деле, это функция SV_CheckStuck, увидев игрока застрявшим, лихорадочно пытается его вызволить из плена, и когда, ну всё труба, ничего не помогает, использует последнее средство - возвращает игрока на позицию pev->oldorigin. В Half-Life, это поведение кстати убрали, поэтому отключение noclip за пределами уровня приведет к тому, что игрок так и останется висеть "в воздухе", как дурак.

Теперь, когда мы имеем какое-никакое представление о том, что такое хуллы и клипноды, рассмотрим работу функции SV_RecursiveHullCheck. Сам принцип её работы - точно такой же бег по двоичному дереву с поиском плоскостей, которые может пересекать наша линия трассинга. Ну и конечно проверка на конечный лист, на предмет того, что мы уже застряли где-то. Поскольку деревья приблизительные, то застрять в них - плёвое дело. А в некоторых адских случаях наша трасса может оказаться вообще обратной. Это также справедливо для сложных брашевых конструкций. В этом случае Quake (да и Half-Life тоже) выдает занятное сообщение backup past 0. !Проверено! Теперь рассмотрим параметры, которые являются результатом работы нашей трассы. startsolid - этот параметр указывает, что стартовая точка трассы находится внутри геометрии, или как вариант - за пределами уровня. allsolid означает, что наша трасса честно прошла весь путь от начальной до конечной точки и нигде на всё протяжении не встретила пустого пространства. При этом правильность остальных параметров находится под большим сомнением и никак не нормируется. В разных движках она может быть разная. Будьте бдительны. Структура plane содержит в себе копию той плоскости, об которую трасса окончательно упёрлась, перед тем как остановится. Тут следует пояснить вот какой момент - трасса в состоянии самостоятельно выйти из геометрии, потому что плоскость развернута к ней "задом". Таким образом переменная startsolid становится истинной. Далее, пройдя по пустому пространству трасса упирается в плоскость, находящаяюся уже в "правильном" направлении. А блуждание трассы внутри геометрии понятно структуру plane не заполняет. Технически трасса вообще может пройти за пределами уровня.

Следующий параметр - fraction. Это самый главный и ценный параметр у трассы, хотя многие, что удивительно весьма смутно себе представляют что это такое и для чего оно нужно. Поясню. fraction - это путь проделанный трассой в диапазоне 0 - 1. Начальной и конечной точкой диапазона считаются trace_start и trace_end соответственно, то есть координаты, заданные пользователем. Если трасса проделала весь путь и ни во что не упёрлась, то fraction будет равен еденичке. Если трасса упёрлась в солид прямо в месте старта (ну например при выполнении кода энтити infodecal), то fraction будет равен нулю. Если трасса проделала ровно половину пути, то fraction будет равен 0.5. Надеюсь это понятно.

Следующий параметр - endpos. Это, попросту говоря конечная точка где трасса упёрлась в солид. Следует отметить, что это соответствует действительности лишь для точечной трассы - т.е. нулевого хулла. А для хулла игрока это может означать некую плоскость куда упёрся весь хулл, хотя конкретно в центре трассы - пустота. Приведу пример для наглядности: на карте ленивый маппер сделал брашевую стойку металлодетектора. Ленивый, потому что игрок в нее ЧСХ не пролезает, а он поленился переделывать. И вот наш игрок упёрся в эту стойку точно по центру. Где будет находится endpos? Правильно, по центру нашей стойки, там где пустое пространство, но сам хулл не пролезает. Внимательные читатели наверняка уже догадались, что bbox - это единственная геометрическая фигура, которой может оперировать наш коллижен детектор. Отсюда, кстати и вырастает принципиальная невозможность сделать "колоизацию как в дуум3" на стандартном коллижен детекторе - он знает ровно одну форму объекта, да еще и фиксированных размеров. Однако мы отвлеклись - последний важный параметр трассы возвращет указатель на энтитю, в которую упёрлась наша трасса. Если мы упёрлись в мир, то это будет указатель на нулевую энтить или вообще NULL. Как реализовно столкновение трассы с энтитями я расскажу чуть позже, а пока упомяну два оставшихся незначительных параметра у трассы.

Это inopen и inwater. Первый параметр полностью аналогичен условию проверку fraction == 1.0f, поэтому в нём нету особого смысла и в дальнейших версиях квейка его попросту убрали за ненадобностью. Второй параметр показывает что трасса целиком проходит в воде. Пользы этого знания нам примерно столько же сколько от ответа шотландского программиста "вы находитесь на воздушном шаре", поэтому данный параметр также в дальнейшем был убран за ненадобностью.

Теперь поговорим о механизме трассировки энтить и комбинировании полученных трасс. Энтити изначально проверяются на пересечение ббокса трассы с ббоксом энтити. Это очень простой и быстрый тест. Для трассы ббокс выстраивается аналогично заполнению absmin и absmax для энтить. Для точечной трассы это будет очень длинная и тонкая коробка (но не тоньше одного юнита). Проверку на попадание в bbox трассы можно было бы делать простым перебором всех имеющихся энтить, но учитывая что за один кадр кол-во трасс может быть каким угодно, подобный перебор нам дорого обходится. Особенно на тех старых компьютерах, для которых и предназначался первый квейк. И снова нам на помощь приходит BSP-дерево, но уже для энтить. Отличие от дерева карты в том, что в узлах прилинкованы не повехрности и не плоскости, а списки энтить Дерево обновляется каждый раз, когда для какой-либо энтить вызывается SV_LinkEdict или как частный случай - UTIL_SetOrigin. Да-да, данная функция отвечает не только за просчет видимости но и за местоположение эдикта в узлах физического дерева. Ну а дальше - уже по накатанной схеме рекурсивная функция бегает по дереву линкованных энтить и ищет все потенциально подходящие энтити в предалх ббокса трассы.

Таким образом вместо тупого перебора 600-900 энтить мы от силы перебираем от 1 до 30. Но попадание в ббокс трассы еще не гарантирует что энтить с ним обязательно пересекается. Тут-то и вступает в действите наша функция BoxInterseсt, о которой я уже упоминал. Ну а далее... Далее у того кто внимательно читает возникнут непонятки. Ну у мира-то ладно, у него есть уже есть предрасчитанные хуллы. А у энтить-то никаких хуллов нету. Как же осуществляется колоизация? А вот как. Для всех энтить налету строится такое ма-а-аленькое BSP-дерево из клипнодов, по форме того же пресловутого ббокса и совпадающее с его размерами.

Чтобы изменить размер такого ббокса достаточно поменять дистанцию у плоскостей, что. как вы понимаете, сделать совсем несложно. Дальше это маленькое деревце точно также кормится на вход функции SV_RecursiveHullCheck и мы получаем результаты его трассировки. Кстати забыл упомянуть, что для брашевых моделей в режиме SOLID_BSP используются их встроенные маленькие деревца, повторяющие их форму. А локальное временное дерево - в основном для монстров и func_pushable.

Правда тут есть один важнейший аспект, на котором я хочу заострить внимание. BSP-деревья по сути своей статичны. Я не раз упоминал о параметрах mins и maxs, вшитых в ветви и содержащих в себе не только размеры, но по сути и позицию (подобно тому как её содержат переменные absmin и absmax). То есть двигать BSP дерево НЕЛЬЗЯ ни в коем случае. Иначе всё изломается к чертям. Это касается всех деревьев без исключения. Неважно мир это у нас или брашевая моделька или даже маленькое локальное дерево для ббокса монстра. Нельзя! и всё тут. Нет, ну то есть конечно можно, если полностью пересчитать его заново.

Легко догадаться, что полный пересчет дерева для каждой энтити при вызове SV_LinkEdict полностью нивелирует достоинства от его применения. К тому же там начнется масса других проблем, имеющих отношение уже к рендереру. Например съедут текстуры, если мы попытаемся развернуть нашу модельку. Что же делать в такой ситуации? Наиболее очевидное решение проблемы - если гора не идёт к магомету, то магомет идет к горе. Мы просто трансофрмируем нашу конечную и начальную точку трассы таким образом, чтобы она очутилась в локальном пространстве брашевой модели или нашего маленького локального дерева для монстров. Это не так уж сложно как может показаться. Дело в том, что настоящее физическое вращение могут иметь только брашевые объекты, для которых маппер установил браш, покрытый текстурой origin. Все остальные энтити вращаться не могут (когда монстр крутится вокруг своей оси, его ббокс остается неподвижным. это легко увидить включив r_drawentities 5 под Half-Life в режиме Software (в режиме Hardware есть баг, из-за чего функция отрисовки ббоксов не вызывается). Таким образом, чтобы трансформировать нашу трассу в пространство трассируемой энтити, достаточно просто отнять значение, соответствующее центру энтити для брашевых или просто от оригина энтити для всех остальных объектов, поочередно от начала трассы и от её конца. Из размеров ббокса трассы вычитать ничего не нужно . А вот для крутящихся объектов всё далеко не так однозначно... Quake их вообще не поддерживает, а код в функции SV_ClipMoveToEntity, помеченный как #ifdef QUAKE2 имеет в себе принципиальное ограничение - он способен вращать энтить всего лишь по одному углу - YAW. В принципе это самый распространенный угол, и зачастую вращение на другие углы попросту не требуется.

Забавно что данная ситуация сохраняется во всех трёх квейках, из чего проистекает логическая невозможность, например сделать дверь, которая будет открываться вверх (поворотом). А вот в Half-Life с этим делом наоборот полный порядок. Правда это скорее всего обусловленно главой On A Rail, где игрок, как мы помним ездил на вагончике не только по одному уровню, но также взбирался на горки и спускался с них. Сделать подобный вагончик практически невозможно ни под одной из трёх квак - как только вагончик пойдет под уклон вы тут же в нём застрянете. Технически же трансформация трассы для крутящихся объектов выглядит так: строим матрицу из углов и оригина для текущей энтити. Поскольку матрица ортонормальная, то выполнять полное инвертирование не обязательно, сойдет и упрощенное. Затем используем, наверняка знакомую вам функцию VectorTransform для начала и конца трассы. Это и будет правильное положение для крутящегося объекта. Впрочем в Quake вам встретится более упрощенный вариант. как я уже говорил, осуществляющий трансформацию только по YAW. Также хотелось бы отметить, что после трасировки нам вовсе не нужно трансформировать полученный результат обратно. Во первых теряется точность из-за двойной трансформации, во вторых это лишняя нагрузка на процессор. Достаточно использовать параметр fraction для нетрансформированных значений начала и конца трассы дабы получить конечную точку. Правда на придется трансформировать плоскость, в противном случае при коллизии такого крутящегося объекта с игроком он не будет скользить вдоль плоскости, а будет как бы упираться в невидимое препятствие, что очень раздражает. Кстати такой баг можно наблюдать, например в движке QFusion и сделанном на его базе War$ow. Кроме всего вышеперечисленного также хотелось бы рассмотреть локальный случай сложной комплексной колоизации в Half-Life. Да-да, речь идет о колоизации по хитбоксам. В данном случае дело обстоит так:

1. строится скелет с учётом текущей анимации, блендингом и бонеконтроллеров 2. начало и конец трассы трансформируются через инвертированную матрицу для каждой кости, но сперва проверятся пересечение ббокса трассы и хитбокса. 3. дальше для каждого хитбокса строится точто такое же маленькое локальное дерево с учётом его размеров. 4. обычная трассировка с использованием SV_RecursiveHullCheck Примечание: в Xash3D не используется SV_RecursiveHullCheck для трассы хитбоксов, но остальное реализовано так же.

Теперь когда вы знаете основной принцип, понять механизм работы трассы стало не так уж и сложно, неправда ли?

Напоследок хочу упомянуть о принципе комбинирования трасс. Собственно для выполнения комплексной трассы используются два набора trace_t. Один из них основной, тот который будет возвращен по завершению работы функции. Другой - вспомогательный, содержит в себе результаты трассировки отдельно взятой энтити. Комбинирование осуществляется по принципу "упирания в геомтерию". То есть если fraction локальной трассы меньше чем у основной, если у локальной трассы оказался истинным параметр startsolid или allsolid (я напомню что каждая локальная трасса делается с глобальными параметрами начала и конца трассы), то такая локальная трасса полностью перезатирает предидущую. В этом же условии присваивается и энтить, в которую упёрлась трасса.

Теперь, когда мы подробно разобрали все аспекты работы трассы, можно наконец перейти к физике и быстренько пробежаться по основным принципам. Физика, кроме перемещения энтить занимается у нас еще одним полезным делом - она вызывает функцию Think, если пользователь задал некоторое время в переменной nextthink, отличное от нуля и заведомо больше текущего времени.

Сама эта функция вызывается абсолютно для всех типов физики, посредством функции SV_RunThink. Если энтить была удалена прямо внутри вызова функции Think, то SV_RunThink это отслеживает и код физики дальше не вызывается. Правда это актуально лишь для Quake, а для Half-Life со своим деликатным флагом FL_KILLME, который выполняется уже в конце кадра для всех помеченных энтить, попросту не имеет значения.

Перечислим названия типов физики, доступных пользователю из игровой библиотеки\виртуальной машины и кратко опишем основные принципы работы:

1. MOVETYPE_NONE Не делает ничего, только вызывает функцию SV_RunThink. В принципе это видно из названия.

2. MOVETYPE_NOCLIP Вызывает SV_RunThink и задает перемещение объекта с указанной линейной и угловой скоростью без каких-либо проверок на столкновения. Перемещение объекта технически осуществляется так: заданная линейная\угловая скорость умножается на параметр frametime (это время за которое успел закончится предидущий кадр), полученный результат прибавляется к положению\углам энтити, затем вызывается SV_LinkEdict\UTIL_SetOrigin.

3. MOVETYPE_WALK. В обычном квейке вызывается прямо из основной функции SV_Physics, в QuakeWorld, Quake2, Half-Life вызывается из ответных пакетов клиента, содержащих информацию о перемещении игрока (т.н. usercmd_t), а в Quake3 даже есть возможно выбирать откуда вызывать физику игрока - из общего цикла или из функции приёма пакетов (квар g_synchronousClients 0\1). К слову сказать вызов физики игрока из функции приёма клиентских пакетов, кроме очевидных преимуществ (более высокая скорость реакции, более точная обработка физики) таит в себе и пару неприятностей. О первой из них знают все модемщики - чем реже приходят от них пакеты к серверу, тем реже вызывается физика игрока и никакой unlag и никакой предиктинг не в состоянии сгладить эффект от того преимущества, которое получают люди, играющие на более толстом канале. А вот вторая нехорошая проблема - то самое пресловутое застревание игрока в лифтах на подъеме\спуске, связанное с частичной рассинхронизацией движения лифта и игрока. Сама же физика заключается в основном в функции SV_MoveTypeFly, которая двигает игрока по заданному направлению и проверяет 4 плоскости вокруг него (боковые, переднюю и нижнюю), Впрочем если какой-нибудь trigger_push придаст игроку ускорение вверх, то будет проверяться уже верхняя плоскость. Так же осуществляется и скольжение вдоль стенки, посредством нехитрой функции SV_ClipVelocity, которая трансформирует направление движения вдоль плоскости.

Все остальные функции являются вспомогательными и призваны обеспечивать поведение игрока в воде (SV_WaterMove), прыжки на суше, прыжки из воды, приседание (если это Half-Life, Quake2, Quake3), передвижение по лестницам и прочие.

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

4. MOVETYPE_STEP Этот тип предназначен для монстров. Поскольку основная функция для перемещения монстров по уровню оформлена в виде движкой функции MoveToOrigin, то данная функция лишь применяет к монстрам гравитацию, проверяет их на нахождение в жидкости, а так же дает возможность неслабо пихнуть монстра, применив к нему pev->velocity. Первоначально эта опция задумывалась для trigger_monsterjump в quake, но впоследствии выяснилось, что она отлично подходит для func_pushable, только в роли trigger_monsterjump, выступает уже сам игрок, сообщающий свою скорость ящику.

5. MOVETYPE_TOSS, MOVETYPE_FLY, MOVETYPE_FLYMISSILE, MOVETYPE_BOUNCE Все эти типы реализуются одной функцией и различаются парой параметров в различных комбинациях:

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

6. MOVETYPE_PUSH Это - наиболее сложный тип физики из Quake. Основная его особенность заключается в том, что он не только позволяет осуществить полноценную колоизацию (правда только с брашевыми моделями), но и может самостоятельно толкать-перемещать энтити, которые к тому же можно перемещать по поверхности такого объекта. То есть реализует довольно реалистичную физическую модель поведения разных там дверей и лифтов. Технически это устроено так: в начале функции SV_Push наш объект перемещается сам согласно его линейной\угловой скорости, подобному тому, как это было описано для MOVETYPE_NOCLIP. Затем (в Quake, увы тупым перебором) проверяются все энтити на предмет пересечения ббокса нашего мувера и их собственного ббокса. Интересен сам метод определения того, какие энтити мы должны подвинуть нашим мувером а какие нет: первое условие, если groundentity нашего объекта является сам мувер. Второе условие - если какая-либо энтить застряла в мувере (мы ведь его уже переместили\повернули). Дальше мы двигаем все необходимые энтити при помощи функции SV_PushEntity, которая делает трассировку по ходу движения и в случае если таковое невозможно (например впереди стена или другой объект), то делается проверка еще раз на застревание в мувере этой энтити. Если она действительно застряла, т.е. дальнейшее движение невозможно, то все изменения для подвинутых энтить откатываются назад (и для мувера в том числе), а сам мувер вызывет функцию Blocked.

7. MOVETYPE_FOLLOW Есть в исходниках Quake, но закоменчен, используется в Half-Life для приаттачивания к игроку оружия. Просто для удобства, чтобы оружие всегда имело такую же видимость как и сам игрок и не пропадало из поля зрения. Впрочем на клиенте есть ответная реализация этого моветипа для студиомоделей, что позволяет приаттачивать одну студиомоделей к другой (только видимую, без физического воздействия). Для этого у студимоделей должны быть схожие скелеты и кости с одинаковыми именами.

Ну вот пожалуй и всё что хотелось бы сказать об устройстве физики в Quake. Впрочем нет, не всё. Мы забыли упомнять такую довольно важную вещь как триггеры. Собственно триггеры мы можем вызывать прямо из функции SV_LinkEdict, задав второй параметр в true. Триггеры находятся в аналогичной цепочке, подобно той, что используется в трассе для перебора энтить, но вызывается она из SV_LinkEdict при помощи функции SV_TouchLinks. Эта функция перебирает все триггеры в заданом объеме и вызывает функцию Touch если текущий эдикт зацепил тот или иной триггер. Триггеры групируются в отдельную цепочку по признаку SOLID_TRIGGER.

Забавно отметить тот факт, что если в Quake брашевая геометрия триггера подменяется аппроксимацией ббокса по его размерам,, а сама модель обнуляется, то в Half-Life есть возможность создавать триггеры произвольной формы, поскольку там брашевая модель никуда не девается, а наоборот используется для проверки пересечения, при помощи функции SV_PointContents. Аналогичным образом в Half-Life осуществляется и проверка на пересечение с лестницей (можно посмотреть в коде pm_shared.c).

Ну вот теперь, пожалуй действительно всё задавайте свои вопросы, если таковые у вас имеются.