WarCraft 3: Hashtable - работаем с хеш-таблицей

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

Хеш-таблица — это структура данных, реализующая интерфейс ассоциативного массива, а именно, она позволяет хранить пары (ключ, значение) и выполнять три операции: операцию добавления новой пары, операцию поиска и операцию удаления пары по ключу. Не будем вдаваться в подробности принципа её работы, об этом вы можете прочитать здесь.
В статье мы рассмотрим самую распространённую область её применения в wc3: прикрепление данных к объекту, на простейшем примере. Статья предполагает, что читатель знаком с основами работы таймеров. Пример будет только на обычном Jass, для совместимости (да и не все умеют работать c v/cJass).
Если вы знакомы с кэшем в wc3, то принцип работы с ним схож, с принципом работы с хеш-таблицей. Только вместо строковых ключей, хеш-таблица использует целочисленные значения (integer).
Допустим, мы хотим создать спелл, в котором врагу на протяжении некоторого времени с малым периодом постоянно наносится урон (для которого wait не подходит).
Мы создали триггер (в редакторе триггеров, для простоты объяснения) с событием каста, дали ему условия и действия:
function Spell takes nothing returns nothing
    local unit caster = GetSpellAbilityUnit()  //Кастер
    local unit target = GetSpellTargetUnit()   //Цель
endfunction

//Проверка спелла
function SpellCond takes nothing returns boolean
    return GetSpellAbilityId()=='A000'
endfunction

//===========================================================================
function InitTrig_Spell takes nothing returns nothing
    set gg_trg_Spell = CreateTrigger()
    call TriggerRegisterPlayerUnitEvent(gg_trg_Spell,Player(0),EVENT_PLAYER_UNIT_SPELL_CAST,null)
    call TriggerAddCondition(gg_trg_Spell,Condition(function SpellCond))
    call TriggerAddAction(gg_trg_Spell,function Spell)
endfunction
Далее, нам нужен таймер, который будет периодически вызывать функцию, внутри которой наносится урон:
function SpellDamage takes nothing returns nothing
    call UnitDamageTarget(...)
endfunction

function Spell takes nothing returns nothing
    local unit caster = GetSpellAbilityUnit()  //Кастер
    local unit target = GetSpellTargetUnit()   //Цель
    local timer t = CreateTimer()              //Создаём таймер
    
    call TimerStart(t,0.04,true,function SpellDamage) //Стартуем таймер
endfunction
Но как передать в функцию, кто кому должен наносить урон, и сколько раз? Тут нам на помощь и приходит хеш-таблица. Перед работой нужно создать и инициализировать глобальную хеш-таблицу, желательно при инициализации карты.
» *1
Хеш-таблица - очень массивный объект и занимает много места в памяти, поэтому рекомендуется создавать только одну на все действия в карте. В противном случае, игра просто может слететь с фаталом или зависнуть от переполнения.
Делать это нужно только один раз, например в этом триггере:
function InitTrig_Spell takes nothing returns nothing
    set gg_trg_Spell = CreateTrigger()
    call TriggerRegisterPlayerUnitEvent(gg_trg_Spell,Player(0),EVENT_PLAYER_UNIT_SPELL_CAST,null)
    call TriggerAddCondition(gg_trg_Spell,Condition(function SpellCond))
    call TriggerAddAction(gg_trg_Spell,function Spell)
    
    set udg_hash = InitHashtable() //Инициализируем хеш-таблицу
endfunction
» *2
Если в карте имеется несколько спеллов, то рекомендуется инициализировать хеш-таблицу в отдельном действии/триггере при инициализации, чтобы избежать накладок во время редактирования или переноса.
Работает хеш-таблица так: [ключ|значение]. Только как ключ мы используем уникальный id объекта, а точнее - id нашего таймера.
На него и будем сохранять нужные нам данные:
function Spell takes nothing returns nothing
    local unit caster = GetSpellAbilityUnit()  //Кастер
    local unit target = GetSpellTargetUnit()   //Цель
    local timer t = CreateTimer()              //Создаём таймер
    local integer h = GetHandleId(t)           //Узнаём id таймера
    
    //Сохраняем объекты с ключом - id таймера
    call SaveUnitHandle(udg_hash,h,1,caster)   //Сохраняем кастера со значением 1
    call SaveUnitHandle(udg_hash,h,2,target)   //Сохраняем цель со значением 2
    call SaveInteger(udg_hash,h,3,125)         //Сохраняем количество ударов, из расчёта, что урон наносится в течение 5 секунд (5/0.04=125).
    
    call TimerStart(t,0.04,true,function SpellDamage) //Стартуем таймер
    
    //Не забываем устранять утечки
    set caster = null
    set target = null
    set t = null
endfunction
» *3
Также можно делать без создания переменной h:
call SaveUnitHandle(udg_hash,GetHandleId(t),1,caster)
Утечек это не вызовет, но немного снизит производительность.
» *4
Аналогичными действиями сохраняются и другие объекты, например группа:
call SaveGroupHandle(udg_hash,h,1,some_group)
Думаю, нет смысла перечислять все функции, так как на это существуют function-листы.
» *5
Некоторые утверждают, что сохранение объектов под ключами 1,2,3... неудобно и неуниверсально, и предлагают сохранять через уникальное значение строки:
call SaveUnitHandle(udg_hash,h,StringHash("caster"),caster)
С ними можно согласиться, вам не придётся давать и запоминать цифры для значений. Вы можете использовать такой способ для удобства.
Готово, данные сохранены, теперь их можно будет достать в функции нанесения урона, на которую запущен таймер.
Доставать данные мы будем тоже по id таймера:
function SpellDamage takes nothing returns nothing
    local timer t = GetExpiredTimer()                  //Наш таймер - истёкший
    local integer h = GetHandleId(t)                   //Узнаём id таймера
    local unit caster = LoadUnitHandle(udg_hash,h,1)   //Достаём кастера из значения 1
    local unit target = LoadUnitHandle(udg_hash,h,2)   //Достаём цель из значения 2
    local integer counter = LoadInteger(udg_hash,h,3)  //Достаём количество ударов
    
    if counter>0 then //Если количество ударов больше 0
        call UnitDamageTarget(caster,target,1.0,true,true,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_NORMAL,null) //Наносим урон цели
        call SaveInteger(udg_hash,h,3,counter-1) //Сохраняем количество ударов, убавленное на 1
    else //Иначе 
        call DestroyTimer(t) //Уничтожаем таймер
        //Очищаем хеш-таблицу, чтобы избежать утечек и наложений
        call FlushChildHashtable(udg_hash,h) //Очищаем ключ по id
    endif
    
    //Не забываем устранять утечки
    set caster = null
    set target = null
    set t = null
endfunction
» *6
Очень часто встречается неправильная конструкция очистки, приводящая к утечкам:
call DestroyTimer(t)
call FlushChildHashtable(udg_hash,GetHandleId(t))
В данном случае, очистка произведена не будет, так как таймер уничтожается раньше получения его id, поэтому в функцию очистки будет подано неправильное значение (0).
Если вы используете конструкцию без переменной с id, то делать нужно так:
call FlushChildHashtable(udg_hash,GetHandleId(t))
call DestroyTimer(t)
То есть сначала очищать, а потом удалять таймер.
В случае с переменной, порядок этих действий значения не имеет.
Спелл готов, данные записываются, достаются и удаляются из хеш-таблицы.
Вот что у нас получилось в итоге:
function SpellDamage takes nothing returns nothing
    local timer t = GetExpiredTimer()
    local integer h = GetHandleId(t)
    local unit caster = LoadUnitHandle(udg_hash,h,1)
    local unit target = LoadUnitHandle(udg_hash,h,2)
    local integer counter = LoadInteger(udg_hash,h,3)
    
    if counter>0 then
        call UnitDamageTarget(caster,target,1.0,true,true,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_NORMAL,null)
        call SaveInteger(udg_hash,h,3,counter-1)
    else
        call DestroyTimer(t)
        call FlushChildHashtable(udg_hash,h)
    endif
    
    set caster = null
    set target = null
    set t = null
endfunction

function Spell takes nothing returns nothing
    local unit caster = GetSpellAbilityUnit()
    local unit target = GetSpellTargetUnit()
    local timer t = CreateTimer()
    local integer h = GetHandleId(t)
    
    call SaveUnitHandle(udg_hash,h,1,caster)
    call SaveUnitHandle(udg_hash,h,2,target)
    call SaveInteger(udg_hash,h,3,125)
    
    call TimerStart(t,0.04,true,function SpellDamage)
    
    set caster = null
    set target = null
    set t = null
endfunction

function SpellCond takes nothing returns boolean
    return GetSpellAbilityId()=='A000'
endfunction

//===========================================================================
function InitTrig_Spell takes nothing returns nothing
    set gg_trg_Spell = CreateTrigger()
    call TriggerRegisterPlayerUnitEvent(gg_trg_Spell,Player(0),EVENT_PLAYER_UNIT_SPELL_CAST,null)
    call TriggerAddCondition(gg_trg_Spell,Condition(function SpellCond))
    call TriggerAddAction(gg_trg_Spell,function Spell)
    
    set udg_hash = InitHashtable()
endfunction
К статье прикрепляю карту-пример со спеллом.

Просмотров: 11 452

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


Это сообщение удалено
SkiL #2 - 6 лет назад -3
есть же разарта статья. или тут есть что то, не рассмотренное у него?
Это сообщение удалено
Msey #4 - 6 лет назад 2
у разарта всё написано по-дибильному, а эта статья - то, что надо.. легка в изучении
XyZoD #5 - 6 лет назад -2
Жалко что не всегда так просто может быть как с сохранением в хэндл таймера(
Hanabishi #6 - 6 лет назад 3
добавил примечания
Амбидекстрия #7 - 6 лет назад -2
call SaveInteger(udg_hash,h,3,counter-1)
зачем, если можно просто set counter = counter -1? или лууп не связан с функцией? объясни напу.
RunixMing47 #8 - 6 лет назад 2
Srezik, а ты попробуй сделать так как ты написал...
counter - это всего лишь локальная переменная в которую мы выгрузили сохраненный на таймер integer, сохранить измененное число нужно что бы в следующий раз загрузить уже измененное число, а не тоже самое.
The Requiem #9 - 6 лет назад 3
Отличная статья.
Zahanc #10 - 5 лет назад 3
Юху! Сколько же времени я зря мучался до этого! Спасибо!
Faion #11 - 5 лет назад 3
>Хеш-таблица - очень массивный объект и занимает много места в памяти, поэтому рекомендуется создавать только одну на все действия в карте. В противном случае, игра просто может слететь с фаталом или зависнуть от переполнения.
Пруф?
Hanabishi #12 - 5 лет назад 2
Faion, внезапно =)
Имеется в виду, что создание хеш-таблиы это утечка, весящая значительно больше любых хендлов, так как её нельзя удалять. Поэтому если при каждом действии создавать новую, память довольно быстро забивается.
pAxsIs #13 - 5 лет назад -2
Если не ошибаюсь, то здесь наоборот нужно выполнить действия
call DestroyTimer(t) Уничтожаем таймер
Очищаем хеш-таблицу, чтобы избежать утечек и наложений
call FlushChildHashtable(udg_hash,h) Очищаем ключ по id
Потому что "t" используется в этом:
local integer h = GetHandleId(t) Узнаём id таймера
Если я прав, то исправте пожалуйста)
Hanabishi #14 - 5 лет назад 2
pAxsIs
В случае с переменной, порядок этих действий значения не имеет.
pAxsIs #15 - 5 лет назад -2
Все я понял) не много ступил
Я решил, что переменная постоянно обновляется, поэтому и решил, что нельзя.
Faion #16 - 5 лет назад 6
Имеется в виду, что создание хеш-таблиы это утечка, весящая значительно больше любых хендлов, так как её нельзя удалять. Поэтому если при каждом действии создавать новую, память довольно быстро забивается.
Кто тебе это сказал? Хт удаляется этой функцией FlushParentHashtable();
DKdevastatorWE #17 - 3 года назад 0
А не будет "противоречий", если я сделаю несколько разных спелов с ключом "ID таймера"?
nvc123 #18 - 3 года назад 0
DKdevastatorWE, там 2 значения
первым значением бери хэндл таймера а вторым число(для первого скила это 1,для второго 2,для третьего 3)
а ещё лучше не юзай хэш таким образом
можно ведь перебирать ячейки хэш-таблицы 1 таймером
DKdevastatorWE #19 - 3 года назад (отредактировано ) 0
nvc123, я имею в виду, если я создам 2 разных таймера в разных триггерах(т.е. разных спелах), у них может получится одинаковый id, когда я их буду вводить в хэш таблицу?
nvc123 #20 - 3 года назад 0
DKdevastatorWE, то что ты называешь ид это хэндл объекта(указатель на объект)
одновременно не может быть 2 объекта с одинаковыми хэндлами