WarCraft 3: [lua] Двигаем снаряды

» Раздел: Триггеры и объекты

Идея

Создать способность, которая запустит снаряд из точки А в точку 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))

Заключение

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

Просмотров: 511

» Лучшие комментарии


PrincePhoenix #1 - 3 недели назад 7
наконец-то, NazarPunk начал писать статьи, как долго мы этого ждали
KaneThaumaturge #2 - 3 недели назад 4
Блин, прям луа захотелось учить, если будет ещё статья, то точно перейду.
prog #3 - 3 недели назад 0
Опять напоминаю про сборщик мусора. Нужно проверить падает ли на автоматической сборке мусора то, что падает при ручной. Что-то мне подсказывает, что код из этой статьи не выдерживает принудительную сборку мусора.
Один из вариантов как отследить момент срабатывания сборщика мусора - добавить финализатор к таблице через метаметод __gc, потом убить все ссылки на эту таблицу и ждать, параллельно засирая память.
В момент срабатывания финальной фазы сборки мусора перед удалением нашей таблицы вызовется __gc из её метатаблицы.
NazarPunk #4 - 3 недели назад (отредактировано ) 8
если будет ещё статья, то точно перейду.
Ради такого грех статью не запилить)
Что-то мне подсказывает, что код из этой статьи не выдерживает принудительную сборку мусора.
Немного подправил заклинание и всё прекрасно работает.
прикреплены файлы
prog #5 - 3 недели назад 0
всё прекрасно работает.
Если работает - отлично. Я бы, правда, для чистоты эксперимента вызывал сборку вне контекста в котором плодятся потенциально мусорные объекты, но это я уже придираюсь. Хотя странно - у тебя же раньше сжирало анонимные таймеры. Или они были до инициализации, как и триггеры которые сжирает если выдавать им события до инициализации?
NazarPunk #6 - 3 недели назад 0
Хотя странно - у тебя же раньше сжирало анонимные таймеры.
Сжирало до инициализации, скорее всего в ней и дело.
prog #7 - 3 недели назад 0
Сжирало до инициализации, скорее всего в ней и дело.
Видимо да.
Steal nerves #8 - 3 недели назад 0
хороший мануал))
NazarPunk #9 - 3 недели назад (отредактировано ) 2
Я бы, правда, для чистоты эксперимента вызывал сборку вне контекста в котором плодятся потенциально мусорные объекты
Запилил наработку для сборщика, нормально код работает при вызове сборщика из другого контекста.

Дополнил главу про нанесение урона.
ScopteRectuS #10 - 2 недели назад (отредактировано ) 0
NazarPunk, чтобы получить урон героя, по-моему, нужно к базовому урону еще добавить основную характеристику героя. А зелёный бонус еще нельзя определить.
pro100master #11 - 2 недели назад -2
ScopteRectuS, можно =)
GetLocalPlayer #12 - 2 недели назад 0
А зелёный бонус еще нельзя определить.
Последние нативки позволяют читать поля объекта задаваемые в редакторе. Так что технически можно.
NazarPunk #13 - 2 недели назад (отредактировано ) 0
чтобы получить урон героя, по-моему, нужно к базовому урону еще добавить основную характеристику героя.
Без бонусов прибавлять ничего не нужно
print(BlzGetUnitBaseDamage(caster, 0), BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0))
А вот с бонусными характеристиками непонятно
можно =)
Обожаю таких людей, которые говорят, что можно и не указывают способ.
Последние нативки позволяют читать поля объекта задаваемые в редакторе. Так что технически можно.
Бонусные характеристика как-бы не задаются в редакторе.
прикреплены файлы
GetLocalPlayer #14 - 2 недели назад 0
Бонусные характеристика как-бы не задаются в редакторе.
Я о полях способностей значение которых можно получить и высчитать зеленую атаку.
NazarPunk #15 - 2 недели назад (отредактировано ) 0
Я о полях способностей значение которых можно получить и высчитать зеленую атаку.
А с предметами и бафами как быть?

Решение тупо оказалось в мануале
local min          = BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0)
local max          = BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
print(min, max)
прикреплены файлы
quq_CCCP #16 - 2 недели назад 0
Простите а есть ли смысл этим заниматся, когда завезли нормальный детект урона и стак баффов? Аксид бомбу берем или ракеты тинкера и улыбаемсо, никакой математики. Т.к на новых патчах изи узнать от кого прилетел снаряд.
GetLocalPlayer #17 - 2 недели назад 0
А с предметами и бафами как быть?
Поля предметов так же можно получить и узнать список их способностей.
А с баффами да, придется костылить.
quq_CCCP:
а есть ли смысл этим заниматся, когда завезли нормальный детект урона и стак баффов?
Должно быть, чтобы разгрузить редактор объектов. Не создавать на каждый снаряд по уникальной способности и баффу.
NazarPunk #18 - 2 недели назад 0
Простите а есть ли смысл этим заниматся, когда завезли нормальный детект урона и стак баффов?
Кому нужно просто и быстро, тот так и сделает, а если мне захочется запустить снаряд с нелинейной скоростью или реализовать столкновение с летающими юнитами?
GetLocalPlayer #19 - 2 недели назад (отредактировано ) 5
а если мне захочется запустить снаряд с нелинейной скоростью или реализовать столкновение с летающими юнитами?
Или возможность запаузить снаряд. Ставит Войд купол, туда союзники накидывают-накидывают атаками и способностями, а оно у границы сферы останавливается. А потом как все разом жахнет.
pro100master #20 - 2 недели назад 1
GetLocalPlayer, у меня такая карта =) Вокруг босс имеет замедление способностей что все его накидают а когда снаряд долетает то замедляет и снижает урон.. Интересный босс все вокруг замедляет =)
quq_CCCP #21 - 2 недели назад 0
GetLocalPlayer, Не понимаю каким боком мы создаем лишние способности? У героя есть способность - допустим это будет кастомный молот бурь, который не оглушает а замедляет, даже если вы будите делать снаряды из эффектов или юнитов вам нада сделать пустышку на карте комманд, но зачем? Просто у героя любая абилка со снарядом, эффект обнулен, а по детекту урона мы узнаем что прилетела абилка и делаем все нужные действия, даже не надо делать по 1 триггеру на абилку с событием юнит кастанул абилку.
prog #22 - 2 недели назад 0
quq_CCCP, у абилок со снарядом есть большая проблема - они не очень любят когда цель каста перемещается триггерно пока снаряд летит.
Ну и, как уже было сказано выше, любое нестандартное поведение снаряда крайне неудобно делать на дефолте - скилшоты, мультиснаряды, нестандартные типы движения, взаимодействие с рельефом, обработка столкновений между снарядами и так далее.
PT153 #23 - 2 недели назад (отредактировано ) 0
Без бонусов прибавлять ничего не нужно
А вот с бонусными характеристиками непонятно
Небольшая ошибочка, урон выбирается в границах (base + dice number * 1, base + dice number * dice sides).

Так что всё понятно, ты забыл прибавить dice number, что есть 2, а потому бонусы учитываются.
GetLocalPlayer #24 - 2 недели назад 0
Не понимаю каким боком мы создаем лишние способности?
Речь о нестандартном поведении снаряда. Вряд ли бы вопрос поднимался, укладывайся задача в базовый набор способностей. Да и чисто с архитектурной точки зрения, полностью триггерный снаряд это объективно здравый ход в сравнении с отловом урона и баффа.
NazarPunk #25 - 2 недели назад 4
Дополнил статью, и итоговый результат уже будет трудно реализовать на стандартных способностях))
PT153 #26 - 2 недели назад (отредактировано ) 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 у самой способности занулять всегда. Кастовать в цикле можно не все способности (например, способности с молниями нельзя), зависит от способности. Как правило таргетные способности со снарядом можно.
Которому тоже можно посвятить целую статью.
Она есть, но лично я бы написал свою, так как в той даммик делается из дурного юнита.

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

В while мне хочется, чтоб target была видима только внутри цикла, потому такое решение. В идеале бы:
while local target = FirstOfGroup(GROUP) ~=nil do
--actions
end
Но lua так не умеет((
prog #28 - 2 недели назад (отредактировано ) 0
NazarPunk, Ну так запили свой итератор и делай это через for... Использование будет выглядеть примерно так:
for target in group_iter(GROUP) do
--actions
end
Где group_iter кастомный итератор принимающий группу и делающий перебор внутри, а переменная target автоматически объявляется как локальная для цикла, если я правильно помню.
Это, естественно, обойдется в лишний вызов функции на каждой итерации, так что в критических для производительности местах придется жертвовать красивостью кода ради скорости.
NazarPunk #29 - 2 недели назад (отредактировано ) 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
prog #30 - 2 недели назад (отредактировано ) 0
NazarPunk, ты сказал что хочешь локальную переменную в цикле - я сказал как её получить. А метод с BlzGroupUnitAt потестить, конечно, тоже стоит. Более того, его тоже можно завернуть в итератор при желании.
NazarPunk #31 - 2 недели назад (отредактировано ) 0
ты сказал что хочешь локальную переменную в цикле - я сказал как её получить.
Так я её и получил, а ты предлагаешь ещё пилить функцию, которая по твоим словам дорогое удовольствие.
А BlzGroupUnitAt потестить, конечно, тоже стоит.
Нужно все переборы потестить и repeat не забыть
repeat
	local target = FirstOfGroup(whichGroup)
	if target ~= nil then
		GroupRemoveUnit(whichGroup,target)
		-- action
	end
until target ~= nil
prog #32 - 2 недели назад (отредактировано ) 0
Так я её и получил, а ты предлагаешь ещё пилить функцию, которая по твоим словам дорогое удовольствие
Итератор позволяет инкапсулировать и повторно использовать общий код для итерации. Кроме мест в которых производительность критически важна, этот лишний вызов функции вполне окупается удобством использования.
Более того, в случае перебора списка юнитов, если так важна производительность перебора - эффективнее будет хранить их в таблице и не дергать нативки вобще. Но, естественно, при однократном переборе результата поиска юнитов нативкой других вариантов кроме перебора группы особо и нет.
NazarPunk #33 - 2 недели назад 2
prog, одним словом, нужно пилить статью "Все способы перебора юнитов" с тестами))
prog #34 - 2 недели назад 0
NazarPunk, ну и самое важное, наверно - при массовом переборе юнитов в группе надо используемые нативки в локалки писать для ускорения доступа к ним.
PT153 #35 - 2 недели назад (отредактировано ) 0
Ещё парочка ошибок.
Если форматирование всё равно ломается, разбей код внутри на блока кода.
Ставить на паузу таймер нужно, потому что ты работаешь с весьма малым периодом, а периодический таймер стартует повторно до запуска своей функции. Также ставить на паузу нужно, если периодический таймер может быть удалён действием извне.
В обоих случаях может произойти ситуация, что таймер запустит свою функцию и будет удалён, GetExpiredTimer() вернёт null, и все действия, которое по логике кода не должны быть выполнены, будут выполнены. Пауза решает данную проблему, потому вообще можно сделать так.
DestroyTimer_origin = DestroyTimer
DestroyTimer = function (t)
    PauseTimer(t)
    DestroyTimer_origin(t)
end
прикреплены файлы
NazarPunk #36 - 2 недели назад 0
PT153, поправил. Форматирование сломалось из-за невнимательности. Хук на уничтожение таймера хорошая вешь, нужно будет статью по хукам запилить и накидать туда полезных примеров. А то начнёшь писать статью, а сослаться некуда и вся полезная информация оказывается в комментариях, которые никто не читает((
PT153 #37 - 2 недели назад 0
NazarPunk, про события каста я давно хочу написать, но лень и хочу подождать выхода ремастера (хотя бы альфа-беты), чтобы информация было актуальна. Вообще, странно, что за 17 лет игры об этом мало информации.
NazarPunk #38 - 2 недели назад (отредактировано ) 2
но лень и хочу подождать выхода ремастера
лень злая штука, а вот врятли в ремастере сильно что-то изменится.
Вообще, странно, что за 17 лет игры об этом мало информации.
Так потому-что никто статьи не пишет, последняя статья от 26.06.2017 JASS: Курс молодого бойца и новичёк при прочтении сломает себе мозг ещё в предисловии. Единственная информация о событиях была найдена здесь:
» раскрыть
Unit - A unit Begins casting an ability – Это событие запускает триггер сразу же после каста спелла (Прим. перев. Мана еще не проплачена, кулдаун еще не начался). Это значит, что такое событие может и должно применяться только для запуска триггеров проверки дополнительных условий (проверка возможен ли каст в данный момент, например, расстояние между кастером и целью слишком мало и так далее). Если вы используете это событие как основное событие каста спелла, ловкие игроки получают возможность сжульничать и запустить триггер спелла, без начала кулдауна и проплаты маны.
Unit - A unit Starts the effect of an ability – Это событие запускает триггер в тот момент, когда спелл уже скастован, кулдаун уже начался и мана проплачена. Это событие идеально подходит для запуска триггера спелла.
Unit - A unit Finishes casting an ability – Это событие запускает триггер в момент, когда юнит завершил кастовать спелл. Это полезно в случаях когда, например, вам нужно удалить юнита, который скастовал определенный спелл, и вы хотите быть уверены, что эффект спелла появится. Например, вам нужно удалить юнита, кастующего спелл Heal, вы должны использовать именно это событие, иначе цель не будет вылечена.
PT153 #39 - 2 недели назад 0
в ремастере сильно что-то изменится.
С чем чёрт не шутит.
Единственная информация о событиях была найдена здесь
Вот тестовая карта, её нужно чутка дополнить для верного определения CHANNEL события. К слову, разницу между CHANNEL и CAST мне объяснили тут.
прикреплены файлы
Bergi_Bear #40 - 2 недели назад 0
PT153, а можно чуть подробней про PauseTimer(GetExpiredTimer()) , про запруживание таймера перед уничтожением я не первый раз это вижу (но не пойму почему), но какова природа? это утечка локального истёкшего таймера?
PT153 #41 - 2 недели назад (отредактировано ) 2
Ставить на паузу таймер нужно, потому что ты работаешь с весьма малым периодом, а периодический таймер стартует повторно до запуска своей функции. Также ставить на паузу нужно, если периодический таймер может быть удалён действием извне.
В обоих случаях может произойти ситуация, что таймер запустит свою функцию и будет удалён, GetExpiredTimer() вернёт null, и все действия, которое по логике кода не должны быть выполнены, будут выполнены. Пауза решает данную проблему, потому вообще можно сделать так.
Ты можешь сделать периодический таймер с периодом 0, внутри коллбека выводить его хендл и удалять таймер. По идее, таймер запустит коллбек 1 раз, но через сообщения ты увидишь, что это не так. Добавишь паузу перед удалением -> будет 1 раз.

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