• Check out the results of the Techtree Contest #19!
  • Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • Create a void inspired texture for Warcraft 3 and enter Hive's 34th Texturing Contest: Void! Click here to enter!
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!

[GUI] Local Variables in GUI

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
This spawns a tranquility effect when the player presses ESC, assigns it to a variable, and then destroys that effect after 2 seconds. This works fine if you run it once, but if you press ESC twice in a row, then only the second effect will be destroyed. This is the order of events:
  1. [1] Player presses ESC the first time
  2. [1] The first effect is spawned, and MyEffect is assigned to it
  3. [1] The trigger begins waiting 2 seconds
  4. [2] Player presses ESC the second time
  5. [2] The second effect is spawned, and MyEffect is re-assigned to it
  6. [2] The trigger begins waiting 2 seconds
  7. [1] The trigger finishes waiting, and destroys MyEffect (which currently is assigned to the second effect)
  8. [2] The trigger finishes waiting, and destroys MyEffect (which is still assigned to the second effect)
Thus leaving the first effect permanently in your map. :sad:

There are a variety of ways to solve this (Dynamic Indexing, or Hashtables), but let's explore an alternate approach: local variables!

Terminology

  1. 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:
    1757544876899.png

    For a good overview on variables, check out this guide.
  2. 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).
  3. 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.
  4. 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.

🛠️ Preface: Tooling that supports local variables

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.
  1. BetterTriggers - reforged-compatible, separate tool that extends GUI functionality to include local variables and a plethora of other niceties.
  2. YDWE PK - a modded editor, similar to JassNewGenPack, but with a lot of additional GUI functionality. Requires a Warcraft 3 client on patch 1.26 or 1.27 to work, but the maps produced will be compatible with later versions (e.g. reforged).

Using Local Variables in GUI

Let's use local variables in GUI to fix the "Tranquility" trigger! :thumbs_up:

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:
  1. All GUI code is converted to JASS or Lua under the hood (depending on the script language selected in Scenario > Map Options > Script Language)
  2. 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 be udg_MyEffect
  3. 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.
So in GUI, we can trick the compiler into making our actions use a local variable by naming the local variable the same as our global variable. Here is an example:

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!

⚠️ Important Note

Be sure that your local variable has the udg_ prefix and matches the spelling and capitalization of your global variable exactly. The variables are case-sensitive, so udg_myeffect is different from udg_MyEffect.

🚗 Under the Hood

To understand why this works, let's take a look at the script that gets generated by that trigger before adding the local variable:
Lua:
function Trig_Tranquility_Actions()
    AddSpecialEffectLocBJ(GetRectCenter(GetPlayableMapRect()), "Abilities\\Spells\\NightElf\\Tranquility\\Tranquility.mdl")
    udg_MyEffect = GetLastCreatedEffectBJ()
    TriggerSleepAction(2)
    DestroyEffectBJ(udg_MyEffect)
end

When the editor converts a GUI action that uses a variable to Lua/JASS, it'll literally just put the raw variable name, e.g. udg_MyEffect. When the Warcraft 3 engine reads any line referencing udg_MyEffect, it'll first check the local scope to see if there are any symbols named udg_MyEffect. If not, it'll go up one scope and check again. It'll keep going up in scope until it reaches the global scope, and then it will find our global variable udg_MyEffect.

Now let's take a look at the generated code after introducing our local variable:
1757548071356.png

On the left (the broken trigger), you'll see the code before the change. On the right, you'll see the code after adding the local variable.

Now that the local variable is there, when the engine tries to resolve the symbol udg_MyEffect, it'll see the local variable in the local scope first and use thateven for the variable assignment! So even though our GUI actions look like it is using our global variable, it is actually just using the local variable.

🤖 Lua vs JASS

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:
❌ Requires local variables to be defined at the beginning of the function/trigger
❌ Requires you to put a "type" for each variable, e.g. local effect udg_MyEffect
❌ You need to null any local agent variables (i.e. any reference-counted types) at the end of the function to avoid a reference leak

Lua:
✅ Locals can be placed anywhere
✅ You do not need to put a type for each variable, e.g. local udg_MyEffect
✅ You do not need to null any local variables at the end of the scope
✅ Supports anonymous functions (a.k.a. lambdas/closures) which can be useful for doing really powerful things in GUI (more on this later)

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:

  1. 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).
  2. 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.
We'll tackle each of these issues separately.

#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
This code looks fine, but it won't work. Let's take a look at the generated code to understand why:
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
With this change, our hero will successfully get his mana back! :thumbs_up:
1757553685770.png


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)
Again, this will fail because the "Loop - Actions" is in its own function and it is trying to use our local variable, "Caster", to damage those units. So let's solve it by assigning a global to the local after the wait:
  • 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)
Now it should damage all the units correctly. :thumbs_up:

✅ Important Tip: Use Unique Variables Per Trigger

I recommend using unique variables for each trigger when possible (unless you know what you're doing). Certain actions can cause other triggers to fire (e.g. the "Unit - Cause unit to damage target" action above which will fire any "unit takes damage" triggers). Let's say that "CasterAfterWait" was also written to in that "unit takes damage" trigger. It would overwrite that variable before finishing the rest of the actions in "HolyLight", potentially causing bugs. This is a broader problem with global state in general, so I recommend staying on the cautious side and creating unique variables per trigger, unless you're sure that your triggers won't fire off another trigger using those same variables.

#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)
To explain:

  1. First we make a local variable that'll shadow our "ElapsedTime" global real variable, which keeps track of how much time has elapsed
  2. TimerStart(CreateTimer(), 0.03, true, function () creates a new timer that runs with a 0.03 second interval. The following true parameter tells the timer to run periodically (you can set this to false for one-shot timers). And last, the function() will tell the timer: "hey, run everything up until the next end through this timer".
  3. 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_ElapsedTime is still accessible. So you'll see the elapsed time correctly go up and be displayed on screen.
  4. 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.
To fix this, we can use the same technique as described in the last section—another global variable:
  • 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)
Now it should correctly check the condition and our code should work! :thumbs_up:

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)
This would not work because "ElapsedTime" would just stay at its value of 0.00. In general, follow these rules and you'll be fine:

  1. Be sure to keep track of which variables are considered "local" vs. "global". It may be helpful to name them as "Local" vs. "Global".
  2. The "local" ones are fine to modify and track across time.
  3. 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" within TimerStart. 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)
Similar to the previous sections, you can use a global variable if you need to use the local variables within if/then/else statements or group actions.

#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:
  1. 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
  2. It is hard to keep track of all the GUI actions which involve multiple functions
  3. Having to switch between global variables and local variables can be pretty error-prone and hard to organize due to the amount of variables
Instead, you can try using a Lua system like CaptureTimer to simplify things. It operates using almost the exact same principles, but makes it much easier to work with:
  1. You provide it a list of global variables to "capture" to track along with the timer
  2. 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)
  3. 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)
With that system, you don't actually end up needing to use local variables at all, which gets rid of a lot of the guesswork and allows you to just focus on writing your triggers! In addition, it'll warn you about any typos (at runtime) and it has some other niceties for performance.

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. :thumbs_up: But with Lua in particular, they can become incredibly powerful—simplifying spells and systems that required multiple triggers and dozens of variables into just a fraction of that. Try giving it a shot for your next spell!
 
I think this is a very interesting topic because it raises a few some interesting complications.

At what point do you say "fuck this, I'll go full jass"?
Because the benefits most of the time are quite minor but comes with considerable drawbacks.

A local variable most of the time in a GUI context is merely a convenience because as a GUI user you already have ways to handling your global scope.

Meanwhile, a lot of cons I can think of:
1. If you're using a third party editor you're at the complete mercy of it being functional, if it breaks in a year's time you can no longer edit your map or at least you lose functionality in every trigger using locals.
2. If you use local shadowing which is handy but still runs into naming conflicts because the variable needs to be declared manually before use.
3. In order to make use of it you need to understand how GUI gets parsed into JASS, at that point why don't you just use JASS? I can think of very few people who know JASS but choose to use GUI.
4. If using local shadowing you make your code very ugly (somewhat subjectively), I was an abuser of custom script hashtables in GUI because of a GUI limitation on what could be used as a key but all the same it does not make for readable / pretty code.
5. If you're using a custom editor you're forcing every other person to do the same, meaning you cannot use this for uploading spells/system or collaborate unless everyone agrees to use the same editor.

I'd say the juice isn't worth the squeeze but I think we've had a pretty consistent policy of letting the users judge what is good so I'll just leave this open for a bit and see what people think.
 
I think this is a very interesting topic because it raises a few some interesting complications.

At what point do you say "fuck this, I'll go full jass"?
Because the benefits most of the time are quite minor but comes with considerable drawbacks.
...
I'd say the juice isn't worth the squeeze but I think we've had a pretty consistent policy of letting the users judge what is good so I'll just leave this open for a bit and see what people think.

It is an interesting debate for sure. :thumbs_up: Back when I first started, I switched to JASS pretty quickly because it was clear that GUI just didn't have the tools to let you do whatever you wanted. Most GUI-based spells were only MPI/wait-based/dummy-caster-based--and people just shrugged off the problems and lived with it.

But over the years, people developed pretty interesting paradigms to really unlock GUI: dynamic indexing for MUI spells, custom eventing via variable events (e.g. like Bribe's DDS), etc. At the time, I always thought those systems felt too complex/bespoke to set-up in GUI--yet they quickly became the standard and still are to this day.

I think it ultimately has to do with the jump between GUI -> programming being too large of a hurdle. For a GUI user, adding a little custom script here and there is not a big deal--you can mostly just copy & paste with a few tweaks--which is why most GUI folks are fine using them to remove leaks and use JASS-based systems. But making the jump completely to JASS/Lua is a really hard thing to do. That's why systems like Bribe's DDS or TriggerHappy's codeless save/load are so popular compared to their pure JASS equivalents--they ultimately expose functionality for GUI users to do what they want to do, without having them leave their "realm" completely.

Funnily enough, I've seen this even with unreal. Their GUI equivalent is "blueprints", and it is so popular that almost every tutorial in unreal will only show the blueprint method. And after playing with unreal myself--being "new" to something again--I've come to see the GUI-users' perspective more and more. GUI is way more discoverable when you're starting out, and the control flow is very obvious. And it is pretty interesting to see what crazy lengths people will go through in GUI/blueprints to avoid having to enter the programming realm. :grin:

Ultimately, I don't really know where that "point" is for people. But I think people just want solutions to their problems without having to jump over a mountain. Personally, I think this method (or using CaptureTimer) is a lot simpler than having to resort to something like dynamic indexing--and that was part of the motivation. The explanations in this tutorial are quite long so people can understand the code behind the curtain, but the method itself usually involves adding only one (or a few) lines of code.



I appreciate the feedback too! I'll try to address them one-by-one:

Meanwhile, a lot of cons I can think of:
1. If you're using a third party editor you're at the complete mercy of it being functional, if it breaks in a year's time you can no longer edit your map or at least you lose functionality in every trigger using locals.
...
5. If you're using a custom editor you're forcing every other person to do the same, meaning you cannot use this for uploading spells/system or collaborate unless everyone agrees to use the same editor.

I totally agree. That's why I only added that in a small section in the preface (with a disclaimer) so people were aware those tools existed. The rest of the tutorial is purely done using the default editor, so I don't think these two are really cons with the method itself--just with using custom tooling for it.

2. If you use local shadowing which is handy but still runs into naming conflicts because the variable needs to be declared manually before use.

I agree that it doesn't give you the freedom that JASS/Lua gives you with locals (scoped naming)--but I don't think this presents any more naming conflicts than you would otherwise have in GUI.

Compared to something like dynamic indexing--which has you convert your variables to arrays and add additional variables--this method has a lot fewer requirements and lets you keep the code looking closer to its original form and all in one trigger.

The main drawback with respect to naming that I can think of is that if you have a typo--or you rename your GUI variable--you'll only know that the code is broken at runtime via testing. That's why in my CaptureTimer system I added some logic to at least warn you if you have a typo (still at runtime, but at least it'll be obvious).

3. In order to make use of it you need to understand how GUI gets parsed into JASS, at that point why don't you just use JASS? I can think of very few people who know JASS but choose to use GUI.
4. If using local shadowing you make your code very ugly (somewhat subjectively), I was an abuser of custom script hashtables in GUI because of a GUI limitation on what could be used as a key but all the same it does not make for readable / pretty code.

Totally fair. :thumbs_up:

For #3, I think you might not really need to know it in great detail. This tutorial goes into that aspect in detail for the sake of the tutorial, but in reality you really just need to have a surface-level knowledge of the tricks in this tutorial to be able to use it in a reliable way, namely:
  1. Creating a local variable to shadow a global variable
  2. Assigning a global variable to the local variable after a wait
On the note of JASS in particular--one particular reason I made this tutorial was that this method hadn't really been revisited since Lua was introduced. Before Lua, this method really wasn't that useful because you'd still need to use some sort of data storage system (like hashtables) to carry state across functions--so you could really only use this with waits. But since Lua allows anonymous functions that can capture the variables in its scope, you can now use this technique with timers--making things a lot easier to be concise in GUI.

For #4, I agree haha. On the other hand, I also think dynamic indexing and hashtables in GUI can get equally as ugly. :ugly: Feels like there isn't really a way around that without some 3rd-party tooling.
 
I don't think any hurdle is impossible to overcome, I am merely pointing out a few things which maybe is not obvious at a glance. Half of the things I mentioned are not even downsides for the right person.

I feel this would be primarily used / desired for people who know jass / lua already but want their stuff to be GUI friendly because there are simply more people who are willing to use your stuff if it has the GUI label slapped on top. But..
1. I might be wrong
2. Even if I am right that is nothing against the tutorial

As you say, maybe this is a lot more appealing to learn than indexing or hashtables, I honestly have a hard time judging this view without bias as I think hashtables are pretty obvious if explained as a 2d array.

I would also guess that some of the downsides of this could be naturally fixed if a specific editor actually becomes widely used similar to JNGP was, if HiveWE releases and is widely used then features like this would be a lot better. You're ahead of the curve, perhaps. Ultimately, if the benefits are big enough people will jump through the hoops.
 
Last edited:
As you say, maybe this is a lot more appealing to learn than indexing or hashtables, I honestly have a hard time judging this view without bias as I think hashtables are pretty obvious if explained as a 2d array.

Yeah in my head, that is mostly the angle I'm coming from. I think hashtables are the most intuitive for sure, but the GUI implementation is still pretty annoying to work with. For example, when you're trying use "Key <Handle>", you either have to use a preset option (like "Last created unit") or a separate handle variable--but you can't easily just say "get the handle ID of <TempUnit>". Most of the time I end up seeing people resort to JASS, which just makes the logic quite long (as you mentioned in your previous post).

The other issue with hashtables in GUI is it isn't very elegant when you're working with periodic spells. For example, if you have a spell that damages an enemy every 1 second--you'll typically have one trigger to detect the spell cast and then one for doing the periodic logic with a timer event. Since you can't "attach" data to the timer that way you would in JASS, people usually end up using a unit group that contains either the caster or the enemy--but that comes with a big problem: it limits that spell to having only one active effect per caster (or per enemy, if you store the enemy in the group).

So to truly have it be MUI and same-unit-castable, you usually end up having to do some unique ID allocation--like in dynamic indexing or vJASS structs--which ends up making things a lot more complicated to write out. :sad:

I would also guess that some of the downsides of this could be naturally fixed if a specific editor actually becomes widely used similar to JNGP was, if HiveWE releases and is widely used then features like this would be a lot better. You're ahead of the curve, perhaps. Ultimately, if the benefits are big enough people will jump through the hoops.

Definitely. And I think that would be a great eventual addition for something like HiveWE. There are a lot of tiny changes to GUI that could suddenly make it really intuitive to use for situations like this.
 
Back
Top