Добавлен nazarpunk,
опубликован
Раздел:
Триггеры и объекты
Идея
Создать способность, которая запустит снаряд из точки А в точку B.
Подготовка
Для начала нам понадобится последняя версия WarCraft III, умение читать справочник и пользоваться английской раскладкой клавиатуры.
Писать код в стандартном редакторе то ещё удовольствие, потому настраиваем себе подсветку во внешнем.
По умолчанию карты компилируются в jass, переключим их в режим lua.
Как и всякие культурные картоделы, способность будем делать на основе ANcl Канал, настройки которого прекрасно описаны здесь.
Осталось только подготовить карту для тестирования и можно приступать к написанию кода.
Двигаем снаряд
Сперва наперво, удаляем всё из стандартных триггеров и создаём блок кода, в котором и будем работать.
Наш код не должен конфликтовать с другим кодом в карте и запускаться после инициализации карты, поэтому применим следущую конструкцию.
do -- создаём область видимости, чтоб не конфликтовать с другим кодом
local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
function InitGlobals()
InitGlobalsOrigin()
-- в этом моменте прошла инициализация карты и можно смело работать
end
end
Более подробно можете прочитать здесь.
Наконец-то настала пора погладить манула открыть справочник и почитать в нём про переменные и циклы. Вооружившись этой бесценной информацией создадим триггер и для каждого игрока добавим событие для отлова каста.
local SpellEffectTrigger = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
end
Подробней про события
Есть пять событий, которые срабатывают при касте в таком порядке:
- EVENT_PLAYER_UNIT_SPELL_CHANNEL
- EVENT_PLAYER_UNIT_SPELL_CAST
- EVENT_PLAYER_UNIT_SPELL_EFFECT
- EVENT_PLAYER_UNIT_SPELL_ENDCAST
- EVENT_PLAYER_UNIT_SPELL_FINISH
Порядок срабатывания можно проверить нехитрым способом
local SpellTrigger = CreateTrigger()
TriggerRegisterPlayerUnitEvent(SpellTrigger, GetLocalPlayer(), EVENT_PLAYER_UNIT_SPELL_EFFECT)
TriggerAddAction(SpellTrigger, function()
local ID = GetHandleId(GetTriggerEventId())
print('-----------------------')
print('channel', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_CHANNEL))
print('cast', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_CAST))
print('effect', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_EFFECT))
print('endcast', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_ENDCAST))
print('finish', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_FINISH))
end)
PT153
EFFECT означает фактический старт каста способности, когда cast time способности и cast point юнита прошли. Во время этого события игрок уже может юнита контролировать, до этого - нет. Также если до этого события сбить каст, то юнит заново начнёт кастовать, после и во время этого события - нет. ENDCAST не может сработать раньше EFFECT, либо во время, либо после.
Вновь открыв справочник на странице с анонимными функциями добавим триггеру действие и сделаем так, чтоб он срабатывал только при касте нашего заклинания.
TriggerAddAction(SpellEffectTrigger, function()
if GetSpellAbilityId() ~= FourCC('Amis') then return end
end)
Почему я не использую условие
Большое количество триггеров перегружает карту и для каста я использую всего один триггер подобным образом:
if GetSpellAbilityId() == FourCC('A001') then
-- spell 1
elseif GetSpellAbilityId() == FourCC('A002') then
-- spell 2
elseif GetSpellAbilityId() == FourCC('A003') then
-- spell 3
end
Притом условие является лишней сущностью от которых один старик с бритвой просил избавляться.
Осознав тот факт, что точки у нас фиксированные и почитав про полярную систему координат и такую единицу измерения угла как радиан объявим нужные для работы переменные.
local caster = GetTriggerUnit() -- юнит, применивший способность
local xa, ya = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
local xb, yb = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
local angle = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
local distance= math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
После таких непосильных трудов, можно наконец-то создать эффект и полюбоваться на плоды делов своих.
do
-- создаём область видимости, чтоб не конфликтовать с другим кодом
local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
function InitGlobals()
InitGlobalsOrigin()
-- в этом моменте прошла инициализация карты и можно смело работать
local SpellEffectTrigger = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
end
TriggerAddAction(SpellEffectTrigger, function()
if GetSpellAbilityId() ~= FourCC('Amis') then return end
local caster = GetTriggerUnit() -- юнит, применивший способность
local xa, ya = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
local xb, yb = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
local angle = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
local distance = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
local missile = AddSpecialEffect('Abilities/Weapons/Mortar/MortarMissile.mdl', xa, ya) -- создаём эффект снаряда
BlzSetSpecialEffectYaw(missile, angle) -- поворачиваем эффект на нужный угол
end)
end
end
Всё прекрасно работает, но как вы догадались, что-бы снаряд двигался - его нужно двигать. Но для начала объявим ещё больше переменных и вынесем их повыше, для удобной настройки.
local ABILITY_ID = FourCC('Amis')
local MISSILE_EFFECT = 'Abilities/Weapons/Mortar/MortarMissile.mdl'
local TIMER_PERIOD = 0.03125 -- Период срабатывания таймера: 1/32 секунды
local SPEED = 1200 -- Расстояние, которое снаряд преодолеет за секунду
local SPEED_INC = SPEED * TIMER_PERIOD -- расстояние, которое снаряд пройдёт на каждый тик таймера
Когда все данные есть на руках, наконец-то можно приступать к движению снаряда используя старые добрые таймеры.
TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
BlzSetSpecialEffectX(missile, BlzGetLocalSpecialEffectX(missile) + cos * SPEED_INC)
BlzSetSpecialEffectY(missile, BlzGetLocalSpecialEffectY(missile) + sin * SPEED_INC)
end)
Можно себя поздравить с первыми успехами, но включив аналитический ум можно заметить, что снаряд летит сквозь рельеф, продолжает лететь при достижении точки каста и долетая до края карты крашит игру. Благо исправить то не составит труда.
Пробему с вылетом за границы карты решает простая функция:
---@param x real
---@param y real
---@return boolean
local function InMapXY(x, y)
return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
end
Проблему с расстоянием можно решить просто посчитав пройденный путь. В итоге немного переделаем таймер.
TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
distance = distance - SPEED_INC -- отнимаем от расстояния пройденный путь
local x, y = BlzGetLocalSpecialEffectX(missile) + cos * SPEED_INC, BlzGetLocalSpecialEffectY(missile) + sin * SPEED_INC -- считаем новое положение снаряда
if
distance <= 0 -- если расстояние равно 0, значит снаряд уже долетел
or
not InMapXY(x, y) --снаряд вышел за пределы карты
then
DestroyEffect(missile)
PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
return -- завершаем функцию, чтоб пропустить дальнейшие действия
end
BlzSetSpecialEffectX(missile, x)
BlzSetSpecialEffectY(missile, y)
end)
Проблема с рельефом решается тремя способами
- заставить снаряд катиться по рельефу
- запускать по параболе и уничтожать при столкновении с рельефом
- забить
Для определения высоты рельефа нам пригодится ещё одна хорошая функция:
local GetTerrainZ_location = Location(0, 0)
---@param x real
---@param y real
---@return real
local function GetTerrainZ(x, y)
MoveLocation(GetTerrainZ_location, x, y)
return GetLocationZ(GetTerrainZ_location)
end
Которая сразу решает проблему с передвижением по рельефу.
BlzSetSpecialEffectHeight(missile, GetTerrainZ(x, y))
Движение по параболе мне кажется красивее, для чего нам пригодится функция:
---@param zs real начальная высота высота одного края дуги
---@param ze real конечная высота высота другого края дуги
---@param h real максимальная высота на середине расстояния (x = d / 2)
---@param d real общее расстояние до цели
---@param x real расстояние от исходной цели до точки
---@return real
function GetParabolaZ(zs, ze, h, d, x)
return (2 * (zs + ze - 2 * h) * (x / d - 1) + (ze - zs)) * (x / d) + zs
end
Теперь можно вспомнить решение прямоугольного треугольника, оптимизировать код и наслаждаться.
do
-- создаём область видимости, чтоб не конфликтовать с другим кодом
local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
function InitGlobals()
InitGlobalsOrigin()
---@param x real
---@param y real
---@return boolean
local function InMapXY(x, y)
return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
end
local GetTerrainZ_location = Location(0, 0)
---@param x real
---@param y real
---@return real
local function GetTerrainZ(x, y)
MoveLocation(GetTerrainZ_location, x, y)
return GetLocationZ(GetTerrainZ_location)
end
---@param za real начальная высота высота одного края дуги
---@param zb real конечная высота высота другого края дуги
---@param h real максимальная высота на середине расстояния (x = d / 2)
---@param d real общее расстояние до цели
---@param x real расстояние от исходной цели до точки
---@return real
local function GetParabolaZ(za, zb, h, d, x)
return (2 * (za + zb - 2 * h) * (x / d - 1) + (zb - za)) * (x / d) + za
end
local ABILITY_ID = FourCC('Amis')
local MISSILE_EFFECT = 'Abilities/Weapons/Mortar/MortarMissile.mdl'
local MISSILE_ARC = 0.5 -- изгиб дуги полёта снаряда
local TIMER_PERIOD = 0.03125 -- период срабатывания таймера: 1/32 секунды
local SPEED = 200 -- расстояние, которое снаряд преодолеет за секунду
local SPEED_INC = SPEED * TIMER_PERIOD -- расстояние, которое снаряд пройдёт на каждый тик таймера
-- в этом моменте прошла инициализация карты и можно смело работать
local SpellEffectTrigger = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
end
TriggerAddAction(SpellEffectTrigger, function()
if GetSpellAbilityId() ~= ABILITY_ID then return end
local caster = GetTriggerUnit() -- юнит, применивший способность
local xa, ya = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
local za = GetTerrainZ(xa, ya) -- высота точки начала полёта
local xb, yb = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
local zb = GetTerrainZ(xb, yb) -- высота точки окончания полёта
local angle = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
local distance = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
local way = 0 -- Пройденный путь
local missile = AddSpecialEffect(MISSILE_EFFECT, xa, ya) -- создаём эффект снаряда
local x, y, z = xa, ya, za -- начальное положение эффекта
TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
way = way + SPEED_INC -- считаем пройденный путь
x, y = x + cos * SPEED_INC, y + sin * SPEED_INC -- считаем новое положение снаряда
if
way >= distance -- если расстояние равно 0, значит снаряд уже долетел
or
not InMapXY(x, y) --снаряд вышел за пределы карты
then
BlzSetSpecialEffectOrientation(missile, angle, 0, 0) -- устанавливаем финальное положение эффекта
DestroyEffect(missile) -- уничтожаем эффект
PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
return -- завершаем функцию, чтоб пропустить дальнейшие действия
end
BlzSetSpecialEffectX(missile, x) -- устанавливаем положение эффекта
BlzSetSpecialEffectY(missile, y) -- устанавливаем положение эффекта
local zNew = GetParabolaZ(za, zb, distance * MISSILE_ARC, distance, way) -- считаем новую высоту эффекта
local zDiff = zNew - z -- считаем разницу высот
BlzSetSpecialEffectZ(missile, zNew) -- устанавливаем новую высоту эффекта
local zAngle = zDiff > 0 and math.atan(SPEED_INC / zDiff) - math.pi / 2 or math.atan(math.abs(zDiff) / SPEED_INC) - math.pi * 2 -- считаем угол наклона снаряда
BlzSetSpecialEffectOrientation(missile, angle, zAngle, 0) -- устанавливаем направление эффекта
z = zNew -- запоминаем новую высоту эффекта
end)
end)
end
end
Наносим урон
В нашем случае алгоритм нанесения урона прост: после взрыва снаряда выбрать всех юнитов в радиусе и нанести им необходимое количество урона. Поэтому для начала обзаведёмся группой, чтоб не создавать её каждый раз заново.
local GROUP = CreateGroup()
Так как у нас видимый круг выбора радиуса, то желательно в коде сразу получить значение из редактора:
Сделать это нам поможет функция
BlzGetAbilityRealLevelField()
вагонов уровней начинается с нуля.
Игра нам даёт возможность получать/изменять некоторые поля у конкретных способностей юнитов не трогая при этом такие же способности у других.
Так как нужное поле имеет тип real и уникально для каждого уровня, то название можно даже угадать: BlzGetAbilityRealLevelField.
Нужное поле угадать не так просто, но можно сильно упростить себе задачу: нажимаем Ctrl+D в редакторе и смотрим на название поля:
Теперь можно просто в IDE набрать ABILITY_RLF_AREA (RLF это какраз аббревиатура от Real Level Field)
Когда с функцией и полем определились, можно и разобрать другие параметры:
---@param whichAbility ability
---@param whichField abilityreallevelfield
---@param level integer
---@return real
function BlzGetAbilityRealLevelField(whichAbility, whichField, level) end
- whichAbility
---@param whichUnit unit
---@param abilId integer
---@return ability
function BlzGetUnitAbility(whichUnit, abilId) end
- whichField
- level
---@param whichUnit unit
---@param abilcode integer
---@return integer
function GetUnitAbilityLevel(whichUnit, abilcode) end
local ability = BlzGetUnitAbility(caster, ABILITY_ID)
local abilityLevel = GetUnitAbilityLevel(caster, ABILITY_ID)
local range = BlzGetAbilityRealLevelField(ability, ABILITY_RLF_AREA_OF_EFFECT, abilityLevel - 1)
Выбрав из циклов самый красивый подходящий, можно провести маленький тест.
GroupEnumUnitsInRange(GROUP, x, y, range)
while true do
local target = FirstOfGroup(GROUP)
if target == nil then break end
GroupRemoveUnit(GROUP, target)
KillUnit(target)
end
Работает не совсем так, как нам бы хотелось. На это есть свои причины: мы не делаем никаких проверок и GroupEnumUnitsInRange проверяет координаты юнита, а не физический размер. Но мы схитрим: добавим в группу юнитов на большем расстоянии:
GroupEnumUnitsInRange(GROUP, x, y, range + 256)
А при переборе уже воспользуемся функцией, которая учитывает размер юнита.
---@param whichUnit unit
---@param x real
---@param y real
---@param distance real
---@return boolean
function IsUnitInRangeXY(whichUnit, x, y, distance) end
Так как тесты лишними не бывают, проведём ещё один.
GroupEnumUnitsInRange(GROUP, x, y, range + 256)
while true do
local target = FirstOfGroup(GROUP)
if target == nil then break end
GroupRemoveUnit(GROUP, target)
if UnitAlive(target) -- юнит жив
and IsPlayerEnemy(GetOwningPlayer(caster), GetOwningPlayer(target)) -- юнит враг
and not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) -- юнит не имунен к маггии
and not IsUnitType(target, UNIT_TYPE_FLYING) -- юнит не летающий
and IsUnitInRangeXY(target, x, y, range) -- юнит на нужном расстоянии
then
KillUnit(target)
end
end
Теперь настало время наносить урон, а так как просто указать значение для каждого уровня способности удел начинающих гуишников, мы придумаем хитрую формулу, чтоб заклинание казалось умным и проработанным. Например:
уровень способности * меньшую из характеристику героя + макимальный урон героя
уровень способности * меньшую из характеристику героя + макимальный урон героя
local damage = abilityLevel
* math.min(GetHeroStr(caster, true), GetHeroAgi(caster, true), GetHeroInt(caster, true))
+ BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
Когда значение посчитано, можно смело наносить урон
UnitDamageTarget(caster, target, damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
Полюбуемся на весь код целиком:
do
-- создаём область видимости, чтоб не конфликтовать с другим кодом
local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
function InitGlobals()
InitGlobalsOrigin()
-- в этом моменте прошла инициализация карты и можно смело работать
---@param x real
---@param y real
---@return boolean
local function InMapXY(x, y)
return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
end
local GetTerrainZ_location = Location(0, 0)
---@param x real
---@param y real
---@return real
local function GetTerrainZ(x, y)
MoveLocation(GetTerrainZ_location, x, y)
return GetLocationZ(GetTerrainZ_location)
end
---@param za real начальная высота высота одного края дуги
---@param zb real конечная высота высота другого края дуги
---@param h real максимальная высота на середине расстояния (x = d / 2)
---@param d real общее расстояние до цели
---@param x real расстояние от исходной цели до точки
---@return real
local function GetParabolaZ(za, zb, h, d, x)
return (2 * (za + zb - 2 * h) * (x / d - 1) + zb - za) * x / d + za
end
local ABILITY_ID = FourCC('Amis')
local MISSILE_EFFECT = 'Abilities/Weapons/Mortar/MortarMissile.mdl'
local MISSILE_ARC = 0.5 -- изгиб дуги полёта снаряда
local TIMER_PERIOD = 0.03125 -- период срабатывания таймера: 1/32 секунды
local SPEED = 200 -- расстояние, которое снаряд преодолеет за секунду
local SPEED_INC = SPEED * TIMER_PERIOD -- расстояние, которое снаряд пройдёт на каждый тик таймера
local GROUP = CreateGroup()
local SpellEffectTrigger = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
end
TriggerAddAction(SpellEffectTrigger, function()
--print(collectgarbage("count") * 1024)
--collectgarbage()
if GetSpellAbilityId() ~= ABILITY_ID then return end
local caster = GetTriggerUnit() -- юнит, применивший способность
local xa, ya = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
local za = GetTerrainZ(xa, ya) -- высота точки начала полёта
local xb, yb = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
local zb = GetTerrainZ(xb, yb) -- высота точки окончания полёта
local angle = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
local distance = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
local ability = BlzGetUnitAbility(caster, ABILITY_ID)
local abilityLevel = GetUnitAbilityLevel(caster, ABILITY_ID)
local range = BlzGetAbilityRealLevelField(ability, ABILITY_RLF_AREA_OF_EFFECT, abilityLevel - 1)
local damage = abilityLevel
* math.min(GetHeroStr(caster, true), GetHeroAgi(caster, true), GetHeroInt(caster, true))
+ BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
local way = 0 -- Пройденный путь
local missile = AddSpecialEffect(MISSILE_EFFECT, xa, ya) -- создаём эффект снаряда
local x, y, z = xa, ya, za -- начальное положение эффекта
TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
way = way + SPEED_INC -- считаем пройденный путь
x, y = x + cos * SPEED_INC, y + sin * SPEED_INC -- считаем новое положение снаряда
if
way >= distance -- если расстояние равно 0, значит снаряд уже долетел
or
not InMapXY(x, y) --снаряд вышел за пределы карты
then
BlzSetSpecialEffectOrientation(missile, angle, 0, 0) -- устанавливаем финальное положение эффекта
DestroyEffect(missile) -- уничтожаем эффект
GroupEnumUnitsInRange(GROUP, x, y, range + 256)
while true do
local target = FirstOfGroup(GROUP)
if target == nil then break end
GroupRemoveUnit(GROUP, target)
if UnitAlive(target) -- юнит жив
and IsPlayerEnemy(GetOwningPlayer(caster), GetOwningPlayer(target)) -- юнит враг
and not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) -- юнит не имунен к маггии
and not IsUnitType(target, UNIT_TYPE_FLYING) -- юнит не летающий
and IsUnitInRangeXY(target, x, y, range) -- юнит на нужном расстоянии
then
UnitDamageTarget(caster, target, damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
end
end
PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
return -- завершаем функцию, чтоб пропустить дальнейшие действия
end
BlzSetSpecialEffectX(missile, x) -- устанавливаем положение эффекта
BlzSetSpecialEffectY(missile, y) -- устанавливаем положение эффекта
local zNew = GetParabolaZ(za, zb, distance * MISSILE_ARC, distance, way) -- считаем новую высоту эффекта
local zDiff = zNew - z -- считаем разницу высот
BlzSetSpecialEffectZ(missile, zNew) -- устанавливаем новую высоту эффекта
local zAngle = zDiff > 0 and math.atan(SPEED_INC / zDiff) - math.pi / 2 or math.atan(-zDiff / SPEED_INC) - math.pi * 2 -- считаем угол наклона снаряда
BlzSetSpecialEffectOrientation(missile, angle, zAngle, 0) -- устанавливаем направление эффекта
z = zNew -- запоминаем новую высоту эффекта
end)
end)
end
end
Наводим красоту
В принципе, заклинание уже можно использовать, но оно ничем не выделяется среди прочих и просто скучное. Для сравнения посмотрим на стандартное заклинание у Механика.
Обратим внимание на некоторые вещи:
- выпускается несколько ракет
- ракета создаётся не под ногами героя
- нет минимальной дистанции каста и при касте под себя выглядит некрасиво
- ракеты оглушают врага
Для начала приведём в порядок стартовую позицию, заодно и заменим героя и снаряд на более подходящих.
local MISSILE_Z_START = 100 -- начальная высота снаряда
local za = GetTerrainZ(xa, ya) + MISSILE_Z_START -- высота точки начала полёта
Так как минимальную дистанцию нельзя задать в редакторе объектов то просто будем сдвигать точку каста.
if distance < MISSILE_MIN_DISTANCE then
distance = MISSILE_MIN_DISTANCE
xb, yb = xb + cos * distance, yb + sin * distance
end
Теперь можно взять отсюда функцию проверки на глубокую воду и сделать различные взрывы.
---@param x real
---@param y real
---@return boolean
function IsTerrainDeepWater (x, y)
return not IsTerrainPathable(x, y, PATHING_TYPE_FLOATABILITY) and IsTerrainPathable(x, y, PATHING_TYPE_WALKABILITY)
end
local MISSILE_EFFECT_GROUND = 'Objects/Spawnmodels/Human/FragmentationShards/FragBoomSpawn.mdl' -- взрыв на суше
local MISSILE_EFFECT_WATER = 'Abilities/Spells/Other/CrushingWave/CrushingWaveDamage.mdl' -- взрыв на воде
DestroyEffect(AddSpecialEffect(IsTerrainDeepWater(x, y) and MISSILE_EFFECT_WATER or MISSILE_EFFECT_GROUND, x, y))
Теперь можно завернуть создание таймера в цикл и полюбоваться на результат.
do
-- создаём область видимости, чтоб не конфликтовать с другим кодом
local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
function InitGlobals()
InitGlobalsOrigin()
-- в этом моменте прошла инициализация карты и можно смело работать
---@param x real
---@param y real
---@return boolean
local function InMapXY(x, y)
return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
end
local GetTerrainZ_location = Location(0, 0)
---@param x real
---@param y real
---@return real
local function GetTerrainZ(x, y)
MoveLocation(GetTerrainZ_location, x, y)
return GetLocationZ(GetTerrainZ_location)
end
---@param za real начальная высота высота одного края дуги
---@param zb real конечная высота высота другого края дуги
---@param h real максимальная высота на середине расстояния (x = d / 2)
---@param d real общее расстояние до цели
---@param x real расстояние от исходной цели до точки
---@return real
local function GetParabolaZ(za, zb, h, d, x)
return (2 * (za + zb - 2 * h) * (x / d - 1) + zb - za) * x / d + za
end
---@param x real
---@param y real
---@return boolean
local function IsTerrainDeepWater(x, y)
return not IsTerrainPathable(x, y, PATHING_TYPE_FLOATABILITY) and IsTerrainPathable(x, y, PATHING_TYPE_WALKABILITY)
end
local ABILITY_ID = FourCC('Amis')
local MISSILE_EFFECT = 'Abilities/Weapons/RocketMissile/RocketMissile.mdl' -- модель снаряда
local MISSILE_EFFECT_GROUND = 'Objects/Spawnmodels/Human/FragmentationShards/FragBoomSpawn.mdl' -- взрыв на суше
local MISSILE_EFFECT_WATER = 'Abilities/Spells/Other/CrushingWave/CrushingWaveDamage.mdl' -- взрыв на воде
local MISSILE_COUNT = { 5, 7, 9 } -- количество снарядов для каждого уровня
local MISSILE_STEP = 100
local MISSILE_Z_START = 100 -- начальная высота снаряда
local MISSILE_MIN_DISTANCE = 300 -- минимальдая дистанция каста
local MISSILE_ARC = 0.5 -- изгиб дуги полёта снаряда
local TIMER_PERIOD = 0.03125 -- период срабатывания таймера: 1/32 секунды
local SPEED = 200 -- расстояние, которое снаряд преодолеет за секунду
local SPEED_INC = SPEED / (1 / TIMER_PERIOD) -- расстояние, которое снаряд пройдёт на каждый тик таймера
local GROUP = CreateGroup()
local SpellEffectTrigger = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
end
TriggerAddAction(SpellEffectTrigger, function()
if GetSpellAbilityId() ~= ABILITY_ID then return end
local caster = GetTriggerUnit() -- юнит, применивший способность
local xa, ya = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
local za = GetTerrainZ(xa, ya) + MISSILE_Z_START -- высота точки начала полёта
local xb, yb = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
local distance = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
local angle = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
if distance < MISSILE_MIN_DISTANCE then
distance = MISSILE_MIN_DISTANCE
xb, yb = xb + cos * distance, yb + sin * distance
end
local ability = BlzGetUnitAbility(caster, ABILITY_ID)
local abilityLevel = GetUnitAbilityLevel(caster, ABILITY_ID)
local range = BlzGetAbilityRealLevelField(ability, ABILITY_RLF_AREA_OF_EFFECT, abilityLevel - 1)
local damage = abilityLevel
* math.min(GetHeroStr(caster, true), GetHeroAgi(caster, true), GetHeroInt(caster, true))
+ BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
for i = 0, MISSILE_COUNT[abilityLevel] - 1 do
local way = 0 -- Пройденный путь
local missile = AddSpecialEffect(MISSILE_EFFECT, xa, ya) -- создаём эффект снаряда
local distanceCurrent = i * MISSILE_STEP + distance -- расстояние между точками
local zb = GetTerrainZ(xa + cos * distanceCurrent, yb + sin * distanceCurrent) -- высота точки окончания полёта
local x, y, z = xa, ya, za -- начальное положение эффекта
TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
way = way + SPEED_INC -- считаем пройденный путь
x, y = x + cos * SPEED_INC, y + sin * SPEED_INC -- считаем новое положение снаряда
if way >= distanceCurrent -- если расстояние равно 0, значит снаряд уже долетел
or
not InMapXY(x, y) --снаряд вышел за пределы карты
then
BlzSetSpecialEffectOrientation(missile, angle, 0, 0) -- устанавливаем финальное положение эффекта
DestroyEffect(missile) -- уничтожаем эффект
DestroyEffect(AddSpecialEffect(IsTerrainDeepWater(x, y) and MISSILE_EFFECT_WATER or MISSILE_EFFECT_GROUND, x, y))
GroupEnumUnitsInRange(GROUP, x, y, range + 256)
while true do
local target = FirstOfGroup(GROUP)
if target == nil then break end
GroupRemoveUnit(GROUP, target)
if UnitAlive(target) -- юнит жив
and IsPlayerEnemy(GetOwningPlayer(caster), GetOwningPlayer(target)) -- юнит враг
and not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) -- юнит не имунен к маггии
and not IsUnitType(target, UNIT_TYPE_FLYING) -- юнит не летающий
and IsUnitInRangeXY(target, x, y, range) -- юнит на нужном расстоянии
then
UnitDamageTarget(caster, target, damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
end
end
PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
return -- завершаем функцию, чтоб пропустить дальнейшие действия
end
BlzSetSpecialEffectX(missile, x) -- устанавливаем положение эффекта
BlzSetSpecialEffectY(missile, y) -- устанавливаем положение эффекта
local zNew = GetParabolaZ(za, zb, distanceCurrent * MISSILE_ARC, distanceCurrent, way) -- считаем новую высоту эффекта
local zDiff = zNew - z -- считаем разницу высот
BlzSetSpecialEffectZ(missile, zNew) -- устанавливаем новую высоту эффекта
local zAngle = zDiff > 0 and math.atan(SPEED_INC / zDiff) - math.pi / 2 or math.atan(-zDiff / SPEED_INC) - math.pi * 2 -- считаем угол наклона снаряда
BlzSetSpecialEffectOrientation(missile, angle, zAngle, 0) -- устанавливаем направление эффекта
z = zNew -- запоминаем новую высоту эффекта
end)
end
end)
end
end
Так как игра не позволяет просто накладывать бафы, на помощь придёт даммикаст. Которому тоже можно посвятить целую статью. Поэтому создание даммика и способности мы опустим, а сразу начнём с кода.
local Dummy = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('dumy'), 0, 0, 0)
local DummyStunId = FourCC('stun')
UnitAddAbility(Dummy, DummyStunId)
local StunAbility = BlzGetUnitAbility(Dummy, DummyStunId)
---@param target unit
---@param durationUnit real
---@param durationHero real
local function DummyCastStun(target, durationUnit, durationHero)
SetUnitX(Dummy, GetUnitX(target))
SetUnitY(Dummy, GetUnitY(target))
BlzSetAbilityRealLevelField(StunAbility, ABILITY_RLF_DURATION_NORMAL, 0, durationUnit)
BlzSetAbilityRealLevelField(StunAbility, ABILITY_RLF_DURATION_HERO, 0, durationHero)
IssueTargetOrderById(Dummy, 852095, target) -- thunderbolt
end
Подробнее
Так как у даммика нет модели, то касты способностей проходят моментально. Потому приказы даммику можно отдавать в цикле. А изменение данных способности перед кастом освобождает нас от создания кучи способностей с разным временем действия.
Поэтому сразу в начале игры можно создать даммика для нейтрально пассивного и сразу выдать ему способность, чтоб небыло лага при первом использовании.
Поэтому сразу в начале игры можно создать даммика для нейтрально пассивного и сразу выдать ему способность, чтоб небыло лага при первом использовании.
Напоследок сделаем нелинейное изменение скорости. Так как точки у нас фиксированные, то мы можем зарание посчитать время полёта и переписать его исходя не из расстояния, а от времени.
Код
do
-- создаём область видимости, чтоб не конфликтовать с другим кодом
local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
function InitGlobals()
InitGlobalsOrigin()
-- в этом моменте прошла инициализация карты и можно смело работать
---@param x real
---@param y real
---@return boolean
local function InMapXY(x, y)
return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
end
local GetTerrainZ_location = Location(0, 0)
---@param x real
---@param y real
---@return real
local function GetTerrainZ(x, y)
MoveLocation(GetTerrainZ_location, x, y)
return GetLocationZ(GetTerrainZ_location)
end
---@param za real начальная высота высота одного края дуги
---@param zb real конечная высота высота другого края дуги
---@param h real максимальная высота на середине расстояния (x = d / 2)
---@param d real общее расстояние до цели
---@param x real расстояние от исходной цели до точки
---@return real
local function GetParabolaZ(za, zb, h, d, x)
return (2 * (za + zb - 2 * h) * (x / d - 1) + zb - za) * x / d + za
end
---@param x real
---@param y real
---@return boolean
local function IsTerrainDeepWater(x, y)
return not IsTerrainPathable(x, y, PATHING_TYPE_FLOATABILITY) and IsTerrainPathable(x, y, PATHING_TYPE_WALKABILITY)
end
local Dummy = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('dumy'), 0, 0, 0)
local DummyStunId = FourCC('stun')
UnitAddAbility(Dummy, DummyStunId)
local StunAbility = BlzGetUnitAbility(Dummy, DummyStunId)
---@param target unit
---@param durationUnit real
---@param durationHero real
local function DummyCastStun(target, durationUnit, durationHero)
SetUnitX(Dummy, GetUnitX(target))
SetUnitY(Dummy, GetUnitY(target))
BlzSetAbilityRealLevelField(StunAbility, ABILITY_RLF_DURATION_NORMAL, 0, durationUnit)
BlzSetAbilityRealLevelField(StunAbility, ABILITY_RLF_DURATION_HERO, 0, durationHero)
IssueTargetOrderById(Dummy, 852095, target) -- thunderbolt
end
local ABILITY_ID = FourCC('Amis')
local MISSILE_EFFECT = 'Abilities/Weapons/RocketMissile/RocketMissile.mdl' -- модель снаряда
local MISSILE_EFFECT_GROUND = 'Objects/Spawnmodels/Human/FragmentationShards/FragBoomSpawn.mdl' -- взрыв на суше
local MISSILE_EFFECT_WATER = 'Abilities/Spells/Other/CrushingWave/CrushingWaveDamage.mdl' -- взрыв на воде
local MISSILE_COUNT = { 5, 7, 9 } -- количество снарядов для каждого уровня
local MISSILE_STEP = 100
local MISSILE_Z_START = 100 -- начальная высота снаряда
local MISSILE_MIN_DISTANCE = 300 -- минимальдая дистанция каста
local MISSILE_ARC = 0.5 -- изгиб дуги полёта снаряда
local TIMER_PERIOD = 0.03125 -- период срабатывания таймера: 1/32 секунды
local SPEED = 200 -- расстояние, которое снаряд преодолеет за секунду
local GROUP = CreateGroup()
local SpellEffectTrigger = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
end
TriggerAddAction(SpellEffectTrigger, function()
if GetSpellAbilityId() ~= ABILITY_ID then return end
local caster = GetTriggerUnit() -- юнит, применивший способность
local xa, ya = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
local za = GetTerrainZ(xa, ya) + MISSILE_Z_START -- высота точки начала полёта
local xb, yb = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
local distance = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
local angle = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
if distance < MISSILE_MIN_DISTANCE then
distance = MISSILE_MIN_DISTANCE
xb, yb = xb + cos * distance, yb + sin * distance
end
local ability = BlzGetUnitAbility(caster, ABILITY_ID)
local abilityLevel = GetUnitAbilityLevel(caster, ABILITY_ID)
local range = BlzGetAbilityRealLevelField(ability, ABILITY_RLF_AREA_OF_EFFECT, abilityLevel - 1)
local damage = abilityLevel
* math.min(GetHeroStr(caster, true), GetHeroAgi(caster, true), GetHeroInt(caster, true))
+ BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
for i = 0, MISSILE_COUNT[abilityLevel] - 1 do
local missile = AddSpecialEffect(MISSILE_EFFECT, xa, ya) -- создаём эффект снаряда
local distanceAll = i * MISSILE_STEP + distance -- расстояние между точками
local zb = GetTerrainZ(xa + cos * distanceAll, yb + sin * distanceAll) -- высота точки окончания полёта
local timeAll = distanceAll / SPEED -- время, за которо снаряд достигнет цели
local timeCur = 0 -- время полёта снаряда
local z = za -- начальняая высота эффекта
local distanceCurrentOld = 0
TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
timeCur = timeCur + TIMER_PERIOD
local distanceCurrent = distanceAll * (timeCur / timeAll)
local x, y = xa + cos * distanceCurrent, ya + sin * distanceCurrent -- считаем новое положение снаряда
if timeCur >= timeAll -- если расстояние равно 0, значит снаряд уже долетел
or
not InMapXY(x, y) --снаряд вышел за пределы карты
then
BlzSetSpecialEffectOrientation(missile, angle, 0, 0) -- устанавливаем финальное положение эффекта
DestroyEffect(missile) -- уничтожаем эффект
DestroyEffect(AddSpecialEffect(IsTerrainDeepWater(x, y) and MISSILE_EFFECT_WATER or MISSILE_EFFECT_GROUND, x, y))
GroupEnumUnitsInRange(GROUP, x, y, range + 256)
while true do
local target = FirstOfGroup(GROUP)
if target == nil then break end
GroupRemoveUnit(GROUP, target)
if UnitAlive(target) -- юнит жив
and IsPlayerEnemy(GetOwningPlayer(caster), GetOwningPlayer(target)) -- юнит враг
and not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) -- юнит не имунен к маггии
and not IsUnitType(target, UNIT_TYPE_FLYING) -- юнит не летающий
and IsUnitInRangeXY(target, x, y, range) -- юнит на нужном расстоянии
then
DummyCastStun(target, abilityLevel * 2, abilityLevel)
UnitDamageTarget(caster, target, damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
end
end
PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
return -- завершаем функцию, чтоб пропустить дальнейшие действия
end
BlzSetSpecialEffectX(missile, x) -- устанавливаем положение эффекта
BlzSetSpecialEffectY(missile, y) -- устанавливаем положение эффекта
local zNew = GetParabolaZ(za, zb, distanceAll * MISSILE_ARC, distanceAll, distanceCurrent) -- считаем новую высоту эффекта
local zDiff = zNew - z -- считаем разницу высот
BlzSetSpecialEffectZ(missile, zNew) -- устанавливаем новую высоту эффекта
local distanceDiff = distanceCurrent - distanceCurrentOld
local zAngle = zDiff > 0 and math.atan(distanceDiff / zDiff) - math.pi / 2 or math.atan(-zDiff / distanceDiff) - math.pi * 2 -- считаем угол наклона снаряда
BlzSetSpecialEffectOrientation(missile, angle, zAngle, 0) -- устанавливаем направление эффекта
distanceCurrentOld = distanceCurrent
z = zNew -- запоминаем новую высоту эффекта
end)
end
end)
end
end
Ключевое изменение здесь то, что новую позицию мы считаем из отношения общего расстояния к прошедшему времени
local distanceCurrent = distanceAll * (timeCur / timeAll)
Теперь возьмём отсюда понравившуюся функцию и применим.
local distanceCurrent = distanceAll * (1 - math.cos((timeCur / timeAll) * math.pi / 2))
Заключение
Статья конечно вышла сумбурной и обширной, но я надеюсь, что у меня получилось раскрыть тему простого движения снарядов. Если остались вопросы, то их всегда можно задать в комментариях.
`
ОЖИДАНИЕ РЕКЛАМЫ...
Чтобы оставить комментарий, пожалуйста, войдите на сайт.
Отредактирован prog
Это, естественно, обойдется в лишний вызов функции на каждой итерации, так что в критических для производительности местах придется жертвовать красивостью кода ради скорости.
Отредактирован nazarpunk
Отредактирован prog
Отредактирован nazarpunk
Отредактирован prog
Более того, в случае перебора списка юнитов, если так важна производительность перебора - эффективнее будет хранить их в таблице и не дергать нативки вобще. Но, естественно, при однократном переборе результата поиска юнитов нативкой других вариантов кроме перебора группы особо и нет.
Отредактирован PT153
Если форматирование всё равно ломается, разбей код внутри на блока кода.
В обоих случаях может произойти ситуация, что таймер запустит свою функцию и будет удалён, GetExpiredTimer() вернёт null, и все действия, которое по логике кода не должны быть выполнены, будут выполнены. Пауза решает данную проблему, потому вообще можно сделать так.
Отредактирован nazarpunk
Unit - A unit Starts the effect of an ability – Это событие запускает триггер в тот момент, когда спелл уже скастован, кулдаун уже начался и мана проплачена. Это событие идеально подходит для запуска триггера спелла.
Unit - A unit Finishes casting an ability – Это событие запускает триггер в момент, когда юнит завершил кастовать спелл. Это полезно в случаях когда, например, вам нужно удалить юнита, который скастовал определенный спелл, и вы хотите быть уверены, что эффект спелла появится. Например, вам нужно удалить юнита, кастующего спелл Heal, вы должны использовать именно это событие, иначе цель не будет вылечена.