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

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Этот комментарий удален
25
Говоря проще, твой юзкейс - это полноценный бред, который не встречается нигде.
Так статья на то и нужна, что предупреждает людей так не делать. Ты ведь не думаешь, что зашедшие читать эту статью знают правильные юзкейсы?
Может там конечно не совсем ясно донесена мысль, но суть такая: не создавайте таблицы в больших количествах. Все остальное вы уже начинаете додумывать.
20
Говоря проще, твой юзкейс - это полноценный бред, который не встречается нигде.
Так статья на то и нужна, что предупреждает людей так не делать. Ты ведь не думаешь, что зашедшие читать эту статью знают правильные юзкейсы?
Может там конечно не совсем ясно донесена мысль, но суть такая: не создавайте таблицы в больших количествах. Все остальное вы уже начинаете додумывать.
Ну, это уже другой юзкейс, я повторюсь "Я говорю, если локальные таблицы постоянно создавать." если ты имеешь ввиду local hashtable, то таких юзкейсов нет вообще. Если же ты просто про "каждая система - своя хештаблица", то даже так добраться до 256 надо очень и очень постараться. Но я тебя понял, ты делаешь акцент на совсем далёких от джасса, которые ещё и умудрились до хеша добраться. :D
Закончим тем, что фактической необходимости в удалении хештаблицы - нет, ибо за всё это время ещё никто не жаловался на лимит хештаблиц.
25
Закончим тем, что фактической необходимости в удалении хештаблицы - нет
Да. Если не создаешь, то и удалять не надо. А глобальные в любом случае существуют всю игру.
ибо за всё это время ещё никто не жаловался на лимит хештаблиц.
Не совсем так xgm.guru/p/wc3/hashtable8000
20
Закончим тем, что фактической необходимости в удалении хештаблицы - нет
Да. Если не создаешь, то и удалять не надо. А глобальные в любом случае существуют всю игру.
... )0
ибо за всё это время ещё никто не жаловался на лимит хештаблиц.
Не совсем так xgm.guru/p/wc3/hashtable8000
И где это в итоге применялось? Нигде? О чём и речь.
18
И где это в итоге применялось? Нигде? О чём и речь.
Если вы были бы автор этой работы, то имели бы представление о том где это используется. Так что призываю использовать те аргументы, за которые можете нести ответственность
20
И где это в итоге применялось? Нигде? О чём и речь.
Если вы были бы автор этой работы, то имели бы представление о том где это используется. Так что призываю использовать те аргументы, за которые можете нести ответственность
Влод, не воспринимай в штыки, но кроме тебя это кто-то использует? Если да, то покажи мне пожалуйста карту или что-нибудь где это чудо-юдо имеет место быть. Утомляете вы оба "если что-то есть, значит оно нужно". Речь о распространении тех или иных систем и в целом их применении. Если же ты один сам и используешь свою систему - это не значит, что она "о том где это используется", если случай единичный. Я разобрал предостаточно карт и что-то нигде этой системы не увидел, хватит уже пытаться оправдать глупости.
18
Конечно я могу рассказать о том кто это использовал и где, но это не изменит тот факт, что вы сочли возможным оценить "применяемость" работы без объективных на то предпосылок, из чего следует:
  1. Что на самом деле объективные предпосылки вам не нужны, иначе вы бы изначально поинтересовались у меня или у других компетентных людей по вопросу "применяемости".
  2. Что упор был сделан на сугубо на личное мнение.
Потому предлагаю сначала изучить примеры в работе, которые были сделаны как раз для ознакомления начинающих, и только после этого мы сможем начать конструктивное обсуждение в соответствующей теме
20
Конечно я могу рассказать о том кто это использовал и где, но это не изменит тот факт, что вы сочли возможным оценить "применяемость" работы без объективных на то предпосылок, из чего следует:
  1. Что на самом деле объективные предпосылки вам не нужны, иначе вы бы изначально поинтересовались у меня или у других компетентных людей по вопросу "применяемости".
  2. Что упор был сделан на сугубо на личное мнение.
Потому предлагаю сначала изучить примеры в работе, которые были сделаны как раз для ознакомления начинающих, и только после этого мы сможем начать конструктивное обсуждение в соответствующей теме
Стрелка поворачивается и обратно, ну, скинь эти примеры, я посмотрю и я более чем уверен, что прямо необходимости превышать лимит хештаблиц в 256 имеет смысл (что я и писал выше). Если же тебя так задевает, что я не считаю твою наработку полезной, то уж извини, моё мнение в этом плане не измениться, ибо просто на просто захломлять карту псевдо API ещё и через cjass - ну нонсенс.
28
ибо просто на просто захломлять карту псевдо API
так ты ж делал псевдо-группы для эффектов, предметов и прочего, вместо массива
20
ибо просто на просто захломлять карту псевдо API
так ты ж делал псевдо-группы для эффектов, предметов и прочего, вместо массива
Верно, но я не считаю её нужной-необходимой. Она даёт удобство работы с группировкой тех хендлов на которых нет стандартного API, но даже так оно имеет больше смысла, нежели расширение количества хештаблиц и всяких надстроек базированных на этом - имеют очень мало смысла, ибо достигнуть коллизии стрингхеша не так уж и легко, разве что только специально стараться это сделать.
По большей части, при особом желании, на всю карту хватит даже 1 хештаблицы, чёрт с ним пусть будет 10. Но куда больше 256? Молчу уже о 8000. В самой игре внутренних хеш таблиц всего 259 (по моей памяти, может и меньше). Потому я не пойму, что такого нужно сделать, чтобы код карты требовал больше хештаблиц, чем вся полноценная игра.
Ну и закончу тем, что моё псевдоАПИ в корне отличается по своему смыслу, потому их сравнивать немного неправильно, однако что моя, что та наработка - не являются де-факто обязательными.
23
Добавьте, пожалуйста, в статью дополнение:
Что если в функции создаётся новый триггер (например, для группы юнитов через событие получения урона) и есть какие либо вводные данные, то можно сохранить данные в хэш по get handle Id этого триггера, а потом в trigger action'е выгружать данные из хэша через get handle id ( get Triggering Trigger() ).
Подобное можно также провернуть и в функции callback для for group или в функции фильтра boolexpr.
Ответы (11)
28
EugeAl, разве об этом нужно писать? Когда игрок уловил что такое хэндл, он поймет, что у них есть айди, по которому можно сохранять значения
23
rsfghd, как показывает практика, нужно разжевывать. По таймеру он скопирует, а вот как без таймера перенести - может и не понять
30
как показывает практика, нужно разжевывать.
Как показывает практика, если нужно разжёвывать то не нужно разжёвывать.
23
nazarpunk, rsfghd, можете сами глянуть, какие вопросы обычно задают. 80% примерно одно и то же, по смежным темам, хотя на сайте 100500 статей.
Если же следовать логике - "не надо разжевывать" и "сами должны понять", то можно прямо сейчас снести все статьи с сайта, и пусть сами варик изучают, ибо нефиг. И тратят килотонны часов на изучение изученного и изобретение колёс, вместо того, чтобы потратить их на стоящий проект или новые открытия. Чисто армейский способ получится - подметание ломом и покраска листвы в зелёный цвет, зато в деле
30
EugeAl, видимо статью как думать через мозг ты так и не нашёл, раз тебе нужно разжёвывать.
23
nazarpunk, а я не про себя писал
А вообще, нормальная статья должна быть такой, чтобы даже круглый идиот её мог понять. Если статью поймут 1,5 человека, "с мозгом", как ты говоришь, ценность такой статьи как источника информации и тем более образовательного контента - равна нулю. Это всё равно что сделать игру, которая запускается только на твоём компе и у пары друзей, и нигде больше. И какой толк от такой игры? Кто играть то будет? Здесь также.
В идеале должны быть ещё перекрёстные ссылки на другие статьи, для полноты, но что имеем, то имеем
30
А вообще, нормальная статья должна быть такой, чтобы даже круглый идиот её мог понять.
Как идиот сможет понять статью которую он не сможет прочитать? Он же идиот.
28
EugeAl, справедливости ради, кому-то действительно не дано кодить*/триггерить, но они могут быть хороши в других делах
23
nazarpunk, прочитать идиот сможет, тут мозгов не надо, в спецшколах их кое-как читать учат. А вот понять итд...
rsfghd, ну если человек даже после разжеванной статьи не научится, тогда ему действительно надо другими делами заниматься. Но все же большинство осилит базу. А дальше зависит от устремлений. Но я к тому, что делать изначально статью для "избранных" - бестолковая затея, разве что ЧСВ ради
30
Но я к тому, что делать изначально статью для "избранных"
Умение думать через мозг теперь считается уделом избранных?
30
nazarpunk, ну и для справки напомню - ты всегда можешь написать свою статью и показать нам, имбицилам, как нужно статьи правильно писать.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.