RSS
Меню сайта

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

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

Copyright C4TNT© 2008

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

Скриптуем #7.1 - конвейер

Начнём с конвейера

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

Для начала нужно создать конвейер на карте. Есть несколько разных вариантов его изготовления, здесь я предлагаю рассмотреть только вариант, использованный в quake2. Там конвейеры были собраны из движущихся платформ, стоящих впритык друг к другу и движущихся синхронно. В дум3 нет аналога квейковскому train, но есть скрипты. Поэтому соберём наш конвейер из набора func_mover-ов, а перемещать будем их с помощью скрипта. Для начала вспомним, что муверы можно двигать с помощью специальных команд move и moveto. Это уже было описано в предыдущих частях этой серии статей, поэтому отмечу только особо интересные факты:
  • Мувер может быть в любой момент заблокирован
  • Если вы двигаете мувер, то следом за ним двигается всё, что прикреплено к нему с помощью bind
  • Если несколько объектов скреплено с помощью bind, то при заклинивании любого из них останавливается вся конструкция.
Благодаря такому поведению муверов можно собрать достаточно простой и удобный конвеер. Для этого достаточно скрепить все кусочки конвеера, передвинуть первый из них на его длину, потом переставить выдвинувшийся за пределы карты кусочек конвейера в его хвост и пересобрать конвейер. Эту операцию можно будет повторить сколько угодно раз, при этом лента будет постоянно двигаться. Края конвейера рекомендуется спрятать куда-нибудь, поскольку их появление и исчезновение будет видно. Впрочем на тестовой карте всё это сделано. Подробнее о перемещении конвейера на схеме:


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

И в этом месте уместно будет рассмотреть несколько полезных методов создания скриптов. Для начала рассмотрим универсальный entity-скрипт. Единственное требование к этим скриптам такое - он должен брать всю информацию из объекта на карте. А в самом коде скрипта указывается только название целевого объекта. Для получения такого поведения достаточно определить функцию с параметром, принимающим entity объект, и добывающую остальную нужную ей информацию уже из этого объекта. Использование этой функции будет выглядеть примерно так:
Conveyor($func_mover_xxx);

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

scriptEvent    void     setKey( string key, string value ); - устанавливает ключ объекта
scriptEvent    string    getKey( string key ); - извлекает ключ из объекта
scriptEvent    float    getIntKey( string key ); - извлекает ключ из объекта и превращает в целое число
scriptEvent    float    getFloatKey( string key ); - то же, но в дробное число
scriptEvent    vector    getVectorKey( string key ); - извлекает вектор
scriptEvent    entity    getEntityKey( string key ); - извлекает энтити. В самом ключе должно быть прописано имя энтити.

Итак, из объектов можно извлекать ключи. Для конвейера вполне логично в каждом из его элементов завести специальное поле (например next), в котором будет имя следующего куска конвейера. Тогда можно делать скрипты для операций с конвеерами, а параметром будет первый энтити конвейера. Попробуем сделать мини-функцию, которая расцепит все элементы конвейера с помощью метода unbind().

ConveyorFlip(entity conveyor_base)
{
entity Current;

    Current = conveyor_base;
    while (!Current)
    {
        Current.unbind();   
        Current = Current.getEntityKey("next");
    }
}
Итак, маленько о том, как это работает. Сначала вызываем эту функцию, при этом энтити conveyor_base равен первому элементу конвейера. Функция работает с энтити Current в цикле. Сначала этот энтити равен conveyor_base, потом мы применяем к Current операцию unbind и проделываем такой трюк: Current = Current.getEntityKey("next"); В результате каждый раз Current будет становиться тем энтити, который ранее был указан у current в поле "next". В итоге функция пройдёт весь конвейер поэлементно.

Продолжим изыскания. Теперь было бы неплохо дополнить эту функцию так, чтобы она могла не только разобрать конвейер на отдельные части, но и собрать его по новой нужным образом. Для этого посмотрим ещё раз на схемку и представим абстрактную ситуацию, когда в конвейере уже переставлено несколько элементов. Фактически получается следующее - у нашего конвейера будет начало физическое (элемент с номером 1) и начало фактическое (элемент справа). Причём порядок скрепления будет меняться примерно так: 1-2-3, 3-1-2, 2-3-1. Обратите внимание на то, что порядок цифр не меняется, меняется только стартовый элемент. Значит достаточно указать функции кроме начального элемента конвейера ещё и крайний правый. (правый условно, конвейер всегда можно реверсировать). Кроме этого желательно знать прошлый крайний элемент для того, чтобы его можно было перенести на другой край. При этом непосредственно в этой функции переносить его не будем, но такая информация будет полезна для создания правильной сцепки. Принцип сцепки будет такой - прицепляем к новому первому элементу всё, что следует за ним вплоть до старого крайнего элемента. То есть если было 1-2-3 и становится 3-1-2 то нужно прикрепить 1 к 2. Тройку (старый крайний) не крепим пока никуда, поскольку его ещё двигать. а всё, что идёт от тройки и до нового начального элемента - крепим к тройке. Если двигать по одному элементу, то это не имеет особого значения, но если двигать несколько сразу, то без такого скрепления получатся проблемы. Кроме этого пусть функция возвращает старый крайний элемент чтобы его можно было передвинуть.
entity ConveyorFlip(entity conveyor_base,entity make_first)
{
entity Current;
entity PF;
entity tmp;
boolean notlast;

    Current = conveyor_base;
    PF = sys.getEntity("");
    notlast = true;

    while (notlast)
    {
        Current.unbind();    
        Current = Current.getEntityKey("next");
        if (!Current) Current = conveyor_base;
        if (Current == conveyor_base) notlast = false;
    }
    Current = conveyor_base;

    notlast = true;
    while (notlast)
    {
        tmp = Current.getEntityKey("next");
        if (!tmp) tmp = conveyor_base;

        if (tmp == conveyor_base) notlast = false;

        if (tmp == make_first) {
            if (tmp.getKey("_first")) return sys.getEntity(""); //return null
        }else{
            if (tmp.getKey("_first"))
            {
                if (!(!PF)) sys.error("Multiple first entitys in conveyor " + conveyor_base.getName()+ "\n");
                PF = tmp;
            }
            tmp.bind(Current);
        }
        Current = tmp;
    }
    if (!(!PF)) {
        PF.setKey("_first","");
        make_first.setKey("_first","1");
        return PF;
    }else{
        make_first.setKey("_first","1");
        return make_first;
    }
}
Здесь появилось много нового. Поэтому некоторые разъяснения. PF хранит энтити, являющуюся предыдущим крайним элементом конвейера.  PF = sys.getEntity(""); - эта строка записывает в него гарантированно несуществующий элемент для правильной работы проверок на существование (!(!PF)). Такого типа условия выполняются если PF представляет существующий на карте энтити и не выполняются в противном случае. Далее мы расцепляем все элементы конвейера чтобы потом их правильно скрепить. Крепление друг к другу нерасцепленых объектов может иногда приводить к очень странным результатам. Далее мы устанавливаем current снова на начало конвейера и движемся по нему в поисках старого крайнего элемента и нового крайнего элемента. Старый крайний элемент у меня отмечается полем "_first". Если у элемента этот ключ равен 1 то этот элемент недавно был первым. Обратите внимание, что во втором цикле я заранее получаю следующий объект вот этой строкой: tmp = Current.getEntityKey("next"); Это сделано потому, что для скрепления конвейера нужно сохранять два элемента подряд - тот, который крепим, и тот, к которому будем крепить. В tmp как раз следующий за current. Рассмотрим достаточно простые проверки

if (!tmp) tmp = conveyor_base; Если следующий элемент не найден то зацикливаем конвейер, следующий элемент равен первому.

if (tmp == conveyor_base) notlast = false; Если конвейер только что зациклился, то можно считать, что он пройден. Это условие отделено от первого только по той причине, что разработчик карты вполне может самостоятельно закольцевать элементы конвейера через next. Тогда первая проверка никогда не выполнится, поскольку next будет всегда заполнен, а вот эта проверка сработает тогда, когда цикл попытается пойти по второму кругу. Обратите внимание на то, что в unbind части применена такая же конструкция.

if (tmp == make_first)  Проверяем, не следующий ли элемент должен быть новым первым. Если он, то проверяем так же не является ли он уже первым. Если является то на этом месте функция заканчивает свою работу, поскольку ничего не меняется. Если же следующий элемент не первый, то смотрим, был ли он раньше первым. Если был, то устанавливаем PF. Если PF к этому моменту уже установлен, то это значит, что два элемента помечены как первые. В этом случае печатаем сообщение об ошибке в консоль попутно и завершаем игру:
if (!(!PF)) sys.error("Multiple first entitys in conveyor " + conveyor_base.getName()+ "\n");
PF = tmp;
Далее крепим этот элемент к current-у. Хоть этот метод и крепит старый крайний элемент наряду с остальными, но на практике это не мешает, поскольку элемент всё равно можно будет переставить с помощью setOrigin. При этом переставится и всё, что прикреплено за ним, но то, что прикреплено раньше, останется на месте.

А здесь просто обновляем положение отметки _first и возвращаем старый крайний элемент если он есть. Если его нет, то считаем старым крайним первый элемент конвейера:
    if (!(!PF)) {
        PF.setKey("_first","");
        make_first.setKey("_first","1");
        return PF;
    }else{
        make_first.setKey("_first","1");
        return make_first;
    }
Теперь мы можем пересобрать элементы конвейера в нужном порядке в любой момент, зная только два энтити. А именно - первый энтити конвейера, который в течении игры не будет меняться и энтити, который станет новым крайним элементом. Осталось заставить это двигаться.

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

Теперь настало время познакомиться с ещё одной концепцией - управляющим потоком. Вообще эта концепция распространена практически везде, где есть многозадачная система программирования. А значит и в скриптах для дума это тоже применимо. Итак, если у вас есть какое-то действие, которое нужно выполнять в течении всего времени работы скрипта, то управляющие потоки - одно из лучших решений. Работает это просто. Для начала нужна функция, которая никогда не завершится, или же завершится тогда, когда выполнение заданой операции потеряет всякий смысл. Например, если конвейер будет удалён с карты. Это можно сделать с помощью цикла while (). В качестве условия указывается проверка существования конвейера. Далее внутри этой функции нужно обеспечить паузу. В действительно многозадачных системах это не является обязательным условием, но дум выполняет каждую функцию до первого оператора, который не может быть выполнен в течении одного кадра игры. В частности такими ператорами являются sys.waitFrame() и sys.wait(). В целях экономии ресурсов будем выполнять минимум операций и отправлять функцию в режим ожидания при первой возможности с помощью waitFrame(). После того, как функция написана, её достаточно запустить с помощью thread Название_Функции() и она будет выполняться параллельно с остальными скриптами, причём возможно до конца игры.

Осталось только подготовить эту функцию. У неё будет несколько параметров: первый энтити конвейера и некоторые свойства самого конвейера. В самом начале я уже описал примерный порядок действий, осталось только оформить их в виде кода.
void ConveyorRoute(entity conveyor_base,vector One, vector Dir)
{
entity Head;
entity ogh;

    Head = conveyor_base;
    while (!(!Head)) {

        ogh = ConveyorFlip(conveyor_base,Head);
        if (ogh != Head) {
            ogh.setOrigin(ogh.getOrigin()-Dir);
        }

        while (ConveyorStatus != 1) sys.waitFrame();

        Head.moveToPos( Head.getWorldOrigin() + One );
        sys.waitFor(Head);
       
        Head = Head.getEntityKey("next");
        if (!Head) Head = conveyor_base;
    }
}

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

ogh = ConveyorFlip(conveyor_base,Head); Собираем конвейер так, чтобы head стал первым. ogh - старый head.

if (ogh != Head) {
  ogh.setOrigin(ogh.getOrigin()-Dir);
}
Если старый и новый head разные элементы, то двигаем старый head по вектору Dir. Этот вектор нужно выбрать так, чтобы элемент, выдвинувшийся за край конвейера, попадал на то место, откуда только что ушёл хвостовой элемент. На схеме в начале статьи это показано.

while (ConveyorStatus != 1) sys.waitFrame(); Это нужно для того, чтобы конвейер можно было останавливать. Если ConveyorStatus не 1, то управляющий поток не двигает конвейер. Попробуйте на тестовой карте набрать в консоли script ConveyorStatus = 0; - этим вы остановите конвейер. Для его запуска нужно присвоить этой переменной снова 1.

Head.moveToPos( Head.getWorldOrigin() + One );
sys.waitFor(Head);
Просто двигаем конвейер за его первый элемент на длину этого первого элемента. One тоже вектор и тоже задаётся вручную.

Head = Head.getEntityKey("next");
if (!Head) Head = conveyor_base;
Просто переходим от head к следующему элементу. Если элементы кончились то идём на начало конвейера. При следующем проходе цикла конвейер вновь будет пересобран в соответствии с новым head и всё повторится сначала.

Пара любопытных вопросов:
1. Попробуйте сказать, в каком положении останавливается конвейер при изменении переменной ConveyorStatus.
2. Можно ли разворачивать такой конвейер?

Надеюсь, что скрипт конвейера у вас получился. Теперь перейдём к генератору бочек.

Генератор бочек и автоматические дверцы.

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

Для решения первой задачи я использовал два target_null, если вам чем-то они не нравятся, то можно использовать любые другие невидимые объекты или даже задать позицию вручную. Два таргета я использовал только для того, чтобы разнообразить появление бочек. А именно - позиция бочки вычисляется по принципу        
SPP = $target_null_1.getWorldOrigin()*Rnd + $target_null_2.getWorldOrigin()*(1-Rnd);
В Rnd сохранено случайное число от 0 до 1. На выходе получаем позиции между $target_null_1 и $target_null_2.

Для остального я использовал trigger_touch. Причиной этому послужило то, что этот триггер способен реагировать на любые энтити. О работе с trigger_touch я ещё вроде особо ничего не писал. Этот триггер всегда вызывает скрипт и поэтому должен иметь заполненое поле call. Кроме этого триггер вызывает не обычные функции без параметров, а функцию с параметром entity. При неправильном применении будут ошибки. Вид у скриптовой функции для такого триггера будет таким:

void TriggerF(entity ent) {...}

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

Вот код всех этих функций для триггеров:
void BarrelEat(entity ent)
{
    if (!ent) return;
    if (ent == $world) return;
    if (ent.getKey( "spawnclass" ) == "idPlayer") {ent.kill(); return;}
    if (ent.getKey( "spawnclass" ) == "idExplodingBarrel" && (BarrelCount < 6))  BarrelCount += 1;
    ent.remove();
}
void Hatch1Mark(entity ent)
{
    if (!ent) return;
    if (ent == $world) return;
    OpenHatch1 = true;
}
void Hatch2Mark(entity ent)
{
    if (!ent) return;
    if (ent == $world) return;
    OpenHatch2 = true;
}
Первая удаляет бочки, вдоволь накатавшиеся по конвейеру. Вторая и третья открывают двери по разные стороны конвейера. Непосредственным открыванием и закрыванием дверей заведует отдельный управляющий поток, а эти функции лишь устанавливают специальные переменные OpenHatch1 и OpenHatch2. Если управляющий поток обнаруживает, что переменная установлена, то он открывает соответствующую дверь.

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

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

void HatchControl() {

vector RealPos;
vector IdealPos;
float Hatch1OpenTime;
float Hatch2OpenTime;
boolean Hatch1Open;
boolean Hatch2Open;
boolean Hatch1Flag;
boolean Hatch2Flag;

    Hatch1OpenTime = $func_mover_26.getFloatKey("move_time");
    if (!Hatch1OpenTime) Hatch1OpenTime = 1;
    Hatch1OpenTime = Hatch1OpenTime/90;

    Hatch2OpenTime = $func_mover_46.getFloatKey("move_time");
    if (!Hatch2OpenTime) Hatch2OpenTime = 1;
    Hatch2OpenTime = Hatch2OpenTime/90;
   
    eachFrame
    {
        RealPos = $func_mover_26.getAngles();
        Hatch1Open = (OpenHatch1 && ConveyorStatus != 0);
        if (Hatch1Open) {
            IdealPos = '0 0 90';
        }else{
            IdealPos = '0 0 0';
        }
        if (RealPos != IdealPos) {
            $func_mover_26.time(Hatch1OpenTime*sys.vecLength(IdealPos - RealPos));
            $func_mover_26.rotateOnce(IdealPos - RealPos);
            if (!Hatch1Flag) {
                if (Hatch1Open) { $func_mover_26.startSoundShader ("silver_sliding_open", SND_CHANNEL_VOICE2 );} else{$func_mover_26.startSoundShader ("silver_sliding_close", SND_CHANNEL_VOICE2 );}
            }
            Hatch1Flag = true;
        }else{
            if (!(!Hatch1Open)) {OpenHatch1 = false;}
            Hatch1Flag = false;
        }

        RealPos = $func_mover_46.getAngles();
        Hatch2Open = (OpenHatch2 && ConveyorStatus != 0);
        if (Hatch2Open) {
            IdealPos = '0 0 -90';
        }else{
            IdealPos = '0 0 0';
        }
        if (RealPos != IdealPos) {
            if (Hatch2Open) $trigger_touch_1.disable();
            $func_mover_46.time(Hatch2OpenTime*sys.vecLength(IdealPos - RealPos));
            $func_mover_46.rotateOnce(IdealPos - RealPos);
            if (!Hatch2Flag) {
                if (Hatch2Open) {
                    $func_mover_46.startSoundShader ("silver_sliding_open", SND_CHANNEL_VOICE2);
                }else{
                    $func_mover_46.startSoundShader ("silver_sliding_close", SND_CHANNEL_VOICE2 );
                }
            }
            Hatch2Flag = true;
        }else{
            if (!Hatch2Open) { $trigger_touch_1.enable(); } else {OpenHatch2 = false;}
            Hatch2Flag = false;
        }
        RealPos = $func_mover_47.getAngles();
        IdealPos = -IdealPos;
        if (RealPos != IdealPos) {
            $func_mover_47.time(Hatch2OpenTime*sys.vecLength(IdealPos - RealPos)/2);
            $func_mover_47.rotateOnce(IdealPos - RealPos);
        }
    }
}
Код достаточно длинный, но не будем забывать, что он управляет несколькими дверями. Кроме этого он ещё включает триггер для удаления бочек когда дверь около этого триггера закрыта и выключает в противном случае. Это сделано для того, чтобы бочки исчезали только за закрытой дверью и игрок этого позора не видел. Итак:

    Hatch1OpenTime = $func_mover_26.getFloatKey("move_time");
    if (!Hatch1OpenTime) Hatch1OpenTime = 1;
    Hatch1OpenTime = Hatch1OpenTime/90;

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

    eachFrame

Это макрос, определённый в doom_defs.script Он эквивалентен for (,1,sys.waitFrame();) То есть это готовый шаблон бесконечного цикла с уже встроеным sys.waitFrame(). К сожалению у многих в этом конфиге пропущено sys, поэтому в скрипте тестовой карты этот макрос заменён на for (,1,sys.waitFrame();)

        RealPos = $func_mover_26.getAngles();
        Hatch1Open = (OpenHatch1 && ConveyorStatus != 0);
        if (Hatch1Open) {
            IdealPos = '0 0 90';
        }else{
            IdealPos = '0 0 0';
        }
        if (RealPos != IdealPos) {
            $func_mover_26.time(Hatch1OpenTime*sys.vecLength(IdealPos - RealPos));
            $func_mover_26.rotateOnce(IdealPos - RealPos);
            if (!Hatch1Flag) {
                if (Hatch1Open) { $func_mover_26.startSoundShader ("silver_sliding_open", SND_CHANNEL_VOICE2 );} else{$func_mover_26.startSoundShader ("silver_sliding_close", SND_CHANNEL_VOICE2 );}
            }
            Hatch1Flag = true;
        }else{
            if (!(!Hatch1Open)) {OpenHatch1 = false;}
            Hatch1Flag = false;
        }

Это основной блок работы с дверью. Сначала получаем текущее состояние двери и сохраняем в realpos. Затем в Hatch1Open сохраняем информацию о том, должна ли быть дверь открыта или закрыта. Далее в зависимости от Hatch1Open выбираем финальную позицию двери. Потом проверяем, совпали ли реальное положение и целевое. Если нет, то поворачиваем дверь по направлению к финальной позиции. При этом каждый раз пересчитываем время движения, поскольку расстояние от настоящего положения двери до финального постоянно уменьшается. Дальше по тексту есть Hatch1Flag. Он нужен только для того, чтобы запустить звук двери, причём один раз ровно в тот момент, когда целевое положение двери и реальное перестают быть равными. Этот флаг восстановится только когда дверь снова достигнет финального положения.

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

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

Ещё есть функция, создающая бочки. Она тоже оформлена как отдельный поток.
void BarrelSpawn()
{
vector SPP;
float Rnd;

    while (1){
    sys.wait(sys.random(4)+3);
    if (BarrelCount <= 2 && sys.random(8)>7) BarrelCount++;
    if (BarrelCount > 0 && !OpenHatch1)
    {
        BarrelCount -= 1;
        Rnd = sys.random(10)/10;   
        SPP = $target_null_1.getWorldOrigin()*Rnd + $target_null_2.getWorldOrigin()*(1-Rnd);
        sys.setSpawnArg("origin", SPP );
        sys.spawn("moveable_explodingbarrel");
    }
    }
}
Принцип её работы прост. Ждём случайное время. Потом проверяем, закрыта ли дверь. Если закрыта и есть запас бочек, то создаём бочку и уменьшаем счётчик запаса бочек на единицу. Уничтожитель бочки при уничтожении бочек пополняет запас бочек. Кроме того если запас мал, то он периодически пополняется случайным образом. Код самого создания бочки особого интереса не представляет.

Ну и осталось запустить это хозяйство в работу:
void main()
{
    BarrelCount = 5;
    OpenHatch2 = false;
    ConveyorStatus = 1;

    cache_sounds();

    thread ConveyorRoute($func_mover_23, '0 128 0', '0 1408 0');
    thread BarrelSpawn();
    thread HatchControl();
    thread CraneRoute();
}
Ещё можно обратить внимание на описание переменных BarrelCount, OpenHatch2 и ConveyorStatus
Его особенность в том, что оно находится вне функций в самом начале файла. В результате эти переменные становятся глобальными и к ним есть доступ из любой функции на карте. Вы даже можете изменять их состояние с помощью команды script переменная=значение; в консоли и смотреть, на что они влияют. Такие переменные - это один из способов связи с управляющими потоками для вашего скрипта. Кроме этого есть и второй способ - изменять ключи доступного управляющему потоку энтити и постоянно читать значения ключей в самом управляющем потоке.

Второй способ удобен тем, что при управлении скриптом из GUI вы сможете непосредственно из GUI менять значение ключей нужного вам объекта. Для этого в GUI в коде кнопок пишется
set    "cmd"    "gui::gui_parm4";
Это даст вам возможность запрограммировать выполнение любой доступной в GUI команды из редактора. Нужно будет просто вписать в соответствующий gui_parm команду так, как она обычно пишется в GUI. Например, я использовал в примере с подъёмными кранами этот трюк и команды в GUI выглядели так:
setkeyval test1 CommandKey FORWARD
с другой стороны иногда могут быть удобнее как раз глобальные переменные. На этом всё, а в следующей главе речь пойдёт о подъёмном кране и том, как он устроен.

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

Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Вход:

Поиск

Ссылки


Статистика

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


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