WarCraft 3: Сам спелл

Создание простого stomp спелла

Сам спелл

Теперь самая трудная часть – создание самого спелла.
Мы могли бы взять за основу подходящий спелл - War Stomp. Но в целях обучения мы не будем этого делать, так как это значительно сократит объем кода.
Давайте возьмем за основу что-нибудь другое, например Channel, или если вы все-таки решили взять War Stomp за основу, поставьте пустые или нулевые значения в поля: targets, art, aoe, damage и effect.
Создадим простейший триггер в GUI:
Trigger – Stomp
             Events
                 Unit – A unit Starts the effect of an ability
             Conditions
                 (Ability being cast) Equal to Stomp
             Actions
Мы используем событие "A unit Starts the effect of an ability". Приведу небольшое описание событий каста обычного спелла (события каста спеллов типа channel, не рассматриваются), чтобы показать различия между ними.
Unit - A unit Begins casting an ability – Это событие запускает триггер сразу же после каста спелла (Прим. перев. Мана еще не проплачена, кулдаун еще не начался). Это значит, что такое событие может и должно применяться только для запуска триггеров проверки дополнительных условий (проверка возможен ли каст в данный момент, например, расстояние между кастером и целью слишком мало и так далее). Если вы используете это событие как основное событие каста спелла, ловкие игроки получают возможность сжульничать и запустить триггер спелла, без начала кулдауна и проплаты маны.
Unit - A unit Starts the effect of an ability – Это событие запускает триггер в тот момент, когда спелл уже скастован, кулдаун уже начался и мана проплачена. Это событие идеально подходит для запуска триггера спелла.
Unit - A unit Finishes casting an ability – Это событие запускает триггер в момент, когда юнит завершил кастовать спелл. Это полезно в случаях когда, например, вам нужно удалить юнита, который скастовал определенный спелл, и вы хотите быть уверены, что эффект спелла появится. Например, вам нужно удалить юнита, кастующего спелл Heal, вы должны использовать именно это событие, иначе цель не будет вылечена.
Преобразуем наш спелл в JASS. Это выглядит примерно так:
function Trig_Stomp_Conditions takes nothing returns boolean
    if ( not ( GetSpellAbilityId() == 'A000' ) ) then
        return false
    endif
    return true
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
endfunction

//===========================================================================
function InitTrig_Stomp takes nothing returns nothing
    set gg_trg_Stomp = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Stomp, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( gg_trg_Stomp, Condition( function Trig_Stomp_Conditions ) )
    call TriggerAddAction( gg_trg_Stomp, function Trig_Stomp_Actions )
endfunction
Вместо 'A000' может быть нечто другое, если вы делаете этот спелл в карте, где уже имеются нестандартные спеллы. Это равкод (rawcode) спелла, он уникален для каждого спелла в карте.
Самый просто способ узнать равкод спелла, это выделить спелл в редакторе объектов и нажать Ctrl+D. В таком режиме первые четыре символа на месте имени спелла и есть его равкод (равкод регистрозависим, и должен быть заключен в одинарные кавычки), следующие четыре символа (имеются только у нестандартных спеллов), это равкод спелла на котором базируется выбранный спелл. После равкодов следует имя спелла, заключенное в квадратные скобки.
В целом этот триггер верен, и намного быстрее создать его в GUI, нежели в JASS. Но, кое-что мы все же изменим:
function Trig_Stomp_Conditions takes nothing returns boolean
    if ( not ( GetSpellAbilityId() == 'A000' ) ) then
        return false
    endif
    return true
endfunction
Эта часть, условие, до смешного запутанна. Сделаем ее более короткой и понятной:
function Trig_Stomp_Conditions takes nothing returns boolean
    return GetSpellAbilityId() == 'A000'
endfunction
Пока мы планируем использовать только одну модель спецэффекта: обычную модель War Stomp. Путь к этой модели: Abilities\Spells\Orc\WarStomp\WarStompCaster.mdl
Когда вы используете подобные пути в JASS, вы должны заключать их в двойные кавычки как строки. Также вы должны заменить все одинарные обратные слеши (\) на двойные (\\). Смысл несет только один слеш, второй используется как специальный символ.
Использование только одного обратного слеша приведет к ошибке при сохранении. Одиночный обратный слеш используется тогда, когда вы хотите использовать кавычку как часть строки в JASS.
Например: "Bob \"Boogieman\" Johnson" будет отображаться игрой как:
Bob "Boogieman" Johnson
Для того чтобы предотвратить лаг в момент первого каста нашего спелла, мы должны подгрузить модель спецэффекта. Воспользуемся native функцией 'Preload' для этого.
Подгрузим спецэффект в функции InitTrig, это выглядит так:
function InitTrig_Stomp takes nothing returns nothing
    set gg_trg_Stomp = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Stomp, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( gg_trg_Stomp, Condition( function Trig_Stomp_Conditions ) )
    call TriggerAddAction( gg_trg_Stomp, function Trig_Stomp_Actions )
    call Preload("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl")
endfunction
Ну что ж, теперь приступим к написание самого спелла. Все что мы сейчас сделаем, будет расположено в функции Trig_Stomp_Actions.
Вы уже должны иметь представление о локальных переменных, так что я пропущу их объяснение.
Первое, что мы должны сохранить в локальных переменных, это кастер и его координаты.
function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
...
Пришло время добавить новую функцию, которая будет использоваться как фильтр для поиска юнитов, на которых спелл может повлиять.
function Stomp_Filter takes nothing returns boolean
    return IsPlayerEnemy(GetOwningPlayer(GetTriggerUnit()), GetOwningPlayer(GetFilterUnit())) and GetWidgetLife(GetFilterUnit()) > 0.405 and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction
Функция должна быть оформлена в соответствии с рядом правил: она не должна брать ни один аргумент и должна возвращать boolean.
Этот фильтр пропускает юнитов, вражеских игроку, имеющих больше чем 0.405 единиц здоровья и не являющихся летающими.
Причиной, по которой мы проверяем единицы здоровья у юнитов, начиная с отметки 0.405, а не с 0, является то, что на самом деле юнит умирает, имея 0.405 или меньше единиц здоровья, а не 0, как полагают некоторые люди.
Причиной, по которой мы проверяем факт того, что юнит не является летающим (flying), вместо того чтобы проверить является ли юнит наземным (ground), являться то, что спелл должен действовать и на 'парящих' (hovering) юнитов.
Функция IsUnitType работает с ошибками, когда используется в фильтрах типа boolexpr, вы можете подробно узнать об этом здесь. Эти ошибки не скажутся на работе нашего фильтра.
(Прим. прев. Ошибки заключаются в следующем. Рассмотрим две функции:
function Trig_Lame_Condition takes nothing returns boolean
     return (IsUnitType(GetTriggerUnit() , UNIT_TYPE_TOWNHALL) == true)
endfunction

function Trig_Lame_Condition takes nothing returns boolean
     return IsUnitType(GetTriggerUnit() , UNIT_TYPE_TOWNHALL)
endfunction
На первый взгляд обе они верны, и должны работать одинаково. Первая из них работает правильно, в то время как вторая всегда возвращает false. Используем return bug, чтобы посмотреть, что на самом деле возвращает функция IsUnitType. В идеале 1 должна соответствовать значению true, а 0 – значению false. Воспользуемся функцией:
function B2I takes boolean b returns integer
     return b
     return 0
endfunction
Оказывается, что конструкция B2I(IsUnitType(whatever, UNIT_TYPE_TOWNHALL)) не возвращает 1, она возвращает 64!
Таким образом, функция IsUnitType(whatever, UNIT_TYPE_TOWNHALL), в случае, когда юнит принадлежит типу TOWNHALL, вместо 'boolean 1', возвращает 'boolean 64' и это integer значение приводится к типу boolean.
Сравнение этого булевого значения ('boolean 64') с true (==true) вернет true, и оно также будет корректно работать в операторах if, elseif и exitwhen.
Но при использовании в булевских выражениях (boolexprs), величина 'boolean 64' будет равна значению false.
Вот такой вот очередной забавный баг от blizzard. Функция IsUnitType также некорректно работает для типов юнитов:
GROUND 8
FLYING 2
RANGED_ATTACKER 1 как и HERO)
Теперь добавим функцию-фильтр в скрип, над функцией Trig_Stomp_Actions. Сейчас мы воспользуемся native функцией Condition для создания фильтра на базе нашей функции и запишем его в переменную типа boolexpr (булево выражение).
Также создадим новую группу юнитов в этой же функции:
function Stomp_Filter takes nothing returns boolean
    return IsPlayerEnemy(GetOwningPlayer(GetTriggerUnit()), GetOwningPlayer(GetFilterUnit())) and GetWidgetLife(GetFilterUnit()) > 0.405 and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
...
В строке объявления переменной типа boolexpr, также показывается, как пользоваться ссылками на функцию – функция на которую вы ссылаетесь, должна быть объявлена выше места, где вы на нее ссылаетесь.
Я использую native функцию Condition(), но также возможно использование и функции Filter(), так как они работают одинаково (Замечание: Они работают не совсем одинаково. Функция Condition() возвращает тип conditionfunc, а функция Filter() – filterfunc. Но оба эти типа, conditionfunc и filterfunc, являются дочерними к типу boolexpr, и могут использоваться в качестве boolexpr аргумента во всех native функциях GroupEnumUnits*. Следовательно, оба типа применимы, и таким образом нет разницы в том, какой из них мы используем в нашем случае).
Сейчас мы создадим спецэффект, соберем юнитов, которым должен будет нанесен урон, и нанесем его.
Существует два способа, для нанесения урона всем юнитам группы: native функция ForGroup(), которая использует другую функцию как параметр, и инициирует ее множественный вызов.
Другой способ - это создание копии группы, и прогон цикла по этой копии группы, с выбором первого юнита группы и выполнением над ним всех нужных операций. В конце каждой итерации выбранный юнит удаляется из группы, таким образом, цикл не будет бесконечным. При завершении цикла мы уничтожаем уже ненужную нам копию группы, тем самым, предотвращая утечку памяти.
Последний метод лучший в данном случае, поэтому воспользуемся им. Чтобы сделать это, нас потребуется функция, копирующая группу.
function CopyGroup takes group g returns group
    set bj_groupAddGroupDest = CreateGroup()
    call ForGroup(g, function GroupAddGroupEnum)
    return bj_groupAddGroupDest
endfunction
Эта функция делает следующее: создает новую группу и записывает ее в переменную bj_groupAddGroupDest (из Blizzard.j) и затем использует native функцию ForGroup(), для запуска функции GroupAddGroupEnum для каждого юнита группы.
Эта функция также из Blizzard.j, она добавляет всех использованных в ней юнитов в группу bj_groupAddGroupDest. Таким образом, мы получаем группу, состоящую из этих же юнитов, что и оригинал, которую и возвращаем.
(Прим. прев. Пусть вам не покажется странным, что автор не пользуется стандартной функцией GroupAddGroup(). Функция, предложенная им, работает быстрее, в этом вы можете убедиться сами, посмотрев код функции GroupAddGroup() в Blizzard.j)
function Stomp_Filter takes nothing returns boolean
    return IsPlayerEnemy(GetOwningPlayer(GetTriggerUnit()), GetOwningPlayer(GetFilterUnit())) and GetWidgetLife(GetFilterUnit()) > 0.405 and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction

function Stomp_CopyGroup takes group g returns group
    set bj_groupAddGroupDest = CreateGroup()
    call ForGroup(g, function GroupAddGroupEnum)
    return bj_groupAddGroupDest
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
...
Сначала, я создаю и уничтожаю спецэффект на позиции кастера. Когда вы уничтожаете спецэффект, проигрывается его анимация смерти.
Если модель содержит только оду анимацию (как эта модель), то проигрывается именно эта анимация, и модель исчезает по ее завершении.
Спецэффекты требуют удаления, для предотвращения утечек памяти, и так как нужная анимация все равно проиграется, я сразу же уничтожаю спецэффект.
Затем я вызываю функцию GroupEnumUnitsInRange(), чтобы собрать в группу всех юнитов в радиусе 100+50*i (где i - уровень спелла).
Затем я копирую группу в переменную 'n'.
Обратите внимание, что я переименовал функцию CopyGroup в Stomp_CopyGroup, во избежании проблем, если такая функция уже объявлена.
Вы также можете оставить прежнее имя функции, но поместить ее в секцию custom script.
Далее, я использую переменную f типа unit и native функцию FirstOfGroup() для прогона цикла по группе.
Этот участок кода делает следующее: записывает в переменную f первого юнита группы, если f пуста (равна null), цикл останавливается.
Иначе, юниту наносится урон, и он удаляется из группы (за счет этого урон занесется только один раз и цикл не будет бесконечным).
Я использую native функцию UnitDamageTarget для нанесения 25*i (уровень) урона. При этом используются ATTACK_TYPE_NORMAL (в GUI - Spell) как типа атаки и DAMAGE_TYPE_MAGIC как тип урона. Я использую null на месте типа оружия, чтобы спелл имел неопределенный тип оружия (тип оружия отвечает за то, какой звук проиграется при нанесении урона. В данном случае звук не нужен).
Пришло время начать работу с кэшем и таймером.
Native функции для работы с кэшем берут следующие параметры: сам кэш, строку 'категория' и строку 'метка'. Эти два строковых параметра дают нам возможность использовать кэш как двумерный массив.
Добавим несколько локальных переменных:
function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
...
Мы добавили следующее:
gc – Переменная, с коротким именем, для хранения кэша.
t – Таймер для создания движения юнитов.
s – Конвертированный в строку, уникальный id таймера, возвращаемый функцией H2I. Это то, что мы подставим на место параметра 'категория', при работе с кэшем. Эта строка также уникальна. Хранение данных в кэш, сделает наш спелл более универсальным.
Теперь самое время сохранить данные, которые потребуются нам в функции обработки движения, в кэш по метке 's' – этот прием обычно называют 'прикреплением' значений к handle.
Сделаем это:
function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
    call StoreInteger(gc, s, "level", i)
    call StoreInteger(gc, s, "group", H2I(g))
    call StoreReal(gc, s, "x", x)
    call StoreReal(gc, s, "y", y)
...
Таким образом, мы добавили четыре новые строки.
Первая строка напрямую сохраняет в кэш уровень спелла.
Следующая строка сохраняет в кэш указатель, уникальный id группы, используя H2I. Позже мы воспользуемся этим id чтобы получить группу.
Последние две добавленные строки сохраняют в кэш координаты кастера, соответственно. Благодаря им мы позже сможем узнать точку каста спелла.
Теперь давайте запустим таймер. Добавим, объявление пустой функции "Stomp_Move", над функцией "Trig_Stomp_Actions", и вызов функции TimerStart.
function Stomp_CopyGroup takes group g returns group
    set bj_groupAddGroupDest = CreateGroup()
    call ForGroup(g, function GroupAddGroupEnum)
    return bj_groupAddGroupDest
endfunction

function Stomp_Move takes nothing returns nothing
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
    call StoreInteger(gc, s, "level", i)
    call StoreInteger(gc, s, "group", H2I(g))
    call StoreReal(gc, s, "x", x)
    call StoreReal(gc, s, "y", y)
    call TimerStart(t, 0.05, true, function Stomp_Move)
...
Как вы видите, мы объявили функцию Stomp_Move выше функции Trig_Stomp_Actions (из-за того, что Trig_Stomp_Actions вызывает Stomp_Move) и ниже функции Stomp_CopyGroup, так как Stomp_Move использует Stomp_CopyGroup.
Таймер установлен на 0.05 секунды. Самые распространенные интервалы таймеров для спеллов это 0.05 и 0.04.
При использовании 0.04, спелл смотрится лучше, но больше шанс появления лагов, и мы не используем это значение в нашем спелле, так как он может двигать большое число юнитов.
Вы можете варьировать это значение – увеличивать или уменьшать, ища компромисс между быстродействием и плавностью движения, соответственно.
Теперь последнее, что нам осталось сделать в главной функции: обнулить все переменные, которые больше не используются, для превращения утечек памяти.
function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
    call StoreInteger(gc, s, "level", i)
    call StoreInteger(gc, s, "group", H2I(g))
    call StoreReal(gc, s, "x", x)
    call StoreReal(gc, s, "y", y)
    call TimerStart(t, 0.05, true, function Stomp_Move)
    set c = null
    call DestroyBoolExpr(b)
    set b = null
    set g = null
    call DestroyGroup(n)
    set n = null
    set f = null
    set gc = null
    set t = null
endfunction
Обратите внимание, что мы не уничтожаем группу, записанную в переменной 'g', так как функция обработки движения еще будет использовать эту группу.
Мы присваиваем переменной t значение null, для предотвращения утечки. В очень редких случаях, это может вызвать проблемы. Если это происходит, всего-навсего удалите строку, в которой t присваивается null. Вы можете подробно узнать об этой ошибке здесь.
(Прим. перв. Речь идет о весьма странной и трудновоспроизводимой ошибке. Очень редко, в случаях одновременного использования таймеров и кэша, могут возникнуть ошибки связанные с обнулением таймера для предотвращения утечек. Так как близзы никогда не обнуляют свои переменные, они вполне могли пропустить такую ошибку движка. Если вы пользуетесь связкой кэш и таймеры, и вдруг заведомо верный код начинает сбоить и вести себя некорректно, уберите строчку обнуления таймера)

Просмотров: 6 998

ssbbssc #1 - 5 лет назад (отредактировано ) 0
отлично, спасибо за статью
джассу пока не учился, но узнал оч. нужную вещь - как узнать равкод, и что он собсно рав кодом и зовется
никто из "убер-про писателей фака" не удосужился об этом упомянуть