Переменные, их типы и операции над ними

Переменные, их имена, значения и типы, область видимости, зарезервированные переменные

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

Чтобы записать нужное значение в переменную используется оператор присваивания «=». Левым операндом оператора должно стоять имя переменной, правым — любое выражение языка возвращающее значение. Например напишем:

my_variable = "some value"

этот код присвоит переменной с именем my_variable строковое значение "some value". Переменная, если она не стоит слева от оператора присваивания, всегда возвращает своё значение:

a = 11
b = 23
c = a + b
Теперь значение переменной c равно 34.
Имена переменных

Для имен переменных мы можем использовать латинские буквы, цифры и символ подчеркивания «_», при этом цифра не должна стоять первым символом в имени. Символ подчеркивания удобен для имен состоящих из нескольких слов, как допустим enemy_units. Вообще можно рекомендовать давать переменным «говорящие» имена: несмотря на то, что такие имена визуально увеличивают код, читать его становится удобнее.

В действительности офп более снисходителен и допускает в именах локальные (в нашем случае русские) символы, однако следует склониться к тому, что это не самая хорошая идея и придерживаться общепринятых правил.

Синтаксис языка нечувствителен к регистру символов, таким образом имена _mySuperPuperVariable и _mysuperpupervariable рассматриваются интерпретатором как одно и тоже имя (эквивалентны).

Будьте осторожны с выбором имен переменных — если выбранное имя случайно совпало с именем команды, например, в результате создания переменной таким образом: «ammo = magazines player», то игра не выдаст ошибку, однако теперь эта команда (в нашем случае «ammo») будет недоступна. Конечно, случайно допустить такую ошибку сложно, ведь большинство команд имеют сложносоставные имена, и, со временем, вы все их запомните, но тем не менее, знать о таком поведении интерпретатора все-же стоит.

Инициализация переменных

Язык не требует обязательного предварительного объявления переменных, но до начала использования они должны быть проинициализированы каким-либо значением.

Если переменная не существовала ранее, то обычное присваивание создаст новую переменную с указанным именем. Переменная не содержащая никакого значения возвращает специальную величину nil (проще говоря, отсутствие какого либо значения), попытка преобразования в строку (см. команду format) вернёт "scalar bool array string 0xfcffffef". Это важно, так как вхождение величины nil в любое выражение приводит к прекращению его дальнейшего вычисления, например, здесь не будет выполнен не только блок then, но и блок else:

if ( nil ) then {
    hint "не будет выполнено"
} else {
    hint "также не будет выполнено"
}

Поэтому внимательно следите за тем чтобы все ваши переменные были проинициализированы.

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

; Здесь проблема в том, что если myGlobalFlag не существует на момент
; вычисления, то вхождение в следующие выражение неинициализарованного
; значения (nil) оборвет его выполнение и команда exit не будет выполнена:
?! myGlobalFlag : exit
; код который будет выполнен, если myGlobalFlag вернула false
используйте:
; В данном случае, если myGlobalFlag неопределена, то не будет
; перехода к «#OK», аналогично тому, как если бы myGlobalFlag вернула false
? myGlobalFlag : goto "OllKorrect"
    exit
#OllKorrect
; код который будет выполнен, если myGlobalFlag
; вернула либо false либо nil (неинициализирована)

Аналогично для sqf:

_proxyFlag = false
// если myGlobalFlag неопределена, то следующее выражение не будет выполнено
if ( myGlobalFlag ) then { _proxyFlag = true };
// теперь, _proxyFlag гарантированно существует и имеет значением false либо true
if ( _proxyFlag ) then {
    // код который будет выполнен, если myGlobalFlag
    // вернула либо false либо nil (неинициализирована)
}

Примечание. Часто с подбными целями используется ряд других «хаков». Как правило основной используемый прием — преобразование значения «опасной» переменной к строке:

if (
    format ["%1", GlobalIdentifier] == "scalar bool array string 0xfcffffef"
) then {
    hint "Symbol not defined";
    GlobalIdentifier = [];
}

Можно преобразовывать в строку не само значение, а результат некоторой операции над ним. Вот несколько примеров:

// Для переменных любых типов:
if ( format ["%1", GlobalVar in []] == "bool" ) then {
    /* инициализация */
}

// Для переменных массивов:
if ( format ["%1", count GlobalArray] == "scalar" ) then {
    /* инициализация */
}

// Для скалярных переменных:
if ( format ["%1", GlobalVar == GlobalVar] == "bool" ) then {
    /* инициализация */
}

if ( format ["%1", GlobalVar + GlobalVar] == "scalar" ) then {
    /* инициализация */
}

Ну и наконец можно вовсе обойтись без некрасивой format:

// Для массивов:
if ( 0 == { _x >= 0 } count [count GlobalArray] ) then {
    /* инициализация */
}

// Для скалярных значений
if ( 0 == { _x == _x } count [GlobalScalar] ) then {
    /* инициализация */
}
Вхождение nil в выражение

Краткая формулировка такого поведения интерпретатора звучит как «вхождение nil в выражение». Это просто запомнить — если в выражении обнаружена неинициализированная величина, вычисление выражения прекращается, само выражение, при этом, также вернёт nil. Например, вхождение nil в условие цикла while, прекратит его выполнение, вызов

nil call { hint "не выполняется" }
также не будет выполнен, и т.д.

Но например

[nil] call { hint "уже гораздо лучше" }

вполне работоспособен, поскольку в данном случае nil и call не участвуют в одном выражении.

Область видимости

Переменные могут отличаться областью видимости. Все переменные не имеющие в имени префикса «_» имеют глобальную область видимости. Это означает, что такая переменная доступна в любом скрипте или функции (включая строки инициализации, поля триггеров и прочее, другими словами из любого места игры, где есть возможность использовать скриптовые выражения). Имя переменной начинающееся с символа подчеркивания делает её локальной, например _temp_variable — локальная переменная. В отличии от глобальных, локальные переменные видны только в том контексте в котором они созданы.

Это влияет и на время жизни переменной — при выходе из контекста (как правило скрипта или блока в котором переменная создана) она удаляется и память отведенная под нее освобождается.

Примечание: под контекстом, здесь и в других местах текста, подразумевается то, что определяет область видимости, например, контекстом команды «call» будет её правый строковый операнд (т.е. функция), аналогично контекстом «do» будет её блок (правый строковый операнд, код заключенный в фигурные скобки «{}»).

Поскольку в sqf функциях могут быть вложенные контексты, то возможна ситуация когда имя переменной созданной во вложенном контексте уже используется во внешнем. Например:

_my_var = "это переменная доступна в текущем и во всех вложенных контекстах";

player sideChat _my_var;

call {
    player sideChat _my_var;
    _my_var = "однако её можно случайно испортить создав другую с тем же именем";
    player sideChat _my_var;
};

player sideChat _my_var;
Для разрешения такой ситуации необходимо явно объявлять переменные приватными:
_my_var = "это переменная доступна в текущем и во всех вложенных контекстах";

player sideChat _my_var;

call {
    player sideChat _my_var;
    private "_my_var"; // <— Объявляем приватную переменную
    _my_var = "однако её можно временно перекрыть объявив другую с тем же именем";
    player sideChat _my_var;
};

player sideChat _my_var;

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

Как видно из приведенных примеров, объявление приватной переменной осуществляется оператором private. Оператор имеет два варианта синтаксиса, в первом случае указывается список имен переменных:

private «variable names list»

операнд «variable names list» — массив имен приватных переменных, имена необходимо задавать как строки заключая их в кавычки:

private [
    "_private_variable1",
    "_private_variable2",
    "_private_variable3"
]

На тот случай, когда надо объявить приватной лишь одну переменную, команда private поддерживает краткий синтаксис:

private "_private_variable"

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

В следующем примере показано как пренебрежение этим правилом приводит к ошибке:

if ( true ) then {
    _some_var = "За пределами контекста блока then меня не существует!"
};
hint _some_var

Эта функция ничего не делает, поскольку после выхода из then блока переменная _some_var будет уничтожена, и соответственно hint не сработает (вхождение nil в выражение). Вы можете заменить строку «hint _some_var» на «hint format ["%1", _some_var]» чтобы убедится в том, что _some_var — неопределенная величина. Поправить ситуацию можно заранее объявив переменную приватной:

private "_some_var";

if ( true ) then {
    _some_var = "Я объявлена в родительском контексте, а в текущем проинициализирована!"
};

hint _some_var

Примечание: вложенность контекстов появляется не только при использовании команды call, каждая из нижеперечисленных команд создает свой контекст: foreach, count, while, do, then, exec. Однако, поскольку контекст, создаваемый командой exec, не имеет родительского контекста, объявление всех локальных переменных запущенного с помощью exec скрипта приватными обычно не будет иметь смысла.

Итак, поскольку это важный момент, рассмотрим его с точки зрения интерпретатора. Встречая во вложенном контексте (блоке, буквально: строковом операнде команд while, do, then, foreach, count, call) любую локальную (с префиксом «_») переменную, интерпретатор просматривает родительские (внешние по отношению к текущему) контексты на наличие переменной с таким именем; если такая переменная уже существует — то будет использована она, в противном случае будет создана новая, принадлежащая текущему контексту, локальная переменная.

Типы переменных

Значения переменных могут относится к различным типам.

В офп существуют следующие основные типы переменных:

Допустим у нас есть переменная _soldier и её значением на данный момент является некий юнит (фактически ссылка на игровой объект); очевидно, что мы не можем сложить его с числом, сравнить с массивом, и т.д. Это не только не имеет смысла, но и в принципе не существует реализации таких действий у интерпретатора языка. Поэтому любое значение хранящееся в переменной неявно связано с типом. Любые действия, выполняемые операторами или командами офп возможно выполнять лишь над данными определенного типа: складывать, умножать, делить, сравнивать — числа, проверять на идентичность — объекты или стороны, объединять — массивы с массивами или строки со строками. При несоответствии типов интерпретатор будет выдавать соответствующую ошибку.

Примечание: всегда обращайте внимание на типы операндов указанные для команд в комрефе, учитывайте тип возвращаемого значения и вы избежите подобных ошибок.

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

Зарезервированные переменные

Существуют также предопределенные зарезервированные переменные:
_this — с этой переменной мы уже знакомы, с её помощью реализован механизм передачи агрументов в скрипты и sqf-функции;
_time — содержит время, прошедшее с момента старта скрипта

Можно отметить также существование специальных, используемых интерпретатором переменных, их имена начинаются с двух символов подчеркивания «__», в будущем их наличие и количество может измениться (именно поэтому вы не должны ни обращаться к ним, ни определять свои с тем же префиксом).

Примечание: примером такой переменной может быть __waitUntil; интересным моментом является то, что задержку ~ интерпретатор раскрывает в подобный код:

__waitUntil = _time + (cas)
&__waitUntil

Числа или величины с типом Number

Использование чисел возможно как непосредственно, допустим: 123.456 — константа, так и в качестве значений переменных.

Форматы записи констант:

1230
0.123e4
.123e4
0.123
.123
0.123e7
1.23e+006
Операции над числами и их приоритеты

Двухместные арифметические операции над числами:

+     возвращает сумму своих операндов
-     вычитает из левого операнда правый и возвращает результат
*     возвращает произведение своих операндов
/     делит левый операнд на правый и возвращает результат
%     возвращает остаток от деления левого операнда на правый,
      важно, что операнды могут быть нецелыми числами.
mod   аналогично %
^     операция возведения в степень

Унарные операции:

-     меняет знак числа на противоположный
+     возвращает само число

Перечислим операции по убыванию приоритетов: унарные плюс и минус имеют высший приоритет, далее следует операция возведения в степень (^), затем идут операции умножения, деления и взятия остатка (*, /, % — их приоритеты равны между собой), и низший приоритет имеют операции сложения и вычитания.

В языке отсутствует операция отбрасывания дробной части числа, но к счастью оператор взятия остатка здесь может работать и с вещественными значениями, что даёт нам возможность легко написать свой int:

_int = { _this - _this % 1 }

Операции проверки отношения и равенства перечислены в главе о булевом типе (см. следующую главу).

Математические функции

Здесь коротко описаны основные математические функции поддерживаемые языком:

abs     абсолютное значение числа;

deg     переводит из радиан в градусы;
rad     переводит из градусов в радианы;

exp     экспонента;
log     десятичный логарифм;

sin     возвращает синус числа;
cos     возвращает косинус числа;
tan     возвращает тангенс числа;

asin    возвращает арксинус числа;
acos    возвращает арккосинус числа;
atan    возвращает арктангенс числа;
atan2   арктангенс результата деления левого операнда на правый,
        двухместная операция;

sqrt    корень квадратный;

pi      число pi.

Приоритеты этих функций выше приоритетов операторов, например:

sqrt 4 * 4
аналогично
( sqrt 4 ) * 4
возвращает 8, в отличии от
sqrt( 4 * 4 )
возвращающего 4.

Примечание: Другие команды могут иметь больший или меньший приоритет, как например, команда select. Во всех случаях когда вы не уверены в порядке вычисления используйте группирующие скобки

Примечание: в языке нет операторов инкрементирования и декрементирования, подобных ++ и --, побитовой арифметики также нет.

Логические или Булевы величины

Переменные булевого типа способны принимать одно из двух значений — true (истина) и false (ложь). Логические величины могут возникать как результат вычисления операций отношения, например «>» больше, «<» меньше или проверка на равенство «==»; как возвращаемое значение некоторых команд, например unitReady, someAmmo или stopped; либо как непосредственно указанные true или flase.

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

Операции отношения

Мы можем сравнивать числа следующими операторами:

>    — true, если левый операнд больше правого
<    — true, если левый операнд меньше правого
>=   — true, если левый операнд больше правого или равен ему
<=   — true, если левый операнд меньше правого или равен ему
==   — true, если левый операнд равен правому
!=   — true, если левый операнд не равен правому

Сравнивать объекты, стороны, группы (переменные с типами Object, Side, Group соответственно) можно только операторами проверки равенства:

==   — true, если левый операнд идентичен правому
       (если это тот-же самый объект, группа, сторона)
!=   - true, если левый операнд не идентичен правому
       (если это разные объекты, группы, стороны)

Аналогично для строк (для переменных с типом String):

==   - true, если левый операнд равен правому
       (если строки одинаковы)
!=   - true, если левый операнд отличен от правого
       (если строки разные)

Операции сравнения строк игнорируют регистр, если сравниваются символы с кодами до 80h (буквы английского алфавита, цифры и некоторые стандартые символы), и учитывают регистр при сравнении символов 80h-FFh (символы локальных кодировок, в нашем случае русские буквы). Таким образом утверждение:

"Hello, Word!" == "hello, word!"

справедливо, тогда как

"Привет, Мир!" == "привет, мир!"

ложно.

Некоторые очевидные равенства:

a > b   равносильно  !(a <= b)
a < b   равносильно  !(a >= b)
a <= b  равносильно  !(a > b)
a >= b  равносильно  !(a < b)

Увы, но в этой главе ничего не говорится о сравнении переменных с типом Array (массивов). Несмотря на то, что эти переменные на деле являются ссылками (см. детали), в языке нет возможности проверки на идентичность таких переменных (еще детали).

Операции над булевыми величинами

В офп присутствуют три основные логические операции:

В языке сценариев офп нет такой важной логической операции как «исключающее или», которая возвращает true, если лишь один из операндов true. Также мы не можем сравнивать логические величины на предмет равенства, поэтому эти операции мы имитируем так:

!(a && b) && (a || b)

Это выражение вернёт true только в том случае, если true либо a либо b, или другими словами, если a не равно b.

Примечание: важно, что выполнять операции можно лишь над переменными, содержащими данные одного типа, то есть объект можно сравнивать только с объектом, сторону со стороной, а строки со строками. Так как результат сравнения, например, стороны с объектом не имеет смысла (более того - такое сравнение невозможно), то при попытке выполнить такой код скриптинтерпретатор сообщит об ошибке несоответствия типов данных (что то типа следующего: «Any Value. Ожидался Object»). Это замечание относится ко всем типам данных и операциям над ними (а не только операциям сравнения).

Старшинство логических операций

Все операторы отношения имеют меньший приоритет чем операторы арифметические. Другими словами запись:

_a - _b > _c * _d
будет равносильна
( _a - _b ) > ( _c * _d )

Поскольку сравнивать булевы величины мы не можем, вопрос о приоритете операторов отношения относительно друг друга не имеет смысла (т.к. все они возвращают boolean).

Приоритет операторов && и || ниже чем у арифметических операторов и операторов отношения:

_a - _b > _c * _d && _n <= _m || _f != _z
равносильно
(( _a - _b ) > ( _c * _d )) && ( _n <= _m ) || (_f != _z)

Приоритет выполнения оператора && больше чем у ||, т.о. выражение:

true || false && false
вернёт true, в отличии от:
(true || false) && false

возвращающего false.

Приоритет унарного «!», выше приоритета любых других операторов.

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

Важной (и скажем так, не лучшей) особенностью языка является полное вычисления выражения даже в том случае когда его результат уже известен. Например следующее выражение нет нужды вычислять полностью:

(1 > 2) && call { hint "Не имеет никакого смысла вызывать меня!"; true }

Очевидно, что раз выражение в первых скобках ложно (единица не больше двойки) то и все выражение никогда не вернёт true, однако скриптинтерпретатор офп вычисляет его полностью. Будьте осторожны.

Эту особенность можно продемонстрировать на таком примере — допустим, существует условие победы в миссии: ( ( ключевой персонаж мертв ) и ( на острове нет боеспособных танков противника ) ). Ключевой персонаж, может быть убит только в самом конце миссии, и сделать это очень непросто, а проверка на боеспособность танков влечет за собой перебор всех юнитов на карте с выяснением ущерба, наличия стрелка и способности вести огонь. Вы решаете ускорить вычисление выражения вынося в начало простую проверку !alive и затем вызов скрипта вычисляющего сложную часть условия:

!alive Villian && call funcVictoryCondition

Это не сработает в связи с указанной выше причиной, и скрипт funcVictoryCondition будет вызываться на каждый тик игрового времени, поэтому придется поступить так:

if ( !alive Villian ) then { call funcVictoryCondition } else { false }

Ну и в завершение рассказа о типе Boolean, давайте детально разберем следующий пример:

leader player == player && 1 < count units player

Это выражение вернёт истину, только если игрок является командиром своего отряда и при этом ему действительно есть кем командовать. Итак, так же как и раньше, мы открываем комреф и смотрим описание команд: leader, player, units, count, ==, < и &&; особенное внимание обратим на типы операндов и возвращаемых значений. Теперь мы можем поэтапно рассмотреть как будет вычисляться наше выражение:

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

Этап 1
leader player == player && 1 < count units player
leader player              -> вернёт человека — лидера игрока
   человек                 -> Object type

Этап 2
человек == player && 1 < count units player
человек == player          -> вернёт булевое значение, true или false
булевое_значение           -> Boolean type


Этап 3
булевое_значение && 1 < count units player
                              units player  -> вернёт массив солдат
                                 массив     -> Array type

Этап 4
булевое_значение && 1 < count массив
                        count массив   -> вернёт количество элементов
                           число       -> Number type

Этап 5
булевое_значение && 1 < число
                    1 < число          -> вернёт булевое значение
                 булевое_значение      -> Boolean type

Этап 6
булевое_значение && булевое_значение   -> вернёт булевое значение
          булевое_значение             -> Boolean type

Проверте себя — закройте страницу и расставьте в этом выражении группирующие скобки.

Строки или величины с типом String

Данные с типом String — это обычные текстовые строки, используемые непосредственно, как например hint "Hello World!", или в качестве значений переменных, допустим:

// создаем переменную,
_greeting = "Hello World!";

// выводим подсказку в правом верхнем углу экрана
hint _greeting;

// плавное появление надписи внизу по центру
titleText [_greeting, "plain down"]
Конкатенация строк

Существует несколько способов объединить строки, первый и более простой — это оператор + (оператор объединения или конкатенации строк):

_example_text = "эта строка будет склеена с " + "другой строкой"

Выведем подсказку с именем игрока:

hint ( "Ваш ник: " + name player )

Примечание: операция + — единственная операция над строками, увы, мы не можем ни разбивать строки, ни брать нужный символ и получать его код, из операций отношения доступны лишь операции проверки равенства.

Преобразование значений других типов в строку — команда format

Другой способ объединения строк заключается в использовании команды format:

format ["%1%2", "эта строка будет склеена с ", "другой строкой"]

Теперь, в строку "%1%2", в места указанные спецификаторами вида %number будут вставлены строки, перечисленные далее.

Этот способ, очевидно, сложнее первого и целесообразность его использования была бы сомнительной, если бы не другое важное свойство команды — типы форматируемых величин могут быть любыми. При этом format преобразует эти величины к строкам и затем «вклеивает» их в управляющую строку. Спецификатор состоит из предваряемого знаком процента «%» числа — индекса элемента который будет помещен на место спецификатора. Управляющая строка может содержать более одного спецификатора указывающего на один и тот же элемент; спецификаторы могут появляться в любом порядке.

Допустим мы хотим вывести здоровье игрока, но есть одна маленькая проблема — команда getDammage возвращает число, а выводить на экран мы можем только строки, поэтому нам необходимо использовать format:

hint format [
    "Здоровье игрока: %1%2", (1 - getDammage player) * 100, "%"
]

Команда getDammage возвращает текущий урон, величину, фактически, обратную здоровью, как значение от 0 до 1; таким образом, выражение « (1 - getDammage player) * 100 » вернёт здоровье игрока в процентах; поскольку знак процента является зарезервированным символом для управляющей строки (командой format он распознается как часть спецификатора "%число"), то его мы «вклеиваем» отдельно.

Попробуйте следующий пример:

_player_info = format ["Игрок %1 сражается на стороне %2\n" +
    "в составе отделения %3 из %4 человек.\nВот они наши герои:\n%5",

    player,                  // объект
    side player,             // сторона
    group player,            // группа
    count units player,      // число
    units player             // массив объектов
];
titleText [_player_info, "plain down"];

Примечания:

Многие средства вывода строк на экран — команды hint, titleText, поля редактирования и текстовые поля в диалогах, и некоторые другие, распознают в тексте, предназначенном для вывода, управляющую последовательность "\n", сами символы последовательности при этом не выводятся, а инициируют переход на новую строку.

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

Этот пример, как и большинство других примеров здесь, дан в sqf синтаксисе, чтобы запустить его поместите текст примера в файл «function_filename.sqf» и используйте выражение: call preprocessFile "function_filename.sqf".

Итак, мы видели как команда format преобразует числа, объекты, стороны, группы и массивы в строки. С любыми другими типами она справится так же хорошо. Все это делает команду незаменимой для многих задач скриптования. При отладке скрипта мы используем её чтобы посмотреть какое значение содержит на текущий момент та или иная переменная, в сложных ситуациях мы «оборачиваем» с её помощью данные неизвестного на данный момент типа — например для безопасного сравнения с контрольной величиной, создаем строки для вывода значений разных типов, как в вышеприведенном примере, и наконец она позволяет нам создавать новые идентификаторы и функции. Последний момент стоит рассмотреть немного подробнее.

Рантайм создание идентификаторов может понадобиться, например, при работе с сохранением/восстановлением статуса объектов количество которых не известно заранее — команды loadStatus и saveStatus требуют уникальную строковую величину для идентификации сохраненного статуса. Мы можем создать его при помощи format:

_statusID = format ["statusID%1", random 10000]

вместо random стоит использовать счетчик statusID'ов — при создании очередного идентификатора инкрементируйте этот счетчик, обеспечивая тем самым уникальность id'ов.

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

call format ["new_variable_n%1 = %1", _count_of_variables]

Массивы или переменные с типом Array

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

["item 0", "item 1", "item 2", "item 3"]

Один массив может содержать значения различных типов, например, такой массив вполне возможен:

["просто строка", 1976, player, group player, false, east]
Операции над массивами

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

_girlfriends = ["Маша", "Даша", "Глаша"]

Для получения значения нужного элемента, используется команда select. Синаксис команды:

array select index
Здесь: array — массив, чей элемент подлежит чтению, а index — индекс читаемого элемента. Такая конструкция возвратит прочитанное значение.

Выведем последний элемент массива на экран командой hint

hint ( _girlfriends select 2 )

Я не опечатался указав в качестве индекса последнего элемента число 2, эта строка действительно выведет подсказку «Глаша». Дело в том, что в скриптах офп как и во многих других языках элементы массивов индексируются с нуля, то есть в нашем случае элемент со значением «Маша» будет иметь индекс равный нулю, со значением «Даша» — единице и со значением «Глаша» — двойке. Это не следует понимать как то, что первый элемент имеет индекс ноль (как, случается, объясняют на форумах), ибо такое объяснение ведет к путанице и непониманию. Просто избегайте самого понятия «первый элемент» и используйте классическое «нулевой элемент». (как пример — прописная истина: в байте восемь бит — с нулевого по седьмой).

Присваивание значения конкретному элементу массива осуществляется командой set имеющей следующий синтаксис:

array set [index, value]

Здесь:

Присвоить шестому элементу массива строку "some string":

_my_array set [6, "some string"]

Если элемент с указанным индексом не существовал ранее, то он будет создан. В случае когда индекс создаваемого элемента указывает за границы массива размер массива будет увеличен; если массив был увеличен более чем на одно место, то в него будет добавлено требуемое количество новых элементов инициализированных в null.

Примечание: однако избегайте обращения к несуществующим элементам массива — если индекс элемента к которому произошла попытка чтения больше размера самого массива, то офп выдаст сообщение об ошибке (по странной причине, это ошибка деления на ноль). Попытка чтения несуществующего элемента с индексом равным размеру массива возвращает nil.

Добавить новый элемент в конец массива:

_stack set [count _stack, "new value"]

Команда count возвращает текущий размер массива, и поскольку индекс верхнего элемента всегда на единицу меньше размера массива, код добавит новый элемент.

Прочитать последний элемент в переменную _top

_top = _stack select (count _stack - 1)

Изменить размер массива можно командой resize. Так мы уменьшаем размер массива на единицу, удаляя тем самым последний элемент:

_stack resize (count _stack - 1)

Безопасно удалить последний элемент массива:

if (count _stack != 0) then {
    _stack resize (count _stack - 1)
}

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

_new_array = []

Важно понимать, что переменная с типом array — это лишь ссылка на сам массив. Поэтому, если вы присваиваете переменной _x_array другую переменную _y_array содержащую массив, то создания нового массива и копирования в него существующего не происходит, и _x_array будет указывать на тот же массив, что и _y_array:

_real_array = ["Маша", "любит", "Пашу"];

// запомним текущее состояние массива
_morning_of_love = format ["%1", _real_array];

_spy_array = _real_array;

_spy_array set [1, "терпеть не может"];

// запомним текущее состояние массива
_evening_of_love = format ["%1", _real_array];

// смотрим результат
hint ( _morning_of_love + "\n\n" + _evening_of_love );

Как видим изменение _spy_array привело к тому же изменению в _real_array.

Если вам надо получить копию массива воспользуйтесь унарным плюсом. Скопировать _y_array в _x_array:

_x_array = +_y_array

Примечание: унарный плюс используемый совместно с операндом с типом array возвращает его копию.

_real_array = ["Маша", "любит", "Пашу"];

// запомним текущее состояние массива
_morning_of_love = format ["%1", _real_array];

_spy_array = +_real_array;

_spy_array set [1, "терпеть не может"];

// запомним текущее состояние массива
_evening_of_love = format ["%1", _real_array];

// смотрим результат
hint ( _morning_of_love + "\n\n" + _evening_of_love );

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

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

Конкатенация (объединение) массивов осуществляется оператором «+» (плюс). То есть мы фактически складываем два массива, например: ["A", "B", "C"] + ["X", "Y", "Z"] вернёт новый объединенный массив ["A", "B", "C", "X", "Y", "Z"]

Например, если вы хотите использовать массив в качестве очереди, то удобнее вставлять элементы в начало, а снимать с конца:

_queue = [new_value] + _queue;          // добавить элемент в начало
_top = _queue select (count _queue - 1) // прочитать элемент с конца
_queue resize (count _queue - 1)        // удалить последний элемент

Допустим у нас есть мужские и женские имена:

_male_names = ["Петр", "Василий", "Генадий"]
_female_names = ["Вера", "Надя", "Люба"]

Теперь получим список всех имен:

_human_names = _male_names + _female_names
hint format ["%1", _human_names]

Аналогично можно вычитать массивы:

_male_only_names = _human_names - _female_names
hint format ["%1", _male_only_names]

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

_i = 0;
while { count _array != _i } do {
    _value = _array select _i;
    _array = [_value] + ( _array - [_value] );
    _i = _i + 1;
}

Удалить восьмой элемент массива:

_array = _array - [ _array select 8 ]

У этого способа есть один недостаток — он сработает корректно, лишь в том случае, если массив не содержит дублирующихся элементов, иначе могут быть удалены все элементы со значением равным удаляемому. Зачастую можно поступить так:

_array set [8, objNull];
_array = _array - [objNull];

Этот способ не универсален, и в общем случае вместо objNull нужно использовать любое уникальное значение гарантированно не встречающееся в массиве, а сам выбор такого значения для каждого конкретного случая остается за Вами.

Надо помнить, что все элементы чьи индексы старше удаляемого, будут сдвинуты влево (в сторону младших индексов).

Вычитание не будет работать, если элементы массивов, сами в свою очередь являются массивами; этому есть вполне объяснимая причина — для нахождения совпадающих элементов необходимо провести сравнение всех (или некоторых, зависит от реализации) элементов массива. Однако вспомним, что мы не можем сравнивать массивы, и похоже этой операции действительно не существует ни в каком виде. По этой же причине выражение « some_array in [some_array] » никогда не даст true, как не покажется это странным на первый взгляд.

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

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

Как мы уже знаем, для обхода всех элементов массива существует команда foreach. Правый операнд foreach — массив для каждого элемента которого будет выполнен блок (последовательность команд заключенная в фигурные скобки) указанный левым операндом. Текущий элемент массива доступен в блоке как зарезервированная переменная _x.

Представим, что обладатели вышеуказанных мужских имен — дети некоего Епифана Поликарпиия:

_epiphans_children = [];
{
    _epiphans_children set [count _epiphans_children, _x + " Епифанович"]

} foreach _male_only_names;

hint format ["%1", _epiphans_children]

Проверить находится ли нужный нам элемент в массиве позволяет команда in:

if (name player in _epiphans_children) then {
    hint "Я давно подозревал, что с вами не все в порядке!"
} else {
    hint "Я счастлив знать, что вас избежала сия участь!"
}

Мы уже видели как использовать команду count для получения количества всех элементов (т.е. размера) массива. Но существует и другая форма использования этой команды - она позволяет узнать количество лишь тех элементов массива которые удовлетворяют определенному условию. её синтаксис похож на синтаксис foreach с той лишь разницей, что блок должен заканчиваться выражением возвращающем true или false, а сама команда вернёт количество элементов для которых выражение оказалось справедливо.

_epiphans_crew_number = {
    name _x in _epiphans_children
} count crew vehicle player;

if ( _epiphans_crew_number > 1 ) then {
    hint format ["%1 идиота на один танк — это слишком!", _epiphans_crew_number]
}

Команды countEnemy и countFriendly возвращают количество юнитов в переданном массиве являющихся противниками или союзниками (соответственно) для указанного юнита:

hint format [
    "игроку известно о %1 юнитах противника и %2 своих юнитов в зоне триггера",
    player countEnemy list battle_zone_trigger,
    player countFriendly list battle_zone_trigger
]

Если в секторе нет врагов известных Стрелке:

if (Strelka countEnemy list sector_trigger == 0) then {
    Strelka sideChat "Наверно они опять заползли под тот камень, из под которого выползли вначале!"
}

Команда countSide возвращает количество юнитов принадлежащих указанной стороне:

hint format [
    "%1 своих в зоне триггера",
    side player countSide list battle_zone_trigger,
]

Команда countType позволяет узнать количество объектов принадлежащих определенному классу или являющихся его потомками (по иерархии наследования в конфиге игры)

hint format [
    "%1 человек в зоне триггера",
    "man" countType list battle_zone_trigger,
]

Часто команду countType используют для проверки на принадлежность объекта классу (или подклассу):

if ( "tank" countType [_my_unit] != 0 ) then {
    hint "это бронетехника"
}

Получить случайный элемент массива можно сгенерировав индекс командой random:

_greetings = [
    "Привет",
    "Здраствуй",
    "Хай",
    "Здоровеньки булы",
    "Доброго времени суток",
    "Пешуиспацтала"
];

hint ( _greetings select ((random count _greetings) - .5) )
     + ", "
     + name player
     + "!";

Команда random возвращает псевдослучайное значение в диапазоне от ноля (0) до указанного правым операндом. При использовании в качестве индекса массива вещественного (дробного, нецелого) значения индекс округляется к ближайшему целому (т.о. даже отрицательное значение большее или равное -0.5 будет округлено до ноля). Полученное случайное число мы перемещаем в диапазон от ( -0.5 ) до ( count array - 0.5 ). Таким образом, при соблюдении неравенства:

( -0.5 ) <= index < ( count array - 0.5 )

мы всегда будем укладываться в область допустимых индексов.

Это решение даёт максимально равномерное распределение (настолько хорошее, насколько хорош сам генератор псевдослучайных чисел) Можно было бы использовать ( random ( count _greetings - 1 ) ), однако при этом крайние элементы массива выбирались бы реже других.

Вложенные массивы

Двухмерные массивы реализуются в скриптах офп как массив массивов. Например адресную книгу пришлось бы организовывать так:

_contacts = [
    ["Петр Хлыщ", "Бобруйск, дом 103 кв. 42", 899-67-23],
    ["Федр Ртутный", "Занзибар, дом 593 кв. 98", 618-59-14],
    ["Мальчег-Спальчег", "Албания, первый поворот за гастрономом, контейнер справа", "911"]
]

Теперь (_contacts select 1) будет возвращать массив:

["Федр Ртутный", "Занзибар, дом 593 кв. 98", 618-59-14]

Соответственно ((_contacts select 1) select 2) — вернёт номер телефона мистера Ртутного. Представим, что Федр сменил место жительства и нам теперь нужно изменить его адрес в нашей книге:

(_contacts select 1) set [1, "Где-то в Англии"]

Если мы собираемся неоднократно обращаться к подмассиву, имеет смысл записать ссылку на него во временную переменную и работать с ней:

_refArr = _contacts select 1  // теперь «_contacts select 1» и _refArr
                              // ссылаются на один и тот же массив
_refArr set [1, "Где-то в Англии"]
_refArr set [2, 534-74-35]

Заметим, что, поскольку _refArr не копия «_contacts select 1», а ссылка на этот подмассив, записывать его обратно не надо — все манипуляции над переменной _refArr в действительности совершаются над массивом на который она ссылается.

Примечание. Очевидно, но на всякий случай поясним: «все манипуляции» — это resize, set, select; присваивание-же любой величины изменит значение переменной — она перестанет указывать (ссылаться) на прежний массив.

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

Более жизненная ситуация — предположим, что у нас есть триггер и мы хотим узнать названия и количество стволов юнитов находящихся в данном триггере в формате который принимают такие команды как addWeaponCargo (массив вида ["gun_classname", number]). Собрать все стволы в один массив не представляется сложным:

_all_weapons = [];
{ // для всех юнитов попавших в триггер
    { // для всех членов экипажа этого юнита
        _all_weapons = _all_weapons + weapons _x
    } foreach crew _x
} foreach list battle_zone_trigger

Здесь мы создаем новый массив _all_weapons и затем в цикле присоединяем к нему массивы возвращаемые командой weapons.

Примечание:
выражение «list battle_zone_trigger» возвращает все объекты входящие на данный момент в триггер с именем battle_zone_trigger (см. описание команды list в комрефе);
команда crew возвращает массив юнитов являющихся членами экипажа указанного юнита, экипажем человека будет сам человек. Таким простым способом можно избежать лишних проверок, и заодно в список не попадет оружие танков или вертолетов.

Теперь нам надо получить итоговый массив, при этом в нем не должно встречаться одинаковых элементов — вместо нескольких одинаковых строк (названия классов стволов) мы будем записывать один двухместный массив вида ["название_класса_ствола", количество_таких_стволов]:

_weapons_collection = [];

while { count _all_weapons != 0 } do {
    _remained_weapons = _all_weapons - [_all_weapons select 0];
    _weapons_collection set [
        count _weapons_collection,
        [
            _all_weapons select 0,
            count _all_weapons - count _remained_weapons
        ]
    ];
    _all_weapons = _remained_weapons;
};

_weapons_collection

Рассмотрим этот код построчно.

Первой строкой мы создаем _weapons_collection, это тот самый массив который мы хотим получить.

Далее мы организуем цикл который будет выполняться до исчерпания исходного массива _all_weapons (по ходу цикла мы будем его уменьшать)

В цикле мы будем брать нулевой элемент _all_weapons, подсчитывать количество его вхождений в массив, записывать эти данные в _weapons_collection, а затем сокращать _all_weapons удалением из него этого нулевого элемента.

В первой строке цикла, вычитанием из _all_weapons его нулевого элемента (будут вычтены все вхождения такого элемента в массив) мы получаем сокращенный массив _remained_weapons. В данный момент в «_all_weapons select 0» находится имя класса ствола, и соответственно размер _remained_weapons будет меньше размера _all_weapons на количество таких стволов в _all_weapons.

Второй командой цикла мы добавляем в формируемый _weapons_collection двухместный массив вида:

[
    _all_weapons select 0,
    count _all_weapons - count _remained_weapons
]

Как уже сказано выше — « _all_weapons select 0 » — это название добавляемого ствола, а разница «  count _all_weapons - count _remained_weapons  » — вернёт количество этих стволов в исходном массиве _all_weapons. То есть, это именно такой массив который «скушают» команды аналогичные addWeaponCargo, что нам и требовалось в постановке задачи.

Третья и последняя команда в теле цикла присваивает переменной _all_weapons массив _remained_weapons, а так как он усеченная копия _all_weapons (см. первую строку цикла), то после исчерпания всех элементов _all_weapons цикл благополучно закончится, сформировав нужный нам _weapons_collection.

Теперь, с полученным массивом мы можем делать разные вещи — например погрузить все оружие в машинy:

{ removeAllWeapons _x } foreach list battle_zone_trigger;
{ _my_car addWeaponCargo _x } foreach _weapons_collection;

Я думаю, что по массивам теперь можно закончить, но мы еще обязательно вернемся к ним при рассмотрении некоторых структур данных и алгоритмов.

Разделяемые области данных

Еще одно интересное применение массивов никак нельзя не упомянуть.

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

Речь идет о «наведении мостов» между параллельно выполняющимися скриптами. Например в следующем примере один из них, «родительский», ждёт пока не будет выполнен другой, «дочерний»:

;parent.sqs

; [флаг_готовности, значение_задержки, сюда_будет_записано_приветствие]
_pipe = [false, 10, ""]

_pipe exec "child.sqs"

@ ( _pipe select 0 )

hint ( _pipe select 2 )

exit
;child.sqs

; задержка заданная в «parent.sqs»
~ ( _this select 1 )

; вернем приветствие
_this set [2, "Hello, father!"]

; установим флаг готовности
_this set [0, true]

exit

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

Переменные других типов

Переменные ссылающиеся на объекты, значения с типом Object

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

Существует специальное значение objNull, возвращаемое одноименной командой, обозначающее отсутствие объекта. Это значение может возвращаться многими командами в случаях когда требуемого объекта не существует. Также его могут принимать некоторые команды, например doTarget, для отмены цели. Однако мы не можем использовать objNull при проверках, так как это значение ничему не равно; во всех таких случаях используйте команду isNull.

Переменные идентифицирующие группы юнитов, значения с типом Group

Значения с типом Group идентифицируют группы юнитов. Команда group возвращает группу к которой принадлежит юнит. Здесь также доступна лишь проверка идентичности — мы можем только сравнивать группы операторами «==» и «!=», а основной способ использования значений Group — многочисленные команды.

Аналогично случаю с типом Object, для групп существует команда grpNull возвращающая «отсутствие существования группы», и её также нельзя использовать в сравнениях — проверка на отсутствие группы должна осуществляться с помощью isNull. Например, группа перестанет существовать (group вернёт grpNull) через некоторое время после гибели последнего члена группы.

Примечеание: Однако группа существует бесконечно долго, если последний член группы был удален с помощью deleteVehicles.

Переменные идентифицирующие стороны, значения с типом Side

Существует лишь несколько значений этого типа, поэтому перечислим их полностью: east, west, resistance, civilian, logic, enemy, friendly. Первые четыре идентифицируют противоборствующие стороны, и они всегда доступны посредством одноименных команд:

east       — всегда возвращает сторону east, приведение к строке даёт "EAST";
west       — всегда возвращает сторону west, приведение к строке даёт "WEST";
resistance — всегда возвращает сторону resistance, приведение к строке даёт "GUER";
civilian   — всегда возвращает сторону civilian, приведение к строке даёт "CIV".

Узнать сторону которой принадлежит некий юнит можно командой side:

hint format ["%1", side player]

Возможна проверка идентичности сторон:

if ( side player == resistance ) then {
    hint ( name player + ", вы повстанец." )
}

Команда sideLogic всегда возвращает сторону logic, с её помощью можно проверить является ли объект лоджиком:

if ( side unknown_object == sideLogic ) then {
    hint "Это лоджик"
}

Стороны enemy, friendly предусмотрены в игре (и в главном конфиге игры есть определения этих сторон) однако, насколько я знаю, не существует юнитов принадлежащих этим сторонам.

Copyright © , 2006–2014.