, Гильдия «Черамор»

WarCraft 3: [lua] Воскрешаем героя

» Раздел: Триггеры и объекты
Для воскрешения героя нужно убедиться что он умер. Для этого можно использовать два события:
  • EVENT_PLAYER_UNIT_DEATH
  • EVENT_PLAYER_HERO_REVIVABLE
Они не реагируют на смерть героя с возможностью перерерождения и можно выбирать любую. Задержку между сообщением о гибели и событием EVENT_PLAYER_HERO_REVIVABLE можно настроить в игровых константах:
(DissipateTime) Время разложения (сек.): исчезновение героя
Теперь напишем простейшее воскрешение героя играющего человека в стартовой локации и в дальнейшем будем использовать как шаблон.
local ReviveTrigger = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS do
	local player = Player(i)
	if GetPlayerController(player) == MAP_CONTROL_USER and GetPlayerSlotState(player) == PLAYER_SLOT_STATE_PLAYING then
		TriggerRegisterPlayerUnitEvent(ReviveTrigger, player, EVENT_PLAYER_HERO_REVIVABLE)
	end
end
TriggerAddAction(ReviveTrigger, function()
	local hero      = GetTriggerUnit()
	local heroOwner = GetOwningPlayer(hero)
	if GetPlayerController(heroOwner) ~= MAP_CONTROL_USER or GetPlayerSlotState(heroOwner) ~= PLAYER_SLOT_STATE_PLAYING then return end
	local x, y = GetPlayerStartLocationX(heroOwner), GetPlayerStartLocationY(heroOwner)
	ReviveHero(hero, x, y, true) -- здесь в дальнейшем будем воскрешать героев
end)

Таймер

Часто используется в AoS картах. Идея проста, после смерти отсчитываем время зависящее от уровня героя и возрождаем. Реализация тоже не очень сложна:
local timer  = CreateTimer()
local dialog = CreateTimerDialog(timer)
TimerDialogSetTitle(dialog, GetUnitName(hero))
TimerStart(timer, GetHeroLevel(hero) * 2, false, function()
	local x, y   = GetPlayerStartLocationX(heroOwner), GetPlayerStartLocationY(heroOwner)
	ReviveHero(hero, x, y, true)
	DestroyTimerDialog(dialog)
	DestroyTimer(GetExpiredTimer())
end)
TimerDialogDisplay(dialog, true)
» Код
do
	local InitGlobalsOrigin = InitGlobals
	function InitGlobals()
		InitGlobalsOrigin()
		
		local ReviveTrigger = CreateTrigger()
		for i = 0, bj_MAX_PLAYER_SLOTS do
			local player = Player(i)
			if GetPlayerController(player) == MAP_CONTROL_USER and GetPlayerSlotState(player) == PLAYER_SLOT_STATE_PLAYING then
				TriggerRegisterPlayerUnitEvent(ReviveTrigger, player, EVENT_PLAYER_HERO_REVIVABLE)
			end
		end
		TriggerAddAction(ReviveTrigger, function()
			local hero      = GetTriggerUnit()
			local heroOwner = GetOwningPlayer(hero)
			if GetPlayerController(heroOwner) ~= MAP_CONTROL_USER or GetPlayerSlotState(heroOwner) ~= PLAYER_SLOT_STATE_PLAYING then return end

			local timer  = CreateTimer()
			local dialog = CreateTimerDialog(timer)
			TimerDialogSetTitle(dialog, GetUnitName(hero))
			TimerStart(timer, GetHeroLevel(hero) * 2, false, function()
				local x, y   = GetPlayerStartLocationX(heroOwner), GetPlayerStartLocationY(heroOwner)
				ReviveHero(hero, x, y, true)
				DestroyTimerDialog(dialog)
				DestroyTimer(GetExpiredTimer())
			end)
			TimerDialogDisplay(dialog, true)
		end)
	
	end
end

Камень Воскрешения

Если вы играли в кампанию Рексара, то наверно видели камни возрождения.
А если вы открывали редактор, то наверно заметили, что их несколько.
Отобразим это в коде. Заодно покроем случай, когда вам захочется сделать несколько типов камней для разных локаций.
local stonesId = { FourCC('nbse'), FourCC('nbsw') }
Так как работать с группой разных юнитов будет удобней, чем каждый раз проверять id в цикле, заодно и создадим группу для камней.
local stonesGroup = CreateGroup()
Исходя из того, что для каждого игрока может быть активным только один камень, создадим таблицу, в которой будем хранить активный камень по id игрока.
local playerStone = {}
Учитывая что у игрока может не быть активного камня, а возрождаться ему где-то нужно, напишем функцию поиска ближайшего камня:
---@param x real
---@param y real
---@return unit
local GetClosestStone = function(x, y)
	local stone
	local distance
	for index = BlzGroupGetSize(stonesGroup) - 1, 0, -1 do
		local unit   = BlzGroupUnitAt(stonesGroup, index)
		local dx, dy = GetUnitX(unit) - x, GetUnitY(unit) - y
		local dist   = math.sqrt(dx * dx + dy * dy)
		if stone == nil or dist <= distance then
			stone    = unit
			distance = dist
		end
	end
	return stone
end
Для реализации смены активного камня воспользуемся функцией TriggerRegisterUnitInRange. Правда этот способ не позволяет определить юнита к которому подходят, но мы уже написали прекрасную функцию GetClosestStone, так что мы молодцы проблема решена заранее.
local RangeTrigger    = CreateTrigger()
TriggerAddAction(RangeTrigger, function()
	local hero        = GetTriggerUnit()
	local heroOwner   = GetOwningPlayer(hero)
	local heroOwnerId = GetPlayerId(heroOwner)
	if not UnitAlive(hero)
			or not IsUnitType(hero, UNIT_TYPE_HERO)
			or GetPlayerController(heroOwner) ~= MAP_CONTROL_USER
			or GetPlayerSlotState(heroOwner) ~= PLAYER_SLOT_STATE_PLAYING
	then return end
	
	if playerStone[heroOwnerId] ~= nil then
		if heroOwner == GetLocalPlayer() then
			AddUnitAnimationProperties(playerStone[heroOwnerId], 'alternate', false)
		end
		UnitShareVision(playerStone[heroOwnerId], heroOwner, false)
	end
	playerStone[heroOwnerId] = GetClosestStone(GetUnitX(hero), GetUnitY(hero))
	if heroOwner == GetLocalPlayer() then
		AddUnitAnimationProperties(playerStone[heroOwnerId], 'alternate', true)
	end
	UnitShareVision(playerStone[heroOwnerId], heroOwner, true)
end)
Если вы внимательно читали, то наверное вспомните, что мы создали группу, но так ничего в неё не добавили. Пора исправить этот недостаток:
GroupEnumUnitsInRect(stonesGroup, bj_mapInitialPlayableArea)
for index = BlzGroupGetSize(stonesGroup) - 1, 0, -1 do
	local stone     = BlzGroupUnitAt(stonesGroup, index)
	local stoneId   = GetUnitTypeId(stone)
	local isExclude = true
	for i = 1, #stonesId do
		if stoneId == stonesId[i] then isExclude = false end
	end
	if isExclude then
		GroupRemoveUnit(stonesGroup, stone)
	else
		TriggerRegisterUnitInRange(RangeTrigger, stone, 256)
	end
end
В завершении добавим триггер воскрешения и можно наслаждаться результатом:
local ReviveTrigger = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS do
	local player = Player(i)
	if GetPlayerController(player) == MAP_CONTROL_USER and GetPlayerSlotState(player) == PLAYER_SLOT_STATE_PLAYING then
		TriggerRegisterPlayerUnitEvent(ReviveTrigger, player, EVENT_PLAYER_HERO_REVIVABLE)
	end
end
TriggerAddAction(ReviveTrigger, function()
	local hero        = GetTriggerUnit()
	local heroOwner   = GetOwningPlayer(hero)
	local heroOwnerId = GetPlayerId(heroOwner)
	if GetPlayerController(heroOwner) ~= MAP_CONTROL_USER or GetPlayerSlotState(heroOwner) ~= PLAYER_SLOT_STATE_PLAYING then return end
	local stone = playerStone[heroOwnerId] ~= nil and playerStone[heroOwnerId] or GetClosestStone(GetUnitX(hero), GetUnitY(hero))
	ReviveHero(hero, GetUnitX(stone), GetUnitY(stone), true)
end)
» Код
do
	local InitGlobalsOrigin = InitGlobals
	function InitGlobals()
		InitGlobalsOrigin()
		
		local stonesId        = { FourCC('nbse'), FourCC('nbsw') }
		local stonesGroup     = CreateGroup()
		local playerStone     = {}
		
		---@param x real
		---@param y real
		---@return unit
		local GetClosestStone = function(x, y)
			local stone
			local distance
			for index = BlzGroupGetSize(stonesGroup) - 1, 0, -1 do
				local unit   = BlzGroupUnitAt(stonesGroup, index)
				local dx, dy = GetUnitX(unit) - x, GetUnitY(unit) - y
				local dist   = math.sqrt(dx * dx + dy * dy)
				if stone == nil or dist <= distance then
					stone    = unit
					distance = dist
				end
			end
			return stone
		end
		
		local RangeTrigger    = CreateTrigger()
		TriggerAddAction(RangeTrigger, function()
			local hero        = GetTriggerUnit()
			local heroOwner   = GetOwningPlayer(hero)
			local heroOwnerId = GetPlayerId(heroOwner)
			if not UnitAlive(hero)
					or not IsUnitType(hero, UNIT_TYPE_HERO)
					or GetPlayerController(heroOwner) ~= MAP_CONTROL_USER
					or GetPlayerSlotState(heroOwner) ~= PLAYER_SLOT_STATE_PLAYING
			then return end
			
			if playerStone[heroOwnerId] ~= nil then
				if heroOwner == GetLocalPlayer() then
					AddUnitAnimationProperties(playerStone[heroOwnerId], 'alternate', false)
				end
				UnitShareVision(playerStone[heroOwnerId], heroOwner, false)
			end
			playerStone[heroOwnerId] = GetClosestStone(GetUnitX(hero), GetUnitY(hero))
			if heroOwner == GetLocalPlayer() then
				AddUnitAnimationProperties(playerStone[heroOwnerId], 'alternate', true)
			end
			UnitShareVision(playerStone[heroOwnerId], heroOwner, true)
		end)
		
		GroupEnumUnitsInRect(stonesGroup, bj_mapInitialPlayableArea)
		for index = BlzGroupGetSize(stonesGroup) - 1, 0, -1 do
			local stone     = BlzGroupUnitAt(stonesGroup, index)
			local stoneId   = GetUnitTypeId(stone)
			local isExclude = true
			for i = 1, #stonesId do
				if stoneId == stonesId[i] then isExclude = false end
			end
			if isExclude then
				GroupRemoveUnit(stonesGroup, stone)
			else
				TriggerRegisterUnitInRange(RangeTrigger, stone, 256)
			end
		end
		
		local ReviveTrigger = CreateTrigger()
		for i = 0, bj_MAX_PLAYER_SLOTS do
			local player = Player(i)
			if GetPlayerController(player) == MAP_CONTROL_USER and GetPlayerSlotState(player) == PLAYER_SLOT_STATE_PLAYING then
				TriggerRegisterPlayerUnitEvent(ReviveTrigger, player, EVENT_PLAYER_HERO_REVIVABLE)
			end
		end
		TriggerAddAction(ReviveTrigger, function()
			local hero        = GetTriggerUnit()
			local heroOwner   = GetOwningPlayer(hero)
			local heroOwnerId = GetPlayerId(heroOwner)
			if GetPlayerController(heroOwner) ~= MAP_CONTROL_USER or GetPlayerSlotState(heroOwner) ~= PLAYER_SLOT_STATE_PLAYING then return end
			local stone = playerStone[heroOwnerId] ~= nil and playerStone[heroOwnerId] or GetClosestStone(GetUnitX(hero), GetUnitY(hero))
			ReviveHero(hero, GetUnitX(stone), GetUnitY(stone), true)
		end)
	
	end
end

Заключение

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


Views: 1 532

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


prog #1 - 2 years ago (изм. ) 2
Голосов: +2 / -0
Использовать поиск ближайшего камня в триггере входа в радиус это немного оверкил - я бы скорее хранил координаты всех камней в списке и перебирал его простой проверкой на нахождение точки в прямоугольнике - мы ведь гарантированно находимся на определенном расстоянии от камня в момент когда происходит эта проверка и нам нет необходимости реально искать ближайший камень, достаточно определить возле какого мы находимся - минус лишняя математика вычисления расстояния и минус лишние итерации в цикле если наш камень не последний в массиве.
NazarPunk #2 - 2 years ago (изм. ) 0
Голосов: +0 / -0
Использовать поиск ближайшего камня в триггере входа это немного оверкил
Поиск ближайшего камня ещё используется для случая, если у героя ещё нет активного камня. Так что это обыкновенное переиспользование кода. Притом подход к камню не такое частое событие, а перебор группы не такой ресурсоёмкий, чтоб экономить на спичках во время пожара)
prog #3 - 2 years ago 1
Голосов: +1 / -0
NazarPunk, я понимаю где еще это используется, но между экономией лишних десяти строк кода или экономией долей миллисекунды выполнения кода я выберу второе ;)
Ну и да, воскрешение у ближайшего камня не всегда хорошая идея - если, например, ближайший камень находится в каком-то секретном или временно непроходимом месте и у игрока еще нет активного камня, то результат будет плачевным - я скорее принудительно выдавал бы игроку какой-то камень на старте и не парился обработкой кейса "камня нет", а в таком случае и поиск ближайшего камня в коде не появляется и можно смело "экономить на спичках" т.к. нет реюза функции поиска ближайшего камня ;)
NazarPunk #4 - 2 years ago (изм. ) 0
Голосов: +0 / -0
Преждевременная оптимизация — корень всех зол.
В статье показан принцип действия с минимальным количеством кода. В реальной карте могут быть дополнительные условия и ограничения, только после которых есть смысл заниматься оптимизацией.
prog #5 - 2 years ago (изм. ) 0
Голосов: +0 / -0
NazarPunk, если бы я занимался оптимизацией - подумал бы как затолкать эти камни в подобие бинарного дерева чтобы ускорить поиск xD

или вы знаете интересные способы воскрешения
Чтобы разбавить критику конструктивом - расскажу свой способ воскрешения.
Это вариация воскрешения у камней, но с нюансами. Главное отличие - триггер привязки камня реализован через покупку предмета в магазине, а не через вход в радиус. Это позволяет ходить возле "камня" не боясь случайно перепривязаться в неудобном месте и заодно позволяет добавить стоимость к процессу привязки камня. А в моем случае я использую еще и перезарядку продажи, чтобы несколько игроков не могли без задержки привязаться к одному камню.
NazarPunk #6 - 2 years ago 0
Голосов: +0 / -0
Главное отличие - триггер привязки камня реализован через покупку предмета в магазине
Тогда всё конечно проще - используешь камень на камень и ненужно искать ближайший камень))
prog #7 - 2 years ago 0
Голосов: +0 / -0
Тогда всё конечно проще - используешь камень на камень и ненужно искать ближайший камень))
Все еще проще - камень продает предметы и привязкой считается момент продажи, после чего предмет удаляется)
NazarPunk #8 - 2 years ago (изм. ) 0
Голосов: +0 / -0
Все еще проще - камень продает предметы и привязкой считается момент продажи, после чего предмет удаляется)
Таким образом можно сделать привязку к другим камням. А потом всего-то нужно умереть для телепортации))
prog #9 - 2 years ago 0
Голосов: +0 / -0
Таким образом можно сделать привязку к другим камням. А потом можно умереть вместо телепортации))
Герой покупает специальный предмет (или юнита) в точке воскрешения. В момент покупки купленый предмет удаляется, а герой привязывается к точке где купил предмет - никакого способа привязаться к точке не находясь в радиусе действия её магазина.
Ярг Восьмой #10 - 2 years ago 0
Голосов: +0 / -0
Давайте ещё сделаем мобильный камень воскрешения, который из предмета в инвентаре, превращается в полноценный камень воскрешения, который вскоре типа рассыпается, как только воскресит героя.
Хотя на сколько я много раз тестировал группы перебора, то всегда первым объектом бывает тот, кто ближе к [ x, y ]. Хотя, может быть, просто тогда были совпадения. Раньше я думал, что он просто пихает туда юниты из общего набора, которые по дистанции просто ближе к точке.
NazarPunk #11 - 2 years ago 0
Голосов: +0 / -0
Давайте ещё сделаем мобильный камень воскрешения, который из предмета в инвентаре, превращается в полноценный камень воскрешения, который вскоре типа рассыпается, как только воскресит героя.
Зачем в полноценный, можно создать эффект могилки, как при воскрешении и привязать её к герою. При смерти воскресить на ней. Будет время запилю.
Хотя на сколько я много раз тестировал группы перебора, то всегда первым объектом бывает тот, кто ближе к [ x, y ]. Хотя, может быть, просто тогда были совпадения.
Совпадения. Когда делал заклинание понял, что никакой зависимости нет, пришлось сортировать ручками.
ScopteRectuS #12 - 2 years ago 4
Голосов: +4 / -0
или вы знаете интересные способы воскрешения
Знаю весьма интересный способ воскрешения героев, который обычно используется во всяких ивентах от вальв в дота 2. Суть заключается в том, что после смерти героя, на месте умершего героя появляется крест перерождения. Если союзные герои нажимают ПКМ по кресту перерождения, то герой начинает воскрешаться. Естесственно процесс можно ускорить, если будут воскрешать мертвого героя сразу несколько союзных героев. В случае, когда процесс воскрешения прерывается, прогресс воскрешения так же обнуляется.

Сделал для своей карты такую же систему. Крест сделан из обычного здания, который ремонтируют союзные герои. В течении всей жизни креста тикает таймер, который проверяет здоровье креста, если он фуловый, то воскрешаем героя и удаляем крест перерождения.
» Код
-----------------------------------------------------------------------------
--  H E R O E S :   R E V I V E                                            --
-----------------------------------------------------------------------------

function RestoreHero( player )
    if hero[ player ] ~= nil then

        if UnitAlive( hero[ player ] ) then
            SetUnitState( hero[ player ], UNIT_STATE_LIFE, GetUnitState( hero[ player ], UNIT_STATE_MAX_LIFE ) )
            SetUnitState( hero[ player ], UNIT_STATE_MANA, GetUnitState( hero[ player ], UNIT_STATE_MAX_MANA ) )
            
        else
            ShowUnit( grave[ player ], false )
            ReviveHero( hero[ player ], GetUnitX( hero[ player ] ), GetUnitY( hero[ player ] ), true )
            SetUnitState( hero[ player ], UNIT_STATE_LIFE, GetUnitState( hero[ player ], UNIT_STATE_MAX_LIFE ) )
            SetUnitState( hero[ player ], UNIT_STATE_MANA, GetUnitState( hero[ player ], UNIT_STATE_MAX_MANA ) )
                                            
            if IsUnitSelected( grave[ player ], GetLocalPlayer( ) ) then
                SelectUnit( grave[ player ], false )
                SelectUnit( hero[ player ], true )
            end
            
            PauseTimer( heroReviveTimer[ player ] )
        end
    end
end

function InitHeroRevive( )
    grave           = { }
    graveLife       = { }
    heroReviveTimer = { }

    ForForce( HEROES_FORCE, 
        function( )
            local enumPlayer = GetEnumPlayer( )

            grave[ enumPlayer ]           = CreateUnit( enumPlayer, BUILDING_GRAVE, HIDDEN_X, HIDDEN_Y, 0.0 )
            heroReviveTimer[ enumPlayer ] = CreateTimer( )

            ShowUnit( grave[ enumPlayer ], false )
            SetUnitPathing( grave[ enumPlayer ], false )
        end 
    )

    TriggerRegisterForceUnitEvent( CreateTrigger( ), HEROES_FORCE, EVENT_PLAYER_UNIT_DEATH, nil, 
        function( )
            if IsUnitHero( GetDyingUnit( ) ) then
                local player = GetOwningPlayer( GetDyingUnit( ) )

                graveLife[ player ] = 1.0

                SetWidgetLife( grave[ player ], graveLife[ player ] )
                SetUnitX( grave[ player ], GetUnitX( hero[ player ] ) )
                SetUnitY( grave[ player ], GetUnitY( hero[ player ] ) )
                ShowUnit( grave[ player ], true )

                if IsUnitSelected( hero[ player ],  GetLocalPlayer( ) ) then
                    SelectUnit( hero[ player ], false )
                    SelectUnit( grave[ player ], true )
                end
                    
                TimerStart( heroReviveTimer[ player ], 0.1, true, 
                    function( ) 
                        if GetWidgetLife( grave[ player ] ) > graveLife[ player ] and GetWidgetLife( grave[ player ] ) < 100.0 then
                            graveLife[ player ] = GetWidgetLife( grave[ player ] )
                    
                        elseif GetWidgetLife( grave[ player ] ) <= graveLife[ player ] and GetWidgetLife( grave[ player ] ) < 100.0 then
                            graveLife[ player ] = 1.0
                            SetWidgetLife( grave[ player ], graveLife[ player ] )
                    
                        elseif GetWidgetLife( grave[ player ] ) >= 100.0 then
                            RestoreHero( hero[ player ] )
                        end
                    end 
                )
            end
        end 
    )
end
NazarPunk #14 - 2 years ago (изм. ) 0
Голосов: +0 / -0
Крест сделан из обычного здания, который ремонтируют союзные герои.
Не всегда герои умеющие ремонтировать подходят. Можно сделать проще: создать эффектом могилку и сверху полоской отображать воскрешение. Ну а находящиеся рядом союзные герои просто ускоряют процесс)) Так как полоска через юнита, ей можно дать ауру регена хп, чтоб обоим выгода была)
ScopteRectuS #15 - 2 years ago (изм. ) 0
Голосов: +0 / -0
NazarPunk, нет, там суть была в том, что противники могут сбить процесс вокрешения, чтобы воскрешающие начинали процесс воскрешения с самого начала. Если посмотрите мой код, там идёт постоянная проверка на то, что могила ремонтируется, в случае прерывания воскрешения весь прогресс сбрасывается. А способность ремонта, вроде, можно настроить на то, чтобы он работал только на могилы.
prog #16 - 2 years ago 3
Голосов: +3 / -0
Перехватывать smart на могилу, выдавать канал и приказывать кастовать его. При сбивании каста забирать канал у героя. Прогресс отображать через хп могилы, которое таймером повышается пропорционально кол-ву активных кастов канала.
МрачныйВорон #18 - 2 years ago (изм. ) 0
Голосов: +0 / -0
у тебя воскрешение мультиплеерное? интересно можно ли морфу через гетлокал плеер задать. кстати помню много способов воскрешении интересных что то такое уникальное, но к сожалению щас не могу придумать ничего такого да не хочется этим заниматься)
NazarPunk #19 - 2 years ago 0
Голосов: +0 / -0
у тебя воскрешение мультиплеерное?
В статье всё мультиплэерно-мультиюнитно))