XGM Forum
Сайт - Статьи - Проекты - Ресурсы - Блоги

Форуме в режиме ТОЛЬКО ЧТЕНИЕ. Вы можете задать вопросы в Q/A на сайте, либо создать свой проект или ресурс.
Вернуться   XGM Forum > Warcraft> Академия: форум для вопросов> Желтая пресса: обучающие статьи
Ник
Пароль
Войти через VK в один клик
Сайт использует только имя.

 
exploder
iOS zealot
offline
Опыт: 19,394
Активность:
Курсы спеллмейкерства: Часть 1: Создание простого stomp спелла.
Автор: Blade.dk
Вольный перевод с английского: exploder
Оригинал

Введение

Этот туториал предназначен, для того чтобы научить читателя делать простые Stomp ('топот') спеллы.

Но, тем не менее, почитая этот туториал, вы научитесь не только этому. Спелл Stomp, в основном используется только как пример, туториал же научит вас основным, очень важным, приемам программирования на JASS и написания спеллов.

Этот туториал НЕ является введением или начальным курсом изучения JASS. Вам рекомендуется (точнее, от вас требуется), чтобы вы ознакомились с основами JASS, перед тем как вы приступите к изучению этого туториала.

(Прим. перев. С основами JASS вы можете ознакомиться здесь)

В процессе чтения туториала вам потребуется WE и некоторые познания в JASS.

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

Типы и приведение типов

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

В JASS существуют следующие типы:

integer – Целые числа, без дробной части.

real – Числа с дробной частью. Например: 0.2, 0.54654675. Число может быть записано без своей дробной части, если это не аргумент, возвращаемый функцией. Таким образом, в большинстве случаев вы можете использовать 1, 35465 , или –340 тоже как реальные числа.

boolean – Может принимать значения true или false.

string – Текст между двойными кавычками. Например, "XGM".

code – Указатель на функцию. Например, call TimerStart(myTimer, 0.05, true, function myFunction). Последний аргумент, function myFunction, как раз имеет тип code.

handle – Handle это объект. Все типы, исключая integer, real, boolean, string и code, производные от handle.

Например, тип timer дочерний по отношению к типу handle. Тип handle родительский по отношению к типу timer.

Тип widget – расширение типа handle. Он потомок типа handle, но в тоже время он родитель таких типов как - unit, destructable и item.

Всякий раз, когда функция берет аргумент, вы можете передать в нее либо заданный тип, либо любой тип-потомок.

Это означает, что если функция берет аргумент типа widget, вы можете передать в нее переменную типов: item, unit или destructable, потому что все эти типы – потомки типа widget.

В случае с переменной, вы можете хранить в ней как данные типа этой переменной, так и данные типов-потомков.

Но как поступить в случае, когда, например, у вас имеется переменная типа handle, и вы хотите использовать ее для передачи в функцию, которая берет параметр типа timer?

Хотя вы знаете, что в данном случае handle будет именно timer, игра не знает об этом и выдаст сообщение об ошибке.

Для того чтобы 'преобразовать' тип handle в тип timer, который примет игра, вы должны использовать подобную функцию:

Код:
function MyFunction takes handle h returns timer
    return h
endfunction

Однако если h не является таймером, функция, в которую вы передаете таймер, будет вести себя так, как будто, вы передали в нее значение 'null' – пустое значение.

Blizzard тоже использовали подобные функции, например:

Код:
function GetDyingDestructable takes nothing returns destructable
    return GetTriggerWidget()
endfunction

Теперь мы знаем, как ведет себя тип handle, и мы можем 'преобразовывать' его и его потомков. Вы также уже должны знать, как работают базовые функции приведения типов от Blizzard, такие как S2I (string в integer), S2R (string в real), I2S (integer в string), I2R (integer в real) R2I (real в integer), R2S (real to string) и R2SW (real в форматированную строку).

Ну, а если, например, мы хотим преобразовать тип handle в тип integer? Это очень часто используется в JASS, при работе с кэшем (gamecache) для создания более универсального кода или 'баз данных'.

На самом деле, все очень просто. Этот метод называется 'the return bug', потому что он использует ошибку игры и редактора.

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

Таким образом, преобразование handle в integer, H2I, возможно. Все что нам нужно, это добавить еще один оператор return:

Код:
function H2I takes handle h returns integer
    return h
    return 0
endfunction

Эта функция (пожалуй, это самая известная JASS функция, которая была написана не Blizzard) берет значение типа handle, h, и возвращает значение типа integer. Как вы видите, первая строчка возвращает h. Вторая – 0.

Первый оператор return возвратит 'h' как идентификатор (id) типа integer, который использует эта конкретная handle переменная. Вторая строка кода никогда не выполнится, она служит лишь для того, чтобы при проверке этой функции на корректность не возникало ошибок.

И конечно, вы можете провести преобразование и в обратную сторону:

Код:
function I2H takes integer i returns handle
    return i
    return null
endfunction

Она работает точно также. Обратите внимание, что последний оператор return возвращает null вместо 0, как это было в предыдущей функции – все потому, что для типа handle, используется именно null, он эквивалентен 0, пустому значению. По правде, говоря, функции-експлойты, использующие return bug, вообще никогда не используют последнее значение, поэтому вы с таким же успехом можете поместить там GetTriggerUnit(), к примеру. Использование значения null, более разумно, так как, он не требует лишнего вызова функции.

А сейчас давайте создадим несколько return bug експлойтов и поместим их в секцию custom script (самый верхний элемент в списке триггеров, имеющий имя вашей карты) карты.

Код:
function H2I takes handle h returns integer
    return h
    return 0
endfunction

function I2G takes integer i returns group
    return i
    return null
endfunction

Оба они понадобятся, для создания спелла. Обратите внимание на функцию I2G (integer в group), вместо преобразования в handle, я напрямую преобразовываю ее в group.

Кэш

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

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

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

Однако, кэш, отлично РАБОТАЕТ во ВСЕХ типах игры, включая мультиплеер, если все данные, которые вы сохраняете и загружаете, используются только в рамках текущей игры.
Это значит, что НЕ возникнет никаких проблем, во время использования его в качестве двухмерного массива или базы данных в мультиплеере, все будет работать отлично.

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

(Прим. прев. Ограничения заключаются в следующем: существует лимит в 256 инициализированных кэш объектов. После превышения лимита, функция инициализации кэш объекта начинает возвращать значение null. Очистка (flush) кэш объектов не уменьшает лимит. Очистка кэша с именем, например, "cache.w3v" и затем инициализация кэша с таким же именем, приводит к тому, что займутся 2 'ячейки', то есть это не является способом обойти лимит. Во время загрузки игры в синглплеере, загружаются и все сохраненные в этом профиле игрока кэши, и все они занимают 'ячейки', и если случится, так что окажутся занятыми все 256 'ячеек' то больше НИ ОДНА карте не сможет инициализировать новый кэш объект. Если вы сохраняете кэш своей карты, то при следующей загрузке этой карты у вас будет меньше 'ячеек' для кэшей)

Что ж, сейчас можно приступить к работе. В редакторе переменных создайте переменную типа gamecache.
Я назову ее "AbilityCache" ("udg_AbilityCache" в терминах JASS), мы собираемся использовать эту переменную, для хранения в ней данных спелла.

Создайте новый триггер, используя GUI, и назовите его InitCache, затем преобразуйте его в JASS:

Код:
function Trig_InitCache_Actions takes nothing returns nothing
endfunction

//===========================================================================
function InitTrig_InitCache takes nothing returns nothing
    set gg_trg_InitCache = CreateTrigger(  )
    call TriggerAddAction( gg_trg_InitCache, function Trig_InitCache_Actions )
endfunction

Большинство из всего этого просто не нужно, так как мы используем этот триггер лишь для инициализации кэша. Удалите весь код снаружи и внутри функции InitTrig_InitCache, за исключением строк function и endfunction:

Код:
function InitTrig_InitCache takes nothing returns nothing
endfunction

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

Строка инициализации и очистки кэша выглядит следующим образом:

Код:
call FlushGameCache(InitGameCache("abilitycache.w3v"))

Я использую "abilitycache.w3v" как имя файла кэша.

Теперь добавим другую строку, которая действительно инициализирует кэш:

Код:
set udg_AbilityCache = InitGameCache("abilitycache.w3v")

JASS триггер InitCache выглядит следующим образом:

Код:
function InitTrig_InitCache takes nothing returns nothing
    call FlushGameCache(InitGameCache("abilitycache.w3v"))
    set udg_AbilityCache = InitGameCache("abilitycache.w3v")
endfunction

Все остальное про использование кэша, будет рассказано далее.

Сам спелл

Теперь самая трудная часть – создание самого спелла.

Мы могли бы взять за основу подходящий спелл - 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. Вы можете подробно узнать об этой ошибке здесь.

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

Функция обработки движения

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

А начнем мы с извлечения из кеша данных, сохраненных другой функцией:

Код:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
...

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

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

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

Код:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    if dur < 1+0.5*i then
    else
    endif
...

Я добавил переменную dur типа real. В нее загружается значение, прикрепленное к таймеру с меткой 'dur' и затем ее значению увеличивается на +0.05 (временной интервал таймера).

Если вы загружаете НЕ сохраненное значение из кэша, то загруженное значение всегда будет равно 0/0.0/"" или null, в зависимости от типа.

Этот спелл толкает юнитов на протяжении 1+0.5*i (i – уровень спелла) секунд, так что нам требуется добавить блок if/then/else.

Ну что ж, давайте, добавим часть кода, которая непосредственно двигает юнитов, на которых действует спелл. Для этого добавим следующие переменные: real ux, real uy, real a, unit f.

Код:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+40*Cos(a), uy+40*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
    else
    endif
...

Как и в главной функции, мы циклически перебираем всех юнитов группы, используя функцию FirstOfGroup().
Для начала, нам нужно сохранить координаты юнита.
Затем мы вычисляем угол (в радианах) между эпицентром спелла и позицией юнита, используя функцию Atan2.

Я не буду рассказывать, как эта функция устроена, я лучше расскажу, как нам ее правильно использовать.
Попросту используйте конструкцию вида Atan2(otherPointY-centerPointY, otherPointX-centerPointX), чтобы получить угол (в радианах) между точками centerPoint и otherPoint.

Спелл должен передвигать юнита. Существует два различных (лучших) способа передвигать юнита:

SetUnitPosition – Эта native функция передвигает юнита в точку с координатами X и Y. Пока юнит передвигается, он не может двигаться и кастовать канальные (channeling) спеллы и вообще ведет себя как будто остановлен. Эта функция не требует дополнительных проверок, и она полностью безопасна.

SetUnitX/Y – Native функции SetUnitX и SetUnitY также меняют X и Y координаты юнита. Однако, юнит не прекращает двигаться, кастовать канальные спеллы и так далее, во время движения. Эти функции работают быстрее, чем SetUnitPosition, но если вы используете координаты за пределами карты, игра вылетит.

Здесь я использую функцию SetUnitPosition для передвижения юнита на 40 единиц, всякий раз, когда таймер срабатывает. Это значит, что скорость движения будет 40*100*0.05 = 800. Применение именно этой функции, в данном случае, проще, так как не требует дополнительных проверок, но главной причиной, по которой я использую эту функцию, является то, что юниты не могут двигаться, пока таймер толкает их, и все возможные касты канальных спеллов, будут остановлены. Следовательно, эта функция идеальна для этого спелла.

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

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

Код:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+40*Cos(a), uy+40*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
    else
        call DestroyGroup(I2G(GetStoredInteger(gc, s, "group")))
        call FlushStoredMission(gc, s)
        call DestroyTimer(GetExpiredTimer())
    endif
...

Сначала, мы уничтожаем группу юнитов, сохраненную в кэше.

Затем полностью очищаем категорию s в кэше. Это действие очищает все данные, которые мы 'прикрепили' к таймеру, таким образом, мы исключаем повторное использование этих данных другими спеллами в дальнейшем.

И в завершении, мы уничтожаем истекший таймер.

Теперь все что нам осталось сделать в этой функции – устранить утечки памяти. Давайте сделаем это:

Код:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+40*Cos(a), uy+40*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
    else
        call DestroyGroup(I2G(GetStoredInteger(gc, s, "group")))
        call FlushStoredMission(gc, s)
        call DestroyTimer(GetExpiredTimer())
    endif
    set gc = null
    call DestroyGroup(g)
    set g = null
    set f = null
endfunction

Вот и все! Вы успешно завершили создании своего собственного stomp спелла.

Вот полный код нашего триггера:

Код:
function Trig_Stomp_Conditions takes nothing returns boolean
    return GetSpellAbilityId() == 'A000'
endfunction

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 Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+40*Cos(a), uy+40*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
    else
        call DestroyGroup(I2G(GetStoredInteger(gc, s, "group")))
        call FlushStoredMission(gc, s)
        call DestroyTimer(GetExpiredTimer())
    endif
    set gc = null
    call DestroyGroup(g)
    set g = null
    set f = null
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)
    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

//===========================================================================
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

Улучшение спелла

Более реалистичное движение

Теперь, когда основа спелла готова, давайте, попытаемся добавить в спелл несколько спецэффектов, для придания ему большей красочности.

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

Начнем с прикрепления начальной скорости юнита к таймеру в виде переменной типа real.

Код:
...
    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 StoreReal(gc, s, "speed", 50)
    call TimerStart(t, 0.05, true, function Stomp_Move)
...

Таким образом, начальная скорость равна 50. Теперь перейдем к функции Stomp_Move и изменим кое-что:

Код:
...
    local real ux
    local real uy
    local real a
    local unit f
    local real p = GetStoredReal(gc, s, "speed")-0.5/(1+0.5*i)
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+p*Cos(a), uy+p*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
        call StoreReal(gc, s, "speed", p)
    else
...

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

Теперь в строке с SetUnitPosition, мы просто используем переменную p, вместо использования константы (40).
Мы также сохраняем новое, уменьшенное значение скорости в кэш.

Добавление эффекта пыли

ЗАМЕЧАНИЕ: Я продолжаю работать с кодом спелла, который мы сделали, УЖЕ С изменениями, которые мы добавили для скорости.

Чтобы сделать эффект толчка более реалистичным, мы добавим эффект пыли.

Я собираюсь использовать для этих целей модель Impale Target Dust (Objects\Spawnmodels\Undead\ImpaleTargetDust\ImpaleTargetDust.mdl), так что давайте добавим ее подзагрузку в функции 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")
    call Preload("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl")
endfunction

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

Код:
...
    local real ux
    local real uy
    local real a
    local unit f
    local real p = GetStoredReal(gc, s, "speed")-0.5/(1+0.5*i)
    local real fx = GetStoredReal(gc, s, "fx")+0.05
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+p*Cos(a), uy+p*Sin(a))
            if fx >= 1 then
                call DestroyEffect(AddSpecialEffectTarget("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl", f, "origin"))
            endif
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
        call StoreReal(gc, s, "speed", p)
        call StoreReal(gc, s, "fx", fx)
        if fx >= 1 then
            call StoreReal(gc, s, "fx", 0)
        endif
    else
...

Сначала мы добавили переменную fx типа real. Мы загружаем в нее значение, прикрепленное к таймеру с меткой "fx". Мы немного увеличиваем ее значение, и если значение больше или равно 1, мы создаем (и сразу же уничтожаем, так как модель эффекта содержит лишь одну анимацию) спецэффект для каждого юнита из группы.

Упрощение заменяемости спецэффектов

ЗАМЕЧАНИЕ: Как и ранее, мы продолжаем работать с тем же кодом, в который уже внесены улучшения, рассмотренные выше.

Эта часть туториала, наглядно покажет вам, пример использования native функции GetAbilityEffectById, которая может извлекать строки из полей спецэффектов любой абилки (ability) в редакторе объектов.

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

Код:
native GetAbilityEffectById         takes integer abilityId, effecttype t, integer index returns string

Функция проста, дам небольшое описание ее аргументов:

integer abilityId – Равкод спелла, из полей которого вы хотите извлечь спецэффект.

effecttype t – Поле в редакторе объектов, из которого будет извлечен спецэффект. Вот список типов эффектов:
  • EFFECT_TYPE_AREA_EFFECT
  • EFFECT_TYPE_CASTER
  • EFFECT_TYPE_EFFECT
  • EFFECT_TYPE_LIGHTNING
  • EFFECT_TYPE_MISSILE
  • EFFECT_TYPE_SPECIAL
  • EFFECT_TYPE_TARGET

Имена типов говорят сами за себя, какие из полей в редакторе объектов они представляют.

integer index – номер эффекта в поле, который вы хотите извлечь, начиная с 0.

Для этого спелла я использую поле EFFECT_TYPE_MISSILE. Наш спелл кастуется мгновенно, и не использует это поле. Наилучшим вариантом считается использовать для своих целей те поля эффектов, которые не используются самим спеллом.

Теперь добавим следующие значения в поле нашего спелла: модель War Stomp (Abilities\Spells\Orc\WarStomp\WarStompCaster.mdl), как первый эффект поля, эффект пыли (Objects\Spawnmodels\Undead\ImpaleTargetDust\ImpaleTargetDust.mdl), как второй эффект поля и точку прикрепления эффекта пыли (строку origin) как третий.
Все верно, мы можем использовать поля эффектов, для хранения любых строк, которые мы можем в дальнейшем использовать в наших JASS спелах.

Теперь давайте, заменим строки путей к моделям эффектов в триггере на вызовы native функции GetAbilityEffectById:

Код:
function Trig_Stomp_Conditions takes nothing returns boolean
    return GetSpellAbilityId() == 'A000'
endfunction

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 Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    local real p = GetStoredReal(gc, s, "speed")-0.5/(1+0.5*i)
    local real fx = GetStoredReal(gc, s, "fx")+0.05
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+p*Cos(a), uy+p*Sin(a))
            if fx >= 1 then
                call DestroyEffect(AddSpecialEffectTarget(GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 1), f, GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 2)))
            endif
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
        call StoreReal(gc, s, "speed", p)
        call StoreReal(gc, s, "fx", fx)
        if fx >= 1 then
            call StoreReal(gc, s, "fx", 0)
        endif
    else
        call DestroyGroup(I2G(GetStoredInteger(gc, s, "group")))
        call FlushStoredMission(gc, s)
        call DestroyTimer(GetExpiredTimer())
    endif
    set gc = null
    call DestroyGroup(g)
    set g = null
    set f = null
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(GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 0), 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

//===========================================================================
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(GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 0))
    call Preload(GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 1))
endfunction


Создание спелла в соответствии со стандартом JESP

Стандарт JESP – это стандарт, упрощающий процесс обмена спеллами. Ничего не заставляет вас придерживаться этого стандарта, но если вы планируете опубликовать ваш спелл в дальнейшем, то было бы лучше, если бы стандарт был соблюден. Как правила спеллы, соответствующие стандарту, легче импортировать, настраивать и использовать, чем несоответствующие этому стандарту спеллы.

Вы можете прочитать спецификацию на стандарт здесь.

(Прим. перев. Русский вариант вы можете найти здесь)

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

Во-первых, вам нужно сменить имена всех ваших функций, таким образом, чтобы они начинались с рабочего имени спелла отделенного нижним пробелом, например, Stomp + _ + имя функции.

Также измените функции Trig_Stomp_Actions и Trig_Stomp_Conditions. Не забудьте, потом изменить их имена в месте их вызова в функции InitTrig_Stomp.

Стандарт JESP, требует, чтобы спелл содержал функции настройки, так что давайте, добавим их.

По возможности все функции настройки должны быть constant функциями.
Вы можете подробно ознакомится с constant функциями здесь.

(Прим. перев. Constant функции как правило работают немного быстрее, чем обычные функции, если не берут параметров и не вызывают обычные функции. Однако механика constant функций до сих пор не ясна. Возможно, интерпретатор создает отдельную таблицу смещений для constant функций, поэтому, при их сравнительно небольшом количестве, поиск их смещения происходит быстрее.
Мое личное мнение на этот счет, заключается в том, что constant функции в некоторых случаях аналогичны inline функциям в C)

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

Код:
constant function Stomp_SpellId takes nothing returns integer
    return 'A000'
endfunction

Добавьте эту функцию, в самое начало кода триггера спелла, и выполните "Replace all" (Заменить все), чтобы заменить id спелла 'A000', функцией Stomp_SpellId().

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

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

Код:
// Stomp spell by Blade.dk

constant function Stomp_SpellId takes nothing returns integer
    return 'A000'
endfunction

Также можете добавить свою контактную информацию.

Вы можете использовать системы подобные системам Local Handle Variables или CSCache.

(Прим. перев. Или собственную разработку XGM, систему Сергея – SCV)

Не забудьте, что вы должны вставить отключенный триггер, содержащий текст JESP стандарта, в вашу карту, таким образом, распространяется информация о нем. Это также обязательное требование стандарта.

В завершение

Спелл, который мы сделали, универсален, и не вызывает утечек памяти.

Если вы успешно завершили его создания, то примите мои поздравления.

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

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

- Спасибо,

Blade.dk

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

Отредактировано exploder, 11.10.2006 в 22:46.
Старый 11.10.2006, 22:32
free0n
Бумбарявка
offline
Опыт: 4,736
Активность:
Убой!!! Хачу продолжения!!! Автору и переводчику дать пирожок!!! =)))
Старый 12.10.2006, 00:33
exploder
iOS zealot
offline
Опыт: 19,394
Активность:
Новых частей пока нет (имеются ввиду оригиналы). Могу нипасать подобную статью сам. Например, про то как делать ауры...
Старый 12.10.2006, 09:12
J
expert
offline
Опыт: 48,447
Активность:
Цитата:
Сообщение от exploder
string – Текст между двойными кавычками. Например, "XGM".

DioD вот тебе истеное определение строки:)
Старый 12.10.2006, 09:25
exploder
iOS zealot
offline
Опыт: 19,394
Активность:
Jon, жжош! Тут очень краткие определения... Строка - указатель на массив символов в памяти, так что вполне возможно что их тоже нужно обнулять...

exploder добавил:
Замечание для тех, кто будет делать спелл, следуя туториалу: НЕ забудьте иницализировать кэш в отдельном триггере...
Старый 12.10.2006, 10:14
Sergey
Старейший
offline
Опыт: 44,363
Активность:
Многое можно было бы подправить с точки зрения последовательности изложения. Но в целом - статья неплоха. Правда если знаешь jass, то многое и без того очевидно, а вот если нет - статья не очень поможет...

В принципе эту статью можно рекомендовать тех, кто изучает jass по моей статье "Осваиваем jass" - после ознакомления с разделом про SCV. У меня есть примеры на эту тему, но нет объяснения, как они работают. Так что эта статья - вроде закрепляющего практического занятия.

exploder
Молодец! Внушительную работу проделал.
Старый 12.10.2006, 11:23
exploder
iOS zealot
offline
Опыт: 19,394
Активность:
Sergey, в ходе статьи еще рассматриваются интересные "подводные камни" JASS. А вообще, да, статья сугубо практическая...
Старый 12.10.2006, 12:54
DioD

offline
Опыт: 45,134
Активность:
размещение модифицированых статей без разрешения автора есть ничто иное как зло какой бы это ни было большой и качественной работой без разрешения blade.dk это на форум выкладывать нельзя.
я понимаю конечно что статья гуд (кстати их там 4 из одной серии странно что было сказано об отсутствии продолжния) но увы в оригинале она звучит лучше, да и кат некоторых авторских деталей меня разочаровал
Старый 12.10.2006, 13:11
Sergey
Старейший
offline
Опыт: 44,363
Активность:
DioD
Ну, вообще-то у нас традиционно не считают целесообразным спрашивать разрешение на перевод статей буржуев. Причем, это не только у нас, но и на Бру тоже. Так что никакого криминала тут нет. Тем более, что тут дана ссылка на статью первоисточник. Перевод - это всегда молификация.
Старый 12.10.2006, 14:09
DioD

offline
Опыт: 45,134
Активность:
как знаете, но я бы скачала спросил автора
Старый 12.10.2006, 14:15
exploder
iOS zealot
offline
Опыт: 19,394
Активность:
Цитата:
я понимаю конечно что статья гуд (кстати их там 4 из одной серии странно что было сказано об отсутствии продолжния) но увы в оригинале она звучит лучше, да и кат некоторых авторских деталей меня разочаровал

Значит, они не были вынесены, как отделные темы... А остальные страницы с комментами я смотреть не стал...
А насчет пары пропущеных абзацев, уж не знаю что тебы в них так интересовало. Тебе класически - лишь бы придратся...

exploder добавил:
DioD, хоть убей не нашел продолжения ни на кампах ни на жаскоме... Дай сслыки раз ты такой глазастый...
Старый 12.10.2006, 20:22
DioD

offline
Опыт: 45,134
Активность:
Ждите сейчас не кину так как у меня джазоком не грузитсо
Старый 13.10.2006, 07:37
exploder
iOS zealot
offline
Опыт: 19,394
Активность:
Ну и где обещаныю сцыли... хех... я начинаю думать что ты что то напутал...
Старый 13.10.2006, 15:06
DioD

offline
Опыт: 45,134
Активность:
http://www.wc3campaigns.net/showthread.php?t=82489
http://www.wc3campaigns.net/showthread.php?t=82663

собственно не знаю видели ли вы пример симпл стомп спелла где все роскошно отлетают в стороны, падают и оглушаются, так вот это собственно и есть вторая часть улучшения навыков кодинга
Старый 13.10.2006, 15:41
exploder
iOS zealot
offline
Опыт: 19,394
Активность:
DioD, ох насмешил... Причем тут вообще эти линки? Даже автор другой, и вообще с серией туторов от Blade.dk никак не связаыные
Старый 13.10.2006, 15:51
DioD

offline
Опыт: 45,134
Активность:
я же сказал что ты даже не понял что переводил...
Старый 13.10.2006, 15:54
exploder
iOS zealot
offline
Опыт: 19,394
Активность:
Оперируя фактом, что заявленых ссылок на продолжение туториалов от Blade.dk (и именно от него, т.к. в конце туториала речь именно о турориалах Spell Making Course) ты не смог дать, считаю все это глупыми отмазами дошкольного возраста. Ты дал ссылки на туториалы абсолютно другого автора, совершенно другой тематики, никак не связанные с данной серией туториалов... И ты еще говоришь, что я не понял, того что переводил?

Отредактировано Jon, 13.10.2006 в 16:48.
Старый 13.10.2006, 16:51
free0n
Бумбарявка
offline
Опыт: 4,736
Активность:
exploder да не спорь с ним. Лучще бы написал что-нибудь подобное про другие типы спеллов - Чтоб было так же много понятно и интерестно.
Старый 13.10.2006, 22:42
Aspid

offline
Опыт: 8,361
Активность:
подрывник...объемы поражают...
прочитаю - откоментирую.
Старый 15.10.2006, 02:38
dk

offline
Опыт: 60,293
Активность:
Принято! +2000 ехр
Посмотреть можно тут.
Сам разделил на части, если не устроит пиши!
Старый 16.10.2006, 16:30

Опции темы Поиск в этой теме
Поиск в этой теме:

Расширенный поиск

Ваши права в разделе
Вы не можете создавать темы
Вы не можете отвечать на сообщения
Вы не можете прикреплять файлы
Вы можете скачивать файлы

BB-коды Вкл.
[IMG] код Вкл.
HTML код Выкл.
Быстрый переход



Часовой пояс GMT +3, время: 07:48.