Введение
Значит-с, я тестировал обновление для моей библиотеки по получению брони и решил в функции GetUnitArmor убрать проверку на то, что юнит жив, и посмотреть, что будет. Результат - эта статья. Но сначала немного о том, как устроена эта библиотека.
Чтобы посчитать броню, юниту, переданному в GetUnitArmor, наносится урон, который потом отлавливается триггером внутри библиотеки. Чтобы триггер сработал, на переданного юнита нужно зарегистрировать событие, причём единожды, ведь в нескольких регистрациях нет смысла. Для этого была создана группа. Если юнит не в группе, то он добавляется в группу и на него регистрируется событие. В следующий раз, когда для этого юнита будет вызвана GetUnitArmor, он уже будет в группе и процесс регистрации будет пропущен.
Однако со временем юниты удаляются из игры, в том числе и зарегистрированные. Очищать триггер от событий можно только полным пересозданием триггера, что весьма затруднительно, да и утечка от события для удалённого юнита если и есть, то несущественная. А нужно ли очищать группу?
Я долго считал, что удалённые юниты сами удаляются из групп. В этом легко убедиться:
- Создайте 10 юнитов и добавьте их в группу.
- Вызовите CountUnitsInGroup и выведите результат. Будет показано 10.
- Убейте одного юнита, дождитесь его полного разложения.
- Вызовите CountUnitsInGroup снова. Теперь результат будет 9.
А до 1.31 другого способа посчитать размер группы и не было. Но на самом деле юнит всё ещё находится в группе, что можно проверить в Reforged с помощью функции BlzGroupGetSize: она вернёт 10. Таким образом, несуществующие или удалённые юниты не удаляются из групп автоматически, просто функция ForGroup, которая используется в CountUnitsInGroup для перебора всех юнитов группы, игнорирует таких юнитов.
В моём же случае нахождение большого числа несуществующих юнитов в группе может сказаться на скорости проверки наличия регистрации, поэтому я написал функцию очищения группы.
код
private function PurgeRegisteredUnits takes nothing returns nothing
local unit u
local group g = RegisteredUnits
set RegisteredUnits = CreateGroup()
loop
set u = FirstOfGroup(g)
exitwhen u == null
call GroupRemoveUnit(g, u)
if UnitExists(u) then
call GroupAddUnit(RegisteredUnits, u)
endif
endloop
call DestroyGroup(g)
set g = null
endfunction
Ничего необычного: старую группу записываем в локальную переменную, новую - в глобальную переменную, перебираем старую группу через цикл с FirstOfGroup, если юнит существует, то добавляем его в новую. После завершения цикла удаляем старую группу.
Обнаружение проблемы
В тестовой карте есть 8 юнитов, на каждый тип брони. Над головой у каждого есть трекер с различной информацией, в том числе с текущим значением брони. Трекер обновляется 32 раза в секунду. Таким образом, в самом старте теста у меня 8 зарегистрированных юнитов, что отражено в 8 сообщениях дебага.
Как я уже сказал в самом начале, в GetUnitArmor я отключил проверку, что юнит жив. Проверка создана с помощью AI нативки UnitAlive. Если юнит мёртв или не существует, эта функция возвращает false. То есть, я убрал не только проверку, что юнит жив, но также и проверку, что юнит вообще существует, что также входило в мои планы.
Я выбираю всех рабочих и приказываю им убить рабочего с бронёй Normal, он пятый в ряду. Пока его труп разлагается, ничего интересного не происходит. Но как только труп разложился, начался спам регистрации несуществующего юнита.
"Ого! - подумал я, - Это же увеличивает счётчик регистрации, который по достижении 1000 приведёт к очистке группы. А я как раз хотел это проверить". Я закрыл игру, добавил в код функции очистки дебаг сообщения, снова запустил тест и проделал всё тоже самое. Осталось дождаться, пока счётчик дотикает и вызовет очистку.
код
private function PurgeRegisteredUnits takes nothing returns nothing
local unit u
local group g = RegisteredUnits
local integer n = 0
local integer i = 0
set RegisteredUnits = CreateGroup()
call DebugMsg("Purging RegisteredUnits group")
loop
set u = FirstOfGroup(g)
exitwhen u == null
call GroupRemoveUnit(g, u)
set n = n + 1
if UnitExists(u) then
call GroupAddUnit(RegisteredUnits, u)
set i = i + 1
endif
endloop
call DebugMsg("RegisteredUnits group purged, old size is " + I2S(n) + ", new size is " + I2S(i))
call DestroyGroup(g)
set g = null
endfunction
В потоке спама регистрации я должен был увидеть два сообщения: первое о начале очистки, второе о завершении с количеством юнитов до очистки и после. До очистки должно быть 8, а после - 7. Но произошло следующее:
Изначально было всего 4 юнита? Новая группа содержит только 4 юнита? 3 уже зарегистрированных юнита снова зарегистрированы? Я несколько раз перепроверил код функции очистки. Может я где-то перепутал переменные? Нет, всё верно.
У тут меня осенило. Полностью разложившийся юнит. Ведь он был пятым. И он всё ещё в группе.
Решение
Когда часы пробивают полночь, карета превращается в тыкву, а когда юнит удаляется из игры, все ссылки на него внутри групп превращаются в null. Точнее, функция FirstOfGroup их конвертирует в null. Когда в переборе доходила очередь до разложившегося юнита, функция FirstOfGroup вместо полноценной ссылки возвращала null, отчего срабатывало условие на выход из цикла, отчего оставшиеся 3 юнита так и не были добавлены в новую группу, и отчего они были зарегистрированы заново.
И что же сделать с этим несуществующим юнитом в группе? А с ним ничего нельзя сделать. Из группы удалить нельзя: функция GroupRemoveUnit его не удаляет даже при наличии полноценной ссылки. Проверка на наличие в группе - функция IsUnitInGroup - тоже не работает. Именно поэтому и был спам сообщений о регистрации несуществующего юнита: функция IsUnitInGroup возвращала false, хотя юнит был в группе.
Тут поможет только полное пересоздание группы. Благо ForGroup игнорирует несуществующих юнитов в группе, не вызывая для них переданную функцию. Ровно так я и решил свою проблему.
код
private function AddUnitToRegistered takes nothing returns nothing
set RegisteredUnitsCount = RegisteredUnitsCount + 1
call GroupAddUnit(RegisteredUnits, GetEnumUnit())
endfunction
private function PurgeRegisteredUnits takes nothing returns nothing
call DebugMsg("Purging RegisteredUnits group")
set RegisteredUnitsCount = 0
set TemporaryGroup = RegisteredUnits
set RegisteredUnits = CreateGroup()
call ForGroup(TemporaryGroup, function AddUnitToRegistered)
call DebugMsg("RegisteredUnits group purged, new size is " + I2S(RegisteredUnitsCount))
endfunction
Выводы
- Если функция FirstOfGroup возвращает null, это не значит, что группа пуста. Возможно, это несуществующий юнит, который был когда-то добавлен в группу, а потом удалён.
- Если нет уверенности, что в группе нет несуществующих юнитов, то перебирайте её посредством ForGroup. Но будьте аккуратны с передачей туда параметров посредством глобальных переменных: действия в функции перебора могут вызвать функцию, которая и вызвала этот перебор, что перезапишет глобальные переменные.
- В Reforged для перебора можно использовать цикл с функциями BlzGroupGetSize и BlzGroupUnitAt с проверкой, что юнит в индексе существует/не null. Главное - не удаляйте юнитов во время перебора, либо начинайте перебор с конца.
Бонус
Пара полезных функций для проверки того, что юнит жив, мёртв или не существует.
function UnitExists takes unit u returns boolean
return GetUnitTypeId(u) != 0
endfunction
function UnitDoesNotExist takes unit u returns boolean
return GetUnitTypeId(u) == 0
endfunction
// Returns false if unit does not exist.
native UnitAlive takes unit id returns boolean
// Returns true if unit does not exist.
function UnitIsAlive takes unit u returns boolean
return not IsUnitType(u, UNIT_TYPE_DEAD)
endfunction
// Returns true if unit does not exist.
function UnitDead takes unit u returns boolean
return not UnitAlive(u)
endfunction
// Returns false if unit does not exist.
function UnitIsDead takes unit u returns boolean
return IsUnitType(u, UNIT_TYPE_DEAD)
endfunction
clear обнуляет полностью весь отряд
Ред. IceFog
Ред. OVOgenez