RSS
Меню сайта

Категории каталога
Doom3 [11]
Статьи о разработке карт к Doom3
Общее [7]
Статьи про общие моменты маппинга

Наш опрос
Навигация в "Файлах"
Всего ответов: 21

Copyright C4TNT© 2008

Главная » Статьи » Маппинг » Doom3

Скриптуем #7.2 - подъёмный кран
Вот и настал этот момент - создание подъёмного крана.

Создание подъёмного крана в Doom3

Для начала нужно определиться с устройством этого крана на карте. Здесь будут описаны принципы построения мостового крана, но некоторое изменение скрипта позволит легко переделать его в башенный и любой другой.

Итак, общий вид крана:


Примерная схема крана.

Итак, кран состоит из такого набора деталей:
  • рельсы (красные). По ним основная часть крана перемещается в плоскости по одной из горизонтальных осей.
  • тележка транспортёра (оранжевая). Эта часть перемещается по рельсам и несёт на себе захват. Кроме этого на тестовой карте сама тележка состоит из двух частей. Одна из её частей может двигаться относительно другой вертикально и именно на ней закреплён захват крана.
  • Захват крана. Обычно его делают из env_crane, но особенность в том, что env_crane не является мувером. Поэтому нужно создать невидимый мувер и закрепить на него env_crane. Этот мувер позволит управлять захватом.
Предположим, что вы такой кран нарисовали. Теперь его нужно правильно скрепить для того, чтобы он мог двигаться в любом направлении. По одному из горизонтальных направлений кран будет вынужден двигаться вместе с рельсами. Движением по второму направлению будет перемещение тележки по рельсам. А движением по третьему направлению будет движение захвата.

Поскольку кран у нас будет двигаться довольно независимо, для него нужно будет завести управляющий процесс. Для подтверждения этого советую изучить GUI для крана из Doom3. Это GUI вызывает скрипт в момент нажатия кнопки и в момент её отпускания. Поэтому проще и удобнее всего реализовать маленькие скрипт-функции, изменяющие переменную статуса крана. А кран уже будет самостоятельно следовать этим командам.

Переменная состояния

Мы уже делали подобное в предыдущем примере с бочками. Но тут этот способ будет рассмотрен более детально.
Итак, у нашего крана будет несколько способностей: движение по оси X, движение по оси Y, движение по оси Z, движение захвата по оси Z, открытие и закрытие захвата, ожидание. Вот такой вот набор. Обратите внимание на то, что движение тележки по z и движение захвата по z разделено. Это сделано для некоторого упрощения кода. Если вы решите сделать кран другого типа, то тоже рекомендую разбивать действия крана на элементарные.

Теперь создаём глобальную переменную и распределяем все эти действия по её значениям.
В начале файла скрипта:

float CraneStatus;            // 0 - stop, +-1 dx, +-2 dy, +-3 dz, +-4 claw up\down, +-5 fingers open\close

То, что за //, да и сами эти чёрточки писать не обязательно. Это нужно только для того, чтобы не запутаться потом в командах. Я выбрал такую систему: 1, 2, 3 - движение по X, Y и Z. 4 - движение захвата вверх и вниз. 5 - открытие и закрытие захвата. А знак +\- у этих значений указывает направление движения.

Управляющий процесс

Теперь напишем управляющий скрипт.

void CraneRoute()
{
   ...инициализация...
   while (1) {
       ...процесс...
   }
}

В инициализации нужно собрать кран в правильном порядке если вы ещё не сделали этого в редакторе. Желательно указать исходный статус крана, например 0 (stop). Кстати, для более удобного обозначения статусов вы можете попробовать поработать с макросами. Если в начале скрипта написать #define НАЗВАНИЕ_МАКРОСА ТЕКСТ_МАКРОСА
то во всём тексте НАЗВАНИЕ_МАКРОСА будет заменяться на ТЕКСТ_МАКРОСА при выполнении этого скрипта. Например, если написать #define CRANE_STOP 0 то в тексте скрипта CRANE_STOP превратится в 0.

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

float CraneWayDelta(float NewStatus, vector Position, float ClawZ, boolean CraneLoaded)
{
    return 10;
}
Как видите, у неё много разных параметров. Первый указывает направление движения и соответствует статусу крана. Конечно можно брать это же значение непосредственно из глобальной переменной статуса, но лучше описать функцию так как здесь. Это позволит вам проверять возможность движения крана не разворачивая его фактически. Position - это позиция тележки крана. В моём примере этот вектор собирается из трёх частей. Его X равен X той части крана, которая может двигаться по х, с Y и Z та же ситуация. ClawZ это положение захвата крана по вертикали. А CraneLoaded - это специальная переменная, равная true, когда кран держит какой-то предмет в захвате, но об этом в конце.

Теперь подумаем, как управлять одной из частей крана.

1. Начинаем движение
2. Двигаемся до того места, до которого может двигаться кран.
3. Останавливаем кран и меняем статус на 0 (stop)

В первом приближении так оно и есть. Остаются только небольшие тонкости. Во первых сначала нужно остановить старое движение крана если оно было. Во вторых ситуация с доступностью участков карты для крана может меняться, поэтому пока кран движется нужно проверять, не закрылась ли перед его носом дверь, например. Об этом нас должна будет информировать функция CraneWayDelta, возвращая значение 0 или меньше. В этом случае кран так же надо остановить. Кроме этого есть ещё и третий случай, когда кран стоит остановить. Это происходит если изменилась команда для крана. Итак, рассмотрим подробнее один управляющий блок.

        if (CraneStatus == 1){
            deltas = CraneWayDelta(CraneStatus, CrP,ClawZ,(!(!BindEnt)));
            PrevCraneSt = CraneStatus;
            sound_crane();
            if (deltas <= 0) {CraneStatus = 0;} else {
                dxPlat.move(EAST,deltas);
                dyPlat.stopMoving();
                dzPlat.stopMoving();
                Claw.stopMoving();
                while ((PrevCraneSt == CraneStatus) && dxPlat.isMoving()) {
                    tmp = dxPlat.getWorldOrigin();
                    CrP_x = tmp_x;
                    deltas = CraneWayDelta(CraneStatus, CrP,ClawZ,(!(!BindEnt)));
                    if (deltas <= 0) {break;}
                    sys.waitFrame();
                }
                if (PrevCraneSt == CraneStatus) CraneStatus = 0;
            }
        }

Первый if проверяет, соответствует ли команда этому блоку. Этот блок реагирует на команду 1
Дальше мы вычисляем расстояние, которое может пройти кран, повинуясь этой команде. CrP и ClawZ - это специальные переменные, которые обновляются каждый цикл чтобы соответствовать положению крана. Вот код, который это делает:

        tmp = dxPlat.getWorldOrigin();
        CrP_x = tmp_x;
        tmp = dyPlat.getWorldOrigin();
        CrP_y = tmp_y;
        tmp = dzPlat.getWorldOrigin();
        CrP_z = tmp_z;
        tmp = Claw.getWorldOrigin();
        ClawZ = tmp_z;

tmp здесь - просто временный вектор. Он нужен потому, что так писать нельзя: dzPlat.getWorldOrigin()_x
Вообще tmp имеет тип vector и в скриптах дума для того, чтобы работать с компонентами вектора, используются такие вариации названия.

        sound_crane();

Эта функция просто запускает звук движущегося крана. Я взял эти звуки и их функции прямо из скрипта крана на карте дельта3. Ничего особо сложного в этой функции нет, она просто запускает определённый звук. Её можно посмотреть в скрипте карты-примера.

        if (deltas <= 0) {CraneStatus = 0;} else {

Тут проверяем расстояние, на которое можно сдвинуть кран и если оно меньше или равно 0, то указываем статус крана равным 0. В противном случае будем его двигать на указанное расстояние.

        dxPlat.move(EAST,deltas);
        dyPlat.stopMoving();
        dzPlat.stopMoving();
        Claw.stopMoving();

Останавливаем все старые движения крана. Одновременно с этим запускаем нужное нам движение. Вот эта команда: dxPlat.move(EAST,deltas); Возможно вам придётся подбирать эту команду так, чтобы кран двигался правильно. В моём случае она должна увеличивать X крана.

        while ((PrevCraneSt == CraneStatus) && dxPlat.isMoving()) {
            tmp = dxPlat.getWorldOrigin();
            CrP_x = tmp_x;
            deltas = CraneWayDelta(CraneStatus, CrP,ClawZ,(!(!BindEnt)));
            if (deltas <= 0) {break;}
            sys.waitFrame();
        }
 
А здесь мы смиренно ждём какого-нибудь из трёх вышеописанных событий. Если команда изменится то перестанет выполняться PrevCraneSt == CraneStatus. PrevCraneSt хранит выполняемую сейчас команду и устанавливается в самом начале управляющего блока. Поэтому если команда изменится, то этот блок мы покинем. Второй вариант - это завершение движения элемента. Тогда перестаёт выполняться dxPlat.isMoving(). А третий вариант проверяется внутри цикла. Там мы обновляем ту часть переменной положения, которая может меняться. Потом вызываем CraneWayDelta и узнаём, можно ли ещё двигаться. Двигаться можно если результат этой функции больше нуля. Кроме этого в цикле выполняется sys.waitFrame() чтобы процесс мог работать в фоновом режиме. Следите за этим в ваших собственных модулях особенно внимательно. Если при каких то условиях ваш вечный цикл не будет проходить через wait - операторы ни разу, то игра для предотвращения зависания закроет вашу карту. При этом в консоли будет написано Runaway loop error. Если быть конкретным, то для срабатывания этой защиты нужно, чтобы интерпретатор прошёл примерно 3000 операций в вашей функции без перерывов.

                if (PrevCraneSt == CraneStatus) CraneStatus = 0;

Эта строка просто обнуляет статус-переменную если только причиной прерывания выполнения команды не была другая команда.

С одним блоком управления разобрались. На самом деле у вас будет по блоку на команду, да и у меня тоже самое. Эти блоки по большей части одинаковы. Поэтому рассмотрим только особо интересные места остальных блоков. Во первых на GUI недостаточно кнопок для того, чтобы управлять и вертикальным движением каретки и вертикальным движением захвата. Поэтому эти два движения комбинированы. А каким образом - сейчас расскажу.

Посмотрите в скрипте код для статуса -3 (каретка вниз). В этом блоке вместо того, чтобы при невозможности движения каретки устанавливать статус в ноль мы его устанавливаем в -4 (захват вниз). Поэтому когда каретка дойдёт до самой нижней точки автоматически начнёт двигаться захват. Команда 4 таким же образом переходит в 3, поэтому при движении захвата вверх и его остановке будет двигаться каретка.

Если вы расширите GUI на пару кнопок, то можно будет вернуть поведение этих команд к нормальному и прицепить освободившиеся -4 и 3 к GUI.

Ещё один интересный момент заключается в работе с таймером и в управлении пальцами захвата крана.

            Finger.setFingerAngle(0);
            Timer = sys.getTime() + 3;
            while (PrevCraneSt == CraneStatus && Timer > sys.getTime()) {
                sys.waitFrame();
            }
            Finger.stopFingers();

setFingerAngle(0) - эта команда действует только на env_crane объекты и устанавливает угол раскрытия пальцев манипулятора. Для того, чтобы открыть захват, нужно устанавливать отрицательный угол, например -90. К сожалению к env_crane не применима команда isMoving(), поэтому и нужна работа с таймером. Принцип работы с таймером довольно прост, нужно получить текущее время с помощью sys.getTime(). После этого сохраняем значение в переменной, удобно сразу прибавить к нему время ожидания (измеряется в секундах). А дальше остаётся только проверять, не дошло ли системное время до установленной нами отметки. Вот эта проверка: Timer > sys.getTime()

К сожалению простым sys.wait здесь не воспользуешься, поскольку нужно ещё проверять, не изменилась ли команда для крана. Если бы вы использовали здесь sys.wait, то кран не реагировал бы на команды во время открытия захвата.

После ожидания или при изменении команды нужно остановить пальцы захвата с помощью Finger.stopFingers();

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

Границы перемещения крана

У нас осталась недоделанная функция CraneWayDelta. Сделаем из неё что-нибудь более или менее работающее.
Для начала разберёмся с основными ограничениями. Я покажу это на некоторой абстрактной схеме, но для того, чтобы подогнать кран к вашей карте, вам придётся сделать свою схему и написать функцию, соответствующую этой схеме.

основные ограничения


Схема основного ограничения.

Основное ограничение наиболее простое. Красным на схеме выделен контур в котором перемещается кран, синим - будущий контур ограничения. Этот контур всегда квадратный. Кран будет всегда находиться внутри синего контура, а точнее его центральная точка будет там находиться. Функция принимает такой вид:

    if (NewStatus == 1) return 212 - Position_x;
    if (NewStatus == -1) return Position_x - 44;
    if (NewStatus == 2) return (212 - Position_y);
    if (NewStatus == -2) return Position_y - 44;

    if (NewStatus == 3) return 400 - Position_z;
    if (NewStatus == -3) return Position_z - 280;

    if (NewStatus == 4) return Position_z - ClawZ + 200;
    if (NewStatus == -4) return ClawZ - Position_z - 16;
   
return 0;
Смотрите, что возвращает эта функция. В зависимости от статуса вычисляем расстояние от края квадрата до центральной точки. Если статус какой-то нестандартный то возвращаем ноль. Обратите внимание на вычисление расстояния для 4 и -4. Там используется не только ClawZ но и координата положения. Так получилось потому, что захват не должен выдвигаться из крана больше чем на всю свою длину и уходить в него больше чем надо. Но поскольку высота точки крепления может меняться за счёт сдвига тележки вверх и вниз, то и начальная позиция захвата может меняться тоже. Поэтому вычитаем позицию каретки из позиции захвата, а потом уже вычисляется расстояние. Этими действиями мы ограничили передвижения крана синим квадратом на картинке. Возможно вам этого хватит, а возможно и нет. Если у вас зона перемещения крана сложнее, то читайте этот параграф дальше.

сложные ограничения



Вот схема более сложной границы для крана.

Теперь попробуем описать это функцией.
Я опишу только как обработать одно направление, с остальными вам нужно будет поступать аналогично.
Пусть этим направлением будет положительное по X. Максимальное положение по X будет 224. поэтому нужно возвращать 224 - x. Но у нас вырезан уголок от y = 32 до y = 64. там максимальный x = 160. В этом случае нужно возвращать 160 - x. Получаем:

    if (NewStatus == 1) { if (Position_y < 64) {return 160 - Position_x;}else{return 224 - Position_x;}}
Рассмотрим ещё и положительное направление по Y.
Тут у нас максимум y = 224. Но есть окошко от y = 96 до y = 160. Окошко наличествует только при x < 64. Получаем
    if (NewStatus == 2) { if (Position_x < 64 && Position_y < 160) {return 96 - Position_y;}else{return 224 - Position_y;}}

Интерфейс крана


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

void CraneStop() {CraneStatus = 0;}
void CraneDX() {CraneStatus = 1;}
void CraneNDX() {CraneStatus = -1;}
void CraneDY() {CraneStatus = 2;}
void CraneNDY() {CraneStatus = -2;}
void CraneDZ() {CraneStatus = 4;}
void CraneNDZ() {CraneStatus = -3;}
void CraneCO() {CraneStatus = 5;}
void CraneCC() {CraneStatus = -5;}
Эти скрипты нужно назначить кнопкам GUI для того, чтобы управлять краном. Пример этого можно посмотреть на карте-примере, поскольку это скорее маппинг, а не скриптинг.

Кроме этого не забудьте реализовать командный блок, который будет обрабатывать остальные команды для крана, даже несуществующие.

Хваталки

Если вы писали скрипт для своего собственного крана и попробовали его протестировать, то наверняка обратили внимание на то, кран может "трогать" объекты, но их никак не удаётся поднять. Дело в том, что для закрепления объекта в захвате крана недостаточно просто закрыть пальцы этого крана на объекте. Нужно дополнительно прикрепить объект к крану с помощью bind. Но для этого придётся сначала узнать, что поймал наш кран. Для этого познакомимся с такой операцией как trace.

TRACE

Итак, в думе есть такая полезная операция как trace. Единственное, что она делает, так это проверяет, во что втыкается выпущенный из заданной точки на карте луч. Объект может быть стеной, энтити или чем либо другим.
Синтаксис trace:

    sys.tracePoint(начало, конец, маска объектов, игнорируемый объект);

Теперь подробнее. Начало и конец - это координаты точек, между которыми будет происходить трассировка. Маска объектов это специальная константа, позволяющая проверять на столкновение только с определёнными объектами.
вот возможные значения этой маски:

MASK_ALL                         любые объекты
MASK_SOLID                     твёрдые объекты (стены)
MASK_MONSTERSOLID       то, что блокирует монстров
MASK_PLAYERSOLID          то, что блокирует игрока
MASK_DEADSOLID            то, что блокирует трупы
MASK_WATER                    вода
MASK_OPAQUE                  небо
MASK_SHOT_RENDERMODEL        то, во что можно стрелять
MASK_SHOT_BOUNDINGBOX       то же, что и solid но энтити пересекаются с лучом не по полигонам, а с помощью охватывающего их паралелограмма.

Игнорируемый объект - объект, который игнорируется трэйсом. Если вы трассируете что-то от поверхности некоторого объекта то в качестве игнорируемого лучше установить этот самый объект. Иначе трэйс может иногда захватывать тот объект от которого его запустили. Если трэйс запускается изнутри объекта то тем более это нужно сделать. Кроме этого есть sys.trace, который проверяет не пересечение луча, а пересечение кубика, движущегося по лучу.

Применим это на практике. Для этого осталось только узнать позицию захвата крана. У модельки захвата есть специальный джоинт "crane" для крепления к нему захваченых объектов. Если от этого джоинта на некоторое расстояние вниз проверить трэйсом наличие объектов, то в результате узнаем, попалось ли что-нибудь крану и что это было. Но для этого сначала нужно получить положение этого джоинта в пространстве и кроме этого какое-нибудь направление, от которого можно будет отсчитать направление вниз. Для получения положения джоинтов есть специальные функции:

getJointHandle("crane") - возвращает номер джоинта. В качестве параметра передаётся название джоинта.

после этого можно использовать

getJointPos(handle) - принимает номер джоинта и возвращает его положение.
getJointAngle(handle) - принимает номер джоинта и возвращает его углы.
Возьмём позицию джоинта и его угол и сделаем из этого положение двух точек:

tmp = Finger.getJointPos(JH);
sys.tracePoint(tmp, tmp - sys.angToRight(Finger.getJointAngle(JH))*48, ...

tmp - sys.angToRight(Finger.getJointAngle(JH))*48 - подобрано опытным путём, sys.angToRight преобразует углы в направление. Есть ещё несколько аналогичных функций, но в конкретном случае подошла именно эта. А на 48 умножаем для того, чтобы увеличить расстояние между точками.

Теперь нужно только выбрать подходящую маску трассировки и указать игнорируемым объектом Finger. После этого трассировка обнаружит объект, если он попал в манипулятор. Осталось только проверить, попал ли объект в захват и получить ссылку на объект если это случилось. Получить пойманный объект можно функцией sys.getTraceEntity(); Она возвращает объект, пойманный последней трассировкой. Если объект пустой, то трассировка ничего не поймала. поэтому проверим объект на существование и закрепим его на кране если он нормальный:

                TraceCrane(Finger,JH);
                BindEnt = sys.getTraceEntity();
                if (!BindEnt || ( BindEnt == $world) || ( BindEnt == $player)) {
                    CraneStatus = -5;
                    BindEnt = sys.getEntity("");
                }else{
                    bindJoint = sys.getTraceJoint();
                    if ( bindJoint ) {
                        bindBody = sys.getTraceBody();
                        if ( bindBody ) {
                            BindEnt.setKey( "bindConstraint bind1", "fixed " + bindBody + " " + bindJoint );
                            BindEnt.bindToJoint( Finger, "crane", 1 );
                        }
                    } else {
                        BindEnt.bindToJoint( Finger, "crane", 1 );
                    }
                    CraneStatus = 4;
                }

В TraceCrane у меня спрятана трассировка. Подробности можно узнать прямо в скрипте примера.

                if (!BindEnt || ( BindEnt == $world) || ( BindEnt == $player1)) {

Это просто проверка на то, подходит ли объект крану. $world, игроков и пустые объекты не берём. Кстати, в старых версиях карты вместо $player1 указано $player, что неправильно. Если объект не подходит, то отдаём команду крану на открытие захвата. Если же объект подходит, то пытаемся получить джоинт, в который попал луч. Это нужно если мы поймали рагдолл. Далее если джоинт существует, то прописываем объекту ключ bindConstraint и крепим его к крану с помощью bindJoint. Если у объекта нет джоинтов, то крепим объект без прописывания ключа. Кроме этого сохраняем объект в специальной переменной для пойманного объекта (BindEnt). После этого отдаём крану команду 4, что заставляет его поднять захват.

В коде обработки открытия захвата нужно проверить, указан ли BindEnt и если указан, то открепить его и заполнить BindEnt неправильным объектом:

            if (!(!BindEnt))
            {
                BindEnt.unbind();
                BindEnt = sys.getEntity("");
            }


Теперь если открыть захват, который что-то держит, то это что-то упадёт.

По большему счёту кран уже готов. Остаются ещё некоторые мелочи, которые вы сможете посмотреть в скрипте тестовой карты. С наступающим всех...

Тестовая карта как всегда тут: scri6.zip
Шапка статьи тут: http://c4tnt.ucoz.ru/publ/4-1-0-19
Скачать модели труб для тестовой карты можно тут: http://c4tnt.ucoz.ru/load/24-1-0-2
Категория: Doom3 | Добавил: c4tnt (27.12.2009)
Просмотров: 2033 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Вход:

Поиск

Ссылки


Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Работаем на керосине