- 25 Сен 2015
- 2,348
- 41
Предисловие
В процессе разработки аддонов иногда приходится реализовывать стандартные умения героев с нуля вручную, чтобы изменить их функционал под себя. И последний раз когда я это делал, меня посетила мысль - выкладывать подобные реализации с разъяснением кода на форуме, дабы дать новичкам пищу для пережевывания, а старичкам, возможно, заготовки на будущее.
Для кого эта статья
Целевой аудиторией являются новички в моддинге, которые хотят освоить скриптинг на языке lua под workshop. Реализация стандартных абилок с нуля - это превосходный способ освоить механику создания своих собственных умений в workshope.
Что мы будем делать
В этой статье мы реализуем ульт акса. Он может немного отличаться от оригинала, но эти отличия вполне можно назвать мелочами, которыми мы позволим себе пренебречь. Кроме того, я пренебрегаю различными тонкостями, что могут влиять на отработку ульта в тех или иных ситуациях, поскольку целью статьи является не создание точной копии умения, а демонстрация процесса создания умения, его прототипа. Естественно, кто-то может найти возможные ошибки или более простые способы решения тех или иных проблем - все это можно изложить в комментариях к посту.
Culling Blade
Итак, что же мы видим, когда используем аксом его ульт:
"ну, он подпрыгивает и бьет наотмашь своим топором противника, лопая его тушку и разбрызгивая на всех вокруг его кишки и кровь"
Это описание вида со стороны, но если углубиться, то имеем следующее:
Применяем на цель, которая должна быть героем. И в случае, если у этой цели здоровья меньше определенного значения, то мы её убиваем и вешаем на ближайших союзников и себя баф скорости передвижения и атаки. Если же здоровье врага больше определенной границы, то наносим незначительный урон и отправляем ульт на перезарядку.
А теперь рассмотрим это описание с точки зрения абилки workshopa.
Здесь мы имеем два варианта, когда все плохо:
1. Применение на цель (target), которая является героем (unit hero)
2. Проверка здоровья цели (попадает ли под порог убийства)
3. Нанесение урона цели
4. Проигрывание анимации
И когда все хорошо:
1. Применение на цель (target), которая является героем (unit hero)
2. Проверка здоровья цели (попадает ли под порог убийства)
3. Убийство цели
4. Поиск союзников в определенном радиусе от нашего героя
5. Наложение бафа (модификатора) на себя и союзников, попавших в радиус.
6. Проигрывание анимации
Естественно шагов можно еще больше накрутить, но мы придерживаемся политики "прозрачности" повествования и лени.
Что мы должны вынести из этого анализа? То, что мы имеем два объекта - абилку и модификатор (баф). Модификатор я реализовывать в ручную не стал, так как нашел здесь подходящий мне modifier_axe_culling_blade_boost. Поэтому сразу переходим к реализации абилки:
Первое, что нам надо, это подготовить скрипт-файл, в котором будем описывать механику работы абилки. Обычно создают подобные файлы по адресу:
...\Steam\SteamApps\common\dota 2 beta\game\dota_addons\"имя аддона"\scripts\vscripts\abilities
Можете и не создавать папку abilities или создать папку с иным именем, это не важно, главное, чтобы лежало все в папке vscripts. Создаем пустой файл с именем axe_culling_blade_custom.lua (ну, как все делают - создаешь txt файл, а там меняешь его формат с txt на lua обычным переименованием) .
Далее заходим в папку
...\Steam\SteamApps\common\dota 2 beta\game\dota_addons\"имя аддона"\scripts\npc\
и открываем файл npc_abilities_custom.txt.
В нем создаем обложку абилки. И, так как я не хотел придумывать чего-то нового (параметры), то просто копируем оригинал и меняем его под себя:
Что мы здесь указали?
BaseClass - каков наш объект, тип реализации умения, в нашем случае это реализация через lua скрипт - ability_lua
ScriptFile - путь к самому скрипту, то есть - abilities/axe_culling_blade_custom.lua
AbilityType - тип абилки указываем ультой (хотя это все можно прописать в скрипте) - DOTA_ABILITY_TYPE_ULTIMATE
AbilityBehavior - указываем, что у нас абилка будет кастоваться на цель - DOTA_ABILITY_BEHAVIOR_UNIT_TARGET
AbilityUnitTargetTeam - цель эта является врагом - DOTA_UNIT_TARGET_TEAM_ENEMY
AbilityUnitTargetType - и цель эта должна быть героем - DOTA_UNIT_TARGET_HERO
Остальное все расписывать не буду, все это можно найти на форуме.
Теперь перейдем в файл скрипта axe_culling_blade_custom.lua:
Ну и давайте по кусочкам пережевывать:
- говорим, что переменная с именем axe_culling_blade_custom теперь является объектом типа класс class({}). Без этого никуда, это самая важная строчка, сердце скрипта и нашей абилки.
Функция GetCastAnimation() - к ней обращается компилятор, когда работает с анимацией каста абилки.
В теле функции мы просто говорим, чтобы на любые запросы к ней, она "возвращала", отвечала, глобальной переменной ACT_DOTA_CAST_ABILITY_4, что можно подсмотреть здесь. Под этой циферкой (переменной) забита анимация прыжка акса с размашистым ударом топором.
Далее мы так же как и в случае с анимацией, переопределяем метод GetBehavior():
Иными словами, говорим, чтобы эта функция на любые расспросы компилятором нашего файла о типе абилки возвращала глобалку DOTA_ABILITY_BEHAVIOR_UNIT_TARGET.
Теперь механизм, логика поведения нашей абилки, переопределяем метод OnSpellStar(), но сначала пара уточнений:
self - текущий класс, т.е. наша абилка
local - тип переменной, что мы определяем (то есть локальный тип переменной, существующей в пределах функции/ блока)
Теперь рассмотрим самодельные функции, что я создавал для большей прозрачности кода:
Теперь вспоминаем о многоуважаемом Рубике и переопределяем два метода, существующих только из-за его ульты:
//хотим ли мы, чтобы наш ульт можно было спиздить? Ну, я не вижу проблем в его отработке на другом герое - таймерами мы никакими не пользовались, так что ульт независим. Пускай его пиздят
//хрен знает, что это за функция - скрыт, когда украден - я не вникал, но если её не переопределить, то могут быть ошибки в случае, когда в функции выше стоит true
Поздравляю, вы превосходны, абилка готова.
Послесловие
Устал. Вспомнил, почему не люблю подобные статьи писать. Думал более дотошно все рассказать, с картинками, но получилось как есть. Вполне приемлемо и некоторые моменты для новичков я уточню здесь:
Много, много мелочей и тонкостей, о которых я не упоминал и не реализовывал в коде, но, как уже говорилось, мы преследуем цель "прозрачности" и "рабочего прототипа", а не "идеального повторения".
Все "переопределяемые" функции можно найти здесь в блоке CDOTABaseAbility. В смысле, те, что можно и иногда нужно переопределять, когда создаете свои абилки.
Все константы, т.е. анимация атаки, типы юнитов, что используются в функциях, сами функци и все, что я не объяснял так же находится там и имеет свое (фиговое, но со временем понятное) описание.
Звуки, где взять их названия? Распаковываете папку soundevents из архива фалов дотки (на форуме поищите инфу, как это делается, ключевое слово GCFScape). И далее в папочке game_sounds_heroes открываете файл нужного вам героя обычным текстовым редактором и смотрите. А в папочке sounds, что лежит там же, где и soundevents, можно и все остальные звуки найти, не только героев.
Партикли, анимация, где её искать? Ну, запускаете в workshope утилиту Particle Manager, или в hammere во вкладке particle. На понравившемся клацаете правой и читаете Assert Info - и вот вам путь до партикля. Так же советую распаковать себе папочку particles из архива дотовксих файлов, чтобы там спокойно смотреть путь к той или иной папке партиклей (для подгрузки).
Советую в дальнейшем самостоятельно реализовывать абилки в такой последовательности
* пассивки - самые простые в реализации умения
* направленные target (как наш ульт акса)
* ненаправленные no target (ну там всякие включаемые или, например, агр у акса)
* направленные point (ну, на точку)
* направленные AOE (по сути те же, что и предыдущие, но с областью)
Естественно это мой взгляд и наверняка я какие-нибудь забыл, но к тому времени как освоите хотя бы вплоть до "no target", уже будете "шарить".
Дополнять/преобразовывать статью я вряд ли буду (скорее что-то новое выложу), но все может быть.
Если хотите почитать похожий от меня материал, то вбивайте в поиск по форуму "кодим функции" и читайте.
В процессе разработки аддонов иногда приходится реализовывать стандартные умения героев с нуля вручную, чтобы изменить их функционал под себя. И последний раз когда я это делал, меня посетила мысль - выкладывать подобные реализации с разъяснением кода на форуме, дабы дать новичкам пищу для пережевывания, а старичкам, возможно, заготовки на будущее.
Для кого эта статья
Целевой аудиторией являются новички в моддинге, которые хотят освоить скриптинг на языке lua под workshop. Реализация стандартных абилок с нуля - это превосходный способ освоить механику создания своих собственных умений в workshope.
Что мы будем делать
В этой статье мы реализуем ульт акса. Он может немного отличаться от оригинала, но эти отличия вполне можно назвать мелочами, которыми мы позволим себе пренебречь. Кроме того, я пренебрегаю различными тонкостями, что могут влиять на отработку ульта в тех или иных ситуациях, поскольку целью статьи является не создание точной копии умения, а демонстрация процесса создания умения, его прототипа. Естественно, кто-то может найти возможные ошибки или более простые способы решения тех или иных проблем - все это можно изложить в комментариях к посту.
Culling Blade
Итак, что же мы видим, когда используем аксом его ульт:
"ну, он подпрыгивает и бьет наотмашь своим топором противника, лопая его тушку и разбрызгивая на всех вокруг его кишки и кровь"
Это описание вида со стороны, но если углубиться, то имеем следующее:
Применяем на цель, которая должна быть героем. И в случае, если у этой цели здоровья меньше определенного значения, то мы её убиваем и вешаем на ближайших союзников и себя баф скорости передвижения и атаки. Если же здоровье врага больше определенной границы, то наносим незначительный урон и отправляем ульт на перезарядку.
А теперь рассмотрим это описание с точки зрения абилки workshopa.
Здесь мы имеем два варианта, когда все плохо:
1. Применение на цель (target), которая является героем (unit hero)
2. Проверка здоровья цели (попадает ли под порог убийства)
3. Нанесение урона цели
4. Проигрывание анимации
И когда все хорошо:
1. Применение на цель (target), которая является героем (unit hero)
2. Проверка здоровья цели (попадает ли под порог убийства)
3. Убийство цели
4. Поиск союзников в определенном радиусе от нашего героя
5. Наложение бафа (модификатора) на себя и союзников, попавших в радиус.
6. Проигрывание анимации
Естественно шагов можно еще больше накрутить, но мы придерживаемся политики "прозрачности" повествования и лени.
Что мы должны вынести из этого анализа? То, что мы имеем два объекта - абилку и модификатор (баф). Модификатор я реализовывать в ручную не стал, так как нашел здесь подходящий мне modifier_axe_culling_blade_boost. Поэтому сразу переходим к реализации абилки:
Первое, что нам надо, это подготовить скрипт-файл, в котором будем описывать механику работы абилки. Обычно создают подобные файлы по адресу:
...\Steam\SteamApps\common\dota 2 beta\game\dota_addons\"имя аддона"\scripts\vscripts\abilities
Можете и не создавать папку abilities или создать папку с иным именем, это не важно, главное, чтобы лежало все в папке vscripts. Создаем пустой файл с именем axe_culling_blade_custom.lua (ну, как все делают - создаешь txt файл, а там меняешь его формат с txt на lua обычным переименованием) .
Далее заходим в папку
...\Steam\SteamApps\common\dota 2 beta\game\dota_addons\"имя аддона"\scripts\npc\
и открываем файл npc_abilities_custom.txt.
В нем создаем обложку абилки. И, так как я не хотел придумывать чего-то нового (параметры), то просто копируем оригинал и меняем его под себя:
Код:
"axe_culling_blade_custom"
{
// General
//--------------------------------------------------------------------------------------------------------
"BaseClass" "ability_lua"
"AbilityTextureName" "axe_culling_blade"
"ScriptFile" "abilities/axe_culling_blade_custom.lua"
"AbilityType" "DOTA_ABILITY_TYPE_ULTIMATE
"AbilityBehavior" "DOTA_ABILITY_BEHAVIOR_UNIT_TARGET"
"SpellImmunityType" "SPELL_IMMUNITY_ENEMIES_NO"
"AbilityUnitTargetTeam" "DOTA_UNIT_TARGET_TEAM_ENEMY"
"AbilityUnitTargetType" "DOTA_UNIT_TARGET_HERO"
"AbilityUnitTargetFlags" "DOTA_UNIT_TARGET_FLAG_MAGIC_IMMUNE_ENEMIES"
"SpellImmunityType" "SPELL_IMMUNITY_ENEMIES_YES"
"AbilityUnitDamageType" "DAMAGE_TYPE_MAGICAL"
"FightRecapLevel" "2"
// Casting
//--------------------------------------------------------------------------------------------------------
"AbilityCastRange" "150"
"AbilityCastPoint" "0.3"
// Time
//--------------------------------------------------------------------------------------------------------
"AbilityCooldown" "15 10 5"
// Cost
//--------------------------------------------------------------------------------------------------------
"AbilityManaCost" "10"
"AbilitySpecial"
{
"01"
{
"var_type" "FIELD_INTEGER"
"kill_threshold" "250 325 400"
}
"02"
{
"var_type" "FIELD_INTEGER"
"damage" "150 250 300"
}
"03"
{
"var_type" "FIELD_FLOAT"
"speed_duration" "3"
}
"04"
{
"var_type" "FIELD_INTEGER"
"speed_aoe" "900"
}
"05"
{
"var_type" "FIELD_INTEGER"
"kill_threshold_scepter" "300 500 700"
}
"06"
{
"var_type" "FIELD_FLOAT"
"speed_duration_scepter" "10"
}
}
}
Что мы здесь указали?
BaseClass - каков наш объект, тип реализации умения, в нашем случае это реализация через lua скрипт - ability_lua
ScriptFile - путь к самому скрипту, то есть - abilities/axe_culling_blade_custom.lua
AbilityType - тип абилки указываем ультой (хотя это все можно прописать в скрипте) - DOTA_ABILITY_TYPE_ULTIMATE
AbilityBehavior - указываем, что у нас абилка будет кастоваться на цель - DOTA_ABILITY_BEHAVIOR_UNIT_TARGET
AbilityUnitTargetTeam - цель эта является врагом - DOTA_UNIT_TARGET_TEAM_ENEMY
AbilityUnitTargetType - и цель эта должна быть героем - DOTA_UNIT_TARGET_HERO
Остальное все расписывать не буду, все это можно найти на форуме.
Теперь перейдем в файл скрипта axe_culling_blade_custom.lua:
Код:
axe_culling_blade_custom = class({})
function axe_culling_blade_custom:GetCastAnimation()
return ACT_DOTA_CAST_ABILITY_4
end
function axe_culling_blade_custom:GetBehavior()
return DOTA_ABILITY_BEHAVIOR_UNIT_TARGET
end
function axe_culling_blade_custom:OnSpellStart()
local caster = self:GetCaster()
local target = self:GetCursorTarget()
local killThreshold = self:GetSpecialValueFor("kill_threshold_scepter")
local speedDuration = self:GetSpecialValueFor("speed_duration")
if caster:HasItemInInventory("item_ultimate_scepter") then
killThreshold = self:GetSpecialValueFor("kill_threshold_scepter")
speedDuration = self:GetSpecialValueFor("speed_duration_scepter")
end
if target:GetHealth() <= killThreshold then
EmitSoundOn("Hero_Axe.Culling_Blade_Success", self:GetCaster())
self:KillUnit(target)
self:GiveBaff(caster,speedDuration)
else
EmitSoundOn("Hero_Axe.Culling_Blade_Fail", self:GetCaster())
ApplyDamage( { victim = target, attacker = caster, damage = self:GetSpecialValueFor("damage"),
damage_type = DAMAGE_TYPE_PURE, ability = self} )
end
end
function axe_culling_blade_custom:KillUnit(target)
target:ForceKill(false)
ParticleManager:CreateParticle("particles/units/heroes/hero_life_stealer/life_stealer_infest_emerge_bloody.vpcf", PATTACH_ABSORIGIN, target)
end
function axe_culling_blade_custom:GiveBaff(caster,speedDuration)
local units = FindUnitsInRadius( caster:GetTeamNumber(), caster:GetAbsOrigin(), caster, self:GetSpecialValueFor("speed_aoe") ,
DOTA_UNIT_TARGET_TEAM_FRIENDLY, DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_FLAG_MAGIC_IMMUNE_ENEMIES, 0, false )
for i = 1, #units do
if units[ i ] then
units[ i ]:AddNewModifier(caster, self, "modifier_axe_culling_blade_boost", { duration = speedDuration})
ParticleManager:CreateParticle("particles/units/heroes/hero_ogre_magi/ogre_magi_bloodlust_buff_symbol.vpcf", PATTACH_ABSORIGIN, units[ i ])
end
end
end
function axe_culling_blade_custom:IsStealable()
return true
end
function axe_culling_blade_custom:IsHiddenWhenStolen()
return true
end
Ну и давайте по кусочкам пережевывать:
Код:
axe_culling_blade_custom = class({})
Функция GetCastAnimation() - к ней обращается компилятор, когда работает с анимацией каста абилки.
Код:
function axe_culling_blade_custom:GetCastAnimation()
return ACT_DOTA_CAST_ABILITY_4
end
В теле функции мы просто говорим, чтобы на любые запросы к ней, она "возвращала", отвечала, глобальной переменной ACT_DOTA_CAST_ABILITY_4, что можно подсмотреть здесь. Под этой циферкой (переменной) забита анимация прыжка акса с размашистым ударом топором.
Далее мы так же как и в случае с анимацией, переопределяем метод GetBehavior():
Код:
function axe_culling_blade_custom:GetBehavior()
return DOTA_ABILITY_BEHAVIOR_UNIT_TARGET
end
Иными словами, говорим, чтобы эта функция на любые расспросы компилятором нашего файла о типе абилки возвращала глобалку DOTA_ABILITY_BEHAVIOR_UNIT_TARGET.
Теперь механизм, логика поведения нашей абилки, переопределяем метод OnSpellStar(), но сначала пара уточнений:
self - текущий класс, т.е. наша абилка
local - тип переменной, что мы определяем (то есть локальный тип переменной, существующей в пределах функции/ блока)
Код:
function axe_culling_blade_custom:OnSpellStart()
local caster = self:GetCaster() // заводим под кастера (тот, кто активирует абилку) переменную caster
local target = self:GetCursorTarget() //заводим под цель (кого атакуем) переменную target
local killThreshold = self:GetSpecialValueFor("kill_threshold_scepter") //считываем порог убийства из special переменных нашей абилки в npc_abilities_custom.txt и заносим в переменную killThreshold
local speedDuration = self:GetSpecialValueFor("speed_duration")//считываем продолжительность бафа из special переменных и заносим значение в переменную speedDuration
//здесь проверяем наличие в инвентаре игрока аганима, чтобы усилить те или иные параметры
if caster:HasItemInInventory("item_ultimate_scepter") then
killThreshold = self:GetSpecialValueFor("kill_threshold_scepter")
speedDuration = self:GetSpecialValueFor("speed_duration_scepter")
end
//теперь проверка здоровья цели
if target:GetHealth() <= killThreshold then //вариант, когда все клево
EmitSoundOn("Hero_Axe.Culling_Blade_Success", self:GetCaster()) //отыгрываем звук успеха
self:KillUnit(target) //самодельная функция убийства, реализация будет ниже
self:GiveBaff(caster,speedDuration) //даем баф (самодельная функция, её реализация будет ниже)
else //вариант, когда все плохо
EmitSoundOn("Hero_Axe.Culling_Blade_Fail", self:GetCaster()) //отыгрываем унылый звук
ApplyDamage( { victim = target, attacker = caster, damage = self:GetSpecialValueFor("damage"),
damage_type = DAMAGE_TYPE_MAGICAL, ability = self} ) //наносим урон цели, магический
end
end
Теперь рассмотрим самодельные функции, что я создавал для большей прозрачности кода:
Код:
function axe_culling_blade_custom:KillUnit(target) //в функцию передается цель, target, с ней и работаем
target:ForceKill(false) //принудительно убиваем цель
ParticleManager:CreateParticle("particles/units/heroes/hero_life_stealer/life_stealer_infest_emerge_bloody.vpcf", PATTACH_ABSORIGIN, target) //отрисовываем кровищу (оригинальный партикль ульты акса у меня не работал, взял гули)
end
Код:
function axe_culling_blade_custom:GiveBaff(caster,speedDuration) //в функцию передается кастующий и время бафа
//ищем героев союзных в радиусе speed_aoe, что в txt файле в специалках указывали
local units = FindUnitsInRadius( caster:GetTeamNumber(), caster:GetAbsOrigin(), caster, self:GetSpecialValueFor("speed_aoe") ,
DOTA_UNIT_TARGET_TEAM_FRIENDLY, DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_FLAG_MAGIC_IMMUNE_ENEMIES, 0, false )
for i = 1, #units do //цикл от 1 до количества найденных юнитов
if units[ i ] then //если юнит существует, т.е. реальный объект, а не nil
//вешаем модификатор
units[ i ]:AddNewModifier(caster, self, "modifier_axe_culling_blade_boost", { duration = speedDuration})
//отрисовываем какую-нибудь анимацию
ParticleManager:CreateParticle("particles/units/heroes/hero_ogre_magi/ogre_magi_bloodlust_buff_symbol.vpcf", PATTACH_ABSORIGIN, units[ i ])
end
end
end
Теперь вспоминаем о многоуважаемом Рубике и переопределяем два метода, существующих только из-за его ульты:
//хотим ли мы, чтобы наш ульт можно было спиздить? Ну, я не вижу проблем в его отработке на другом герое - таймерами мы никакими не пользовались, так что ульт независим. Пускай его пиздят
Код:
function axe_culling_blade_custom:IsStealable()
return true
end
//хрен знает, что это за функция - скрыт, когда украден - я не вникал, но если её не переопределить, то могут быть ошибки в случае, когда в функции выше стоит true
Код:
function axe_culling_blade_custom:IsHiddenWhenStolen()
return true
end
Поздравляю, вы превосходны, абилка готова.
Послесловие
Устал. Вспомнил, почему не люблю подобные статьи писать. Думал более дотошно все рассказать, с картинками, но получилось как есть. Вполне приемлемо и некоторые моменты для новичков я уточню здесь:
Много, много мелочей и тонкостей, о которых я не упоминал и не реализовывал в коде, но, как уже говорилось, мы преследуем цель "прозрачности" и "рабочего прототипа", а не "идеального повторения".
Все "переопределяемые" функции можно найти здесь в блоке CDOTABaseAbility. В смысле, те, что можно и иногда нужно переопределять, когда создаете свои абилки.
Все константы, т.е. анимация атаки, типы юнитов, что используются в функциях, сами функци и все, что я не объяснял так же находится там и имеет свое (фиговое, но со временем понятное) описание.
Звуки, где взять их названия? Распаковываете папку soundevents из архива фалов дотки (на форуме поищите инфу, как это делается, ключевое слово GCFScape). И далее в папочке game_sounds_heroes открываете файл нужного вам героя обычным текстовым редактором и смотрите. А в папочке sounds, что лежит там же, где и soundevents, можно и все остальные звуки найти, не только героев.
Партикли, анимация, где её искать? Ну, запускаете в workshope утилиту Particle Manager, или в hammere во вкладке particle. На понравившемся клацаете правой и читаете Assert Info - и вот вам путь до партикля. Так же советую распаковать себе папочку particles из архива дотовксих файлов, чтобы там спокойно смотреть путь к той или иной папке партиклей (для подгрузки).
Советую в дальнейшем самостоятельно реализовывать абилки в такой последовательности
* пассивки - самые простые в реализации умения
* направленные target (как наш ульт акса)
* ненаправленные no target (ну там всякие включаемые или, например, агр у акса)
* направленные point (ну, на точку)
* направленные AOE (по сути те же, что и предыдущие, но с областью)
Естественно это мой взгляд и наверняка я какие-нибудь забыл, но к тому времени как освоите хотя бы вплоть до "no target", уже будете "шарить".
Дополнять/преобразовывать статью я вряд ли буду (скорее что-то новое выложу), но все может быть.
Если хотите почитать похожий от меня материал, то вбивайте в поиск по форуму "кодим функции" и читайте.
Последнее редактирование модератором: