Главная
Новости
Статьи
Ссылки
Гостевая книга
Форум
Почта:
Прочее
Общая информация
Скачать
Инструменты
Авторы проекта
Глоссарий объектов

Статьи по Spirit of Half-Life

SoHL — Совместное перемещение объектов и Movewith.

Автор: BUzer

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

Если вы ещё не скачали этот замечательный мод, то бегом сюда:
Spirit of Half-Life Revision Bild 1.8 + Spirit of Half-Life: Revision Build 1.8 SDK

Итак, давайте начнем с простейшего — а именно, с поезда, представленного func_train'ом, и двигающегося по прямой без всяких вращений-извращений. Для примера как нельзя лучше подойдет демонстрационный поезд из прилагающейся к Спириту карты (чтобы им полюбоваться, можете загрузить Спирит, и запустить из консоли карту spiritdemo).

тележка из мода Spirit of Half-Life с эффектами movewith
Как видно на картинке, поезд буквально окружен разнотипными ентитями, которые надо заставить двигаться вместе с ним. В наборе присутствуют "лучи света" из фар, вращающиеся стрелки на приборной панели, стекла с дверями, а также спрайты, символизирующие свет задних красных огней.
Если вы мельком прогляните свойства этих и множества других ентить в Спирите, то, скорее всего, заметите у них общую детать — все они имеют параметр под названием Moves with. Привязать через него одну ентитю к другой — не просто, а очень просто! После того, как вы пропишете в этом параметре имя какого-нибудь объекта, то ваша ентитя начнет постоянно суммировать его скорость со своей, или, по-просту говоря, станет двигаться вместе с ним.
Таким образом, для реализации вышеприведенного примера, нам надо всего-лишь зайти в свойства всех этих ентитей, и прописать им в параметре 'Moves with' имя поезда, с которым они должны двигаться, и готово! После этого мы можем хоть во время движения поезда бить стекла, давить там кнопки, и хлопать дверями — они, двигаясь вместе с train'ом, будут работать так же, как и обычные.
MoveWith-объекты могут также быть связаны в цепочку — например, у вас может быть поезд, к которому прикреплена дверь, к которой прикреплено бьющееся стекло, и к которому, в свою очередь, прикреплена какая-нибудь декаль.
Любая ентитя может быть прикреплена к другой через moveswith, поэтому если у какой-то вы не обнаружили параметра "Moves with", то вам всего-лишь надо добавить его вручную — отключите SmartEdit и добавьте ключевое значение под назанием "movewith".

Но чудес, как всегда, не бывает, и данная система обладает определенными ограничениями. Собственно, успешным в той или иной степени обходом каковых нам, скорее всего, и придется в дальейшем заниматься.

Во-первых, самым главным врагом movewith-системы является вращение тех ентить, к которым происходит прикрепение. Нашему предыдущему примеру это не грозит: func_train месте со своим барахлом мирно двигается вперёд-назад по своему пути, даже и не помышляя о вращении вокруг своей оси. Но что если нам просто жизненно необходимо будет сделать разворачивающийся на поворотах поезд с бьющимися стеклами? Ну, собственно, давайте проэкспериментируем, что из этого выйдет. В свойствах одного из path_corner'ов пути зададим угловую скорость вращения для нашего поезда, и будем наблюдать за ним. Вот он едет-едет, подъезжает к повороту, и тут — бац — все стёкла, кнопки, двери улетают сквозь стены в неизвестном направлении, а спрайты и стрелки на приборах просто даже не подумали разворачиваться вместе с поездом, и он оказался повернутым к ним боком. Мдаа, не порядок... И в чем же дело?

А дело в том, что когда "главный" объект начинает вращаться, то прикреплённые к нему тоже вращаются, но не вокруг общей оси вращения, а каждый вокруг своей. Какая чушь получается в результате, догадаться в принципе не сложно. Расправляться с ошибкой будем таким образом — мы все знаем, что осью вращения для брашевой ентити является origin-браш, значит, соответственно, нам придется по возможности для всех движущихся вместе с поездом ентить создать origin-браши, расположенные точно в центре этого поезда (давно ли последний раз создавали origin-браш для func_breakable? :) Привыкайте :\). Вобщем, чтобы избежать ошибок при вращении, origin'ы всех совместно движущихся ентить должны совпадать.

Ну ладно, для брашевых ентить можно указать ось вращения "ориджином", но как же быть с точечными ентитями, типа спрайтов? Для них-то осью вращения являются всегда они сами, и с этим ничего не поделаешь! И что делать, если тип брашевой ентити обязывает находиться центру вращения не где-то в центре поезда, а, например, в центре самой ентити, как у тех вращающихся стрелок на приборной панели? Или как у вращающейся двери? В ответ на это лишь можно только глубоко вздохнуть, и сказать, что в принципе, теоретически, это реализуемо, но надо оперировать довольно сложными ентитями, работающими с векторами направления, так что оставим это на потом.

Хотя есть один хитрый способ расположения ориджинов, позволяющий иногда удовлеторить и общий центр вращения, и центр вращения самой ентити. Например, его можно использовать на одной из стрелок на нашем поезде, правда её придется сдвинуть в центр приборной панели.

Вот, взгляните на эту простенькую картинку:

размещение осей блоков для поезда в Spirit of Half-Life
Тут мы имеем func_rotating (мы видим его сбоку), который должен двигаться вместе с func_train, и у каждого из них есть своя ось вращения, помеченная на картинке словом "origins". Теперь давайте мысленно продолжим эти оси по направлению к точке их пересечения (ось поезда — вверх, а ось rotating'а — влево). Вот в этой точке мы и должны разместить origin-браши для обоих этих ентить. Способ ограничивает довольно сильно, ведь редко когда оси вращения способны пересечься подобным образом, но тем не менее, возьмите на заметку.

Итак, что мы имеем? Лучи света из фар, бьющиеся стекла, двери, и кнопка спешно научились проходить повороты благодаря origin-брашам. Спрайты и стрелки, равно как и вращащиеся двери, мы оставили на потом. Ну что же, давайте покатаемся на этом произведении... чтобы через десять секунд обнаружить, что наши двери, продолжают нагло выпендриваться — вопреки всем ожиданиям, они даже после разворота поезда продолжнают открываться ту сторону, куда открывались с самого начала. То есть, несмотря на то, что они вращаются как надо месте с поездом, они всё же не способны были изменить направление их открытия. Это приводит к подобным багам: поезд развернулся на 90 градусов, и дверь стала отъезжать не в стену поезда, а внутрь или наружу. Вот эту проблему действительно никак не пофиксить, разве что заблокировать дверь мультисорсом до тех пор, пока поезд не примет правильное для двери направление, либо заставить открываться дверь не вбок, а вверх или вниз (на мой взгляд — второй вариант лучше :) ).

Собственно, вращения были только первой проблемой, с которой можно столкнуться. Вторую могут создать игрок или другие монстры, попавшиеся на пути движения брашевых ентить. В принципе, для поездов это не так актуально — они просто двигают и давят. Внмательнее надо следить за этим при создании дверей. Например, вы делаете дверь с окном, куда вставляте стекло из func_breakable, которое через movewith привязываете к этой двери. Если дверь вращающаяся, то вы также должны сделать origin-браш для этого стекла, совпадающий с ориджином самой двери. Если кто-то попадётся на пути движения двери или стекла, то они без труда его отодвинут, но если это окажется невозможным (объект во что-то упрётся с другой стороны), то тогда движение стекла и двери могут потерять синхронизацию. Это происходит из-за того, что ентитя, с которой движется какая-то другая, передаёт ей не свои актуальные координаты, а лишь скорость (вектор) её движения, и этот вектор не меняется даже тогда, когда движение ентити что-то блокирует. Лучший способ бороться с этим — по возможности делать объекты, сквозь которые можно пройти, или просто стараться избегать таких ситуаций.

Лирическое отступление: Наверное, люди, знакомые с movewith, подумали — нафига столько надо было про него писать? Ну блин... Если честно — сам не знаю :\

И наконец, третий аспект, который необходимо рассмотреть. Наверное, многие сильно расстроятся, когда я скажу, что через movewith нельзя привязать что-то к монстру, игроку, или к func_pushable. Да-да, это именно так, к сожалению. Но сильного повода для расстройства нету, так как помимо Movewith в Спирите есть ещё не менее интересные методы создания совместного движения, такие, как использование объектов trigger_motion или motion_manager — с их помощью такие привязки делаются довольно просто. Вот о них и об их ограничениях давайте и поговорим.


Но для начала кропарь теории :\ Конечно, человек, программирующий в SDK, объяснил бы всё это техничнее, чем я, но тем не менее... :) Вобщем, положение любой ентити в пространстве характеризуется координатами её центра и углом, на который она повернута. Также, если говорить о движущихся ентитях, типа func_train`а, то эти две характеристики также обладают показателем скорости их изменения — то есть, скоростью передвижения и угловой скоростью вращения. Скорость передвижения задаётся в виде вектора.

Итак, trigger_motion и motion_manager — это триггеры, влияющие на положение и скорость заданных ентить. Давайте, пожалуй, начнём с первой

Trigger_motion — это триггер, который при активации задаёт для ентити её новое местоположение или угол, либо меняет её скорость движения или вращения (либо делает всё это сразу, если нужно :) ) Рассмотрим параметры этого триггера:

  • Name — ясен пень, имя триггера для активации.
  • Target to affect [LE] — имя ентити, параметры которой мы будем регулировать (насчёт того, что значит это LE в скобках — это имеет отношение к locus-системе, которую разберём в статье Система локусов и calc-энтити),
  • Position — основа для рассчёта нового положения нашей ентити (оставьте пустым, если вы не хотите задавать новые координаты). Здесь может быть либо имя другого объекта-ентити, параметры которой будут использованы при рассчёте, либо числовые значения, заданные в подобной форме: "128 32 -57", где эти три числа яляются координатами по x, y, z соответственно. Как именно будет использоваться информация, заданная в этом поле, определяется содержимым следущего:
  • Meaning of Position — Если равно "Set new position", то новое положение подсчитается однозначно — ентитя переместится по координатам, указанным в поле "Position". Если равно "Add offset", то это будет значить, что координаты, заданные в "Position" — это вектор, по которому надо сместить ентитю относительно её текущего положения.
    Пример: имеем проименованный func_wall, кнопку, и trigger_motion. Кнопке указываем вызвать trigger_motion, у которого, в свою очередь, в "Target to affect" пишем имя func_wall'а, в поле "Position" — "64 0 0", и поле "Meaning of Position" — "Add offset". Теперь при каждом нажатии на кнопку этот func_wall будет телепортироваться на 64 пикселя вправо, относительно вида сверху в редакторе.
  • Angles — по аналогии с "Position", это яляется основой для рассчёта угла. Тип задаваемых данных различен в зависимости от значения следующего поля:
  • Meaning of Angles — при "Set new angle" для ентити устанавливается новый угол, задаваемый вектором. В "Angles" при этом стоит имя другой ентити, угол которой надо присвоить этой, либо вектор, заданный числовыми значениями (x, y, z)
    при "Rotate by [LV]" угол, указанный вектором в "Angles" сумируется с текущим.
    при "Rotate by (Y Z X)" ожидается, что в "Angles" будут указаны три значения угла в градусах (pitch, yaw, roll), на которые надо повернуть ентитю, относительно её текущего положения.
  • Velocity и Meaning of Velocity — действует по аналогии, но работает со скоростью движения, задаваемой вектором. Первые три значения поля "Meaning of Velocity" подразумевают указание либо имя ентити, скорость которой будет использована, либо вектора числовыми значениями. Последнее значение подразумевает значения в градусах, на которые будет изменено направление движения ентити. Имейте ввиду, что при присвоении ентите новой скорости, она обсчитывается в соответствии с её "физической" моделью — например, присвоение скорости для func_wall ничего не даст, потому что движок вообще не осуществляет проверку её скорости, считая её не способной двигаться, а присвоение скорости игроку, func_pushable, или монстру приведет к тому, что они сдвинутся с места, как будто их толкнули из пустоты, и потом плавно затормозят от силы трения (весьма интересный эффект, можно использовать, как отдача от ударной волны).
  • И, наконец, AVelocity и Meaning of AVelocity — они изменяют скорость вращения, измеряемую в градусах (наверное, в секунду)

Ну, в принципе, слегка разобрались, что это за зверь такой, осталось только попрактиковаться — и вам, и мне :). К слову, некоторым образом, можно использовать эту штуку, как аналог movewith — вызывая его мультименеджером тридцать раз в секунду, чтобы он постоянно сдвигал что-нибудь, например спрайт, в координаты какого-нибудь монстра или func_pushable, либо передавал ему её скорость. Но зачем использовать такие методы, когда у нас есть ещё одна ентитя для управления положением и скоростью!?

Motion_manager — это своеобразный аналог trigger_motion, но он действует постоянно, как будто trigger_motion активируют очень много раз в секунду. Параметры у него организованны слегка по-другому:

  • Name — собственно, имя. Вообще-то motion_manager имени не требует, начиная работать сразу с загрузки карты, поэтому оно нужно для использования с locus-системой.
  • Target to affect — объект, на который действует motion_manager.
  • Position — как и в trigger_motion, здесь задается либо имя ентити, либо числовые значения, которые потом будут считаться либо за координаты, либо за вектор скорости — в зависимости от значения следующего поля:
  • Meaning of Position — рассмотрим варианты — первые два влияют на изменение координат подконтрольной ентити, а остальные три — на её скорость движения, не используйте их, если ваша ентитя не поддерживает скорость:
    • "Set position" — установка центра ентити, контролируемой motion_manager'ом, точно по координатам, заданным в "Position".
    • "Offset position" — начиная с этого значения и ниже, в поле "Position" задается вектор скорости. Если будет задано числовое значение вектора , то после загрузки карты вы обнаружите подконтрольную ентитю двигающейся в его направлении и забивающей на все препятствия. Если она натолкнется на игрока или монстра, то она пройдет сквозь него, и он при том будт не в состоянии двигаться. Гораздо полезнее указать имя какой-нибудь ентити, например, func_pushablе'а — в таком случае, при её движении, подконтрольная ентитя постоянно будет смещаться на её вектор скорости, и, по-просту говоря, следовать за ней, как при movewith. Надо сказать, это обычный способ заставить двигаться что-нибудь, типа info_target'а или спрайта вместе с func_pushable. Но учтите, если это не какой-нибудь спрайт или звук, а брашевый объект, то лучше его сделать not-solid, потому что расталкивать монстров и игроков он не намерен.
      Пример: имеем motion_manager, кубический func_pushable, и env_sprite, размещенный чуть выше его верхушки. В свойствах motion_manager'а пишем в поле "Target to affect" имя env_sprite, в поле "Position" — имя func_pushablе'а, и в "Meaning of position" ставим "offset position". Всё. Теперь спрайт намертво прикреплен в нашей толкаемой игроком коробке, и даже самые страшные глюки не в силах его отодрать от неё.
    • Set velocity — как ясно из названия, устанавливает вектор скорости ентити равным заданному в "Position". По своей сути, он повторяет то же самое, что и предыдущее значение — эмулирует movewith, только то просто смещает ентитю, а это присваивает ей скорость, заставляя её саму пройти это расстояние. Разница в том, что, как я уже говорил, не все ентити работают со скоростью, но зато она не будет проходить сквозь игрока, а станет отталкивать его, как обычно.
    • Accelerate by — скорость подконтрольной ентити постоянно суммируется с заданной в "Position", набирая тем самым ускорение. Если задать вектор числами, то после загрузки карты вы увидите вашу ентитю уносящейся в бесконечность, набирая скорость.
    • Follow position — забавная штучка, заставляет плавно сместиться ентитю с торможением в конце вдоль заданного вектора, относительно своей текущей позиции.
  • Facing — основа для рассчёта вращения или установки угла ентити. Способ её применения определяется значением следующего поля:
  • Meaning of Facing — первые два варианта подразумеают, что в "Facing" будет указан числовой вектор, либо ентитя, вектор направления угла которой будет использован. Вторые два подразумевают указание трех чисел, определяющих углы в градусах — pitch, yaw, и roll.
    • Face direction — просто устанавливает angle ентити равным направлению вектора, заданному в "Facing".
    • Rotate by [LV] — производит вращение текущего угла на заданный вектор (я что-то не совсем это срастил, надо потестить)
    • Rotate by (x y z) — вращает текущий угол на заданные в градусах углы.
    • Set avelocity — устанавливает угловую скорость вращения ентити, заданную в градусах на какой-то период времени (на секунду, кажется). Как и случае со скоростью, не все ентити могут работать с угловой скоростью вращения.

Уфф, ну вот, пока что всё. По этим ентитям остается только добавить, что если им поставить флаг "debug", то они постоянно будут писать в консоли, какое они на кого оказывают воздействие.

Естественно, это не все манипуляции со скоростями и координатами ентитей — довольно богатые возможности предоставляют calc_ентити, умеющие рассчитывать координаты отдельных частей тел монстров, или вектора, направленные от одной ентити к другой.. В следующий раз, наверное, напишу об их использовании, или о locus-системе. Дай только бог самому разобраться.. :)


GL & HF!





Half-life Red Alert Expantion
Presented by BUzer
All rights reserved
  Counter.CO.KZ -     !
Hosted by uCoz