ЧАСТЬ 1. Скрипты. Основы.
Руководство переехало http://wiki.doomgod.com/index.php/Введение_в_ACS Эта версия не обновляется.
Начнём с классификации скриптов. 1) скрипты без классификатора
script 1 (void)
этот скрипт вызывается командами типа ACS_Execute. может получать максимум ТРИ параметра :
script 1 (int param1, int mul,int a)
{
SetResultValue(param1+mul*a);
}
данный скрипт перемножает второй и третий параметры и добавляет к произведению первый параметр, после чего возвращает значение, которое можно использовать при вызове ACS_ExecuteWithResult, про это попозже. 2) Скрипты вызываемые при запуске карты
script 1 Open
Такие скрипты вызываются один раз для каждой карты. их вызывает не объект, поэтому нельзя использовать TID=0 в качестве ссылки на вызвавший объект или на игрока. 3) Скрипты входа игрока
script 1 enter
Вызывается при входе на карту ДЛЯ КАЖДОГО ИГРОКА. обычно игрок один, поэтому вызывается тоже один раз. Отличается от предыдущего тем, что этот скрипт запускает как-бы сам игрок, поэтому здесь TID=0 ссылается на самого игрока, поэтому, например, игроку можно сразу задать новый тег :
script 1 enter
{
Thing_changeTID(0,6000);
}
Есть ещё некоторые другие типы скриптов, например, Death, который вызывается при смерти игрока, но это я пропущу. Итак, с типом скрипта определились, теперь надо понять, из чего состоит тело скрипта. ЛЮБОЙ скрипт состоит из смеси трёх типов конструкций : операторов, ветвлений и циклов. Операторы и последовательности операторов — это любые команды, например, по изменению высот полов, потолков, изменению переменных и прочее, например
{
int b;
Thing_Raise(5);
Floor_RaiseByValue(...)
{ //начало блока операторов
int a = 5;
b += a;
print(i:a,s:" ",i:b);
}
}
операторы бывают следующие : 1) арифметические + - * / % - взятие остатка << сдвиг вправо (умножение на 2^x, например, 4 <<3>> сдвиг влево ~ — инверсия побитовая. !0x00FF00FF = 0xFF00FF00 (0x это префикс 16-ричного числа) | побитовое ИЛИ. 0x00FF0000 | 0xFF00FF00 = 0xFFFFFF00 & побитовое И. 0xFF00FF00 & 0x00FFFF00 = 0x0000FF00 ^ это не возведение в степень, а операция XOR. если в каких-то разрядах (бинарных) стоят разные значения (1 и 0 или 0 и 1), то в результате в том-же разряде будет 1, иначе 0. операция забавная, но если вы о ней ничего не знаете, то вряд-ли она вам понадобится. 0x00FF00FF ^ 0x0000FFFF = 0x00FFFF00 += -= *= /= %= |= &= — агрегаты (a+=2 эквивалентно a = a + 2) ++ инкремент. a++ эквивалентно a +=1 или a = a+1. есть префиксный и постфиксный инкремент, но это тонкости, потом расскажу. -- декремент. 2) логические 2.1) сравнения < <= > >= == != (последние два — равно и не равно) тут всё забавно. Результатом выполнения этих конструкция является ЧИСЛО. Это число равно 0, если результат ложный (a = (3==2) - а будет рано 0, так как 3 не равно двум, а значит, результатом сравнения будет число 0). Если же результат сравнения истинный (a = 0; a = (a == 0)), то результутом будет "не ноль". например, 1. или -14543. Но не 0. 2.2 логические операции && логическое И. если оба операнда не равны нулю(то есть "правда"), то и результат не будет равен нулю. если хоть один равен нулю, то результат — ноль ("ложь") || логическое ИЛИ. если хоть один из операндов не равен нулю, то и результат не будет равен нулю. ! — логическое отрицание. Превращает правду в ложь, ложь в правду. не ноль в ноль, ноль — в не ноль. 3) вызов функции ИмяФункции() ИмяФункции(параметры) например int a = sin(0.5) // вызов функции sin int b = ACS_ExecuteWithResult(1,0,1,2,3) // вызовет функцию ACS_ExecuteWithResult(), которая выполнит скрипт номер 1 с параметрами 1,2 и 3 и вернёт значение. Для функции из примера выше будет 1 + 2*3 = 7, то есть b станет равно 7. Пользовательские функции объявляются следующим образом :
function int ИмяФункции(список параметров)
{
return значение;
}
или
function void ИмяФункции(список параметров)
{
}
или
function void ИмяФункции(void)
{
}
или
function int ИмяФункции(void)
{
return значение;
}
void значит "пусто", нет параметров или нет возвращаемого значения. если возвращаемое значение есть, то его надо "вернуть" с помощью инструкции return. например : [code] function int abs(int val) { if (val<0>=0 то "return -val;" не выполняется, и выполняется следующая инструкция, то есть "return val" — число и так больше или равно нуля, ничего изменять в нём не надо. Следующий тип конструкций, один из типов которых как раз использован в предыдущем примере — это [color=orange]ветвления[/color]. их два типа : if() /else if переводится как "если". то есть if (a==0) можно перевести как "если а равно нулю, то" Скобочки возле if обязательны. если выражение в этих скобочках равно нулю (а мы ещё помним, что все инструкции логические и сравнения возвращают числа?) то следующий за if оператор [color=orange]НЕ[/color] ВЫПОЛНИТСЯ. иначе выполнится оператор не обязательно один, может быть сразу много, тогда их надо объединять в [color=orange]блок операторов[/color] : [code] int b = Random(0,1); if (b) { // начало блока операторов SpawnSpot(...)
... b = 10; } // конец блока операторов Door_close(...) // выполнится всегда. [/code] если после оператора добавить слово "else", то это создаст альтернативную ветвь выполнения : [code] int b = Random(0,1); if (b) SpawnSpot(...) // выполнится либо этот оператор else { // или этот блок операторов b = 10; Door_open(...) } Door_close(...) // выполнится всегда. [/code] ещё типичным использованием if являются конструкции "else if" [code] b = random(1,4) if (b==1) { // здесь окажемся если b = 1 ... } else if (b==2) { ... } else if (b==3) { ... } else { // здесь b равно 4 } [/code] альтернативой "if" и "else if" является конструкция "switch" [code] int b = random(0,5) switch(b) { case 0: case 1: Door_open(...) Spawn(...) break; если b рано 0 или 1, то будут выполнены оба оператора и break тут-же переместит выполнение скрипта за блок switch case 2: c = 1; case 3: Door_close(...) // если b равно 2, то выполнится и "c = 1", и "Door_close", если b рано 3, то только Door_close. Это потому, что перед "case 3" не стоит "break". break; default: SpawnSpot(...) break; } [/code] третий тип конструкций : это [color=orange]циклы[/color]. из три типа : 1)for(инициализация;условие;постоператор) оператор_1; это работает так. 1.1) выполняется оператор из "инициализация") 1.2) проверяется "условие", если оно рано нулю (то есть ложь), то происходит выход из цикла, оператор_1 не выполняется при этом. 1.3) если "условие" не равно нулю, то выполняется оператор_1 (который может быть блоком операторов или даже отсутствовать ) 1.4) выполняется "постоператор" 1.5) смотри 1.2, то есть зацикливаемся. пример : [code] for (b=0;b<10>0;b--); // здесь нет операторов, но цикл запустится и будет корректно работать! смотрите. // первым делом выставит b в 100 // затем начнёт проверять условие. Для этого он выполнит выражение "условие" и сравнит результат с нулём. выражение — это вызов функции МояФункция(b) и логическое умножение результата на выражение "b>0". есть четыре варианта : МояФункция(b) равна нулю, а b>0, МояФункция(b) не равна нулю, а b=0, МояФункция(b) равна нулю и b=0 и наконец МояФункция(b) не равна нулю и b>0. только в последнем случае цикл будет продолжен, то есть функция МояФункция(b) будет вызвана до тех пор, пока или она сама не вернёт 0, или b не станет меньше нуля. Что именно делает и возвращает функция МояФункция(int b), зависит только от вашего желания. пример привожу, чтобы вы не пугались подобных конструкций. [/code] 2) цикл while(условие) оператор если "условие" не равно 0, то будет выполнен оператор (или блок операторов) например [code] while(random(0,100)) { spawn(...) b++; } // пока случайно не выпадет так, что случайное число от нуля до 100 не окажется нулём, спавнить что-нибудь while(1) { spawn(...) b++ if (b>c) break; } // пока "один не есть 0", то есть до бесконечности выполнять блок операторов. Но выход из цикла всё-же предусмотрен! [/code] 3) констукция do оператор while (); аналогична предыдущей, но "оператор" будет выполнен как минимум один раз. В предыдущем примере есть "бесконечный" цикл. Но благодаря инструкции break из него можно выйти. break завершает выполнение цикла, а инструкция continue заставляет выполнять оператор(имеется в виду блок операторов) сначала. вот здесь-то и становится полезен "постоператор" инструкции for. блок операторв прекращает работать, а постоператор всё-же выполняется.
[size=18][center][color=orange]ЧАСТЬ 2. Еденицы измерений и их перевод[/color][/center][/size] Эта часть будет посвящена арифметике с фиксированной точкой, углам, расстояниям и проблемам перевода. самым фундаментальным типом является тип int. Он может принимать любые значения от -2147483648 до 2147483647 то есть 32 битное целое со знаком. Все остальные типы есть производные от int : fixed, str. Это не самостоятельные типы, это переименованный int. Кстати, в связи с этим есть забавное ограничение — пользовательские функции могут принимать и возвращать "только int". Ну и смысла писать что-либо ещё просто нет, везде используйте int и не парьтесь. Итак, у нас есть только int. Как же получить всякие дроби, градусы, радианы, строки в конце концов? В ACS используется арифметика с фиксированной точкой. старшая половина двоичных разрядов считается целой частью, младшая — дробной. точность — гарантируется 4 десятичных знака после запятой, и ещё чуть-чуть больше. Работает это следующим образом (буду использовать десятичные разряды для наглядности) — допустим, у нас есть разрядная сетка на 4 разряда+ знак, куда мы можем записать числа -9999, -9998, ..., 0000,0001, ..., +9999 теперь мы говорим, что старшие два разряда — целая часть, а младшая — дробная часть. то есть +99.99, но это точка нарисованная, физически на уровне типа int её нет. сложение и вычитание производятся обычным образом : 01.15 + 02.85 = 0115 + 0285 = 0400 = 04.00, то есть четыре в смысле "fixed point". или 400 в смысле int. Функции сами разбираются, что им передали — фиксированную точку или целое. Если функция просит "Map units" или "byte angle", то она требует простого int, иначе — фиксированную точку. продолжу обзор операций с плавающей точкой. Умножение и деление ,взятие остатка работают несколько иначе. есть число 02.00 умножить на 02.00 оператором "*", то получится 4 0000, то есть ноль, с переполнением разрядной сетки. не влез такой большой результат, хотя вроде два на два умножили. если бы мы умножали два в фиксированной точке на два целых, то всё было-бы нормально : 02.00 * 00.02 = 200 * 2 = 400 = 04.00. Если же нам надо обязательно умножить два числа в виде с фиксированной точкой, то надо использовать функции fixedmul(a,b) и fixeddiv(a,b), соответственно умножает или делит а на b. fixedmul(02.00,02.00) = 04.00 Кстати, вы ещё не забыли, что наши десятичные выкладки над четырёхразрядными числами являются модельными? и на самом деле в ACS числа имеют 32 двоичных разряда? один из которых — знак числа, в расчётах участвует опосредовано? Теперь немного вернёмся к ACS. если написать "int a = 1", то мы получим "1" в значении целое, а в смысле fixed point это будет "0.0000152587890625". если же мы напишем "int i = 1.0", то целое представление будет "65536", а дробное — "1.0". ещё раз напомню, что нигде не написано, в каком именно виде хранится ваше число, а точнее, оно хранится в однозначном наборе битов, которое можно интерпретировать двумя (точнее тремя, но об этом попозже, третий тип — номер строки в таблице строк) разными способами. Какой именно выберет программа зависит от того, в какой функции вы используете ваши числа. sin(), cos(), SetActorPosition() используют фиксированную точку, GetSectorFloorZ() использует целые числа, а FadeRange() например использует для разных параметров разное представление. Цветовые составляющие принимает в целых числах от 0 до 255, а интенсивность — в фиксированной точке от 0.0 до 1.0 (или от 0 до 65535). перевести число из фиксированной точки в целое можно, если сдвинуть его вправо на 16 разрядов : 1.0 >> 16 == 1. обратное преобразование очевидно : 1 << 16 == 1.0. Получить целую и дробную части дробного числа можно несколькими способами : 1) операцией % - взятие остатка и делением. 245.12 % 1.0 = 0.12; 245.12 / 1.0 = 245 (целое значение) 2) побитовыми операциями (наложение маски) 245.12 & 8000FFFF = 0.12, 245.12 & FFFF0000 = 245.0 Углы бывают двух видов — byte angle и fixed point angle. первые могут принимать 255 разных значений, где 0 — восток (0 градусов) 64 — север (90 градусов) 128 — запад (180 градусов) 192 — юг (270 градусов). Вторые принимают значения от 0.0 до 1.0. К сожалению, я бы предпочёл в радианах, но что есть, то есть. перевод из первого во второе выглядит так : int fixedangle = byteangle <<8>> 8; Эти операции всего-лишь "растягивают" или "сжимают" наши значения углов из диапазона 0..255 в диапазон 0.65535. Чтобы окончательно развеять всякую магию, запишу это так : int fixedangle = byteangle * 256; //256 = 2^8 то есть при байтовом угле = 64 (90 градусов) fixed будет равен 16384 или "0.25", то есть четверть окружности или 90 градусов. Все верно. важнейшим математическим объеком является вектор. Это либо два числа (X и Y), или длина и угол. Длину можно получить по теореме Пифагора, правда, там фигурирует корень квадратный, а такой встроенной функции в ACS нет. Возьмите из http://zdoom.org/wiki/Sqrt любую на выбор. точнее, последние две там работают с fixed point числами, остальные — с целыми. Угол можно узнать при помощи функции VectorAngle(). за обратные преобразования угла в и длинны в координаты X и Y отвечают sin() и cos() : X = cos(angle) * length; Y = sin(Angle) * length; Если длина у вас в виде фиксированной точки, то вместо "*" используйте функции fixedmul().
[size=18][center][color=orange]ЧАСТЬ 3. Массивы, строки. Облать видимости[/color][/center][/size] сначала хотелось-бы начать с конца заголовка и объяснить такую вещь, как область видимости переменных. их несколько типов : 1) локальная область видимости. Переменная объявлена внутри функции или скрипта. объявление выглядит следующим образом : [code] script 1 (void) { int i; int j = 10; int k = j + sin(j*0.05); print(i:k); } [/code] здесь локально объявляются три переменных, причём две из них ещё и определяются. значения этих переменных потеряются после выполнения скрипта и их нельзя использовать в других скриптах — в других скриптах эти переменные не определены. А если их и определить, то это будут другие переменные : [code] script 1 (void) { int i; int j = 10; int k = j + sin(j*0.05); print(i:k); }
script 2 (void) { i = 10; // ошибка : переменная i не определена int j; print(i:j); // скорее всего будет выведен ноль, хотя в преведущем скрипте переменная с таким же именем могла быть выставлена в 10 } [/code] В отличие от c++ и с, после объявления локальной переменной она доступна везде в теле скрипта или функции ниже по тексту. Следующий уровень — уровень карты. Эти переменные объявляются ВНЕ тела цикла : [code]
int myvar = 1; script 1 (void) { print (i:myvar++); } [/code] такая переменная доступна во всех скриптах и её значение не теряется после выхода из скриптов. При хождении по хабу из карты в карту все локальные переменные сохраняются, но не могут быть использованы в других картах. Критически важной особенностью переменных уровня карты является то, что можно объявлять МАССИВЫ таких переменных. [code] int arrayofint[100]; script 1 (void) { for (int i=0;i<100;i++) arrayofint = 100-i; } [/code] Здесь объявлен массив из 100 элементов — с нулевого по 99. В скрипте мы производим его ннициализацию числами со 100 до 1, то есть arrayofint[0] == 100, а arrayofint[50] = 50. Массивы бывают не только одномерные, но и многомерными, например [code] int coordinates[10][2]; script 1 (void) { for (int i=0;i<10>var1) print(s:"ok"); } [/code] В этом примере объявлены три глобальных переменных : два int и один массив. обратите внимание, что его размер не указан. При этом размер этого массива действительно не определён и в него можно записать много(не знаю точно сколько именно, возможно десятки и сотни мегабайт) значений типа int. Также надо обратить внимание на конструкцию типа "номер":"имя переменной". Именно номер определяет, что это за переменная, а имя только используется в скриптах. При переходе на другую карту в этой другой карте доступ к переменным можно будет получить, ещё раз ив определив с тем-же номером(не обязательно с тем-же именем : [code] global int 1:var2; global int 2:sdata; script 1 (void) { print(i:var2); // если на преведущей карте script 1 отработал, то выведет "10" } [/code] Есть ещё тип world, он аналогичен global, но распространяется только на хаб, между хабами эти переменные обнуляются. [color=orange]Строки.[/color] строки в ACS — это номера строк в массиве строк, который(массив) формируется из всех строк, встреченных в тексте : [code] script 1 (void) int a = "Hello"; Spawn("DoomImp",...); int b = "world"; print(s:a;s:", ",s:b); //выведет "Hello, world" print(i:a,s:" ",i:" ",s:" ",i:b); // выведет "0 3 2", то есть используются строки с индексами 0, 3 и 2. [/code] Поскольку при таком подходе доступа к содержимому строк нет, то и изменять такие строки нельзя. [code] script 1 (void) int a = "Hello"; Spawn("DoomImp",...); int b = "world"; int c = a + b; // 0 + 2 print(s:c); // выведет "world", поскольку переменная c содержит индекс 2, который соответствует этой строке. [/code] тем не менее длинну такой строки в символах можно узнать при помощи функции strlen(), а значение каждого конкретного символа — при помощи функции getchar(string,pos). Однако в ZDoom всё-же есть возможность печати произвольно составленных, неконстантных строк. Для этого существует классификатор "a", который печатает массив символов, заканчивающийся нулём. Символы являются символами ASCII, допускаются символы из обеих половин таблицы, в том числе и русские буквы, если конечно они есть в текущем шрифте. [code] int array[100]; script 1 (void) { int s = "Hello, world"; int len = strlen(s); for (int i=0;i<len;i++) array[i] = getchar(s,i); print(a:array); // выведет те-же "Hello, world" } [/code] Выведет то-же, но вот только изменять такие массивы уже можно, добавляя слова, меняя буквы, сдвигая, модифицирую как вам захочется итд.
[size=18][center][color=orange]Часть 4. Функции вывода на экран текста и изображений[/color][/center][/size] ещё не написанно.
[size=18][center][color=orange]Жду ваши комментарии, вопросы, предложения[/color][/center][/size] а я продолжу завтра. |