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
К статье прикрепляю карту-пример со спеллом.
`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
0
6
10 лет назад
Отредактирован DKdevastatorWE
0
nvc123, я имею в виду, если я создам 2 разных таймера в разных триггерах(т.е. разных спелах), у них может получится одинаковый id, когда я их буду вводить в хэш таблицу?
2
28
10 лет назад
2
DKdevastatorWE, то что ты называешь ид это хэндл объекта(указатель на объект)
одновременно не может быть 2 объекта с одинаковыми хэндлами
2
7
7 лет назад
2
Спасибо автору, хорошая и понятная статья.
0
1
4 года назад
0
Попробовал переделать триггер под перемещение юнита
function Move takes nothing returns nothing
	local timer t = GetExpiredTimer()
	local integer h = GetHandleId(t)
	local unit caster = LoadUnitHandle(udg_hash,h,1)
	local integer counter = LoadInteger(udg_hash,h,2)

	if counter>0 then
		call SetUnitFacingToFaceLocTimed(caster, GetSpellTargetLoc(),0)
		call SetUnitPositionLoc(caster,PolarProjectionBJ(GetUnitLoc(caster),40.00,0))
		call SaveInteger(udg_hash,h,2,counter-1)
	else
		call DestroyTimer(t)
		call FlushChildHashtable(udg_hash,h)
	endif

	set caster = null
	set t = null
endfunction

function Dash takes nothing returns nothing
	local unit caster = GetSpellAbilityUnit()
	local timer t = CreateTimer()
	local integer h = GetHandleId(t)
	call SaveUnitHandle(udg_hash,h,1,caster)
	call SaveInteger(udg_hash,h,2,125)

	call TimerStart(t,0.04,true, function Move)

	set caster = null
	set t = null
endfunction

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

//===========================================================================
function InitTrig_Dash takes nothing returns nothing
	set gg_trg_Dash = CreateTrigger(  )
	call TriggerRegisterAnyUnitEventBJ( gg_trg_Dash, EVENT_PLAYER_UNIT_SPELL_EFFECT )
	call TriggerAddCondition( gg_trg_Dash,Condition(function SpellCond))
	call TriggerAddAction( gg_trg_Dash, function Dash )
	set udg_hash = InitHashtable()
endfunction
И не могу понять, где ошибка (я нубло), если убрать элемент если/тогда/инчае функции Move, и без подсчета целочисленной, то юнит двигается, но бесконечно, пока не упрется в край карты. Подскажите, что здесь нужно исправить?
0
26
4 года назад
0
И не могу понять, где ошибка
1 - используют дебаг
2 - поясняют что за ошибка и что делалось что бы ее избежать, экстрасенсы в отпуске
0
1
4 года назад
0
Hate:
И не могу понять, где ошибка
1 - используют дебаг
2 - поясняют что за ошибка и что делалось что бы ее избежать, экстрасенсы в отпуске
Уже не нужно XD
0
26
4 года назад
0
Николай8:
Hate:
И не могу понять, где ошибка
1 - используют дебаг
2 - поясняют что за ошибка и что делалось что бы ее избежать, экстрасенсы в отпуске
Уже не нужно XD
в следующий раз лучше воспользоваться модулем QA
2
15
2 года назад
2
Поскольку эта статья входит в список статей XGM по Варкрафту, думаю стоит доработать её, а именно информацию о ресурсоёмкости хэш-таблиц, миф о которых развеивается Анрайзом: xgm.guru/p/wc3/Jass-MythBusters-9RA
0
17
2 года назад
0
думаю стоит доработать её, а именно информацию о ресурсоёмкости хэш-таблиц
Что за ресурсоёмкость
4
20
2 года назад
Отредактирован Unryze
4
думаю стоит доработать её, а именно информацию о ресурсоёмкости хэш-таблиц
Что за ресурсоёмкость
Речь об этом высказывании:
Что не является правдой, ибо память выделятся динамически для каждой ячейки. Есть изначальный размер CGameHashTableManager (который существует по факту всегда), ну и каждая ХТ - имеет свой вес:
Размер Хештаблицы:
То бишь 0x34 байта выделены всегда, каждая ХТ по стандарту - это 0x28 байт опять, и каждая новая ячейка - 4 байта (исключение boolean (1 байт) и string (каждый символ = 1 байт)).
0x34 = 52 байта.
0x28 = 40 байт.
Загруженные файлы
0
26
2 года назад
0
Спустя 10 лет еще кому-то не пофиг? Вообще я в комментах отвечал, что имелось в виду.
У нас нет действия типа DestroyHashtable, то есть таблицы нельзя удалять. Поэтому если в каком-то действии постоянно создаются таблицы, память будет течь.
То бишь 0x34 байта выделены всегда, каждая ХТ по стандарту - это 0x28 байт опять, и каждая новая ячейка - 4 байта (исключение boolean (1 байт) и string (каждый символ = 1 байт)).
Здесь главный вопрос деаллокается ли память обратно при удалении элементов. Если да, то утечку памяти хотя бы можно минимизировать очисткой. Если нет, то это вообще жопа и мое утверждение верно на 100%.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.