Хеш-таблица — это структура данных, реализующая интерфейс ассоциативного массива, а именно, она позволяет хранить пары (ключ, значение) и выполнять три операции: операцию добавления новой пары, операцию поиска и операцию удаления пары по ключу. Не будем вдаваться в подробности принципа её работы, об этом вы можете прочитать здесь.
В статье мы рассмотрим самую распространённую область её применения в wc3: прикрепление данных к объекту, на простейшем примере. Статья предполагает, что читатель знаком с основами работы таймеров. Пример будет только на обычном Jass, для совместимости (да и не все умеют работать c v/cJass).
Если вы знакомы с кэшем в wc3, то принцип работы с ним схож, с принципом работы с хеш-таблицей. Только вместо строковых ключей, хеш-таблица использует целочисленные значения (integer).
Допустим, мы хотим создать спелл, в котором врагу на протяжении некоторого времени с малым периодом постоянно наносится урон (для которого wait не подходит).
Допустим, мы хотим создать спелл, в котором врагу на протяжении некоторого времени с малым периодом постоянно наносится урон (для которого 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 таймера:
Доставать данные мы будем тоже по 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, то делать нужно так:
Если вы используете конструкцию без переменной с 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
К статье прикрепляю карту-пример со спеллом.
2 - поясняют что за ошибка и что делалось что бы ее избежать, экстрасенсы в отпуске
Ред. Unryze
0x28 = 40 байт.
У нас нет действия типа DestroyHashtable, то есть таблицы нельзя удалять. Поэтому если в каком-то действии постоянно создаются таблицы, память будет течь.
Ред. Unryze
Непонятно какая речь про жалкость 40 байт, если люди умудряются память даже точками засрать, которые казалось бы имеют размер еще меньше.
Суть в том, что если есть объект, который можно создать, но нельзя удалить, это утечка. В варе такого дофига.
Hanabishi:
Hanabishi: