What is a Hashtable?
Hashtables are data structures. A variable array works by storing multiple pieces of data, imagine it as the X axis in a graph. Now imagine a variable array that is complicated enough to store data not just along the X axis, but also the Y. This allows us to have a much more varied variable (pardon the wordplay) Hashtables work by saving the value of the variable until you need it later. It then loads the value of the variable so it can be used with the exact same data as before.
Why use a Hashtable?
Simply put, hashtables are easy ways to make your spells MUI. Spells that are not MUI will immediately be rejected from the Spells section of THW. Unless you only have a single instance of a unit on your map, not being MUI will cause multiple casts of the spell to malfunction. Hashtables have allowed me to advance worlds ahead of were I was when I started triggering. They are open ended and any spell that isn't instant will need hashtables or recycling.
At the same time, using hashtables has its drawbacks. It is sometimes quite annoying to have to load every value, and if you make a mistake in a spell it can be quite a long time before you realize you didn't save or load a value correctly. Hashtables are also slower than arrays.
Now let's look at how to make a hashtable.
Trigger
Events
Map initialization
Conditions Actions
Hashtable - Create a hashtable Set EX_Hash = (Last created hashtable)
What does that Trigger Above Mean?
Broken down, the trigger means that at the start of the map, a hashtable will be created. The hashtable will be assigned the variable EX_Hash. Most hashtables are going to be created at the start of the map. You need to assign the hashtable a variable name. This is because you will probably have multiple hashtables. If you import a spell or a system, it will probably have a hashtable associated with it. If you assign your variables to (last created hashtable) instead of your hashtable variable, you may accidently load a value from a different hashtable!
Handles and Hashtables
What is a Handle?
A handle is anything that is not a real, boolean, integer, or string. Handles can be saved with hashtables, allowing us to extract exact information portaining to that handle whenever we want to. Simply put, it's like saving a new variable for each handle, even if the variable is changed later, we can load the value of whatever the handle is and still have the original starting variable.
Here is an example of a hashtable without handles. While it is still useful, it can be even better with them
Trigger
Events
Map initialization
Conditions Actions
Hashtable - Create a hashtable Set EX_Hash = (Last created hashtable)
Events Conditions Actions
Set EX_Real = 20.00 Hashtable - Save EX_Real as 1 of 1 in EX_Hash Set EX_Real = 4566.00 Set EX_Real = 12345.00 Set EX_Real = 100000000.00 Set EX_Real = (Load 1 of 1 from EX_Hash)
In this example, when the even happens, the real number 20 will be saved into value 1 of 1 in the hashtable EX_Hash. Afterwards, you can change the variable however you like, and it will not affect the value 1 of 1 in the hashtable. You can load the value after changing EX_Real, and the loaded value will be what you first saved it as, in this example the number is 20.
How can we use Handles Effectively?
Handles are used in conjunction with hashtables. A handle describes the value being saved. If you were saving EX_Real to the triggering unit of an ability, you change it to say (key (triggering unit)). Key simply is a way of identifying handles. Let's look below at a simple damage over time spell that uses handles to correctly damage every unit targetted. Please note that leaks will be addressed fully in this example.
Trigger
Events
Unit - A unit Starts the effect of an ability
Conditions
(Ability being cast) Equal to Damage Over Time
Actions
Set EX_Target = (Target unit of ability being cast) Set EX_Damage = 20.00 Set EX_Timer = 5.00 Unit Group - Add (Target unit of ability being cast) to EX_Group Hashtable - Save EX_Timer as 1 of (Key (Target unit of ability being cast)) in EX_Hash
Events
Time - Every 1.00 seconds of game time
Conditions Actions
Unit Group - Pick every unit in EX_Group and do (Actions)
Loop - Actions
Set EX_Timer = (Load 1 of (Key (Picked unit)) from EX_Hash) If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
EX_Timer Greater than 0.00
Then - Actions
Unit - Set life of (Picked unit) to ((Life of (Picked unit)) - EX_Damage) Hashtable - Save (EX_Timer - 1.00) as 1 of (Key (Picked unit)) in EX_Hash
Else - Actions
Unit Group - Remove (Picked unit) from EX_Group Hashtable - Clear all child hashtables of child (Key (Picked unit)) in EX_Hash
This is a lot to look at. Spend a minute or two trying to roughly get the idea of what is happening in this spell. The targetted unit of the ability "Damage Over Time" is being added to the group "EX_Group". Every second, members of the group are checked to see how much time they have left in the spell. If they are not done with the duration of the spell, they suffer twenty damage. If they are done with the duration of the spell, they are removed from the damage group, and the child hashtables are cleared. This means that all that extra data solely belonging to the picked unit is destroyed. It is the hashtable equivelent of destroying memory leaks. Always make sure to include it at the end of spells. The absolute most important thing to pay attention to right now is how the hashtable values are saved. They correspond to how you identify the unit. EX_Timer is saved in the first part as (key (target unit of ability being cast)). At the second trigger it is being referenced to with (key (picked unit)). This is because the unit is being selected as picked unit instead of target unit of ability being cast. This difference in handles is crucial to understanding the beast known as the hashtable. If you are going to use a trigger like this, please make it more efficient by assigning unit references (picked unit, for example) to variables and then calling them.
Quick guide for recreating this trigger
EX_Timer is a real
EX_Group is a unit group
EX_Damage is a real
Where can Handles Take You?
Handles are quite possibly the most important parts of hashtables. It allows it to be MUI, and it gives hashtables their dynamic quality. You must learn to effectively use handles if you want your spells to be MUI.
Set udg_VARIABLE = GetHandleId(udg_UNIT)
A great little line, this baby is highly important to hashtables. Used with custom script, this will automatically pick the correct key corresponding to the unit. In the custom script, VARIABLE will be the name of an Integer variable, while UNIT will be the name of the unit variable. Look below for an example of how to use this.
Set Unit = (Picked unit) Custom script: set udg_Handle = GetHandleId(udg_Unit) -------- Handle is my integer variable, Unit is my unit variable -------- Hashtable - Save Timer as 1 of Handle in Hash
Useful Examples of the Hashtable in Action
Please note that triggers are in the third section. Both spells are by Jazztastic, please give credit if used. Main purpose of these is to show examples of how hashtables and handles can be used effectively in spells
Tank Attack!, a Spell With Movement
Most spells use dummy units to deal damage and create visual effects for the spell. "Movement" is a term I use when talking about physically moving the dummy unit to help create effects for a spell.
Provided for you is the trigger to "Tank Attack!", a spell that creates a horde of miniature tanks to run over your enemies. Tanks are created every 0.04 seconds. 25 tanks are created over 1 second.
Missile Launch, a Spell With Knockback
Knockback is when a spell causes a unit to slide backwards from its spot. It is effectively stunned because it can take no actions. Knockback is a desireable effect to have in a map because it looks nice, and is more interesting than just a stun.
Provided for you is the trigger to "Missile Launch", a spell that shoots a missile at target location. Upon coming within range of a unit, the missile explodes, damaging all units in X range and knocking them back a random distance between X and X. It is similar to Tank Attack! in that it uses movement, but I consider this a step up from Tank Attack!. I recommend using imported explosions for the explosion effects.
The Triggers and Explainations for Tank Attack! and Missile Launch
Trigger
Variables
Events
Map initialization
Conditions Actions
-------- ------------------- -------- -------- Tank Attack -------- -------- ------------------- -------- Hashtable - Create a hashtable Set TA_Hash = (Last created hashtable) Set TA_Aoe = 100.00 Set TA_Damage = 1.00 -------- ------------------- -------- -------- Missile Launch -------- -------- ------------------- -------- Hashtable - Create a hashtable Set ML_Hash = (Last created hashtable) Set ML_Aoe = 120.00
I personally like to keep my hashtables seperate for my spells. It is not necessary, but I recommend it to help keep track of your stuff. Also, a few variables that only need to be loaded once are present in this map init trigger
Tank Attack Trigger
Tank Attack Cast
Events
Unit - A unit Starts the effect of an ability
Conditions
(Ability being cast) Equal to Copter Attack!
Actions
Set TA_Caster = (Triggering unit) Sound - Play SteamTankBeep <gen> Set TA_Level = (Level of (Ability being cast) for TA_Caster) Set TA_Target = (Target point of ability being cast) Set TA_Time = 1.00 Unit Group - Add TA_Caster to TA_Group1 Hashtable - Save TA_Level as 1 of (Key (Triggering unit)) in TA_Hash Hashtable - Save TA_Time as 2 of (Key (Triggering unit)) in TA_Hash Custom script: call RemoveLocation(udg_TA_Target) Trigger - Turn on Tank Attack Loop <gen>
Tank Attack Loop
Events
Time - Every 0.04 seconds of game time
Conditions Actions
Unit Group - Pick every unit in TA_Group1 and do (Actions)
Loop - Actions
Set TA_Level = (Load 1 of (Key (Picked unit)) from TA_Hash) Set TA_Time = (Load 2 of (Key (Picked unit)) from TA_Hash) If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
TA_Time Greater than 0.00
Then - Actions
Set TA_Caster = (Picked unit) Set TA_Point1 = (Position of TA_Caster) Unit - Create 1 Mini Tank for (Owner of TA_Caster) at TA_Point1 facing ((Facing of TA_Caster) + (Random real number between -25.00 and 25.00)) degrees Set TA_Tank = (Last created unit) Unit - Add a 2.00 second Generic expiration timer to TA_Tank Unit - Make TA_Tank Explode on death Unit Group - Add TA_Tank to TA_Group2 Hashtable - Save TA_Level as 1 of (Key (Last created unit)) in TA_Hash Hashtable - Save (TA_Time - 0.04) as 2 of (Key (Picked unit)) in TA_Hash Custom script: call RemoveLocation(udg_TA_Point1) Set TA_Loops = (TA_Loops + 1)
Else - Actions
Hashtable - Clear all child hashtables of child (Key (Picked unit)) in TA_Hash Unit Group - Remove (Picked unit) from TA_Group1
Unit Group - Pick every unit in TA_Group2 and do (Actions)
Loop - Actions
Sound - Play SteamTankMovement <gen> Set TA_Level = (Load 1 of (Key (Picked unit)) from TA_Hash) Set TA_Tank = (Picked unit) Set TA_Point1 = (Position of TA_Tank) Set TA_Point2 = (TA_Point1 offset by 25.00 towards (Facing of TA_Tank) degrees) Unit - Move TA_Tank instantly to TA_Point2 Custom script: set bj_wantDestroyGroup = true Unit Group - Pick every unit in (Units within TA_Aoe of TA_Point2 matching (((Matching unit) belongs to an enemy of (Owner of TA_Tank)) Equal to True)) and do (Actions)
Loop - Actions
Special Effect - Create a special effect attached to the chest of (Triggering unit) using Objects\Spawnmodels\Human\HumanBlood\BloodElfSpellThiefBlood.mdl Special Effect - Destroy (Last created special effect) Unit - Cause TA_Tank to damage (Picked unit), dealing (TA_Damage x (Real(TA_Level))) damage of attack type Spells and damage type Normal
Tank Attack End
Events
Unit - A unit Dies
Conditions
(Unit-type of (Triggering unit)) Equal to Mini Tank
Actions
Unit Group - Remove (Triggering unit) from TA_Group2 Hashtable - Clear all child hashtables of child (Key (Triggering unit)) in TA_Hash Set TA_Loops = (TA_Loops - 1) If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
TA_Loops Less than or equal to 0
Then - Actions
Trigger - Turn off (This trigger)
Else - Actions
In the first trigger, notice the sound being played. You will have to use the sound editor to get this sound from the game. In the second trigger it is important to notice that both the movement for the units and their creation run off the same timer.
Quick guide for recreating this trigger
TA_Angle is a real
TA_Aoe is a real
TA_Caster is a unit
TA_Tank is a unit
TA_Damage is a real
TA_Group1 is a unit group
TA_Group2 is a unit group
TA_Hash is a hashtable
TA_Level is an integer
TA_Point1 is a point
TA_Point2 is a point
TA_Point3 is a point
TA_Target is a point
TA_Timer is a real
TA_Loops is an integer
Following are the triggers for Missile Launch
Missile Launch Triggers
Missile Launch Cast
Events
Unit - A unit Starts the effect of an ability
Conditions
(Ability being cast) Equal to Missile Launch
Actions
Set ML_Caster = (Triggering unit) Set ML_Point1 = (Position of ML_Caster) Unit - Create 1 Missile for (Triggering player) at ML_Point1 facing (Facing of ML_Caster) degrees Unit Group - Add (Last created unit) to ML_Group Set ML_Timer = 2.00 Hashtable - Save ML_Timer as 1 of (Key (Last created unit)) in ML_Hash Trigger - Turn on Missile Launch Loop <gen> Custom script: call RemoveLocation(udg_ML_Point1)
Missile Launch Loop
Events
Time - Every 0.04 seconds of game time
Conditions Actions
Unit Group - Pick every unit in ML_Group and do (Actions)
Loop - Actions
Set ML_Timer = (Load 1 of (Key (Picked unit)) from ML_Hash) Set ML_Missile = (Picked unit) If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
ML_Timer Greater than 0.00
Then - Actions
Hashtable - Save (ML_Timer - 0.04) as 1 of (Key (Picked unit)) in ML_Hash Set ML_Point1 = (Position of ML_Missile) Set ML_Point2 = (ML_Point1 offset by 30.00 towards (Facing of ML_Missile) degrees) Unit - Move ML_Missile instantly to ML_Point2 Custom script: set bj_wantDestroyGroup = true If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
(Number of units in (Units within ML_Aoe of ML_Point2 matching (((Matching unit) belongs to an enemy of (Owner of ML_Missile)) Equal to True))) Equal to 0
Then - Actions
Custom script: set bj_wantDestroyGroup = true Unit Group - Pick every unit in (Units within 300.00 of ML_Point2 matching (((Matching unit) belongs to an enemy of (Owner of ML_Missile)) Equal to True)) and do (Actions)
Loop - Actions
Unit - Cause ML_Missile to damage (Picked unit), dealing 250.00 damage of attack type Siege and damage type Demolition Unit Group - Add (Picked unit) to ML_Group2 Set ML_Point3 = (Position of (Picked unit)) Set ML_Angle = (Angle from ML_Point2 to ML_Point3) Set ML_Timer = 1.40 Set ML_Distance = ((Random real number between 50.00 and 150.00) x 0.04) Hashtable - Save ML_Timer as 1 of (Key (Picked unit)) in ML_Hash Hashtable - Save ML_Angle as 2 of (Key (Picked unit)) in ML_Hash Hashtable - Save ML_Distance as 3 of (Key (Picked unit)) in ML_Hash Set ML_Loops = (ML_Loops + 1) Custom script: call RemoveLocation(udg_ML_Point3)
Unit - Explode ML_Missile Special Effect - Create a special effect at ML_Point2 using war3mapImported\ExplosionBIG.mdx Special Effect - Destroy (Last created special effect) Set ML_Timer = 0.00 Hashtable - Save ML_Timer as 1 of (Key (Picked unit)) in ML_Hash
Hashtable - Clear all child hashtables of child (Key (Picked unit)) in TA_Hash Unit - Explode ML_Missile Unit Group - Remove ML_Missile from ML_Group
Unit Group - Pick every unit in ML_Group2 and do (Actions)
Loop - Actions
Set ML_KnockBackUnit = (Picked unit) If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
(ML_KnockBackUnit is alive) Equal to True
Then - Actions
Set ML_Timer = (Load 1 of (Key (Picked unit)) from ML_Hash) If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
ML_Timer Greater than 0.00
Then - Actions
Hashtable - Save (ML_Timer - 0.04) as 1 of (Key (Picked unit)) in ML_Hash Set ML_Angle = (Load 2 of (Key (Picked unit)) from ML_Hash) Set ML_Distance = (Load 3 of (Key (Picked unit)) from ML_Hash) Set ML_Point1 = (Position of ML_KnockBackUnit) Set ML_Point2 = (ML_Point1 offset by ML_Distance towards ML_Angle degrees) Unit - Move ML_KnockBackUnit instantly to ML_Point2 Special Effect - Create a special effect attached to the origin of ML_KnockBackUnit using Abilities\Spells\Human\FlakCannons\FlakTarget.mdl Special Effect - Destroy (Last created special effect) Unit - Order ML_KnockBackUnit to Stop
Else - Actions
Hashtable - Clear all child hashtables of child (Key (Picked unit)) in ML_Hash Unit Group - Remove (Picked unit) from ML_Group2 Set ML_Loops = (ML_Loops - 1) If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
ML_Loops Less than or equal to 0
Then - Actions
Trigger - Turn off (This trigger)
Else - Actions
Else - Actions
Hashtable - Clear all child hashtables of child (Key (Picked unit)) in ML_Hash Unit Group - Remove ML_KnockBackUnit from ML_Group2 Set ML_Loops = (ML_Loops - 1) If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
ML_Loops Less than or equal to 0
Then - Actions
Trigger - Turn off (This trigger)
Else - Actions
Quick guide for recreating this trigger
ML_Angle is a real
ML_Aoe is a real
ML_Caster is a unit
ML_Distance is a real
ML_Group1 is a unit group
ML_Group2 is a unit group
ML_Hash is a hashtable
ML_KnockBackUnit is a unit
ML_Missile is a unit
ML_Point1 is a point
ML_Point2 is a point
ML_Point3 is a point
ML_Timer is a real
ML_Loops is an integer
Miscellaneous Hashtable Information
You are only allowed to have 256 hashtables
Having the creation of the hashtable in the trigger will not only make it innefficient, it will also make it NOT MUI
Multiple spells can use the same hashtable. You should have no more than 10 hashtables in your map
Hashtables are also useful for systems, not just spells
Credits
I would like to thank Wyrmlord for his awesome tutorial on hashtables. It helped get me going, but I didn't think it was clear enough, so heres mine :) I would also like to thank Dr. SuperGood and Maker for some information. Finally, I would like to thank Archangel for some trigger corrections. Please comment and rate, I like feedback.
Change Log
v1.0
Uploaded tutorial to The Hive.
v1.1
Fixed incorrect information, fixed example triggers, added Change Log and Future Changes to Come sections.
v1.2
Put [goto] and [point] into the tutorial to link the table of contents to the corresponding section.
v1.3
Put hidden ta gs around the triggers to increase readability.
v1.4
Updated spells in the example section. They now work better and without leaks.
v1.5
Added the Custom Script section.
Future Changes to Come
Any more feedback given by the community on information that is incorrect or needs to be added shall be considered. More example spells may follow.
Last edited by Jazztastic; 08-13-2011 at 06:32 AM.
I'd suggest adding a key, or a link to separate parts. (Click here, scrolls to basic info, click here 2, scrolls to Tank Attack!
Also, a map showing hashtables in action would help.
Also, this little line can help alot with hashtables, as it makes it much more efficient:
Custom script: set udg_YOURINTEGER = GetHandleId(udg_UNIT)
You probably should include linebreaks and documentation for your larger spells.
I recommend using AnarchainBedlams miniature tank model, because multiple units casting this spell with the regular steam tank's model will cause massive lag!
I'd suggest removing that line altogether, it can sound like advertising and has no major correlation with hashtables.
=======Triggering Errors/Suggestions=======
And - All (Conditions) are true
No point in that, its already set that way....
If (All Conditions are True) then do (Then Actions) else do (Else Actions)
((Picked unit) is alive) Equal to True
Set ML_KnockBackUnit = (Picked unit)
You could set ML_KnockBackUnit = (Picked Unit) before the if/then/else.
Custom script: set bj_wantDestroyGroup = true
If (All Conditions are True) then do (Then Actions) else do (Else Actions)
What's the point of that? Remove the bj_wantDestroyGroup = true
If - ConditionAnd - All (Conditions) are true Conditions
(ML_Group is empty) Equal to True (ML_Group2 is empty) Equal to True
You should use an integer and increase it when the spell is cast and decrease it when the spell is over so that you're not checking if the groups are empty every loop.
This looks very good, very useful too.
Last edited by Archangel678; 06-15-2011 at 02:17 PM.
Archangel I am fixing all your suggestions except for the last. I do not understand the method you are suggesting. How would using an integer be different, wouldn't you still have to check every loop whether the integer was zero or not?
Maker, thank you for the information. My bad for begging for rep.
I'm updating it now.
EDIT: I have updated my tutorial, with everything mentioned by both Archangel and Maker except for that integer check thing. Most notable I added change log and future updates section.
Set EX_Target = (Target unit of ability being cast) Custom script: set udg_ID = GetHandleId(udg_EXTarget) Set EX_Damage = 20.00 Set EX_Timer = 5.00 Unit Group - Add (Target unit of ability being cast) to EX_Group Hashtable - Save EX_Timer as 1 of udg_ID in EX_Hash
While during the loop time...
Custom script: set udg_ID = GetHandleId(GetEnumUnit()) Set EX_Timer = (Load 1 of udg_ID from EX_Hash)
Where ID is an integer variable
But Maker said it's GUI so stick to GUI, but the above is more efficient and faster...
I won't reaper things already mentioned. However you could you hidden tags for triggers, making your tutorials cleaner and easier to step through.
A very good and easy idea. Thank you
Quote:
Do like this during the casting time...
Set EX_Target = (Target unit of ability being cast)
Custom script: set udg_ID = GetHandleId(udg_EXTarget) Set EX_Damage = 20.00 Set EX_Timer = 5.00 Unit Group - Add (Target unit of ability being cast) to EX_Group Hashtable - Save EX_Timer as 1 of udg_ID in EX_Hash
While during the loop time...
Custom script: set udg_ID = GetHandleId(GetEnumUnit())
Set EX_Timer = (Load 1 of udg_ID from EX_Hash)
Where ID is an integer variable
But Maker said it's GUI so stick to GUI, but the above is more efficient and faster...
While efficiency is very very important in coding, this tutorial was aimed more at noobs. But you are very correct, making the handles via custom script it more efficient. I'll certainly change it in my version of the spell.
In your tank attack cast trigger you save target point of ability into a variable and then into a hashtable. Then you immediately remove it. This means the point no longer exists. After that you try to use it in the looping trigger. You should remove it when you don't need it anymore, most likely right before you flush the child hashtable.
These triggers are old, I need to do some updating. The target point isn't even used in the loop, I need to repost the new triggers. Btw Maker, is there a function similar to set bj_wantDestroyGroup = true that can be used for conditions? Or do I have to assign the group to a variable and then clear? Efficiency matters :)
I didn't quite get this part, but here's what I came up with:
Set ML_GroupCheck = (Units within ML_Aoe of ML_Point2 matching (((Matching unit) belongs to an enemy of (Owner of ML_Missile)) Equal to True)) Set ML_GroupCheckInteger = (Number of units in ML_GroupCheck) If - Conditions
I'm pretty sure it works, cause its just replacing the if with a variable which replaced another variable which was cleared. So there shouldn't be any leaks. I don't think it's very efficient tho