Код, записанный на скриптовом языке, состоит из команд и их операндов. Несмотря на то, что в оригинальном комрефе разработчики игры не делят команды на операторы, функции, ключевые слова и т.д. мы будем иногда проводить такое деление. Например, команды математических операций и команды управления выполнением будут часто называться операторами, а команды возвращающие значение — функциями (как, например, математические функции), хотя иногда я буду забывать об этом и называть их просто командами.
Некоторые команды могут возвращать значение, самой близкой аналогией здесь может быть пример из математики: мы говорим, что функция sin возвращает синус аргумента. Аналогично и в языках программирования — запись sin(x) вернёт синус x.
Одни команды не имеют операндов, например команда player всегда возвращает ссылку на игрока, а команду west можно было бы назвать зарезервированной константой, так как она также всегда возвращает одно и то же значение — сторону west.
Другие команды могут иметь один или два операнда: если у команды один операнд, то он всегда правый, иначе, это левый и правый операнды. Если же команде требуется больше данных, то один из операндов будет массивом, при этом команда может иметь как один, так и два операнда. Чтобы всегда быть уверенными в том верно ли вы используете ту или иную команду, лучше всего всегда иметь под рукой комреф или один из нескольких его переводов.
Команды можно объединять в более сложные выражения, например такое выражение, вернёт водителя техники, в которой находится «Второй» отделения игрока:
driver vehicle (units player select 1)
Откройте комреф и посмотрите описание следующих команд: driver, vehicle, units, player и select. После того как мы ознакомились с описанием команд, отметили про себя количество и типы их операндов, мы можем подробно рассмотреть, как будет вычисляться этот код интерпретатором:
Синим курсивом выделено текущее подвыражение вычисляемое интерпретатором; зеленым — промежуточный результат вычислений Этап 1driver vehicle (units player select 1) units player-> вернёт массив солдатмассив-> Array typeЭтап 2driver vehicle (массив select 1) массив select 1-> вернёт солдатасолдат-> Object typeЭтап 3driver vehicle солдат vehicle солдат-> вернёт грузовикгрузовик-> Object typeЭтап 4driver грузовик-> вернёт водителяводитель-> Object type
Это значение (водитель) может использоваться в дальнейшем, например, так мы записываем его в переменную _my_driver, а затем отбираем у него все оружие:
_my_driver = driver vehicle (units player select 1) removeAllWeapons _my_driver
или делаем это одним выражением:
removeAllWeapons driver vehicle (units player select 1)
Примечание: здесь используется допущение, что нужный нам водитель и второй номер группы игрока существуют, если эти условия не будут выполняться — скрипт выдаст ошибку; это учебный пример, в действительной практике нужно выполнять явные проверки.
Возможность возвращать значение — очень важный момент, поскольку построение сложных (комплексных, составных) выражений синтаксически основано именно на том, что команды могут иметь операнды и возвращать значения. Поэтому, при разборе и написании выражений часто удобно думать, что круглые скобки возвращают своё содержимое, переменная возвращает своё значение, а непосредственно указанное число возвращает самоё себя.
При вычислении выражений языка учитывается приоритет выполнения команд.
Аналогично тому, как в математической записи
Каждый скрипт, запущенный на выполнение командой exec, будет работать в отдельном параллельном потоке со своей собственной областью данных под локальные переменные. Вызывающая сторона после выполнения команды exec переходит к следующей команде в потоке, не дожидаясь завершения запущенного скрипта.
Параллельно с выполнением команд sqs-скрипта в игре могут происходить различные события, и фактически выполнение команд одного скрипта происходит с определёнными микропаузами. Это следует учитывать, так как в редких случаях результат проверки некоторого условия (например, жив ли персонаж, с которым мы работаем) десятками строк ранее (в основном это будет долгий цикл), может стать неактуальным строкой ниже.
Скрипт sqs имеет построковую структуру, другими словами, самостоятельные выражения должны отделяться друг от друга символом конца строки. С момента введения в язык функций sqf стало возможным использовать для этого «точку с запятой» («;») и записывать в одной строке более одного выражения, однако надо следить, чтобы символ «;» не стоял в строке первым, поскольку ранее было решено зарезервировать такую конструкцию за комментарием. Также в sqs скриптах возможны все операторы sqf функций (if then else, while do, foreach), однако записываться они должны в одну строку, что очень затрудняет написание и ухудшает читабельность такого кода.
Как мы уже знаем, скрипт вызывается следующей конструкцией:
«arg» exec "script_filename.sqs"
«arg» — передаваемый в скрипт параметр.
Интерпретатор не поддерживает передачу списка аргументов (и соответственно обращения к ним по имени из скрипта), поэтому чтобы передать более одного аргумента мы должны использовать массив:
[_arg0, _arg1, _arg2] exec "my_script.sqs"
С другой стороны, синтаксис команды exec обязывает нас всегда указывать её левый операнд, даже если мы не хотим ничего передавать в скрипт. В качестве фиктивного аргумента вполне подойдёт ноль:
0 exec "some_script.sqs"
Переданный параметр доступен из скрипта как предопределённая переменная _this.
Примечание: в ранних версиях игры единственным допустимым типом левого операнда exec был Array, поэтому сложилось так, что самым популярным вариантом вызова без параметров стала передача пустого массива: «[] exec "scriptname.sqs"».
Как уже сказано, за убогостью языковых средств, для передачи ряда аргументов нам приходится использовать массив, таким образом, чтобы удобно обращаться к каждому из них по имени мы сами должны создать переменные и записать в них соответствующие элементы массива. Получить указанный элемент массива позволяет команда select:
_arg0 = _this select 0 _arg1 = _this select 1 _arg2 = _this select 2
Если же мы передавали в скрипт скаляр, то для обращения к нему можно непосредственно использовать _this.
Комментарий в sqs синтаксисе записывается как отдельная строка предварённая символом «;». Может смутить, что тот же символ используется для разделения выражений записанных в одну линию, поэтому следите, чтобы комментарий всегда стоял отдельной строкой.
; Это комментарий, здесь можно писать что угодно, но лучше ; комментировать свой код, хотя бы для себя.
Sqs скрипт небогат на возможности управления потоком выполнения, помимо разного рода задержек (или приостановок выполнения, о них ниже), все, что есть в нашем распоряжении — это оператор условного выполнения «?:» и безусловный переход goto
Оператор имеет синтаксис «? condition : expression» и в чем-то схож с if then else с «урезанным» else — выражение expression будет выполнено лишь в том случае, если условие condition вернёт true.
Примечание для тех, кто знаком с языком C — оператор не имеет ничего общего с привычным тернарным «?:», в sqs этот оператор отличается семантикой, имеет два операнда и не возвращает значения. Заменой тернарному оператору «?:» может служить select с булевым правым операндом, а также (приятный сюрприз!) «if then else» — в рассматриваемом языке он возвращает значение (см. детали)
Например, выйдем из скрипта, если он получил пустой массив в качестве аргумента:
? count _this == 0 : exit
Но в большинстве случаев мы хотим выполнять не одно выражение, а несколько; если их немного можно использовать «;» для того, чтобы записать их в одной строке:
? count _this == 0 : hint "Ошибка: не передано ни одного агрумента!"; exit
Однако когда мы хотим выполнить более значительный объем кода, этот вариант совершенно не годится, и мы вынуждены использовать оператор перехода goto. Оператор осуществляет переход на указанную аргументом метку. Метка должна стоять отдельной строкой и предваряться знаком шарп («#»).
Тот же код с использованием goto:
?! count _this == 0 : goto "ок" hint "Ошибка: не передано ни одного агрумента!" exit #ок
Фактически, скудный набор (или сладкая парочка, судя по популярности sqs в отличие от sqf) из goto и оператора условного выполнения позволяет реализовать весь спектр средств управления потоком. Хотя это и не самый удобный вариант в сравнении с готовыми возможностями sqf, но если уж использовать goto, то знать о них надо. Рассмотрим самые распространённые.
Конструкцию «если ... то ... иначе» короче всего можно записать если изменить условие на обратное — обратите внимание на оператор «нот» («!»):
?! «condition» : goto "else" ; код выполняемый если «condition» истинно goto "endif" #else ; код выполняемый если «condition» ложно #endif
В случае отсутствия части else, все упрощается до следующего:
?! «condition» : goto "endif" ; код выполняемый если «condition» истинно #endif
Так записывается цикл «do ... while ... », проверка условия выполняется в конце, т.о. тело цикла всегда выполняется, по меньшей мере, один раз:
#do ; тело цикла выполняемое пока «condition» истинно ? «condition» : goto "do"
Цикл «while ... do ... ». В отличие от предыдущего «do while», тело цикла «while do» может не выполнится ни разу (проверка его условия выполняется в начале). Обычно этот цикл записывают так:
#while ?! «condition» : goto "end_loop" ; тело цикла выполняемое пока «condition» истинно goto "while" #end_loop
Однако существует вариант более популярный (он выносит из цикла лишний оператор):
goto "loop_entry" #while ; тело цикла выполняемое пока «condition» истинно #loop_entry ? «condition» : goto "while"
Аналогично записывается цикл со счетчиком:
_counter = _start_number #loop ; тело цикла _counter = _counter + 1 ? _counter != last_number : goto "loop"
«Swicth» или выбор ветвления по ключу, имеющему тип String:
? _key in ["c1", "c2", "c3"] : goto _key goto "default" #c1 ; выбор s1 goto "end_switch" #c2 ; выбор s2 goto "end_switch" #c3 ; выбор s3 goto "end_switch" #default ; выбор по умолчанию #end_switch
Если значение, по которому надо сделать выбор не относится к типу String, можно получить строку функцией format и затем использовать её в качестве цели goto. Этот способ неплох для типов Number (но надо следить, чтобы величина была целым числом) и Side:
_key = side zold ; если есть метки для всех существующих сторон (лоджик, например, ; относится к собственной строне), то проверку можно опустить ? _key in [west, east, resistance, civilian] : goto format ["%1", _key] goto "default" #WEST ; выбор для стороны west goto "end_switch" #EAST ; выбор для стороны east goto "end_switch" #GUER ; выбор для стороны resistance goto "end_switch" #CIV ; выбор для стороны civilian goto "end_switch" #default ; выбор по умолчанию #end_switch
В случае, когда ключевые величины — целые числа, подряд следующие друг за другом лучше обойтись без использования format:
; массив переходов _jmptbl = ["nA", "nB", "nC", "nD"] ; если мы укладываемся в диапазон допустимых значений: перейти ? _key >= 0 && _key < count _jmptbl : goto ( _jmptbl select _key ) goto "default" #nA ; выбор nA goto "end_switch" #nB ; выбор nB goto "end_switch" #nC ; выбор nC goto "end_switch" #nD ; выбор nD goto "end_switch" #default ; выбор по умолчанию #end_switch
Таблица переходов _jmptbl содержит имена меток для перехода, но возможны и другие варианты с таблицами имён других sqs-скриптов, sqf-функций, а также содержащие sqf код непосредственно; все это даёт интересные возможности при реализации алгоритмов.
Подводя черту под этим обзором, необходимо отметить один момент. Когда бы ни использовался оператор «goto» вы должны чётко представлять себе ту управляющую конструкцию, в которой он задействован. Логика кода должна быть очевидной, поэтому не стоит злоупотреблять этим оператором. Единственной оговоркой может быть следующее — во всех типах циклов с помощью goto можно имитировать операторы «break» (досрочный выход из цикла) и «continue» (досрочная передача управления в начало цикла), это вполне допустимо и не должно ухудшить код. Можно также порекомендовать, использовать вложенные отступы для лучшего зрительного восприятия кода, как если бы вы писали на C (или другом высокоуровневом языке), в противоположность тому, как это обычно принято в asm.
И всегда, когда есть такая возможность, старайтесь использовать возможности sqf-функций.
Часто в сценарии нам нужно приостановить выполнение скрипта до наступления какого-либо события или просто на некоторое время, для этих случаев у sqs скриптов есть несколько удобных инструментов: это операторы «@» — ждёт выполнения условия, «&» — ждёт до указанного момента и «&» — пауза.
Оператор «@» ожидает наступления выполнения указанного нами условия, приостанавливая на время ход работы скрипта. Синтаксис оператора прост:
@ «condition»
«condition» — любое выражение возвращающее true или false.
Фактически оператор выполняет выражение «condition» каждый такт игрового времени и заканчивает свою работу, как только выражение вернёт true. Например, следующий код ожидает посадку игрока в любую технику:
@ vehicle player != player
Если мы хотим выполнять некоторые действия во время ожидания (к примеру, проверку критического условия, когда дальнейшая работа скрипта не имеет смысла), то можно разместить любое количество выражений в одну строку разделяя их символом «;», в таком случае условием будет считаться последнее указанное выражение:
@ if ( ! alive _soldier ) then { exit }; vehicle _soldier != _soldier
Если условие нетривиально и код перестаёт умещаться в экранную ширину (не более 80 символов), то код проверки удобно оформить sqf-функцией, вынести её в отдельный файл и подключать через preprocessFile:
_my_condition = preprocessFile "my_condition.sqf" @ call _my_condition
Всегда делайте это через переменную, это избавит скрипт от циклической загрузки файла.
Оператор «&», приостанавливает ход работы скрипта до указанного аргументом времени:
_waitUntil = _time + 10 &_waitUntilДанный пример ждёт десять секунд.
Примечание: переменная _time содержит время прошедшее с начала старта скрипта
Используется гораздо чаще предыдущего оператора, пример, приведенный выше может быть записан проще с помощью паузы «~»:
~ 10
Выход из скрипта должен осуществляться командой exit, часто её удобно использовать для досрочного выхода из скрипта:
? count units player == 0 : exit
Ну и, ради приличия, не забывайте добавлять её в конец каждого своего скрипта -))
Функции sqf были добавлены в язык позднее, и по ряду причин получили меньшую популярность чем sqs скрипты. Тем не менее, это наиболее удобное и мощное скриптовое средство, и единственная возможность создавать подпрограммы. Ранее в sqs скриптах не было механизма позволяющего выносить многократно выполняющиеся действия в подпрограммы, что приводило в общем случае к дублированию кода, и меньшей его прозрачности. Тот факт, что из одного sqs скрипта можно запустить другой мало спасает, так как выполняться он будет как отдельная ВМ.
Sqf функции развивают идею интерпретируемых строк («code string»), использовавшихся ранее в командах foreach и двухоперандной форме count. Поскольку рассматриваемый язык является интерпретируемым, то всегда существует возможность рантайм парсинга и выполнения любой строки содержащей выражения языка. Чтобы сделать эту возможность более полной были добавлены следующие команды:
call | — | самая важная, рапарсивает и выполняет строку, вызываемая сторона может получать аргументы, и возвращать значение |
---|---|---|
if then else | — | команды реализующие ветвление «если ... то ... иначе» |
while do | — | команды реализующие цикл «пока ... выполнять ...» |
private | — | теперь в одном файле может присутствовать несколько самостоятельных участков кода, и эта команда решает возникшую проблему с пространствами имён |
loadFile | — | возвращает содержимое файла как строку |
preprocessFile | — | содержимое файла обрабатывается си подобным препроцессором и возвращается как строка |
Для нас это в первую и главную очередь означает, что можно создавать свои функции и подпрограммы, ну и конечно использовать средства управления потоком специально придуманные для «белых людей». Их хоть и не много, этих средств, но нас, пишущих для офп, тоже не густо, так что нам пока хватит -))
Далее мы рассмотрим эти средства подробно.
Как уже сказано, функция sqf представляет собой обычную строковую величину содержащую выражения языка. Оператор call осуществляет вызов функции, фактически выполняя все выражения записанный в этой строке и возвращая результат последнего вычисления. Например, самая простая (и бесполезная) функция может выглядеть так:
_units_player = "units player"
теперь вызов « call _units_player » вернёт массив юнитов игрока. Здесь использовались двойные кавычки для того, чтобы подчеркнуть, что код выполняемый call — всего лишь строка, однако рекомендуется использовать для этих целей исключительно фигурные скобки «{}».
Соответственно, чтобы выполнить любой sqf код размещенный в файле (рекомендуемое расширение .sqf) необходимо предварительно получить строку командой preprocessFile и лишь затем вызвать оператором call:
call preprocessFile "my_function.sqf"
Здесь, выражение « preprocessFile "my_function.sqf" » вернёт обработанное препроцессором содержимое файла "my_function.sqf" в виде строки, после чего, код записанный в этой строке выполнит команда call. (Препроцессор удалит коментарии и выполнит макроподстановку, подробнее об этом мы расскажем чуть позже.)
Теперь, ради удобства, мы можем благополучно забыть о том, что функции являются строками, и будем вспоминать об этом лишь тогда, когда это понадобится непосредственно.
Функции могут принимать аргументы, так же как это делают sqs скрипты. Пример функции возвращающей преобразованный в строку аргумент:
_get_as_string = { format ["%1", _this] }
Мы можем вызывать эту функцию так: « player call _get_as_string », проверим:
hint ( player call _get_as_string )
Одна sqf функция может содержать другие; например, создав файл с именем «my.sqf» и следующим содержанием:
private "_subfunction"; _subfunction = { hint "эта функция находится в другой функции" }; call _subfunction
и вызвав его из инита игрока так: « call preprocessFile "my.sqf" », мы увидим появившуюся подсказку с текстом «эта функция находится в другой функции».
Функции sqf выгодно отличает от sqs скриптов то, что в процессе их выполнения не происходят игровые события, то есть по внутриигровому времени они выполняются мгновенно. За это достоинство приходится платить — в sqf невозможны приостановки, вечные циклы и прочее — все то, что так необходимо в классическом сценарии. Другими словами sqf функции являются не заменой sqs скриптов, а очень полезным и удобным дополнением.
Функции состоят из выражений разделяемых символом «;». Пример, демонстрирующий общий синтаксис sqf функции:
_find_assoc_data = { // обьявляем переменные приватными private ["_value", "_array", "_index", "_ok"]; // получаем аргументы и инициализируем переменные _value = _this select 0; _array = _this select 1; _index = 0; _ok = false; // макро с осмысленными именами для удобного доступа // к нужным элементам массива #define VALUE (( _array select _index ) select 0) #define DATA (( _array select _index ) select 1) // сам алгоритм while { !_ok && _index < count _array } do { if ( VALUE == _value ) then { _ok = true } else { if ( VALUE > _value ) then { _index = _index + _index + 1 } else { _index = _index + _index + 2 }; }; }; if ( _ok ) then { DATA } else { [] } }
Не будем вдаваться в то чем занимается эта функция, пример приведен для общего представления синтаксиса. Отметим, что нам не приходится конструировать руками цикл, мы используем для этого оператор while do. Опять же, мы не строим нечитаемую конструкцию из меток и переходов для того, чтобы выполнить то или иное выражение — просто используем «if then else». Легко заметить, что нет необходимости записывать все выражения в одну строку, как это приходится делать в sqs. Код становится читабельнее, и фигурные скобки вкупе с отступами «лесенкой» удобно отмечают начало и конец каждого логического блока.
Да, поскольку в названии главы значится «блочная структура», придется сказать об этом пару слов. Так как функции фактически состоят из операторов управления потоком и их операндов (именно так, поскольку это обычные команды принимающие строковые аргументы), а также учитывая то, что сами операторы определяют области видимости имён, из всего этого следует, что блочная структура, в некотором роде, имеет место быть. В главе о переменных мы узнаем о некоторых следствиях связанных с этой особенностью.
Передача и получение аргументов sqf функций полностью аналогичны тому как это делается в sqs. Например, функция принимающая скаляр может просто манипулировать предопределенной переменной _this, содержащей переданное значение:
_stupid_hint = { _this = "Hint: " + _this; hint _this }
Вызов с передачей аргумента выглядит так:
"Stupid example" call _stupid_hint
Если передается несколько значений, то также, как и в случае с sqs используется массив:
_hint_two_strings = { hint ( ( _this select 0 ) + "\n" + ( _this select 1 ) ) }
Вызов с передачей нескольких аргументов будет выглядеть так:
["Hello,", "Word!"] call _hint_two_strings
Для более удобной работы с аргументами, часто имеет смысл записать их в переменные:
_my_sqf_function = { private ["_arg0", "_arg1", "_arg2"]; _arg0 = _this select 0; _arg1 = _this select 1; _arg2 = _this select 2; }
Здесь все аналогично случаю с sqs, за одним небольшим отличием — поскольку в одном файле может быть описано несколько функций, то должна решаться проблема пространств имён. Этот момент более подробно рассмотрен в главе о переменных, а пока мы просто скажем, что все переменные sqf функций должны быть указаны приватными (т.е. принадлежащими текущему контексту) командой private, как это показано в примере выше.
Ну и наконец, отметим, что если мы не хотим передавать аргументы, то можем этого не делать — у call в отличии от exec может отсутствовать левый операнд.
Некоторые тонкости.
Нужно также осветить один момент относительно автоматической переменной _this — эта переменная будет присутствовать лишь в том случае, когда вы передавали в функцию аргумент, поэтому будьте внимательны, переписывая её значение:
"origin value" call { "anything" call { _this = "new value" }; hint _this // печатает "origin value" }
Здесь все верно, вложенная функция переписывает собственный _this, но следующий пример показывает возможную ошибку:
"origin value" call { call { _this = "new value" }; hint _this // печатает "new value" }
Так как вложенная функция не получила аргумента, то для нее, соответственно, не была создана переменная _this, поэтому присваивание «_this = "new value"» перепишет родительский _this.
Функции называются так не случайно, от простых подпрограмм их отличает то, что они способны возвращать значение. Вспомним, что sqf функции в данном языке возвращают последнее вычисленное выражение, например такая функция возвращает количество гранатометчиков в переданной группе:
_count_grenadiers = { { "" != secondaryWeapon _x } count units _this }
Эта функция состоит из одного выражения, результат его вычисления и будет возвращен вызывающей стороне:
hint format ["%1", player call _count_grenadiers]
Заметьте, что в конце тела функции не надо ставить точку с запятой, иначе последним выражением будет считаться то, что идет после «;», т.е. ничего. В качестве иллюстрации, вызов
call { 10 }
вернёт число 10, в отличии от вызова
call { 10; }
который возвращает Nothing. Если функция заканчивается одной из веток ветвления «if then else», то точка с запятой должна отсутствовать не только после ветвления, но и в обеих ветках:
_max = { if ( _this select 0 >= _this select 1 ) then { _this select 0 } else { _this select 1 } }
Примечание: в общем случае, точка с запятой не обязана стоять после каждого выражения — этот символ отделяет одно выражение от другого, а не является признаком его конца, как в некоторых других языках.
Очевидный момент, но все же отметим его: одиночно стоящая константа, переменная либо команда, также являются выражением и соответственно вычисляется:
// возвращает все оружие группы одним массивом: _get_group_weapons = { private "_w"; _w = []; { _w = _w + weapons _this } foreach ( units _this ); _w // <- это выражение будет вычисленно и его результат возвращен }
Естественно, что мы можем использовать sqf функции так же, как используем команды возвращающие значение — вызывая их непосредственно из выражений:
_small_arms = count ( ( player call _get_group_weapons ) - ["Binocular", "NVGoggles"] ) - ( player call _count_grenadiers );
Теперь _small_arms содержит количество единиц стрелкового оружия отделения игрока.
Мы уже неоднократно использовали в приводимых примерах строчные комментарии, как несложно было заметить, символом такого комментария является двойной слэш «//» — все, что идет после него до конца строки может быть произвольным текстом, и будет игнорироваться интерпретатором.
// Это строчный комментарий hint "подсказка" // перед комментарием может располагаться кодЕсли ваш комментарий занимает несколько строк, или вы хотите временно отключить часть кода, то удобно воспользоваться многострочным комментарием — для этого заключите текст между открывающим «/*» и закрывающим «*/» символами, так как это сделано в примере:
/* Это многострочный комментарий */Надо отметить, что комментарии (как и макро) обеспечиваются работой команды preprocessFile, и справедливы только для файлов подключаемых этой командой.
Препроцессинг, осуществляемый командой preprocessFile, позволяет использовать, помимо комментариев, макроподстановку. Макроподстановка — это механизм замены символического имени на произвольный текст. Макро определяется директивой препроцессора #define, создадим для примера макро MY_SOLDIERS:
#define MY_SOLDIERS (units player)
теперь в файле обрабатываемом preprocessFile все вхождения имени MY_SOLDIERS будут заменены на (units player). Можно создавать многострочные макро используя символ переноса на следующую строку «\».
Макро может принимать параметры:
#define SOLDIERS(G) (units G)
Теперь можно использовать «SOLDIERS(player)» для получения массива юнитов группы игрока.
Макро особенно удобно использовать для доступа к элементам массивов. Допустим мы не хотим создавать лишние переменные для доступа к аргументам функции:
#define ARG0 (_this select 0) #define ARG1 (_this select 1) #define ARG2 (_this select 2)
Здесь, как видим, используются «бессмысленные» имена макро, на деле они, конечно, будут отражать предназначение аргументов.
Или еще одна область применения — язык, к сожалению, не поддерживает пользовательские типы данных, поэтому вместо структур С или записей Паскаля, приходится использовать массивы. Самая большая неприятность здесь заключается в том, что надо помнить по какому смещению в массиве находится то или иное поле, а в случае изменения порядка в котором они следуют придется переписывать весь код, где происходит обращение к этим массивам. Конечно, макро может очень помочь в этой ситуации, ради примера посмотрим как могли бы выглядеть макроопределения для доступа к полям гипотетической структуры хранящей данные о юнитах:
// макро чтения #define mGetVehicleType(s) ((s) select 0) #define mGetPosition(s) ((s) select 1) #define mGetIdentityHandle(s) ((s) select 2) #define mGetStatusHandle(s) ((s) select 3) #define mGetAzimut(s) ((s) select 4) #define mGetSkill(s) ((s) select 5) #define mGetRank(s) ((s) select 6) // макро модификации #define mSetVehicleType(s,v) ((s) set [0,v]) #define mSetPosition(s,v) ((s) set [1,v]) #define mSetIdentityHandle(s,v) ((s) set [2,v]) #define mSetStatusHandle(s,v) ((s) set [3,v]) #define mSetAzimut(s,v) ((s) set [4,v]) #define mSetSkill(s,v) ((s) set [5,v]) #define mSetRank(s,v) ((s) set [6,v])
Областью действия макро является текст sqf скрипта, от точки определения макро, до конца файла.
Пару слов скажем о именах макро. В языках чувствительных к регистру символов принято давать имена макро в верхнем регистре, это позволяет не беспокоится о случайном совпадении имени переменной с именем макро, а также делает их легко узнаваемыми. Однако наш интерпретатор игнорирует регистр, поэтому такой подход не будет иметь смысла — единственное, что можно порекомендовать, это использование «специального» префикса, например «mcr_».
Вот мы и добрались непосредственно до операторов управления потоком. Как мы уже отметили их не так много: ветвление «if then else» и цикл «while do», а также старенький, но от этого не менее полезный «foreach». Такие вещи как например «switch» не поддерживаются, но мы покажем как можно их имитировать.
Этот составной оператор позволяет выполнить тот или иной код в зависимости от условия. Синтаксис оператора:
if ( «condition» ) then { «then_expression» } else { «else_expression» }
Часть «else» необязательна, и если она не нужна, то все сокращается до следующего:
if ( «condition» ) then { «then_expression» }
Здесь:
«condition» | — | условие |
---|---|---|
«then_expression» | — | код который будет выполнен если «condition» истинно |
«else_expression» | — | код который будет выполнен если «condition» ложно |
Подробнее скажем о условии — условием является любое выражение языка возвращающее булево (логическое) значение. Всего существует два таких значения — true (истина) и false (ложь), например утверждение «A > B» будет истинно (и соответственно вернёт true) если A действительно больше B, в противном случае утверждение ложно (и возвращает false).
О булевом типе можно более подробно прочитать в главе о переменных, а пока отметим лишь, что значения с таким типом возвращают некоторые команды, например alive, canFire, canMove, fleeing, captive, оператор in и операторы отношения:
> | — | истина, если левый операнд больше правого, иначе ложь |
---|---|---|
< | — | истина, если левый операнд меньше правого, иначе ложь |
>= | — | истина, если левый операнд больше правого или равен ему, иначе ложь |
<= | — | истина, если левый операнд меньше правого или равен ему, иначе ложь |
== | — | истина, если левый операнд равен правому, иначе ложь |
!= | — | истина, если левый операнд не равен правому, иначе ложь |
Над значениями булевого типа можно совершать следующие логические операции (слева дан си-подобный вариант синтаксиса, справа в скобках — синтаксис которым могут воспользоваться сторонники паскаля):
&& (and) | — | логичекое «И», истина, если оба операнда истина, иначе ложь |
---|---|---|
|| (or) | — | логичекое «ИЛИ», истина, если хотя бы один из операндов истина, иначе ложь |
! (not) | — | логичекое «НЕ», истина, если операнд ложь, иначе ложь |
Эти операторы, также, возвращают значения с типом Boolean.
Допустим есть выражение « A > B && C == D » (читается как «A больше B и C равно D»), где A, B, C и D — числа (величины с типом Number), тогда « A > B » и « C == D » будут подвыражениями с типом Boolean, (операторы больше «>» и равно «==» возвращают Boolean), и одновременно операндами логического «И» (&&); соответственно типом возвращаемого значения нашего выражения будет тип возвращаемый оператором «И» (&&) — Boolean.
Создавая, таким образом, подвыражения булевого типа, и совершая логические операции над ними мы получаем новые булевые [под]выражения; продолжая этот процесс рекурсивно мы можем конструировать утверждения любой сложности.
Некоторые тонкости.
Примечание: эти интересные детали лучше пропустить при первом прочтении.
Замечательной особенностью оператора «if then else» рассматриваемого языка, является то, что он может возвращать значение. Например:
_n = true; hint ( if ( _n ) then { "1" } else { "2" } )
щелкнет хинтом "1", или хинтом "2", если изменить _n на false.
Чтобы понять почему так происходит, давайте заглянем в comref и посмотрим, что представляют собой команды if, then и else. Ок, теперь нам понятно, что виновником такого поведения является команда then — именно она возвращает, в нашем случае, строчку "1"; при этом значение, возвращаемое командой if станет левым операндом then, а массив, возвращаемый командой else — правым операндом. Расставим, для наглядности, скобки:
( if ( _n ) ) then ( { "1" } else { "2" } )
Непривычно, но такая запись совершенно корректна для данного языка.
Итак, мы видим, что команда else фактически возвращает массив с двумя строковыми величинами, а then, в зависимости от состояния своего левого операнда (он имеет специальный тип "IF") выполняет или нулевой, или первый элемент этого массива. Это легко проверяется такими примерами:
_n = true; if ( _n ) then [{ hint "1" }, { hint "2" }]
hint ( {"Alpha"} else {"Beta"} select 0 )
hint ( {"Alpha"} else {"Beta"} select 1 )
Здесь же уместно вспомнить, что команда select имея правым операндом булево значение возвращает
или первый, или нулевой элементодин из двух элементов своего левого операнда-массива:
_n = true; hint ( {1} else {2} select _n )
Вот так забавно разработчики реализовали, вполне привычные, на первый взгляд, вещи.
_A = 1; _B = 2; _cond = if ( _A < _B ); _case = [ { hint "_A < _B" }, { hint "_B < _A" } ]; _cond then _case
Теперь мы знаем какое безобразие прячется за маской благопристойности свойственной классическим языкам, скрывая от нас свою порочную природу :-). Цикл «while do», рассматриваемый далее, препарируется столь же легко, займитесь им на досуге самостоятельно.
Составной оператор «while do» реализует цикл, тело которого повторяется до тех пор пока условие в части «while» истинно:
while { «condition» } do { «loop_body» }
Тело цикла «loop_body» будет выполняться пока истинно условие «condition». Заметьте, что условие в части «while» заключается в фигурные (т.е. это обычная анонимная sqf-функция), а не круглые (как условие в «if») скобки, будьте внимательны.
Например, запишем цикл со счетчиком:
private "_counter"; _counter = 0; while { _counter != 20 } do { // ваши действия _counter = _counter + 1 }
Так организуется прямой обход массива:
private "_index"; _index = 0; while { _index < count _array } do { // некоторые действия над текущим элементом // массива (_array select _index) _index = _index + 1 }
А так обратный:
private "_index"; _index = count _array; while { _index > 0 } do { _index = _index - 1 // некоторые действия над текущим элементом // массива (_array select _index) }
Цикл типа «do while», как правило, используются реже чем «while do», но если вдруг вам он понадобился, то можно вынести проверку в конец таким образом:
private "_condition"; _condition = true; while { _condition } do { «тело цикла» _condition = «настоящее условие» }
Язык не поддерживает операторы break и continue, вы не можете выйти в любой момент из цикла
или начать новую итерацию, поэтому используйте
возможности
break = false; while { _real_condition_ && !break } do { // если необходимо прервать цикл if ( условие ) then { break = true } else { // продолжение цикла // если необходимо перейти в начало цикла if ( ! условие ) then { // продолжение цикла } } }
Цикл while не может выполняться бесконечно, и если количество итераций превысит 10000, то интерпретатор остановит работу скрипта и выдаст ошибку.
Часто встречающейся задачей является выполнение некоторых действий над всеми элементами массива. Удобным решением может быть цикл foreach, например, предыдущий пример обхода массива с «while» мог быть записан так:
{ // некоторые действия над текущим элементом // массива (автоматическая переменная _x) } foreach _array
Как видим, код может быть короче и лаконичнее, в примере мы обошлись без указателя текущего элемента («_index»), вместо этого используется зарезервированная переменная «_x» — на каждой итерации цикла она автоматически принимает значение текущего элемента массива.
Следующий пример восстановит здоровье всех членов отряда игрока:
{ _x setDammage 0 } foreach units player
Интересной особенностью foreach является то, что мы можем в ходе цикла менять сам обрабатываемый массив, например удалить некоторые его элементы (изменив тем самым размер массива), и интерпретатор это корректно учтёт.
Однако не всегда выбор надо делать в пользу foreach. Например, если нужен параллельный обход сразу двух массивов, то мы все равно не обойдемся без указателя текущего элемента (_index в примере с «while»), также если нужна возможность досрочно прекратить обход (например при поиске перебором), то «while» тоже будет правильным выбором.
Примечание: эта часть несколько сложнее остальных, поэтому при первом чтении её можно пропустить.
Самый простой вариант switch может быть осуществлен лишь для ключей удовлетворяющим следующим условиям: они должны быть целыми числами, подряд следующими друг за другом без пропусков (как 4, 5, 6, 7, 8, 9 и т.д.), тогда мы используем массив содержащий анонимные функции, а ключ будем использовать как индекс этого массива:
// привести ключ к нужному диапазону _key = _this - 4; // таблица анонимных функций _switch = [ { hint "4" }, { hint "5" }, { hint "6" }, { hint "7" }, { hint "8" }, { hint "9" } ]; // если индекс укладывается в границы массива — вызвать нужную функцию if ( _key >= 0 && _key < count _switch ) then { call (_switch select _key) }
Это конечно здорово, но как быть если наши ключи строковые? Здесь тоже есть варианты.
Если строковое значение по которому надо сделать выбор не содержит пробелов и в целом удовлетворяет требованиям к именам (см. главу Переменные), то можно использовать ключ как имя функции:
private "_switches"; // имена функций для наших ключей _switches = [ "_key_MY_VALUE1", "_key_MY_VALUE2", "_key_MY_VALUE3" ]; // если для ключа имеется действие (в _this ожидается ключ) if ( "_key_" + _this in _switches ) then { // объявить имена функций принадлежащими текущему контексту блока then private _switches; // сами действия _key_MY_VALUE1 = { hint "MY VALUE 1" }; _key_MY_VALUE2 = { hint "MY VALUE 2" }; _key_MY_VALUE3 = { hint "MY VALUE 3" }; // косвенный вызов call (call (" _key_" + _this)); } else { // если для ключа не нашлось действия hint ( "not found: """ + _this + """") }
Можно запустить этот пример так:
« "MY_VALUE2" call preprocessFile "этот пример помещенный в файл.sqf" »
В плане оптимизации можно убрать проверку « "_key_" + _this in _switches ». В этом варианте мы лишаемся действия по умолчанию (если нужный ключ не нашелся), в остальном все остается так же:
private "_key_name"; _key_name = "_key_" + _this; // предохранимся, на случай если в текущем контексте // вдруг окажется переменная с тем же именем private _key_name; // имена функций private [ "_key_MY_VALUE1", "_key_MY_VALUE2", "_key_MY_VALUE3" ]; // сами действия _key_MY_VALUE1 = { hint "MY VALUE 1" }; _key_MY_VALUE2 = { hint "MY VALUE 2" }; _key_MY_VALUE3 = { hint "MY VALUE 3" }; // косвенный вызов call (call _key_name);
Последний, самый сложный вариант — выбор по ключу являющимся свободной строкой. В этом случае никакие особенности языка нам не помощники, и придется реализовывать поиск в массиве пар.
private ["_pairs", "_findValueInPairs"]; // массив пар: ключ/функция _pairs = [ "- key 1 -", { hint "Key 1" }, "- key 2 -", { hint "Key 2" }, "- key 3 -", { hint "Key 3" } ]; // ищет прямым перебором ключ и возвращает сопоставленное ему значение _findValueInPairs = { private ["_a", "_k", "_i"]; _a = _this select 0; // массив пар _k = _this select 1; // искомый ключ _i = 0; /* пока ( (не вышли за границы массива) и (не нашли искомое значение) ) —> продолжать поиск */ while { if ( _i < count _a ) then { _a select _i != _k } else { false } } do { _i = _i + 2 }; /* если в процессе поиска мы не вышли за границы массива ключ был найден, вернуть ассоциированную с ним функцию иначе вернуть анонимную функцию выводящую сообщение о неудаче */ if (_i < count _a) then [{_a select (_i + 1)}, {{hint "not found"}}] }; call ([_pairs, _this] call _findValueInPairs);
Увы, нам приходится использовать прямой перебор; поскольку язык не позволяет использовать операторы «больше» и «меньше» со строками, мы не можем оптимизировать сам подход используя древовидное иерархическое хранение данных.
Обратите внимание на то как написано условие для while — вместо более очевидного:
_i < count _a && _a select _i != _k
используется возвращающий значение «if then else», объяснение здесь простое — скрипт-интерпретатор в игре, в отличии от подавляющего большинства языков, всегда вычисляет все условие полностью и мы можем вылететь за границы массива, порадовав игрока сообщением об ошибке (комментарий)
Кстати, наше условие можно упростить, убрав часть else:
if ( _i < count _a ) then { _a select _i != _k }
Поясним: если условие « _i < count _a » не будет выполнено, то команда «then» вернёт неопределенную величину, что прекратит работу цикла.
Примечание: более того, while, в действительности, может принимать не только булево значение, но справедливым будет считаться лишь случай когда выражение вернуло true.
И конечно, этот пример слишком сложен для простого switch (если вы не собираетесь делать выбор из сотен вариантов), и скорее подходит для хранения связанных данных.
Примечание. В примерах использовано всего несколько пар ключ/функция; если их немного, конечно, лучше использовать вложенные «if then else» и лишь когда их более одного десятка, следует прибегать о описанным методам. Кроме того (и ради этого, в основном, я рассказал о них) все эти примеры показывают как могут быть реализованы в sqf структуры данных ключ/значение.
Язык «некоторым образом» поддерживает рекурсию. «Некоторым образом» — потому, что глубина рекурсии очень невелика, и если ваша функция «нырнет» глубже положенного, то игра, банально, вылетит. Вот пример простой тестовой рекурсивной функции:
_str = ""; _recurse_test = { if ( _this < 29 ) then { _str = _str + format ["[%1]-> ", _this]; _this+1 call _recurse_test; _str = _str + format [" <-[%1]", _this]; } }; 0 call _recurse_test; hint _str;
На двух разных машинах экспериментально выяснено, что максимальная глубина равна 29 рекурсивным вызовам. Однако не стоит сильно доверять этой цифре, поэтому всегда проверяйте текущую глубину, и прекращайте работу своего скрипта если глубина слишком большая.
Конечно, это рисковано, и лучше обходится совсем без рекурсии, но иногда это очень помогает, например при написании прототипа скрипта, как в примере обхода дерева:
_plain = []; _push = { _plain set [count _plain, _this] }; // разворачивает дерево _unwrapTree = { if ( _this in [_this] ) then { // если не массив - вывести элемент в поток _this call _push } else { // если массив - "<array>" call _push; // обозначить это дело { _x call _unwrapTree } foreach _this; // и повторить процедуру для каждого его элемента "</array>" call _push; }; }; [ 1, 2, "some string", ["A","B","C"], magazines player, [ ["r1c1","r1c2","r1c3","r1c4","r1c5"], ["r2c1","r2c2","r2c3","r2c4","r2c5"], ["r3c1","r3c2","r3c3","r3c4","r3c5"] ] ] call _unwrapTree; _plain
Уже после того как вы отладили прототип, можно ради безопасности переписать такие подпрограммы на стек.