- Joined
- Nov 11, 2006
- Messages
- 7,778
Local Variables in GUI
Since the dawn of Warcraft 3, one of GUI's biggest drawbacks was its lack of local variable support. Variables defined in GUI are always considered global—which means they are defined once and can be accessed anywhere in your map's code. This is convenient, but once you try to track state over time, you'll quickly start running into issues.Consider a very simple example of a timed effect:
-
Tranquility
-

Events
-


Player - Player 1 (Red) skips a cinematic sequence
-
-

Conditions
-

Actions
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\NightElf\Tranquility\Tranquility.mdl
-


Set VariableSet MyEffect = (Last created special effect)
-


Wait 2.00 seconds
-


Special Effect - Destroy MyEffect
-
-
- [1] Player presses ESC the first time
- [1] The first effect is spawned, and MyEffect is assigned to it
- [1] The trigger begins waiting 2 seconds
- [2] Player presses ESC the second time
- [2] The second effect is spawned, and MyEffect is re-assigned to it
- [2] The trigger begins waiting 2 seconds
- [1] The trigger finishes waiting, and destroys MyEffect (which currently is assigned to the second effect)
- [2] The trigger finishes waiting, and destroys MyEffect (which is still assigned to the second effect)

There are a variety of ways to solve this (Dynamic Indexing, or Hashtables), but let's explore an alternate approach: local variables!
Terminology
- Variable - a named storage that can hold a value. In GUI, each variable can store a specific type (e.g. a unit, an effect, a string), and you'll create them through the variable editor or by clicking on the New Global Variable button in the trigger editor toolbar:
For a good overview on variables, check out this guide. - Global Variable - a variable that is defined once and can be accessed globally, i.e. anywhere throughout your code. All variables in GUI are global and can only hold a single value at a time (excluding arrays, but conceptually each index of an array is like its own variable that can hold a single value at a time).
- Local Variable - a variable that gets defined in a specific scope and can only be accessed locally, i.e. within that scope. In Warcraft 3, the scope of a local variable is typically a function (for JASS) or a block (for Lua). In the example above, local variables would allow each trigger to have their own "local" MyEffect variable without running the risk of one trigger execution overwriting the variable of the other.
- Function - a reusable block of code. In GUI, we aren't really exposed to functions—but under the hood, the actions for a GUI trigger are contained within their own dedicated function in JASS or Lua.
|
Before going into the details on how to use local variables in the stock GUI editor, it is worth noting that some tools extend GUI to allow you to use local variables. However, once you use local variables in these tools, you'll need to continue using them to develop your triggers going forward, as they are not compatible with the standard editor GUI.
|
Using Local Variables in GUI
Let's use local variables in GUI to fix the "Tranquility" trigger!First, it's possible to use local variables in GUI, but it requires a bit of custom scripting and trickery through a technique called shadowing. There are a few things to note that make this technique possible:
- All GUI code is converted to JASS or Lua under the hood (depending on the script language selected in Scenario > Map Options > Script Language)
- All GUI variables are given the prefix "udg_" (user-defined global) under the hood. So if I have a variable named
MyEffect, it's real name in the map script would beudg_MyEffect - If you define a local variable with the same name as a global variable, the local variable will take priority in any functions using that variable in that local variable's scope. This is known as shadowing.
Lua
JASS
-
Tranquility
-

Events
-


Player - Player 1 (Red) skips a cinematic sequence
-
-

Conditions
-

Actions
-


Custom script: local udg_MyEffect
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\NightElf\Tranquility\Tranquility.mdl
-


Set VariableSet MyEffect = (Last created special effect)
-


Wait 2.00 seconds
-


Special Effect - Destroy MyEffect
-
-
-
Tranquility
-

Events
-


Player - Player 1 (Red) skips a cinematic sequence
-
-

Conditions
-

Actions
-


Custom script: local effect udg_MyEffect
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\NightElf\Tranquility\Tranquility.mdl
-


Set VariableSet MyEffect = (Last created special effect)
-


Wait 2.00 seconds
-


Special Effect - Destroy MyEffect
-


Custom script: set udg_MyEffect = null
-
-
With just one line of code (two lines in JASS), the trigger now works however many times you spam ESC!
|
| This technique works with both Lua and JASS maps. But if you intend on creating a new map and using this technique often, I recommend using Lua. JASS has a few drawbacks that make this technique a little more annoying: JASS: local effect udg_MyEffectLua: local udg_MyEffect |
Shadowing Arrays
Shadowing arrays behaves the same way, but in Lua you'll need to initialize the local variable to an empty array/table{}. In JASS, you'll need to denote the local variable as an array type.Lua
JASS
-
Esc
-

Events
-


Player - Player 1 (Red) skips a cinematic sequence
-
-

Conditions
-

Actions
-


Custom script: local udg_MyEffectArray = {}
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\NightElf\Tranquility\Tranquility.mdl
-


Set VariableSet MyEffectArray[1] = (Last created special effect)
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\Human\slow\slowtarget.mdl
-


Set VariableSet MyEffectArray[2] = (Last created special effect)
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\Items\AIso\BIsvTarget.mdl
-


Set VariableSet MyEffectArray[3] = (Last created special effect)
-


Wait 2.00 seconds
-


For each (Integer A) from 1 to 3, do (Actions)
-



Loop - Actions
-




Special Effect - Destroy MyEffectArray[(Integer A)]
-
-
-
-
-
Esc
-

Events
-


Player - Player 1 (Red) skips a cinematic sequence
-
-

Conditions
-

Actions
-


Custom script: local effect array udg_MyEffectArray
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\NightElf\Tranquility\Tranquility.mdl
-


Set VariableSet MyEffectArray[1] = (Last created special effect)
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\Human\slow\slowtarget.mdl
-


Set VariableSet MyEffectArray[2] = (Last created special effect)
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\Items\AIso\BIsvTarget.mdl
-


Set VariableSet MyEffectArray[3] = (Last created special effect)
-


Wait 2.00 seconds
-


For each (Integer A) from 1 to 3, do (Actions)
-



Loop - Actions
-




Special Effect - Destroy MyEffectArray[(Integer A)]
-
-
-
-
Problems with Local Variables in GUI
This technique isn't particularly new, so why isn't it used more often?It boils down to two main reasons:
- Some GUI actions will run in their own separate function, such as if/then/else conditions or unit-groups. Because they run in their own function, they won't have access to the local variable (so they'll instead read from the global variable, defeating the purpose).
- Local variables are useful for running code properly across "waits", but waits are imprecise (particularly below 0.3 seconds, depending on latency) and have some drawbacks with respect to how they wait in engine time vs. game time (e.g. when someone lags/the game pauses). So for most triggered spells, missiles, etc. folks will resort to using Timers instead. However, since timers in GUI are only available through a GUI event, you often have to end up putting your main logic in a separate trigger—which means those local variables from your original trigger won't be available to use.
#1: Supporting If/Then/Else Conditions and Unit Group Actions
Consider the following (arbitrary) example—when the unit casts holy light, we'll wait a second and if the caster has maximum health, we'll replenish some mana and spawn an effect:-
HolyLight
-

Events
-


Unit - A unit Starts the effect of an ability
-
-

Conditions
-


(Ability being cast) Equal to Holy Light
-
-

Actions
-


Custom script: local udg_Caster
-


Set VariableSet Caster = (Triggering unit)
-


Wait 1.00 seconds
-


If (All Conditions are True) then do (Then Actions) else do (Else Actions)
-



If - Conditions
-




(Percentage life of Caster) Greater than or equal to 100.00
-
-



Then - Actions
-




Unit - Set mana of Caster to ((Mana of Caster) + 25.00)
-




Special Effect - Create a special effect attached to the chest of Caster using Abilities\Spells\Human\ManaFlare\ManaFlareBoltImpact.mdl
-




Special Effect - Destroy (Last created special effect)
-
-



Else - Actions
-
-
-
Lua:
function Trig_HolyLight_Func004C()
if (not (GetUnitLifePercent(udg_Caster) >= 100.00)) then
return false
end
return true
end
function Trig_HolyLight_Actions()
local udg_Caster
udg_Caster = GetTriggerUnit()
TriggerSleepAction(1.00)
if (Trig_HolyLight_Func004C()) then
SetUnitManaBJ(udg_Caster, (GetUnitStateSwap(UNIT_STATE_MANA, udg_Caster) + 25.00))
AddSpecialEffectTargetUnitBJ("chest", udg_Caster, "Abilities\\Spells\\Human\\ManaFlare\\ManaFlareBoltImpact.mdl")
DestroyEffectBJ(GetLastCreatedEffectBJ())
else
end
end
It is a little hard to read, but the actions are all contained in
Trig_HolyLight_Actions, but the if-condition is actually stored in its own function Trig_HolyLight_Func004C() (note: this happens with the one-line if/then/else as well). Since that function doesn't have access to our local udg_Caster, it'll end up reading the global value instead, so the trigger won't be checking the caster like we'd expect it to.To fix this, we'll need to do something slightly counter-intuitive: we'll need to switch back to using globals. Part of the reason GUI only allows globals is because a lot of functions (if/then/else conditions, unit groups, etc.) generate separate functions and they wanted those variables to be easily accessible anywhere. But if we only use globals, we'll run the risk of running into issues with variables being overwritten as soon as we introduce time into the equation (e.g. a wait, a timer, etc.).
So the key is to combine the two: use a local variable to keep track of the state that needs to cross the "wait" line, and then switch back to using a global. For example:
-
HolyLight
-

Events
-


Unit - A unit Starts the effect of an ability
-
-

Conditions
-


(Ability being cast) Equal to Holy Light
-
-

Actions
-


Custom script: local udg_Caster
-


Set VariableSet Caster = (Triggering unit)
-


Wait 1.00 seconds
-


-------- --------
-


-------- Immediately after the wait, assign a global variable to the local "Caster" --------
-


-------- Use this global from here-on, and it will work in the if/then/else condition --------
-


-------- --------
-


Set VariableSet CasterAfterWait = Caster
-


If (All Conditions are True) then do (Then Actions) else do (Else Actions)
-



If - Conditions
-




(Percentage life of CasterAfterWait) Greater than or equal to 100.00
-
-



Then - Actions
-




Unit - Set mana of CasterAfterWait to ((Mana of CasterAfterWait) + 25.00)
-




Special Effect - Create a special effect attached to the chest of CasterAfterWait using Abilities\Spells\Human\ManaFlare\ManaFlareBoltImpact.mdl
-




Special Effect - Destroy (Last created special effect)
-
-



Else - Actions
-
-
-
You may run into this same problem with unit group actions, e.g. "Pick every unit...". This is because the "Loop - Actions" section runs in a separate function. Thankfully, solving this is the exact same process.
Consider this example of a holy light that, strangely enough, should damage everyone around the caster after 1 second:
-
HolyLight
-

Events
-


Unit - A unit Starts the effect of an ability
-
-

Conditions
-


(Ability being cast) Equal to Holy Light
-
-

Actions
-


Custom script: local udg_Caster
-


Set VariableSet Caster = (Triggering unit)
-


Wait 1.00 seconds
-


Set VariableSet TempLoc = (Position of Caster)
-


Custom script: bj_wantDestroyGroup = true
-


Unit Group - Pick every unit in (Units within 512.00 of TempLoc.) and do (Actions)
-



Loop - Actions
-




Unit - Cause Caster to damage (Picked unit), dealing 100.00 damage of attack type Spells and damage type Normal
-
-
-


Custom script: RemoveLocation(udg_TempLoc)
-
-
-
HolyLight
-

Events
-


Unit - A unit Starts the effect of an ability
-
-

Conditions
-


(Ability being cast) Equal to Holy Light
-
-

Actions
-


Custom script: local udg_Caster
-


Set VariableSet Caster = (Triggering unit)
-


Wait 1.00 seconds
-


Set VariableSet TempLoc = (Position of Caster)
-


Set VariableSet CasterAfterWait = Caster
-


Custom script: bj_wantDestroyGroup = true
-


Unit Group - Pick every unit in (Units within 512.00 of TempLoc.) and do (Actions)
-



Loop - Actions
-




Unit - Cause CasterAfterWait to damage (Picked unit), dealing 100.00 damage of attack type Spells and damage type Normal
-
-
-


Custom script: RemoveLocation(udg_TempLoc)
-
-
#2: Supporting Timers (Advanced, Lua-Only)
Up until now, all the triggers have dealt with standard waits which have problems around precision and latency. Due to those problems, most spells and systems on our site will instead use timers, which are very precise and consistent even with very low periods (e.g. 0.01). This becomes incredibly important for missile systems, knockback systems, and so on.Unfortunately, with GUI you'll only be able to access timers via the events, which require you to use a separate trigger to handle your periodic logic (which wouldn't have access to our locals).
However, thanks to Lua, there is a way to write your periodic logic in GUI in a way that is fully multi-unit instanceable (i.e. multiple units can cast your spell at the same time without it malfunctioning). But we do have to be careful about how we write it.
First, consider an example that purely just creates a timer, ticks it every 0.03 seconds (displaying it in-game), and then ends the timer:
-
HolyLight
-

Events
-


Unit - A unit Starts the effect of an ability
-
-

Conditions
-


(Ability being cast) Equal to Holy Light
-
-

Actions
-


Custom script: local udg_ElapsedTime
-


Set VariableSet ElapsedTime = 0.00
-


Custom script: TimerStart(CreateTimer(), 0.03, true, function ()
-


-------- --------
-


-------- Put all your periodic logic between the 'function()' and 'end)' lines --------
-


-------- --------
-


Set VariableSet ElapsedTime = (ElapsedTime + 0.03)
-


Game - Display to (All players) the text: (String(ElapsedTime))
-


If (All Conditions are True) then do (Then Actions) else do (Else Actions)
-



If - Conditions
-




ElapsedTime Greater than or equal to 1.00
-
-



Then - Actions
-




Custom script: DestroyTimer(GetExpiredTimer())
-
-



Else - Actions
-
-


Custom script: end)
-
-
- First we make a local variable that'll shadow our "ElapsedTime" global real variable, which keeps track of how much time has elapsed
TimerStart(CreateTimer(), 0.03, true, function ()creates a new timer that runs with a 0.03 second interval. The followingtrueparameter tells the timer to run periodically (you can set this tofalsefor one-shot timers). And last, thefunction()will tell the timer: "hey, run everything up until the nextendthrough this timer".- Even though this timer technically starts its own function (which has its own scope), Lua functions inherit the scope of the function they are defined in, so our
local udg_ElapsedTimeis still accessible. So you'll see the elapsed time correctly go up and be displayed on screen. - However, this breaks at the if/then/else condition, due to the problem mentioned in the previous section. "ElapsedTime" is local, but the if/then/else condition makes a separate function that reads off the global "ElapsedTime" (which is 0). So the the condition will always fail, and thus the timer will never end.
-
HolyLight
-

Events
-


Unit - A unit Starts the effect of an ability
-
-

Conditions
-


(Ability being cast) Equal to Holy Light
-
-

Actions
-


Custom script: local udg_ElapsedTime
-


Set VariableSet ElapsedTime = 0.00
-


Custom script: TimerStart(CreateTimer(), 0.03, true, function ()
-


Set VariableSet ElapsedTime = (ElapsedTime + 0.03)
-


-------- --------
-


-------- Here we can assign a global variable to the value stored in our local "ElapsedTime" --------
-


-------- We can use that global version in our if-then-else conditions and unit-group logic --------
-


-------- --------
-


Set VariableSet GlobalElapsedTime = ElapsedTime
-


Game - Display to (All players) the text: (String(ElapsedTime))
-


If (All Conditions are True) then do (Then Actions) else do (Else Actions)
-



If - Conditions
-




GlobalElapsedTime Greater than or equal to 1.00
-
-



Then - Actions
-




Custom script: DestroyTimer(GetExpiredTimer())
-
-



Else - Actions
-
-


Custom script: end)
-
-
Note that we still only modify the local version "ElapsedTime", because only the local version is safe for tracking data over time. If you did something like this instead:
-
Custom script: TimerStart(CreateTimer(), 0.03, true, function ()
-
Set VariableSet GlobalElapsedTime = (ElapsedTime + 0.03)
- Be sure to keep track of which variables are considered "local" vs. "global". It may be helpful to name them as "Local" vs. "Global".
- The "local" ones are fine to modify and track across time.
- The "global" ones are fine to use to copy a local value for use in If/Then/Else functions and group "Loop - Actions", but you should only use that value for instantaneous access. You shouldn't rely on the globals for data that needs to be accessed across time, however.
#2.b: Supporting One-shot Timers
Writing logic for one-shot timers (i.e. timers that just wait X seconds and then complete) is essentially the same, just provide "false" instead of "true" withinTimerStart. Here is a sample of the tranquility trigger using a timer instead of a wait:-
Esc
-

Events
-


Player - Player 1 (Red) skips a cinematic sequence
-
-

Conditions
-

Actions
-


Custom script: local udg_MyEffect
-


Special Effect - Create a special effect at (Center of (Playable map area)) using Abilities\Spells\NightElf\Tranquility\Tranquility.mdl
-


Set VariableSet MyEffect = (Last created special effect)
-


-------- --------
-


-------- The "false" indicates the timer will run the function once after 2 seconds pass --------
-


-------- --------
-


Custom script: TimerStart(CreateTimer(), 2, false, function ()
-


Special Effect - Destroy MyEffect
-


Custom script: DestroyTimer(GetExpiredTimer())
-


Custom script: end)
-
-
#2.c: CaptureTimer
Now you know how to use local variables in GUI reliably, but it may still feel a bit janky for a number of reasons:- It is easy to make typos (e.g. when defining the local variable), it must have the
udg_prefix and match the spelling and capitalization exactly - It is hard to keep track of all the GUI actions which involve multiple functions
- Having to switch between global variables and local variables can be pretty error-prone and hard to organize due to the amount of variables
- You provide it a list of global variables to "capture" to track along with the timer
- Whenever the timer fires, it will populate the global variables with the captured state (similar to what we did for the if/then/else scenarios)
- Once your timer action completes, it'll remember the state of the global variables for the next tick (allowing you to increment elapsed time and such)
Feel free to check out the link above for more info, but for now I'll just leave some sample code as a reference in case it looks appealing:
Periodic Timer
One-Shot Timer
-
SiphonLife
-

Events
-


Unit - A unit Starts the effect of an ability
-
-

Conditions
-


(Ability being cast) Equal to Siphon Life
-
-

Actions
-


-------- --------
-


-------- Set up your globals with the state you want to track --------
-


-------- Then write their names in a capture list variable (like below, be sure to match spelling and capitalization exactly!) --------
-


-------- --------
-


Set VariableSet Caster = (Triggering unit)
-


Set VariableSet Target = (Target unit of ability being cast)
-


Set VariableSet ElapsedTime = 0.00
-


Custom script: local captureList = { "Caster", "Target", "ElapsedTime" }
-


-------- --------
-


-------- Start a periodic timer that runs every 0.1 seconds, providing your list of variables --------
-


-------- Then execute your logic as normal between the 'function ()' and 'end)' lines, using your regular variables --------
-


-------- --------
-


Custom script: CaptureTimer.StartPeriodic(captureList, 0.1, function ()
-


Unit - Cause Caster to damage Target, dealing 2.00 damage of attack type Spells and damage type Normal
-


Unit - Set life of Caster to ((Life of Caster) + 2.00)
-


Set VariableSet ElapsedTime = (ElapsedTime + 0.10)
-


If (All Conditions are True) then do (Then Actions) else do (Else Actions)
-



If - Conditions
-




ElapsedTime Greater than or equal to 3.00
-
-



Then - Actions
-




-------- Once your spell is complete, run EndPeriodicLoop --------
-




Custom script: CaptureTimer.EndPeriodic()
-
-



Else - Actions
-
-


Custom script: end)
-
-
-
Actions
-

Special Effect - Create a special effect attached to the chest of (Triggering unit) using Abilities\Spells\Orc\Ensnare\ensnareTarget.mdl
-

Set VariableSet MyEffect = (Last created special effect)
-

Custom script: local captureList = { "MyEffect" }
-

Custom script: CaptureTimer.Delay(captureList, 1, function ()
-

Special Effect - Destroy MyEffect
-

Custom script: end)
-
Conclusion
Local variables in GUI are quite useful for simplifying your triggers when used effectively. However, they aren't perfect. For cases where you need to share data across triggers, it is generally better to resort to other means of storage (hashtables, dynamic indexing, etc.).I wrote this tutorial because this concept was mostly forgotten due to the number of issues you could run into getting them to work with the rest of your GUI actions. They just require a few tricks to get them working correctly.








Feels like there isn't really a way around that without some 3rd-party tooling.