Введение

Значит-с, я тестировал обновление для моей библиотеки по получению брони и решил в функции 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
`
ОЖИДАНИЕ РЕКЛАМЫ...
3
Очищает ли GroupClear() от несуществующих юнитов?
Ответы (6)
28
MACOH, хороший вопрос, думаю да, надо в рефе проверить.
15
PT153, не дошли руки проверить в итоге или все же проверил?
19
Meddin, не похоже, чтобы память утекала.
код
void __cdecl GroupClear(DWORD whichGroup)
{
    CGroup *g;

    g = ConvertHandle<CGroup>(whichGroup);
    if ( g )
        CGroup::Clear(g);
}

void __thiscall CGroup::Clear(CGroup *this)
{
    this->units.vtable->Clear(&this->units);
}

void __thiscall CUnitSet::Clear(CUnitSet *this)
{
    CUnitListNode *node;

    while (TRUE)
    {
        node = this->items.root.value;
        if ( (int)node <= 0 )
            break;
        CUnitListNode::~CUnitListNode(this->items.root.value);
        SMemFree(node, CUnitListNode::TypeName, -2, 0);
    }
    this->size = 0;
}
18
Помню в карте какой-то видел как раз в несколько секунд (мб 30-60) глобальный триггер для отлова урона пересоздавали и заново вешали события урона на юнитов из группы. Саму группу не помню чтоб пересоздавали, делали через ForGroup вроде.

По поводу ForGroup и повторных вызовов перебора, это касается не только этой функции, а впринципе логики перевызовов (что касается глобалок для передачи данных - их можно сразу в локалки запихнуть и к ним уже обращаться).
Например рефлект урона - если на 2ух юнитах будет висеть рефлект, при нанесении урона одним юнитом другому, событие получения урона будет постоянно перевызываться. Чтоб такого небыло делают логическую глобалку которая не позволяет отражать отраженный урон.
Ответы (2)
28
OVOgenez, триггер пересоздать можно, просто это муторно, да и толку мало.
что касается глобалок для передачи данных - их можно сразу в локалки запихнуть и к ним уже обращаться
Не поможет. Колбеки вызывается по порядку для каждого юнита. Если колбек уже для первого юнита триггерит перевызов, глобалки перезапишутся и колбеки для последующих юнитов будут работать с изменёнными глобалками.
20
У тебя очепятка и в итоге противоречие самому себе
Загруженные файлы
Чтобы оставить комментарий, пожалуйста, войдите на сайт.