Добавлен , опубликован
Раздел:
Триггеры и объекты

Идея

Создать способность, которая запустит снаряд из точки А в точку 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
Поле способности, которое мы уже узнали ABILITY_RLF_AREA_OF_EFFECT
  • 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))

Заключение

Статья конечно вышла сумбурной и обширной, но я надеюсь, что у меня получилось раскрыть тему простого движения снарядов. Если остались вопросы, то их всегда можно задать в комментариях.
`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
0
17
5 лет назад
0
Не понимаю каким боком мы создаем лишние способности?
Речь о нестандартном поведении снаряда. Вряд ли бы вопрос поднимался, укладывайся задача в базовый набор способностей. Да и чисто с архитектурной точки зрения, полностью триггерный снаряд это объективно здравый ход в сравнении с отловом урона и баффа.
4
29
5 лет назад
4
Дополнил статью, и итоговый результат уже будет трудно реализовать на стандартных способностях))
0
28
5 лет назад
Отредактирован PT153
0
(только для channel способностей)
Нет, CHANNEL и FINISH ловят все способности. Различия на channel и не channel нет.
оно наступает после снятия маны и начала перезарядки способности.
Это не так. Мана и кд снимаются ПОСЛЕ завершения триггера с EFFECT событием или после паузы в нём.
Почему же нужно использовать EFFECT
Кратко: EFFECT означает фактический старт каста способности, когда cast time способности и cast point юнита прошли.
Дополнительно:
Во время этого события игрок уже может юнита контролировать, до этого - нет. Также если до этого события сбить каст, то юнит заново начнёт кастовать, после и во время этого события - нет. ENDCAST не может сработать раньше EFFECT, либо во время, либо после.
Лучше убрать лишнюю информацию и оставить только нужную.

if GetSpellAbilityId() == FourCC('A001') then
	-- spell1
	return
elseif GetSpellAbilityId() == FourCC('A002') then
	-- spell2
	return
elseif GetSpellAbilityId() == FourCC('A003') then
	-- spell3
	return
end
return тут не нужен.

local SPEED_INC = SPEED / (1 / TIMER_PERIOD) -- расстояние, которое снаряд пройдёт на каждый тик таймера
local SPEED_INC = SPEED * TIMER_PERIOD
Деление может только запутать читателя. Расстояние = скорость * время, это все знают.
return DestroyTimer(GetExpiredTimer())
Зачем возвращать nil? Это запутает не подкованных читателей.
Перед удалением периодические таймеры нужно ставить на паузу.
PauseTimer(GetExpiredTimer())
DestroyTimer(GetExpiredTimer())
return
Исодя из этого, перепишем полёт исходя не из расстояния, а от времени.
"Исходя". А ниже сломалось форматирование. Опечатку заметил только из-за того, что браузер подчеркнул в цитате.
while true do
Так будет лучше.
local target = FirstOfGroup(GROUP)
while target ~= nil do  -- если можно просто while target do, то так и сделать.
    -- actions
    target = FirstOfGroup(GROUP)
end
Так как у даммика нет модели, то касты способностей проходят моментально. Потому приказы даммику можно отдавать в цикле.
Насчет модели не уверен, всё же стоит занулять cast point и cast backswing у даммика, а cast time у самой способности занулять всегда. Кастовать в цикле можно не все способности (например, способности с молниями нельзя), зависит от способности. Как правило таргетные способности со снарядом можно.
Которому тоже можно посвятить целую статью.
Она есть, но лично я бы написал свою, так как в той даммик делается из дурного юнита.

Статья хорошая. Осталось поправить все недочеты, и будет очень хорошо.
0
29
5 лет назад
0
PT153, поправил всё по мере возможности.

В while мне хочется, чтоб target была видима только внутри цикла, потому такое решение. В идеале бы:
while local target = FirstOfGroup(GROUP) ~=nil do
--actions
end
Но lua так не умеет((
0
24
5 лет назад
Отредактирован prog
0
NazarPunk, Ну так запили свой итератор и делай это через for... Использование будет выглядеть примерно так:
for target in group_iter(GROUP) do
--actions
end
Где group_iter кастомный итератор принимающий группу и делающий перебор внутри, а переменная target автоматически объявляется как локальная для цикла, если я правильно помню.
Это, естественно, обойдется в лишний вызов функции на каждой итерации, так что в критических для производительности местах придется жертвовать красивостью кода ради скорости.
0
29
5 лет назад
Отредактирован nazarpunk
0
Ну так запили свой итератор и делай это через for...
Через for я хочу потестить другой способ:
for index = 1, BlzGroupGetSize(whichGroup) do
	local target = BlzGroupUnitAt(whichGroup, index)
	-- action
end
GroupClear(whichGroup)
или так
for index = 1, BlzGroupGetSize(whichGroup), -1 do
	local target = BlzGroupUnitAt(whichGroup, index)
	GroupRemoveUnit(whichGroup, target)
	-- action
end
0
24
5 лет назад
Отредактирован prog
0
NazarPunk, ты сказал что хочешь локальную переменную в цикле - я сказал как её получить. А метод с BlzGroupUnitAt потестить, конечно, тоже стоит. Более того, его тоже можно завернуть в итератор при желании.
0
29
5 лет назад
Отредактирован nazarpunk
0
ты сказал что хочешь локальную переменную в цикле - я сказал как её получить.
Так я её и получил, а ты предлагаешь ещё пилить функцию, которая по твоим словам дорогое удовольствие.
А BlzGroupUnitAt потестить, конечно, тоже стоит.
Нужно все переборы потестить и repeat не забыть
repeat
	local target = FirstOfGroup(whichGroup)
	if target ~= nil then
		GroupRemoveUnit(whichGroup,target)
		-- action
	end
until target ~= nil
0
24
5 лет назад
Отредактирован prog
0
Так я её и получил, а ты предлагаешь ещё пилить функцию, которая по твоим словам дорогое удовольствие
Итератор позволяет инкапсулировать и повторно использовать общий код для итерации. Кроме мест в которых производительность критически важна, этот лишний вызов функции вполне окупается удобством использования.
Более того, в случае перебора списка юнитов, если так важна производительность перебора - эффективнее будет хранить их в таблице и не дергать нативки вобще. Но, естественно, при однократном переборе результата поиска юнитов нативкой других вариантов кроме перебора группы особо и нет.
2
29
5 лет назад
2
prog, одним словом, нужно пилить статью "Все способы перебора юнитов" с тестами))
0
24
5 лет назад
0
NazarPunk, ну и самое важное, наверно - при массовом переборе юнитов в группе надо используемые нативки в локалки писать для ускорения доступа к ним.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.