Курс JASS + vJASS

Содержание:

Условия и логические выражения

Вступление

Наверняка, вы уже знакомы с условиями в гуи, но посмотрите на такой триггер:
Это довольно простой триггер. Когда любой юнит бросает молот бурь, если процент текущего здоровья цели меньше или равен 50%, то этот триггер убивает её. Как видим, тут есть два условия, способность должна быть молотом бурь и у цели должно быть не больше 50% здоровья. Условия для срабатывания триггеров мы будем рассматривать вместе с триггерами в следующих уроках, а сейчас сосредоточимся на конструкции IF THEN ELSE.
Названа конструкция IF THEN ELSE (ЕСЛИ ТО ИНАЧЕ) по аналогии с тем, как она работает. У неё есть условие и два варианта действий. Если условие соблюдено, то выполняется действие в блоке ТО. Если условие НЕ соблюдено, выполняется действие в блоке ИНАЧЕ. Тут для вас не должно быть ничего нового.
В джассе есть много вариантов записи такой конструкции. Например, если нам не нужен блок ИНАЧЕ, то мы его не пишем, а не оставляем пустым, как в гуи. Есть и другие варианты записи, которые мы рассмотрим в этом уроке. Для простоты, я иногда буду называть их ифами от английского слова if (если), а иногда буду писать их название полностью if-else (если-иначе). Слово THEN (ТОГДА или ТО) я буду пропускать потому, что и так ясно, если есть условие, то есть и действие для него.

IF ELSE

Посмотрим на то, как выглядит конструкция if-else в джассе:
if (условие) then
    действие 1
else
    действие 2
endif
Как и в гуи она состоит из трех частей: условие, что делать если, что делать иначе. Такая конструкция никак не влияет на код до или после себя, она влияет только на код, который находится внутри неё. На месте "действие 1" пишется код, который сработает если условие соблюдено. На месте "действие 2" пишется код, который сработает если условие НЕ соблюдено. С условием всё немного сложнее, на его месте может быть значение типа boolean или все что возвращает тип boolean. Напоминаю, у типа boolean есть всего два возможных значения это true (правда) и false (ложь). То есть если на месте условия окажется значение true, то сработает код на месте "действие 1". Если на месте условия окажется значение false, то сработает код на месте "действие 2". Но я не просто так написал там слово "условие". Посмотрите на такой пример:
if (true) then
    call BJDebugMsg("Правда")
else
    call BJDebugMsg("Ложь")
endif
В результате выполнения такого кода на экран всегда будет выводиться одно и то же сообщение: "Правда". А если заменить true на false, то всегда будет выводиться сообщение: "Ложь". Как видите, в этом нет никакого практического смысла. Обычно, на месте условия мы сравниваем какие-то заранее неизвестные значения. Например, текущее здоровье юнита, как в том триггере выше. То есть, обычно, там находиться именно условие.
Так все же при чем тут boolean. Думаю, для вас не будет секретом то, что в результате выполнения арифметических операций (+, -, *, /) и выражений получаются числа (тип данных real или integer). Так вот, в результате выполнения операций сравнения (==, >, <, >=...) и логических операций (and, or, not) получается значение типа boolean. Поэтому, мы можем использовать перечисленные мной операции на месте условия. Также некоторые функции возвращают значение типа boolean, их мы тоже можем использовать. А еще, само собой, в условии можно использовать переменные типа boolean. Более подробно разберемся со всем этим ближе к концу урока, а для начала, чтобы было проще, будем только сравнивать числа.
Во избежание путаницы, запомните несколько вещей. Когда я буду говорить, что условие соблюдено, или условие верно, или условие выполнено, или условие сработало... Знайте, я имею в виду одно и то же. Все верные условия возвращают true, все не верные условия возвращают false. Еще один момент касательно true (правда) и false (ложь). На вопрос типа "5 больше 2?" наверняка вы ответите либо ДА, либо НЕТ. Так true (ДА) и false (НЕТ) пишутся в гуи, но в переводе с английского на русский, слова "true" и "false" это именно "правда" и "ложь". Дело в том, что древнегреческие философы делили все утверждения на истинные и ложные. Вот и программисты заимствовали их подход и теперь у нас "5 больше 2" это правда, а "5 меньше 2" это ложь. Думаю суть вам ясна, у нас есть всего два абстрактных понятия, которые в разных контекстах имеют множество названий и формулировок.

Сравнение чисел

Все операции сравнения чисел пишутся примерно так же, как в математике:
"==" равно - если числа равны, вернет значение true, иначе вернет false. Пишется оно именно так с двумя знаками равно потому, что просто один знак "=" уже занят операцией присвоения значения переменной. Пример, 2 == 2 вернет true, 2 == 3 вернет false.
"!=" не равно - если числа равны, вернет значение false, если они не равны вернет true. Пример, 5 != 4 вернет true, 5 != 5 вернет false.
">" больше - если число слева больше числа справа, вернет true, иначе вернет false. Пример, 5 > 2 вернет true, 5 > 7 вернет false.
"<" меньше - если число слева меньше числа справа, вернет true, иначе вернет false. Пример, 1 < 3 вернет true, 4 < 2 вернет false.
">=" больше или равно - если число слева больше числа справа или они равны, вернет true, иначе вернет false. Пример, 5 >= 2 и 5 >= 5 вернут true, 3 >= 4 вернет false.
"<=" меньше или равно - если число слева меньше числа справа или они равны, вернет true, иначе вернет false. Пример, 5 <= 10 и 5 <= 5 вернут true, 7 <= 8 вернет false.
Для демонстрации сделаем простенькую функцию. Она будет выводить сообщение на экран, в котором будет сказано, что переданное функции число позитивное или негативное:
function PrintIsPositive takes integer number returns nothing
    if (number >= 0) then
 	    call BJDebugMsg("Число " + I2S(number) + " позитивное")
    else
 	    call BJDebugMsg("Число " + I2S(number) + " негативное")
    endif
endfunction
Проверим функцию. Напишите следующие три строчки в любой триггер, например с инициализацией карты:
call PrintIsPositive(-5)
call PrintIsPositive(0)
call PrintIsPositive(2)
В результате выполнения такого триггера на экран будет выведены следующие три строчки:
Число -5 негативное
Число 0 позитивное
Число 2 позитивное
Конечно, далеко не каждый математик согласится с тем, что ноль позитивный, но у нас он немного побудет именно таким.
Посмотрите на перевод всех тех английских слов в конструкции: if - если, then - тогда, else - иначе, end if - конец если. У нас прям получается предложение: если [число] больше или равно пяти, тогда вывести на экран "число [число] позитивное", иначе вывести на экран "число [число] негативное". Не то чтобы я намекал на то, что знания английского облегчат вам жизнь, но неплохо было бы знать немного английских слов. Отсутствие знания английского языка не критично, можно программировать и без него, но зная некоторые слова будет легче запоминать, что делает та или иная функция.

IF ELSEIF ELSE

Давайте переделаем нашу функцию, добавив ей вариант когда ноль это нейтральное число. Для этого мы можем вложить еще один if-else внутрь блока else:
function PrintIsPositive takes integer number returns nothing
    if (number > 0) then
        call BJDebugMsg("Число " + I2S(number) + " позитивное")
    else
	    if (number < 0) then
	        call BJDebugMsg("Число " + I2S(number) + " негативное")
	    else
	        call BJDebugMsg("Число " + I2S(number) + " нейтральное")
	    endif
	endif
endfunction
Как и в прошлый раз, впишем в триггер с инициализацией карты следующие три строчки:
call PrintIsPositive(-5)
call PrintIsPositive(0)
call PrintIsPositive(2)
На этот раз на экране будут такие три сообщения:
Число -5 негативное
Число 0 нейтральное
Число 2 позитивное
Если честно, то выглядит не очень. В джассе у нас есть более удобный вариант для записи длинных цепочек условий:
if (условие 1) then
    действие 1
elseif (условие 2) then
    действие 2
elseif (условие 3) then
    действие 3
else
    действие 4
endif
В таком случае, если не будет соблюдено первое условие, то будет проверено второе, а если и оно не верно, то третье и т.д. Их может быть сколько угодно, но только одно из действий будет выполнено. Сработает то действие, условие которого будет первым верным по списку. Если не сработает ни одно условие, то будет выполнено действие в блоке else.
Перепишем нашу функцию и протестируем её в том же триггере:
function PrintIsPositive takes integer number returns nothing
    if (number > 0) then
        call BJDebugMsg("Число " + I2S(number) + " позитивное")
    elseif (number < 0) then
        call BJDebugMsg("Число " + I2S(number) + " негативное")
    else
        call BJDebugMsg("Число " + I2S(number) + " нейтральное")
    endif
endfunction
Поведение не изменилось, но зато код стал короче и понятнее.

IF и IF ELSEIF без ELSE

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

IF

Например, следующая функция убьет юнита, но сделает она это только при условии, что процент его текущего здоровья меньше или равен 50:
function FinishOff takes unit u returns nothing
    if (GetUnitLifePercent(u) <= 50) then
        call KillUnit(u)
    endif
endfunction
Ранее не знакомая вам функция GetUnitLifePercent возвращает текущее количество здоровья юнита в % (значение real от 0 до 100)
Проверьте функцию FinishOff, например, в таком триггере:
Код из триггера:
call FinishOff(GetSpellTargetUnit())
Здесь была использована еще не знакомая вам функция GetSpellTargetUnit. Она возвращает юнита, который является целью использованной способности.
Если юнит использует молот бурь на цель, у которой процент здоровья меньше 50, то наш триггер убьет её.

IF ELSEIF

Теперь разберем более сложный пример в котором мы сравним удобство гуи и джасса в плане написания ифов. А заодно напишем длинную конструкцию IF ELSEIF без ELSE.
В гуи есть только один вариант написания ифов. Если нам нужно выполнить какие-то действия при каком-то условии, но при этом ничего не нужно делать в ином случае, то мы просто оставляем блок "иначе" пустым. Если же наоборот, у нас есть набор из нескольких условий и действий, из которых нужно выполнить только что-то одно, то мы делаем матрешку из условий. Например, как на скриншоте:
Когда умирает юнит данный триггер проверяет несколько условий и в зависимости от типа юнита, создает какую-то враждебную нежить на его месте: для рабочего - вурдалака, для пехотинца - скелета, для стрелка - скелета-лучника.
Выглядит не очень. Сделаем такой же триггер, но частично на джассе, для этого напишем такую функцию:
function RaiseDead takes unit u returns nothing 
    if (GetUnitTypeId(u) == 'hpea') then
        call CreateUnitAtLoc(Player(12), 'ugho', GetUnitLoc(u), GetUnitFacing(u))
    elseif (GetUnitTypeId(u) == 'hfoo') then
        call CreateUnitAtLoc(Player(12), 'nske', GetUnitLoc(u), GetUnitFacing(u))
    elseif (GetUnitTypeId(u) == 'hrif') then
        call CreateUnitAtLoc(Player(12), 'nska', GetUnitLoc(u), GetUnitFacing(u))
    endif
endfunction
Здесь вам незнакома только одна функция GetUnitTypeId, она возвращает ИД типа юнита. По ИД мы проверяем, если тип умершего юнита равен 'hpea' (ИД рабочего), то мы создаем юнита по ид 'ugho' (вурдалак). Если это не рабочий, то проверяем может это 'hfoo' (пехотинец) и если да, то создаем 'nske' (скелет). Если же это и не похотинец, то проверяем может это 'hrif' (стрелок) и если да, то создаем 'nska' (скелет-лучник). Если же это и не стрелок, то уже ничего не произойдет. Игрок с номером 12 это "нейтрально-враждебный", так как в джассе нумерация игроков идет с нуля, а не единицы как в гуи.
Как видим, конструкция иф в джассе более удобная чем её аналог в гуи, ничего никуда не съезжает и в ней легче ориентироваться.
Если вы вдруг забыли где смотреть ИД юнитов, то перейдите по ссылке.
Теперь создайте триггер с событием "любой юнит умирает" и впишите туда вызов нашей функции в которую предайте триггерного юнита:
call RaiseDead(GetTriggerUnit())
Должно получится как на скриншоте:
Возможно стоило бы еще добавить какой-то визуальный эффект и убрать труп, но не будем перенагружать пример.

Логические переменные и возвращаемые значения

Настало время более подробно разобраться с тем, какими еще способами ​можно использовать тип boolean.

Переменные

Мы можем сохранить результат логических операций и операций сравнения в переменную с типом boolean:
globals
    boolean first = 5 > 3
    boolean second = 5 < 3
endglobals
По итогу, глобальной переменной first будет присвоено значение true, а переменной second будет присвоено значение false.
Для простоты понимания, во всех примерах с ифами мы использовали только выражения, но не использовали переменные типа boolean. Давайте посмотрим на пример, в котором используем локальную переменную типа boolean в ифе:
function PrintIsBiggerThenZero takes integer number returns nothing
    local boolean con = number > 0
 
    if (con) then
        call BJDebugMsg("Число " + I2S(number) + " больше нуля")
    else
        call BJDebugMsg("Число " + I2S(number) + " не больше нуля")
    endif
endfunction
Эта функция принимает на вход целое число, если оно больше нуля, то на экран будет выведено сообщение: "Число [number] больше нуля". В противном случае на экран будет выведено сообщение "Число [number] не больше нуля".
Практического смысла в таком примере мало, но о такой возможности стоит знать. Я бы мог навести и сложный пример, в котором такой подход был бы оправданным, но не стану вас грузить. В дальнейших уроках использование переменных типа boolean будет оправданным и полезным, но сейчас крайне сложно подобрать хороший пример.

Параметры функции

Некоторые функции, в том числе и ваши собственные, могут принимать значение типа boolean в качестве аргумента. Обычно такое значение играет роль переключателя, настройки или выбора режима работы. Например, есть такая нативная функция:
native AddHeroXP takes unit whichHero, integer xpToAdd, boolean showEyeCandy returns nothing
Она даёт опыт герою. На вход она получает юнита (героя), количество опыта и переключатель, который отвечает за визуальный эффект получения уровня. Если передать значение true и полученного опыта хватит для получения нового уровня, то будет показан визуальный эффект получения уровня. Если же передать значение false, то визуального эффекта не будет и герой получит уровень "молча".

Возращаемое значение

Также функции могут возвращать тип boolean. Например, среди стандартных функций есть такая функция:
function IsUnitDeadBJ takes unit whichUnit returns boolean
    return GetUnitState(whichUnit, UNIT_STATE_LIFE) <= 0
endfunction
Как видим, данная функция принимает на вход юнита и проверяет количество его здоровья. Если здоровье юнита равно нулю или оно меньше нуля, то она вернет true, иначе она вернет false. Таким образом она узнает мертв ли юнит, а мы можем использовать результат её работы в ифе.

Операции сравнения

В этом уроке мы уже познакомились с операциями сравнения чисел (>, >=, <, <=, ==, !=). Теперь же узнаем про операции сравнения для всех оставшихся типов данных. Их немного, всего две операции сравнения для всех типов данных: равно (==) и не равно (!=). Эти две операции проверяют одинаковы ли значения по бокам. Разница лишь в том, что операция равно (==) вернет true только если значения одинаковые и false если они не одинаковые, а операция не равно (!=) сделает всё наоборот. В прочем, вы уже ознакомились с ними на примере чисел, но думаю нам стоит быстро пробежаться по тому, как оно сравнивает все основные типы и узнать некоторые нюансы.

real и integer

Вы уже и так знаете, как сравниваются числа. Скажу только то, что у вас есть возможность сравнивать значение типа real с типом integer. Типы boolean и code можно сравнивать только с сородичами (одинаковый тип).

boolean

Из за того, что у типа boolean есть всего два возможных значения, я могу перечислить все возможные варианты сравнения: true == true и false == false вернут true, а true == false и false == true вернут false. Такую проверку имеет смысл делать только в том случае, когда нужно проверить в обоих ли переменных находиться одинаковое значение.

Ссылочнее типы

Ссылки считаются одинаковыми, если они ведут на один и тот же объект, например на одного и того же юнита. Все ссылочные типы (кроме code) можно сравнивать между собой, то есть любой ссылочный тип с любым другим ссылочным типом, но практического толку от этого крайне мало. А вот сравнивать ссылки одинакового типа бывает очень полезно. Приведу в пример такой триггер:
Код из триггера:
if (GetTriggerUnit() == GetSpellTargetUnit()) then
    call AddHeroXP(GetTriggerUnit(), 50, true)
endif
Вам уже знакомы все три эти функции GetTriggerUnit, GetSpellTargetUnit и AddHeroXP. Тут логика такая, если юнит применивший способность и его цель совпадают, то становится очевидно, что он применил способность на себя. Мы его за это вознаграждаем дополнительным опытом. Изначально целью способности "благодать" не может быть применивший её юнит, не забудьте это исправить перед проверкой триггера. Если вы не хотите подталкивать паладина к жадности, замените оператор "равно" (==) на "не равно" (!=).Тогда ему будет давать опыт за применение способности на других юнитов, то есть за исцеление союзников и урон по вражеской нежити.
Обратите внимание на то, что на этот раз я не создал новую функцию, а сразу вписал код в триггер. По сути от этого ничего не поменялось, этот же код можно вынести в функцию. Но я должен показать вам возможность, которая позволит смешивать код JASS с действиями GUI:
Большая часть кода не поменялась поэтому я не стал его вам отдельно писать. Теперь если герой использует благодать на другого юнита, то ему даст 50 опыта. Если же герой использует способность на себя, то всем игрокам будет показано сообщение, в котором игра обзовет владельца героя эгоистом. Еще, как видите, я использовал не только джасс код, но и дейтсвие гуи, а именно вывод сообщения всем игрокам. Использовать ли чистый джасс или же смешивать его с гуи дело ваше, но я бы рекомендовал привыкать к чистому джассу.

Логические операции

К логическим операциям относят следующие три операции: and (и), or (или), not (нет). С чисто технической точки зрения это операции над типом boolean, которые также возвращают значение типа boolean. Но с такого ракурса их может быть сложно понять, поэтому сначала ознакомимся с ними "по гуманитарному".

Логическое И (AND)

Суть операции and заключается в том, чтобы объединить несколько условий и потребовать выполнить их все. Например, нам нужно чтобы юнит был пехотинцем и принадлежал красному игроку. В списке требований может быть сколько угодно условий.
В гуи оператор and используется преимущественно в виде такого блока-списка:
Джасс немного уступает по степени удобства в этом плане, в нем операция and выглядит так: условие1 and условие2. Пример:
function PrintIsInRange takes integer number, integer rangeFrom, integer rangeTo returns nothing
     if (number >= rangeFrom and number <= rangeTo) then
         call BJDebugMsg("Число " + I2S(number) + " находиться в диапазоне от " + I2S(rangeFrom) + " до " + I2S(rangeTo))
     else
         call BJDebugMsg("Число " + I2S(number) + " выходит за рамки диапазона от " + I2S(rangeFrom) + " до " + I2S(rangeTo))
     endif
endfunction
Данная функция проверяет находиться ли число (переменная number) в заданном диапазоне (переменные min и max). Делает она это следующим образом, у нас есть два условия, число (number) должно быть больше\равно нижней границы (min) и меньше\равно верхней границы (max). Операция and объединяет их, и только если оба условия будут выполнены, то на экран будет выведено сообщение "Число [number] находиться в диапазоне от [min] до [max]", иначе на экран будет выведено сообщение: "Число [number] выходит за рамки диапазона от [min] до [max]".
Проверяем, например, таким кодом в триггере с инициализацией карты:
call PrintIsInRange(5, 3, 7)
call PrintIsInRange(2, 5, 13)
На экран будет выведено два сообщения:
Число 5 находиться в диапазоне от 3 до 7
Число 2 выходит за рамки диапазона от 5 до 13
Если нужно написать множество условий, то нужно записать их через слово and: условие1 and условие2 and условие3 and ... условие99 and условие100.

Логическое ИЛИ (OR)

Операция or требует выполнить хотя бы одно условие из заданного списка. Например, юнит должен быть пехотинцем или бугаем. В списке требований может быть сколько угодно условий.
В гуи оператор or используется преимущественно в виде такого блока-списка:
В джассе, операция or выглядит так: условие1 or условие2. Пример:
if (GetUnitTypeId(GetTriggerUnit()) == 'nshe' or GetUnitTypeId(GetTriggerUnit()) == 'npig') then
    call CreateItemLoc('shrs', GetUnitLoc(GetTriggerUnit()))
endif
Функция CreateItemLoc создает предмет по ИД в заданной точке. 'nshe' - ИД овечки, 'npig' - ИД свиньи, 'shrs' - ИД предмета "мясо кодоя". Если умерший юнит это овечка или свинья, то на месте её смерти будет создано мясо кодоя.
Если нужно написать множество условий, то нужно записать их через слово or: условие1 or условие2 or условие3 or ... условие99 or условие100.

Логическое НЕТ (NOT)

Операция not переворачивает условие с ног на голову. Например, если у нас было условие, что юнит должен быть пехотинцем, то с оператором not оно потребует кого угодно только не пехотинца. Пишется операция not таким образом: not условие. Пример:
if (not (GetUnitTypeId(GetTriggerUnit()) == 'hfoo')) then
    call BJDebugMsg("Умер не пехотинец")
endif
Если вставить такой код в триггер с событием во время смерти юнита, то он будет выводить на экран сообщение "Умер не пехотинец", когда умрет кто угодно, но только не пехотинец. Такого же эффекта можно было достичь просто заменив знак "==" на "!=". В целом, можно обходиться и без операции not. С помощью операции not можно перевернуть значение типа boolean с true на false и наоборот с false на true. Например, среди стандартных функций есть такая:
function IsUnitAliveBJ takes unit whichUnit returns boolean
    return not IsUnitDeadBJ(whichUnit)
endfunction
Тут была использована уже знакомая нам функция IsUnitDeadBJ, которая возвращает true если юнит мертв и false если он жив. Функция IsUnitAliveBJ возвращает true если юнит жив и false если он мертв. По сути, она просто использует функцию IsUnitDeadBJ и переворачивает её результат.

Техническая сторона

Ну вот мы плавно подошли к тому, что логические операции это такие же операции, как и остальные уже рассмотренные нами операции. Только вот, арифметические операции берут два числа и возвращают число, операции сравнения берут два числа (или другой тип) и возвращают значение типа boolean, а логические операции берут значения типа boolean и возвращают значение типа boolean:
and проверяет если все значения равны true, то возвращает true, а иначе возвращает false.
or проверяет если хоть одно значение равно true, то возвращает true, но если все значения равны false, то возвращает false.
not переворачивает значение, если было true станет false и наоборот.
Все операции можно смешивать в одно выражение, например, в такое:
not (4 + 2 > 5 and GetRandomInt(5 + 3 * 2, 100) <= 80)
Никакого смысла в данном примере нет, просто демонстрация возможности.
А в следующем уроке мы поговорим о порядке выполнения операций.

`
ОЖИДАНИЕ РЕКЛАМЫ...