Четверг, 21.11.2024, 20:23


Главная
Регистрация
Вход
Welcome to Home Приветствую Вас Гость | RSS  
Меню сайта

Категории раздела
Мои статьи [25]

Мини-чат

Статистика

Онлайн всего: 3
Гостей: 3
Пользователей: 0

Главная » Статьи » Мои статьи

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

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

Оригинал этой статьи я считаю одним из лучших пособий для изучения джасса, однако с момента написания статьи прошло достаточно много времени и некоторые вещи успели измениться – ретурн баг(как и все связанные с ним функции) был убран и была добавлена возможность использования хеш-таблиц.Так как ретурн баг убрали, то и код статьи стал нерабочим и люди, читающие статью могут и не догадываться почему “злостный we” сыплет кучу сообщений об ошибках при попытке сохранения карты, а Warcraft отказывается запускать эту карту. Такие проблемы могут отпугнуть человека, решившего изучать джасс. При его изучении я и сам столкнулся с данными проблемами и далеко не сразу понял причину их возникновения, однако статья помогла мне понять основные принципы и алгоритмы при создании спелов,надеюсь читатель также почерпнёт для себя что-то полезное.Также я не остановился на обновлении статьи и добавил в статью описание работы с функцией параболы,а спел соответственно теперь отталкивает юнитов не только по земле,но и по воздуху.Также в оригинале не было сделано оглушение двигаемых юнитов,что мной конечно же рассмотрено.

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

Также я планирую не останавливаться на исправлении этой статьи и написать ей продолжения, в которых опишу создание спелов других типов.
Следующая статья будет иметь название "Работа со снарядом и его траекторией",в ней будет создан спел под названием "Топор мощи".Все спелы будут добавлены Кэрну и в конце цикла статей он будет полноценным кастомным героем с набором красивых способностей.

Данная статья рассчитывает на то, что читатель прочитал статью Сергея “Осваиваем jass” - кроме 11-14 глав, также советую ознакомиться со статьей Hashtable - работаем с хеш-таблицей.

Также рекомендую читателю установить себе Jass New Gen Pack – полезная надстройка над обычным World Editor, из которой вам потребуется подсветка синтаксиса(позволит быстро найти ошибку, пропущенную букву или опечатку в вызове функций), также при нажатии Ctrl+LeftClick на функции откроется окно с её содержимым – даст возможность раскрывать бж-функции и т.д.
Здесь находится тема с его обсуждением

 2.Типы данных

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

В 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.

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

 3.Сам спел

Итак приступим к созданию самого спела.

Создайте в редакторе объектов переменную типа хеш-таблица и назовите её Hash.
Создайте триггер с событием инициализации и добавьте в него следующую строчку через Custom Script:

set udg_Hash=InitHashtable()

Этой строкой мы инициализировали хеш-таблицу и присвоили её значение переменной Hash(приставка udg_ используется для записи переменной).
Примечание:Данное действие следует выполнять один раз в игре, повторная инициализация хеш-таблицы нежелательна, так как каждый раз будет создаваться новая и записываться в ту же переменную.
За основу спела возьмем Канал, так как применение спела “Громовая поступь” вызывает деформацию рельефа, что в свою очередь вызывает на средних и больших картах подвисание, также это упростит задачу и сократит код, да и не особо научитесь вы многому с таким подходом. Как настроить Канал можете посмотреть в прикрепленной карте-примере.

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
local unit preloader=CreateUnit(Player(0),'u000',0.,0.,0.)
 call UnitAddAbility(preloader,'A001')
 call SetUnitAbilityLevel(preloader,'A001',4)
 call KillUnit(preloader)
 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 DestroyEffect(AddSpecialEffect(("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl",0.,​0.))
set preloader=null
endfunction

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

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

Также в целях избавления лагов при первом создании дамми-кастера и при давании ему способности, которой он будет станить я добавил прелоад дамми – ‘u000’, для прелоада даваемой во время игры способности её нужно дать юниту, установить максимальный возможный для её уровень и удалить юнита, что я и сделал.В конце обнулил переменную юнита, дабы избежать утечек.

Ну что ж, теперь приступим к написание самого спелла. Все что мы сейчас сделаем, будет расположено в функции Trig_Stomp_Actions.
Вы уже должны иметь представление о локальных переменных, так что я пропущу их объяснение.

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

function Trig_Stomp_Actions takes nothing returns nothing
 local unit caster = GetTriggerUnit()
 local real x = GetUnitX(caster)
 local real y = GetUnitY(caster)
 local integer level = GetUnitAbilityLevel(caster, '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_STRUCTURE) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction

Функция должна быть оформлена в соответствии с рядом правил: она не должна брать ни один аргумент и должна возвращать boolean.

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

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

Причиной, по которой мы проверяем факт того, что юнит не является летающим (flying), вместо того чтобы проверить является ли юнит наземным (ground), являться то, что спелл должен действовать и на 'парящих' (hovering) юнитов, также проверяем является ли юнит зданием, чтобы избежать действия на здания.

Теперь добавим функцию-фильтр в код, над функцией 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_STRUCTURE) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction
function Trig_Stomp_Actions takes nothing returns nothing
local unit caster=GetTriggerUnit()
local real x = GetUnitX(caster)
local real y = GetUnitY(caster)
local integer level = GetUnitAbilityLevel(caster, 'A000')
local boolexpr b = Condition(function Stomp_Filter)
local group g = CreateGroup()
local unit dummy

В строке объявления переменной типа 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. Таким образом, мы получаем группу, состоящую из этих же юнитов, что и оригинал, которую и возвращаем.

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 caster = GetTriggerUnit()
 local real x = GetUnitX(caster)
 local real y = GetUnitY(caster)
 local integer level = GetUnitAbilityLevel(caster, '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.*level, b)
 set n = Stomp_CopyGroup(g)
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 call UnitDamageTarget(caster, f, 50.+50.*level, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
 set dummy=CreateUnit(GetOwningPlayer(caster),'u000',x,y,0.)
 call UnitApplyTimedLife(dummy,'BTLF',0.1)
 call UnitAddAbility(dummy,'A001')
 call SetUnitAbilityLevel(dummy,'A001',level)
 call IssueTargetOrder(dummy,"thunderbolt",f)
 call GroupRemoveUnit(n, f)
 endloop
...

Сначала, я создаю и уничтожаю спецэффект на позиции кастера. Когда вы уничтожаете спецэффект, проигрывается его анимация смерти.

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

Спецэффекты требуют удаления, для предотвращения утечек памяти, и так как нужная анимация все равно проиграется, я сразу же уничтожаю спецэффект.
Затем я вызываю функцию GroupEnumUnitsInRange(), чтобы собрать в группу всех юнитов в радиусе 100+50*level (где level - уровень спелла).

Затем я копирую группу в переменную 'n'.

Обратите внимание, что я переименовал функцию CopyGroup в Stomp_CopyGroup, во избежании проблем, если такая функция уже объявлена.

Вы также можете оставить прежнее имя функции, но поместить ее в секцию custom script.

Далее, я использую переменную f типа unit и native функцию FirstOfGroup() для прогона цикла по группе.

Этот участок кода делает следующее: записывает в переменную f первого юнита группы, если f пуста (равна null), цикл останавливается.

Иначе юниту наносится урон, на позиции кастера создается дамми-юнит, ему дается заранее созданный и настроенный спел на основе “Молот бурь”, этому спелу устанавливается такой же уровень, как и у примененного “Топота”, дамми-юниту даётся таймер жизни на 0.1 секунды, чтобы он успел скастовать стан на юнита f, по истечению которых дамми-юнит умирает, затем юнит f удаляется из группы (за счет этого урон и стан будет нанесен только один раз и цикл не будет бесконечным).
Я использую native функцию UnitDamageTarget для нанесения 50.+50.*level (уровень) урона. При этом используются ATTACK_TYPE_NORMAL (в GUI - Spell) как типа атаки и DAMAGE_TYPE_MAGIC как тип урона. Я использую null на месте типа оружия, чтобы спелл имел неопределенный тип оружия (тип оружия отвечает за то, какой звук проиграется при нанесении урона. В данном случае звук не нужен).
Пришло время начать работу с хеш-таблицей и таймером.

Native функции для работы с хеш-таблицей берут следующие параметры: саму хеш-таблицу,2 целочисленных “ключа”. Эти два числа дают нам возможность использовать хеш-таблицу как двумерный массив.

Добавим несколько локальных переменных:

function Trig_Stomp_Actions takes nothing returns nothing
 local unit caster = GetTriggerUnit()
 local real x = GetUnitX(caster)
 local real y = GetUnitY(caster)
 local integer level = GetUnitAbilityLevel(caster, 'A000')
 local boolexpr b = Condition(function Stomp_Filter)
 local group g = CreateGroup()
 local unit dummy
 local group n
 local unit f
 local timer t = CreateTimer()
 local integer id=GetHandleId(t)
 call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
 call GroupEnumUnitsInRange(g, x, y, 100.+50.*level, b)
 set n = Stomp_CopyGroup(g)
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 call UnitDamageTarget(caster, f, 50.+50.*level, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
 set dummy=CreateUnit(GetOwningPlayer(caster),'u000',x,y,0.)
 call UnitApplyTimedLife(dummy,'BTLF',0.1)
 call UnitAddAbility(dummy,'A001')
 call SetUnitAbilityLevel(dummy,'A001',level)
 call IssueTargetOrder(dummy,"thunderbolt",f)
 call GroupRemoveUnit(n, f)
 endloop
...

Мы добавили следующее:

t – Таймер для создания движения юнитов.

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

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

Сделаем это:

function Trig_Stomp_Actions takes nothing returns nothing
 local unit caster = GetTriggerUnit()
 local real x = GetUnitX(caster)
 local real y = GetUnitY(caster)
 local integer level = GetUnitAbilityLevel(caster, 'A000')
 local boolexpr b = Condition(function Stomp_Filter)
 local group g = CreateGroup()
 local unit dummy
 local group n
 local unit f
 local timer t = CreateTimer()
 local integer id=GetHandleId(t)
 call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
 call GroupEnumUnitsInRange(g, x, y, 100.+50.*level, b)
 set n = Stomp_CopyGroup(g)
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 call UnitDamageTarget(caster, f, 50.+50.*level, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
 set dummy=CreateUnit(GetOwningPlayer(caster),'u000',x,y,0.)
 call UnitApplyTimedLife(dummy,'BTLF',0.1)
 call UnitAddAbility(dummy,'A001')
 call SetUnitAbilityLevel(dummy,'A001',level)
 call IssueTargetOrder(dummy,"thunderbolt",f)
 call GroupRemoveUnit(n, f)
 endloop
 call SaveInteger(udg_Hash,id,0,level)
 call SaveGroupHandle(udg_Hash,id,1,g)
 call SaveReal(udg_Hash,id,2,x)
 call SaveReal(udg_Hash,id,3,y)
...

Таким образом, мы добавили четыре новые ячейки.

В нулевой ячейке сохраняем уровень спела.

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

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

Теперь давайте запустим таймер. Добавим, объявление пустой функции "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 caster = GetTriggerUnit()
 local real x = GetUnitX(caster)
 local real y = GetUnitY(caster)
 local integer level = GetUnitAbilityLevel(caster, 'A000')
 local boolexpr b = Condition(function Stomp_Filter)
 local group g = CreateGroup()
 local unit dummy
 local group n
 local unit f
 local timer t = CreateTimer()
 local integer id=GetHandleId(t)
 call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
 call GroupEnumUnitsInRange(g, x, y, 100.+50.*level, b)
 set n = Stomp_CopyGroup(g)
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 call UnitDamageTarget(caster, f, 50.+50.*level, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
 set dummy=CreateUnit(GetOwningPlayer(caster),'u000',x,y,0.)
 call UnitApplyTimedLife(dummy,'BTLF',0.1)
 call UnitAddAbility(dummy,'A001')
 call SetUnitAbilityLevel(dummy,'A001',level)
 call IssueTargetOrder(dummy,"thunderbolt",f)
 call GroupRemoveUnit(n, f)
 endloop
 call SaveInteger(udg_Hash,id,0,level)
 call SaveGroupHandle(udg_Hash,id,1,g)
 call SaveReal(udg_Hash,id,2,x)
 call SaveReal(udg_Hash,id,3,y)
...

Как вы видите, мы объявили функцию 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 caster = GetTriggerUnit()
 local real x = GetUnitX(caster)
 local real y = GetUnitY(caster)
 local integer level = GetUnitAbilityLevel(caster, 'A000')
 local boolexpr b = Condition(function Stomp_Filter)
 local group g = CreateGroup()
 local unit dummy
 local group n
 local unit f
 local timer t = CreateTimer()
 local integer id=GetHandleId(t)
 call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
 call GroupEnumUnitsInRange(g, x, y, 100.+50.*level, b)
 set n = Stomp_CopyGroup(g)
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 call UnitDamageTarget(caster, f, 50.+50.*level, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
 set dummy=CreateUnit(GetOwningPlayer(caster),'u000',x,y,0.)
 call UnitApplyTimedLife(dummy,'BTLF',0.1)
 call UnitAddAbility(dummy,'A001')
 call SetUnitAbilityLevel(dummy,'A001',level)
 call IssueTargetOrder(dummy,"thunderbolt",f)
 call GroupRemoveUnit(n, f)
 endloop
 call SaveInteger(udg_Hash,id,0,level)
 call SaveGroupHandle(udg_Hash,id,1,g)
 call SaveReal(udg_Hash,id,2,x)
 call SaveReal(udg_Hash,id,3,y)
 call TimerStart(t, 0.05, true, function Stomp_Move)
 set caster = null
 call DestroyBoolExpr(b)
 set b = null
 set g = null
 call DestroyGroup(n)
 set n = null
 set f = null
 set t = null
 set dummy=null
endfunction

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

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

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

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

function Stomp_Move takes nothing returns nothing
 local timer t=GetExpiredTimer()
 local integer id=GetHandleId(t)
 local integer level=LoadInteger(udg_Hash,id,0)
 local group g=LoadGroupHandle(udg_Hash,id,1)
 local real x = LoadReal(udg_Hash,id,2)
 local real y = LoadReal(udg_Hash,id,3)
 local group n = Stomp_CopyGroup(g)
...

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

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

function Stomp_Move takes nothing returns nothing
 local timer t=GetExpiredTimer()
 local integer id=GetHandleId(t)
 local integer level=LoadInteger(udg_Hash,id,0)
 local group g=LoadGroupHandle(udg_Hash,id,1)
 local real x = LoadReal(udg_Hash,id,2)
 local real y = LoadReal(udg_Hash,id,3)
 local group n = Stomp_CopyGroup(g)
 local real dur = LoadReal(udg_Hash,id,4)+0.05
 if dur < 0.75+0.25*level then
 else
 endif
...

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

Примечание : переменная dur не случайно имеет такое значение – как я говорил выше во время движения юнита функцией SetUnitX()\SetUnitY() его следует обездвижить, и в нашем случае это обеспечивается наложением стана на 1\1.25\1.5\1.75 секунды(зависимость будет выглядеть 0.75+0.25*level,где level – уровень способности).

Если вы загружаете НЕ сохраненное значение из хеш-таблицы, то загруженное значение всегда будет равно 0/0.0/"" или null, в зависимости от типа.
Этот спелл толкает юнитов на протяжении 0.75+0.25*level(где level – уровень способности) секунд, так что нам требуется добавить блок if/then/else.

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

function Stomp_Move takes nothing returns nothing
 local timer t=GetExpiredTimer()
 local integer id=GetHandleId(t)
 local real x = LoadReal(udg_Hash,id,0)
 local real y = LoadReal(udg_Hash,id,1)
 local integer level=LoadInteger(udg_Hash,id,2)
 local group g=LoadGroupHandle(udg_Hash,id,3)
 local real dur = LoadReal(udg_Hash,id,4)+0.05
 local group n = Stomp_CopyGroup(g)
 local real tx
 local real ty
 local real angle
 local unit f
 if dur < 0.75+0.25*level then
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 set tx = GetUnitX(f)
 set ty = GetUnitY(f)
 set angle = Atan2(ty-y, tx-x)
 if GetUnitState(f,UNIT_STATE_LIFE)>0.405 and IsTerrainPathable(tx+30.*Cos(angle),ty+30.*Sin(angle),PATHING_TYPE_FLYABILITY) == false then
 call SetUnitX(f,tx+30.*Cos(angle))
 call SetUnitY(f,ty+30.*Sin(angle))
 else
 call GroupRemoveUnit(g,f)
 endif 
 call GroupRemoveUnit(n, f)
 endloop
 call SaveReal(udg_Hash,id,4,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, но если вы используете координаты за пределами карты, игра вылетит. 
Здесь я использую функцию SetUnitX/Y для передвижения юнита на 30 единиц, всякий раз, когда таймер срабатывает. Это значит, что скорость движения будет 30/0.05 = 600.Я применяю эти функции для движения юнитов потому что они работают быстрее, однако пришлось ввести проверку на проходимость точки с помощью IsTerrainPathable, куда собираюсь двигать юнита, также минус этой функции в свободном поведении юнита компенсируется тем, что он во время движения будет оглушён и не сможет совершать каких-либо действий. Если же точка перед юнитом будет непроходимой, то удаляю его из группы, так как при следующих срабатываниях таймера точка проходимость не изменит и стоит удалить его из группы, чтобы не совершать лишних действий.

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

Ниже приведено описание функции IsTerrainPathable.

» Вырезка из “Способности Warcraft III: проводник”

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

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

function Stomp_Move takes nothing returns nothing
 local timer t=GetExpiredTimer()
 local integer id=GetHandleId(t)
 local real x = LoadReal(udg_Hash,id,0)
 local real y = LoadReal(udg_Hash,id,1)
 local integer level=LoadInteger(udg_Hash,id,2)
 local group g=LoadGroupHandle(udg_Hash,id,3)
 local real dur = LoadReal(udg_Hash,id,4)+0.05
 local group n = Stomp_CopyGroup(g)
 local real tx
 local real ty
 local real angle
 local unit f
 if dur < 0.75+0.25*level then
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 set tx = GetUnitX(f)
 set ty = GetUnitY(f)
 set angle = Atan2(ty-y, tx-x)
 if GetUnitState(f,UNIT_STATE_LIFE)>0.405 and IsTerrainPathable(tx+30.*Cos(angle),ty+30.*Sin(angle),PATHING_TYPE_FLYABILITY) == false then
 call SetUnitX(f,tx+30.*Cos(angle))
 call SetUnitY(f,ty+30.*Sin(angle))
 else
 call GroupRemoveUnit(g,f)
 endif 
 call GroupRemoveUnit(n, f)
 endloop
 call SaveReal(udg_Hash,id,4,dur)
 else
 call GroupClear(g)
 call DestroyGroup(g)
 call PauseTimer(t)
 call DestroyTimer(t)
 call FlushChildHashtable(udg_Hash,id)
 endif
...

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

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

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

function Stomp_Move takes nothing returns nothing
 local timer t=GetExpiredTimer()
 local integer id=GetHandleId(t)
 local real x = LoadReal(udg_Hash,id,0)
 local real y = LoadReal(udg_Hash,id,1)
 local integer level=LoadInteger(udg_Hash,id,2)
 local group g=LoadGroupHandle(udg_Hash,id,3)
 local real dur = LoadReal(udg_Hash,id,4)+0.05
 local group n = Stomp_CopyGroup(g)
 local real tx
 local real ty
 local real angle
 local unit f
 if dur < 0.75+0.25*level then
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 set tx = GetUnitX(f)
 set ty = GetUnitY(f)
 set angle = Atan2(ty-y, tx-x)
 if GetUnitState(f,UNIT_STATE_LIFE)>0.405 and IsTerrainPathable(tx+30.*Cos(angle),ty+30.*Sin(angle),PATHING_TYPE_FLYABILITY) == false then
 call SetUnitX(f,tx+30.*Cos(angle))
 call SetUnitY(f,ty+30.*Sin(angle))
 else
 call GroupRemoveUnit(g,f)
 endif 
 call GroupRemoveUnit(n, f)
 endloop
 call SaveReal(udg_Hash,id,4,dur)
 else
 call GroupClear(g)
 call DestroyGroup(g)
 call PauseTimer(t)
 call DestroyTimer(t)
 call FlushChildHashtable(udg_Hash,id)
 endif 
call DestroyGroup(n)
set n=null
set f=null
set g=null
set t=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_STRUCTURE) 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 timer t=GetExpiredTimer()
 local integer id=GetHandleId(t)
 local real x = LoadReal(udg_Hash,id,0)
 local real y = LoadReal(udg_Hash,id,1)
 local integer level=LoadInteger(udg_Hash,id,2)
 local group g=LoadGroupHandle(udg_Hash,id,3)
 local real dur = LoadReal(udg_Hash,id,4)+0.05
 local group n = Stomp_CopyGroup(g)
 local real tx
 local real ty
 local real angle
 local unit f
 if dur < 0.75+0.25*level then
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 set tx = GetUnitX(f)
 set ty = GetUnitY(f)
 set angle = Atan2(ty-y, tx-x)
 if GetUnitState(f,UNIT_STATE_LIFE)>0.405 and IsTerrainPathable(tx+30.*Cos(angle),ty+30.*Sin(angle),PATHING_TYPE_FLYABILITY) == false then
 call SetUnitX(f,tx+30.*Cos(angle))
 call SetUnitY(f,ty+30.*Sin(angle))
 else
 call GroupRemoveUnit(g,f)
 endif 
 call GroupRemoveUnit(n, f)
 endloop
 call SaveReal(udg_Hash,id,4,dur)
 else
 call GroupClear(g)
 call DestroyGroup(g)
 call PauseTimer(t)
 call DestroyTimer(t)
 call FlushChildHashtable(udg_Hash,id)
 endif 
call DestroyGroup(n)
set n=null
set f=null
set g=null
set t=null
endfunction


function Trig_Stomp_Actions takes nothing returns nothing
 local unit caster = GetTriggerUnit()
 local real x = GetUnitX(caster)
 local real y = GetUnitY(caster)
 local integer level = GetUnitAbilityLevel(caster, 'A000')
 local boolexpr b = Condition(function Stomp_Filter)
 local group g = CreateGroup()
 local unit dummy
 local group n
 local unit f
 local timer t = CreateTimer()
 local integer id=GetHandleId(t)
 call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
 call GroupEnumUnitsInRange(g, x, y, 100.+50.*level, b)
 set n = Stomp_CopyGroup(g)
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 call UnitDamageTarget(caster, f, 50.+50.*level, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
 set dummy=CreateUnit(GetOwningPlayer(caster),'u000',x,y,0.)
 call UnitApplyTimedLife(dummy,'BTLF',0.1)
 call UnitAddAbility(dummy,'A001')
 call SetUnitAbilityLevel(dummy,'A001',level)
 call IssueTargetOrder(dummy,"thunderbolt",f)
 call GroupRemoveUnit(n, f)
 endloop
 call SaveInteger(udg_Hash,id,0,level)
 call SaveGroupHandle(udg_Hash,id,1,g)
 call SaveReal(udg_Hash,id,2,x)
 call SaveReal(udg_Hash,id,3,y)
 call TimerStart(t, 0.05, true, function Stomp_Move)
 set caster = null
 call DestroyBoolExpr(b)
 set b = null
 set g = null
 call DestroyGroup(n)
 set n = null
 set f = null
 set t = null
 set dummy=null
endfunction

function InitTrig_Stomp takes nothing returns nothing
local unit preloader=CreateUnit(Player(0),'u000',0.,0.,0.)
 call UnitAddAbility(preloader,'A001')
 call SetUnitAbilityLevel(preloader,'A001',4)
 call KillUnit(preloader)
 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 DestroyEffect(AddSpecialEffect(("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl",0.,​0.))
set preloader=null
endfunction

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

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

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

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

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

...
 call SaveInteger(udg_Hash,id,0,level)
 call SaveGroupHandle(udg_Hash,id,1,g)
 call SaveReal(udg_Hash,id,2,x)
 call SaveReal(udg_Hash,id,3,y)
 call SaveReal(udg_Hash,id,5,30.)
 call TimerStart(t, 0.05, true, function Stomp_Move)
...

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

…
 local real speed=LoadReal(udg_Hash,id,5)
…
 if dur < 0.75+0.25*level then
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 set tx = GetUnitX(f)
 set ty = GetUnitY(f)
 set angle = Atan2(ty-y, tx-x)
 if GetUnitState(f,UNIT_STATE_LIFE)>0.405 and IsTerrainPathable(tx+speed*Cos(angle),ty+speed*Sin(angle),PATHING_TYPE_FLYABILITY) == false then
 call SetUnitX(f,tx+speed*Cos(angle))
 call SetUnitY(f,ty+speed*Sin(angle))
 else
 call GroupRemoveUnit(g,f)
 endif 
 call GroupRemoveUnit(n, f)
 endloop
 call SaveReal(udg_Hash,id,4,dur)
 call SaveReal(udg_Hash,id,5,speed-(1.2-0.2*level))
…

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

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

Примечание: значение 30. Также стоит заменить на speed и в условии проходимости, чтобы она корректно проверялась.

Мы также сохраняем новое, уменьшенное значение скорости в хеш-таблицу.
Почему я использую именно зависимость speed-(1.2-0.2*level) ,спросите вы? Объясню – так как базовая скорость у нас 30. за срабатывание таймера, при этом так как время работы таймера равно 0.75+0.25*level (1\1.25\1.5\1.75) секунды, то поделив время его работы на период 0.05 получим 20\25\30\35 срабатываний. Скорость за эти такты не должна упасть до нуля или ниже, поэтому я взял минимальную скорость, равной 10(для более-менее плавного, но не полного замедления).Чтобы за 20\25\30\35 тактов таймера скорость упала не ниже 10, я должен уменьшать скорость на 1\0.8\0.6\0.4 за такт, что выражено условием speed-(1.2-0.2*level).

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

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

Чтобы сделать эффект толчка более реалистичным, мы добавим эффект пыли.
Я собираюсь использовать для этих целей модель Impale Target Dust (Objects\Spawnmodels\Undead\ImpaleTargetDust\ImpaleTargetDust.mdl), так что давайте добавим ее подзагрузку в функции InitTrig:

function InitTrig_Stomp takes nothing returns nothing
local unit preloader=CreateUnit(Player(0),'u000',0.,0.,0.)
 call UnitAddAbility(preloader,'A001')
 call SetUnitAbilityLevel(preloader,'A001',4)
 call KillUnit(preloader)
 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("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl")
 call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDus​t.mdl",0.,0.))
 call Preload("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl")
 call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl",0.,0​.))
set preloader=null
endfunction

Теперь перейдем в функцию нанесения урона и стана. Добавим создание эффекта, прикрепленного к груди(“chest”) юнитов в цикле:

…
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 call UnitDamageTarget(caster, f, 50.+50.*level, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
 set dummy=CreateUnit(GetOwningPlayer(caster),'u000',x,y,0.)
 call UnitApplyTimedLife(dummy,'BTLF',0.1)
 call UnitAddAbility(dummy,'A001')
 call SetUnitAbilityLevel(dummy,'A001',level)
 call IssueTargetOrder(dummy,"thunderbolt",f)
 call DestroyEffect(AddSpecialEffectTarget("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTar​getDust.mdl",f,"chest"))
 call GroupRemoveUnit(n, f)
 endloop
…

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

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

Эта часть туториала, наглядно покажет вам, пример использования 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), как второй эффект поля и точку прикрепления эффекта пыли (строку chest) как третий.Поподробней о возможных точках крепления можете прочитать в статье WebStera Анимация от "А" до "Я"

Все верно, мы можем использовать поля эффектов, для хранения любых строк, которые мы можем в дальнейшем использовать в наших 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_STRUCTURE) 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 timer t=GetExpiredTimer()
 local integer id=GetHandleId(t)
 local integer level=LoadInteger(udg_Hash,id,0)
 local group g=LoadGroupHandle(udg_Hash,id,1)
 local real x = LoadReal(udg_Hash,id,2)
 local real y = LoadReal(udg_Hash,id,3)
 local real dur = LoadReal(udg_Hash,id,4)+0.05
 local real speed=LoadReal(udg_Hash,id,5)
 local real dist=LoadReal(udg_Hash,id,6)+speed
 local group n = Stomp_CopyGroup(g)
 local real tx
 local real ty
 local real angle
 local unit f
 if dur < 0.75+0.25*level then
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 set tx = GetUnitX(f)
 set ty = GetUnitY(f)
 set angle = Atan2(ty-y, tx-x)
 if GetUnitState(f,UNIT_STATE_LIFE)>0.405 and not IsTerrainPathable(tx+speed*Cos(angle),ty+speed*Sin(angle),PATHING_TYPE_FLYABILITY) then
 call SetUnitX(f,tx+speed*Cos(angle))
 call SetUnitY(f,ty+speed*Sin(angle))
 else
 call GroupRemoveUnit(g,f)
 endif
 call GroupRemoveUnit(n, f)
 endloop
 call SaveReal(udg_Hash,id,4,dur)
 call SaveReal(udg_Hash,id,5,speed-(1.2-0.2*level))
 call SaveReal(udg_Hash,id,6,dist)
 else
 call GroupClear(g)
 call DestroyGroup(g)
 call PauseTimer(t)
 call DestroyTimer(t)
 call FlushChildHashtable(udg_Hash,id)
 endif
call DestroyGroup(n)
set n=null
set f=null
set t=null
set g=null
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
 local unit caster = GetTriggerUnit()
 local real x = GetUnitX(caster)
 local real y = GetUnitY(caster)
 local integer level = GetUnitAbilityLevel(caster, 'A000')
 local boolexpr b = Condition(function Stomp_Filter)
 local group g = CreateGroup()
 local unit dummy
 local group n
 local unit f
 local timer t = CreateTimer()
 local integer id=GetHandleId(t)
 call DestroyEffect(AddSpecialEffect GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 0), x, y))
 call GroupEnumUnitsInRange(g, x, y, 100.+50.*level, b)
 set n = Stomp_CopyGroup(g)
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 call UnitDamageTarget(caster, f, 50.+50.*level, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
 set dummy=CreateUnit(GetOwningPlayer(caster),'u000',x,y,0.)
 call UnitApplyTimedLife(dummy,'BTLF',0.1)
 call UnitAddAbility(dummy,'A001')
 call SetUnitAbilityLevel(dummy,'A001',level)
 call IssueTargetOrder(dummy,"thunderbolt",f)
 call DestroyEffect(AddSpecialEffectTarget GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 1),f, GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 2))
 call GroupRemoveUnit(n, f)
 endloop
 call SaveInteger(udg_Hash,id,0,level)
 call SaveGroupHandle(udg_Hash,id,1,g)
 call SaveReal(udg_Hash,id,2,x)
 call SaveReal(udg_Hash,id,3,y)
 call SaveReal(udg_Hash,id,5,30.)
 call TimerStart(t, 0.05, true, function Stomp_Move)
 set caster = null
 call DestroyBoolExpr(b)
 set b = null
 set g = null
 call DestroyGroup(n)
 set n = null
 set f = null
 set t = null
 set dummy=null
endfunction

function InitTrig_Stomp takes nothing returns nothing
local unit preloader=CreateUnit(Player(0),'u000',0.,0.,0.)
 call UnitAddAbility(preloader,'A001')
 call SetUnitAbilityLevel(preloader,'A001',4)
 call KillUnit(preloader)
 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, 1))
 call DestroyEffect(AddSpecialEffect GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 1),0.,0.))
 call Preload(GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 0))
 call DestroyEffect(AddSpecialEffect GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 0),0.,0.))
set preloader=null
endfunction

 5.4.Ещё более реалистичное движение

Думаю простое отбрасывание по земле хоть и смотрится хорошо, однако если юниты будут отлетать ещё и по воздуху, то это будет вдвойне хорошо и красивее. Юниты у нас будут отлетать по параболической траектории, значит добавим в код функцию определения высоты по параболе:

…
function Trig_Stomp_Conditions takes nothing returns boolean
 return GetSpellAbilityId() == 'A000'
endfunction

function Parabola takes real h, real d, real x returns real
 return (4 * h / d) * (d - x) * (x / d)
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_STRUCTURE) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction
…

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

Итак разберемся как использовать эту функцию:

h – первый передаваемый параметр типа real, этим параметром мы должны передавать максимальную высоту параболы.

d – полное расстояние, на которое раскидывается парабола(ниже приведу поясняющий рисунок).

x – расстояние, которое было пройдено от начала параболы.

Рисунок

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

…
 local real dur = LoadReal(udg_Hash,id,4)+0.05
 local real speed=LoadReal(udg_Hash,id,5)
 local real dist=LoadReal(udg_Hash,id,6)+speed
…
 call SaveReal(udg_Hash,id,4,dur)
 call SaveReal(udg_Hash,id,5,speed-(1.2-0.2*level))
 call SaveReal(udg_Hash,id,6,dist)
…

С помощью этих строк мы постоянно прибавляем к переменной dist пройденное расстояние со скоростью speed(постоянно изменяющейся) и сохраняем в хеш-таблицу.

Таким образом использовать функцию параболы мы научились, но что с изменением высоты юнита? Так как наши юниты не являются летающими, то простое измением их высоты функцией SetUnitFlyHeight() не даст никакого результата, однако есть выход – добавление\удаление юниту способности “Превратиться в ворона”(или аналогичной по действию), после которого мы можем спокойно изменять высоту как вздумается.

Приступим к добавлению изменения высоты юнитов в функцию движения:

…
 loop
 set f = FirstOfGroup(n)
 exitwhen f == null
 set tx = GetUnitX(f)
 set ty = GetUnitY(f)
 set angle = Atan2(ty-y, tx-x)
 if GetUnitState(f,UNIT_STATE_LIFE)>0.405 and not IsTerrainPathable(tx+speed*Cos(angle),ty+speed*Sin(angle),PATHING_TYPE_FLYABILITY) then
 call SetUnitX(f,tx+speed*Cos(angle))
 call SetUnitY(f,ty+speed*Sin(angle))
 call UnitAddAbility(f, 'Amrf')
 call UnitRemoveAbility(f, 'Amrf')
 call SetUnitFlyHeight(f,Parabola(175.,20.*((0.75+0.25*level)/0.05),dist),0.)
 else
 call UnitAddAbility(f, 'Amrf')
 call UnitRemoveAbility(f, 'Amrf')
 call SetUnitFlyHeight(f,0.,0.)
 call GroupRemoveUnit(g,f)
 endif
 call GroupRemoveUnit(n, f)
 endloop
…
Категория: Мои статьи | Добавил: Enemy1PK (02.04.2017)
Просмотров: 378 | Рейтинг: 0.0/0
Всего комментариев: 0
avatar
Вход на сайт

Поиск

Друзья сайта
  • Официальный блог
  • Сообщество uCoz
  • FAQ по системе
  • База знаний uCoz

  • Copyright MyCorp © 2024uCoz