RSS
Меню сайта

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

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

Copyright C4TNT© 2008

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

Скриптуем #4

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

Но к сожалению скрипт этих лампочек имеет некоторые недостатки. А именно, лампочка просто гаснет, хотя гораздо реалистичнее была бы вспышка и плавное затухание. И кроме этого скрипт будет вылетать с ошибкой при отсутствии лампочки или эффекта для неё и из редактора невозможно добавлять или убирать лампочки по своему усмотрению. В связи с этими тревожными событиями в этот раз мы рассмотрим оператор while и порядочное количество функций объекта sys (да, sys именно объект).

Будем решать проблемы по порядку.

Для начала займёмся анимацией лампочки. Итак, вопрос дня: "Как регулируется яркость лампочки в Doom3".  Те, кто рисует карты недавно,  сразу скажут, что яркость зависит от радиуса лампочки...   ...а те, кто это делает уже давно - что яркость лучше регулировать, изменяя цвет лайта, а радиус только для тяжёлых случаев. Чем же можно воспользоваться из скрипта? А из скрипта можно менять и то и другое. Но при смене радиуса источник, к сожалению, начинает считаться двигающимся и созданная компилятором карта теней перестаёт действовать. В результате при большом количестве таких источников начинаются тормоза. По этой причине предлагаю остановиться на color-е, тем более что этот вариант позволяет создать более красочные эффекты.

Итак, меняем цвет

А как именно это сделать, можно узнать, например в doom_events.script. Вообще, если вам нужно что-то от объекта, но вы не знаете, как это делать - в первую очередь смотрите этот файл, там есть все методы всех объектов с кратким описанием. Описание, правда исключительно English, но чуть позже будет и перевод.

В общей секции (all entities) нашлось такое чудо:

setColor( float red, float green, float blue );

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

float k;
for (k=1;k>0;k-=0.1)
{
    light.setColor (k,k,k);
    sys.wait(0.1);
}

Обратите внимание, в этом цикле k меняется от 1 к 0 с шагом 0.1. Причина для этого проста - в думе цвета задаются нормированными величинами, то есть 1 - самый яркий цвет, 0 - самый тёмный цвет, а всё, что между ними - промежуточная яркость. Можете опробовать этот код на тестовой карте из прошлого выпуска. А теперь разберёмся с тем, как и что влияет на затухание цвета:

float k;
float a = 0.1;
float b = 0.1;
for (k=1;k>0;k-=a)
{
    light.setColor (k,k,k);
    sys.wait(b);
}

Попробуйте изменять a и b и выяснить их влияние (тем более, что при изменении скрипта карту достаточно просто перезапустить, компилировать ничего не надо). Если лень - читайте дальше.



Оба эти параметра влияют на скорость затухания цвета, только a в прямой пропорции, а b в обратной. Но я думаю, что вы просто не смогли не обратить внимания, что при увеличении a цвет изменяется довольно таки дёргано. Поскольку мы хотим (ведь хотим???) писать качественные скрипты, такие некрасивости нужно устранять в зародыше. А значит вроде бы оптимальным вариантом является такой выбор a и b, когда a минимально, а регулировка скорости происходит только за счёт b. Но тут на нашем светлом пути встаёт страшный монстр FPS. Действительно, даже такая гениальная игра, как дум, не сможет выполнять наш скрипт обгоняя частоту кадров. А если и сможет, то плавного затухания мы не увидим. В действительности же sys.wait ждёт столько, сколькоему сказано, но никак не меньше, чем некоторый дбсолютный минимум. Собственно, графическая часть игры и скрипты работают асинхронно, что может пораждать разного рода артефакты.

Значит надо это как-то решать другим способом. Для этой цели разработчики игры "рекомендуют" следующий способ:
Пользоваться командой sys.waitFrame(); которая ждёт один кадр (быстрее всё равно не будет) а остальное выбирать как раз таки за счёт параметра a. Длительность одного кадра они предусмотрительно запихали в переменную GAME_FRAMETIME. Так и поступим:

float k;
float a = 0.1;
for (k=1;k>0;k-=a)
{
    light.setColor (k,k,k);
    sys.waitFrame();
}

Остаётся научиться правильно работать с a. А для этого нужно сначала определиться, как мы вообще будем измерять интервал, за который лампочка должна погаснуть. Попробую угадать, о чём вы подумали. О секундах? Нет? А жаль...
Ну ладно, всё же, наверное, время в такой динамичной игре лучше измерять в секундах, а не в световых годах. И так:

float xtime; - время, в секундах
float a;

??? - нечто, в котором из времени мы получаем a

for (k=1;k>0;k-=a)
{
    light.setColor (k,k,k);
    sys.waitFrame();
}
Как бы придумать это самое нечто. На самом деле ничего страшного нет, просто нужно вспомнить школьный курс физики. А именно: a - скорость, с которой гаснет лампочка. Время мы знаем. И расстояние на самом деле тоже знаем, просто ещё не сообразили, где оно. А может и сообразили уже.


Ну, помучаю вас немного, и скажу что расстояние у нас от k=1 до k=0, то есть ровно 1 условная единица. Ну и a = 1/xtime. Но на самом деле не совсем так, как раз по той причине, что внутри цикла sys.waitFrame(), который расходует время. Поэтому a = GAME_FRAMETIME/xtime;

float xtime;
float k;
float a;

a = GAME_FRAMETIME/xtime;

for (k=1;k>0;k-=a)
{
    light.setColor (k,k,k);
    sys.waitFrame();
}
Ну а теперь СТРАШНОЕ.

Создаём собственную функцию.


Это может смутить многих (по крайней мере собственные функции я видел только в очень высококлассных модах типа Damnatio_Memoria). Но на самом деле ничего страшного, ведь в общем мы это делали уже тыщу раз. Заготовим место:

void LightDown()
{
}

И воткнём туда код.

void LightDown()
{
    float time;
    float k;
    float a;

    a = GAME_FRAMETIME/xtime;

    for (k=1;k>0;k-=a)
    {
        light.setColor (k,k,k);
        sys.waitFrame();
    }
}

Вроде всё пристойно, но как бы этой функции приделать параметры, чтобы можно было её потом для любого лайта использовать. Для этого случая у функций как раз есть скобочки после названия. Вписываем туда данные, получаемые из внешнего мира и всё!
void LightDown(entity light, float xtime)
{
    float k;
    float a;

    a = GAME_FRAMETIME/xtime;

    for (k=1;k>0;k-=a)
    {
        light.setColor (k,k,k);
        sys.waitFrame();
    }
}
И её уже можно использовать. Теперь можно использовать в скрипте такую конструкцию: LightDown(переменная с лайтом, время затухания);
Ну вот уже и полный аналог получился. Но у него есть один большой плюс: в отличае от light.fadeOutLight его можно модифицировать. Чем мы и займёмся прямо сейчас.

Дополняем и исправляем.

Для начала стоит обратить внимание на то, что аналог fadeOutLight - это слишком громко сказано, на самом деле между ними есть существенная разница: fadeOutLight не задерживает скрипт на себе, а наш вариант - задерживает. Попробуем устранить, но для этого нужен небольшой экскурс в многопоточность.

Многопоточность.

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

waitForThread( float threadNumber ); - ждать, пока отдельный поток завершится
killthread( string threadName ); - "снять задачу"

и специальной команды thread.

после этой команды пишут имя функции и её параметры и в результате функция запускается отдельно от вызвавшего её скрипта и работает с ним параллельно.

Так что для параллельного запуска нашей функции достаточно написать:
thread LightDown(лампочка,время);

Цвета.

Если вы пробовали менять цвет лампочке, то наверняка заметили и это отличие. А именно, лампочка всё равно становится белой перед затуханием. Исправим и этот недочёт, а заодно добавим возможность не только затухания, но и перехода к другому цвету. Для этого можно передать функции ещё три параметра с компонентами цвета, а можно (к счастью) обойтись и одним - vector'ом. Это очень специфический тип - эдакий строенный float. Он активно используется для хранения координат, углов и цветов. Постоянные такого типа выглядят так: '1.0 2.0 3.0' - три значения через пробел. В редакторе так у объектов записан origin. Ну и кроме этого придётся немного вспомнить математику. А именно, сейчас у нас есть переменная k, которая плавно убывает от 1 к 0. Старый цвет лампочки мы спросим у самой лампочки (если вы всё же смотрели в doom_events.script, то видели там около setColor ещё и getColor) а новый получим праметром. Осталось решить, как бы это всё замешать, чтобы получить плавный переход. Для начала попробуем умножить новый цвет на k (делаем это теоретически, и помним про то, как умножают вектор на число - просто все его компоненты нужно умножить на это самое число), если k=1 - то в результате получаем этот самый цвет, если же k=0 то получаем чёрный. В остальных случаях получаем какой-то процент цвета. Применим к старому цвету те же рассуждения. А смешать два цвета можно, сложив вектора с этими цветами. Но k = 1 в начале перехода. А в начале у нас должен быть только старый цвет, значит новый цвет умножаем на 1-k (при k=0 как раз будет только новый цвет)
Получаем такую формулку: цвет = старый_цвет * k + новый_цвет * (1-k). Можно заметить, что при промежуточных значениях k цвета смешиваются достаточно правильно (по принципу 90%/10%, 80%/20% и т.д.). И если указать одинаковые цвета - ничего вообще не будет происходить при изменении k. Теперь перепишем в коде:

void LightDown(entity light, float xtime,vector newcolor)
{
    float k;
    float a;
    vector oldcolor;
    vector color;
   
    a = GAME_FRAMETIME/xtime;
    oldcolor = light.getColor();

    for (k=1;k>0;k-=a)
    {
        color = oldcolor*k + newcolor*(1-k);
        light.setColor (color_x,color_y,color_z);
        sys.waitFrame();
    }
    light.setColor (newcolor_x,newcolor_y,newcolor_z);
}
Для доступа к компонентам вектора используются такие "окончания" к названию переменной: _x, _y, _z
Можете потестировать, оно уже работает. Но тем не менее продолжим. А именно, сделаем источник света программируемым. В конце функции есть ещё один setColor для того, чтобы финальный цвет стал таким, как указано, даже если в цикле он таким не стал.

Программируемый источник света.


В память о безвременно ущедьшем Quake2 сделаем аналог триггера lightramp, только с большими возможностями. Делать его будем из последнего варианта функции LightDown. Кстати, её есть смысл переименовать в LightColor, например. Дальше в примерах я буду пользоваться именно этим именем.

Итак, желаемый результат такой: функция принимает строку с кодом вида "abcd..." и управляет лайтом, следуя этому коду. При этом переменная xtime будет задавать время выполнения одного шага программы (так гораздо удобнее)
А код простой: буквы от a до z - яркость источника света, а - минимум, z - максимум. Уже готовую функцию трогать не будем (Главное правило программиста:работает - не трогай). Создадим ещё одну функцию с такими же параметрами и дополнительным параметром для строки:
void LightProg(entity light, float xtime,vector newcolor,string lightcode)
{
}
Переходить от цвета к цвету мы умеем, а сейчас научимся работать со строками. Итак, есть строка с кодом. Нужно как-то отщипнуть от неё кусочек. Для операций со строками в думе есть несколько функций:

scriptEvent    float    strLength( string text ); - вычисляет, сколько в строке знаков
scriptEvent    string    strLeft( string text, float num ); - берёт некоторое (num) количество знаков строки слева.
scriptEvent    string    strRight( string text, float num ); - то же, но справа
scriptEvent    string    strSkip( string text, float num ); - пропускает несколько первых знаков
scriptEvent    string    strMid( string text, float start, float num ); - выдёргивает кусочек строки, начиная с символа с номером start.

Как вы наверное догадались, num - точное количество взятых символов.
Итак, есть вариант брать первый знак с помощью strLeft и подрезать рабочую строку с помощью strSkip, пока она не кончится. Как вариант можно брать нужный символ с помощью strMid в цикле, при этом длинну цикла можно выяснить с помощью strLength. Лично мне по душе второй вариант, о нём и напишу:

void LightProg(entity light, float xtime,vector newcolor,string lightcode)
{
    float pos; //Тут будет храниться номер изучаемого в данный момент символа.
    float lenstr; //Тут будет храниться длина строки. Желательно пользоваться этим методом, вместо того, чтобы сравнивать счётчик в цикле со strLength(). Это просто оптимизация.
    string symbol;
    float citime = xtime;

    lenstr = sys.strLength(lightcode);

    for (pos=0;pos<lenstr;pos++)
    {
       symbol = sys.strMid(lightcode,pos,1); //берём один знак, начиная с позиции pos
       if (symbol == "a") {LightColor(light,citime,newcolor * 0.038);}
       if (symbol == "b") {LightColor(light,citime,newcolor * 0.076);}
       if (symbol == "c") {LightColor(light,citime,newcolor * 0.114);}
       if (symbol == "d") {LightColor(light,citime,newcolor * 0.152);}
       if (symbol == "e") {LightColor(light,citime,newcolor * 0.19);}
       if (symbol == "f") {LightColor(light,citime,newcolor * 0.228);}
       if (symbol == "g") {LightColor(light,citime,newcolor * 0.266);}
       if (symbol == "h") {LightColor(light,citime,newcolor * 0.304);}
       if (symbol == "i") {LightColor(light,citime,newcolor * 0.342);}
       if (symbol == "j") {LightColor(light,citime,newcolor * 0.38);}
       if (symbol == "k") {LightColor(light,citime,newcolor * 0.418);}
       if (symbol == "l") {LightColor(light,citime,newcolor * 0.456);}
       if (symbol == "m") {LightColor(light,citime,newcolor * 0.494);}
       if (symbol == "n") {LightColor(light,citime,newcolor * 0.532);}
       if (symbol == "o") {LightColor(light,citime,newcolor * 0.57);}
       if (symbol == "p") {LightColor(light,citime,newcolor * 0.608);}
       if (symbol == "q") {LightColor(light,citime,newcolor * 0.646);}
       if (symbol == "r") {LightColor(light,citime,newcolor * 0.684);}
       if (symbol == "s") {LightColor(light,citime,newcolor * 0.722);}
       if (symbol == "t") {LightColor(light,citime,newcolor * 0.76);}
       if (symbol == "u") {LightColor(light,citime,newcolor * 0.798);}
       if (symbol == "v") {LightColor(light,citime,newcolor * 0.836);}
       if (symbol == "w") {LightColor(light,citime,newcolor * 0.874);}
       if (symbol == "x") {LightColor(light,citime,newcolor * 0.912);}
       if (symbol == "y") {LightColor(light,citime,newcolor * 0.98);}
       if (symbol == "z") {LightColor(light,citime,newcolor);}
       if (symbol == "+") {citime = citime/2;}
       if (symbol == "-") {citime = citime*2;}
    }
}


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

Тестовая карта и сам скрипт тут
Категория: Doom3 | Добавил: c4tnt (14.11.2008)
Просмотров: 996 | Комментарии: 5 | Рейтинг: 0.0/0
Всего комментариев: 4
1 c4tnt  
1
Нда... а до while так и не добрался dry

2 Archi  
1
Подожди я еще этого не понял sad
Ответ: Может в следующей части что-нибудь особо загадочное расписать подробнее?

3 Archi  
1
Ага, понял. Можно продолжать наш путь biggrin

4 HAL  
1
Ты туда не вложил файл .proc, вот игра и орёт что cannot find file scri2.proc и закрывает вечеринку.
Ответ: к этим описаниям прилагается только map файл. Остальное можно сделать из него либо в редакторе либо с помощью консольной команды dmap. Впрочем, для того, чтобы работать со скриптами, всё равно нужно уметь работать с редактором.

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

Поиск

Ссылки


Статистика

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


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