How to create MUI loops with waits in GUI

Tutorial By Bogrim

This tutorial covers how to make loops work with waits and multi-unit instances (MUI). It's a frequent problem for GUI triggers. Before I begin on this tutorial, let's reflect on what can cause a trigger to not work in multiple instances:


• The variables in your trigger are used by other triggers and alter the variables' data while the trigger is still in effect, causing the trigger to run differently than intended or even stop working.

• The variables in your trigger only refer to a single unit at a time, and does not take into account if multiple units are casting the same spell.
While the latter is a flaw of design, the first issue is what we're looking for to prevent in a loop.

Local Variables
Using local variables is an essential part of creating MUI triggers. If you already know how to use Local Variables, skip this section of the tutorial.

There are two types of variables in the trigger editor:

Global, created in the GUI and can be used by any trigger.
Local, created within the trigger itself and can only be used by the same trigger. No matter how many times the event runs, each instance of the trigger will contain its own specific variables.
The World Editor already operates with pre-generated variables of both types. There are a fairly lot of preset global variables, such as Integer A and Integer B (used in standard loops). There is only one preset local variable, (Triggering unit).

Now, where every trigger is in risk of losing the MUI aspect is when you introduce a wait action into the trigger, opening up for the possibility of non-local variables changing during the wait. Let's take an example:

In this trigger, we want to create a special effect on a unit 2 seconds after the use of an ability.
Trigger:
  • Trigger
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
    • Actions
      • Wait 2.00 seconds
      • Special Effect - Create a special effect attached to the overhead of (Triggering unit) using Abilities\Spells\Other\TalkToMe\TalkToMe.mdl

This trigger works fine. However, the moment we refer to another unit than (Triggering unit), the trigger stops working:
Trigger:
  • Trigger
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
    • Actions
      • Wait 2.00 seconds
      • Special Effect - Create a special effect attached to the overhead of (Target unit of ability being cast) using Abilities\Spells\Other\TalkToMe\TalkToMe.mdl

This happens because “(Target unit of an ability being cast)” is not a local variable, despite it being on the same list as “(Triggering unit)”. So to refer to another unit in the event response than the triggering unit, we have to create our own local variable - with Jass implements through custom scripts.

All the local variables you want to create has to be listed in the beginning of the trigger with custom scripts. This is the code to create a local variable:

local <variable type> <variable name> = <data>

Know that variable types don't always share the same name as their GUI counterparts. For instance, a point is really called a "location" and unit groups are just called "group".

Also know that you can name your local trigger anything you want (without spaces or special characters other than "_"), but global variable names always begin with the prefix of "udg_".

For the data, there is a really simple method of finding the text you need. Let's use the above trigger as an example. First I create a new empty trigger and set a global variable as the only action in the whole trigger:
Trigger:
  • Untitled Trigger 001
    • Events
    • Conditions
    • Actions
      • Set Temp_Unit = (Target unit of ability being cast)

Then convert the trigger to custom text:

While the trigger's interface suddenly became much larger, what we're looking for is the line with the global variable, the global variable name starting with "udg_", so the line we're looking for is this one:

Evidently, "GetSpellTargetUnit()" is the script for the data we need. Just copy and paste. As such, the Custom Script should look like this:

local unit u = GetSpellTargetUnit()

"u" is the variable name I choose. There's really no reason to bother with long names because the only difference is the time it takes to type out.

Now we'll insert this into the trigger:
Trigger:
  • Untitled Trigger 001
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
    • Actions
      • Custom Script: local unit u = GetSpellTargetUnit()
      • Wait 2.00 seconds
      • Special Effect - Create a special effect attached to the overhead of (Target unit of ability being cast) using Abilities\Spells\Other\TalkToMe\TalkToMe.mdl

This doesn't change much to how the trigger works, but the local variable has now been created and the event data is stored in the trigger. Since GUI cannot refer to local variables, we'll simply assign the value to a global variable now. To do that, we need only use the "Set Variable" action in Jass, which is as simple as

set <variable> = <value>

Remember that global variables start with the prefix of "udg_", so your Custom Script should look like this:
Trigger:
  • Custom Script: set udg_Temp_Unit = u

Since we're handling an object, it's necessary to reset the local variable at the end of the trigger to avoid leaking memory. This is done with the same type of action, just inserting "null" in place of the value. (Read more on how to prevent memory leaks in another tutorial if you need to know more.)
Trigger:
  • Custom Script: set u = null

So the final trigger will look like this:
Trigger:
  • Trigger
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
    • Actions
      • Custom Script: local unit u = GetSpellTargetUnit()
      • Wait 2.00 seconds
      • Custom Script: set udg_Temp_Unit = u
      • Special Effect - Create a special effect attached to the overhead of Temp_Unit using Abilities\Spells\Other\TalkToMe\TalkToMe.mdl
      • Custom Script: set u = null

The trigger is now MUI and will work no matter how many units that cast an ability because we stored the data as a local variable.

Again, local variables are the key in creating MUI triggers because of its ability to store data within the trigger itself and therefore avoid variable conflicts entirely, which is where almost every trigger stops being MUI.

Loops with Waits
Loops is where everything gets tricky. Every standard GUI user's rule of thumbs is never to place a Wait inside a loop. Let's review how Loops work and what causes them to bug:

A loop works with 3 integer variables:

[list=*]• Loop Integer: A global variable that keeps track of how many times the trigger actions have been repeated. (Begin value + Number of times repeated.)
Begin value: A variable that serves as an anchor for the loop and as an additive value for the Loop integer.
End value: A variable that estimates when the loop should exit.[/list]
What happens when a loop "breaks" is that the global variable (Integer A) is altered during the wait period (typically by another loop running using Integer A) and therefore causing the loop to lose track of its progress. Running a test on a loop with in-game messages displaying the loop variable will typically show an error in style with this:

1
2
3
4
5
6
3 - this is where the loop variable had interference from another loop and went haywire.

To avoid this we need to stop using a global variable and use a local instead. But how is that possible in GUI when you cannot specify local variables in place of the global?

It's simple: We create an array indexing system which ensures that the same global variable is never used twice at the same time. This allows for any loop with waits to run without interference from other triggers.

What most people don't know is that just as other triggers can change the loop variable during the loop, so can the trigger itself to your advantage. The integer values are first used at the beginning of every loop, so if you reset the variables before the end of the loop (and after the wait), you can set the variables using the same method as I illustrated with local variables previously.

Now to create such an indexing system, we need to create 3 global integer variables to create a MUI system:

"C", an integer array which ensures we always use a new variable when starting a loop.
"Increasing_Array", an integer variable which we use to keep track of which arrays that always have been used.
"Temp_Integer", this is the variable we use to fill the local variables into the GUI actions.

The way the system works is that C keeps generating new variables for our loop and Increasing_Array prevents the same variables from being used twice, while Temp_Integer is how we will use local variables in a GUI format.

Now let me present you a structure of a MUI loop:
Trigger:
  • Actions
    • Custom Script: local integer i
    • If (Increasing_Array Equal to 8192) then do (Set Increasing_Array = 0) else do (Set Increasing_Array = (Increasing_Array + 1))
    • Custom script: set i = udg_Increasing_Array
    • Custom script: set udg_Temp_Integer = i
    • For each (Integer C[Temp_Integer]) from 1 to 10, do (Actions)
      • Loop - Actions
        • Wait 0.00 seconds
        • Custom script: set udg_Temp_Integer = i

This is interesting because although Temp_Integer is an integer used by many triggers, because the last action in the loop sets the global variable's value to that of the local variable, the loop will always run the correct variable and a variable that never conflicts with any other.

Logically we could just keep increasing the Increasing_Array value to no end, but there is a hard-coded limit on 8192 with arrays where everything past that number is read as "0", so we have to reset the array.

Now let's try and create a spell which causes damage over time using this trigger structure. What we do differently is that we store more local variables to keep track of more objects:
Trigger:
  • Trigger
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Slow
    • Actions
      • Custom Script: local integer i
      • Custom Script: local unit u = GetSpellTargetUnit()
      • If (Increasing_Array Equal to 8192) then do (Set Increasing_Array = 0) else do (Set Increasing_Array = (Increasing_Array + 1))
      • Custom script: set i = udg_Increasing_Array
      • Custom script: set udg_Temp_Integer = i
      • For each (Integer C[Temp_Integer]) from 1 to 10, do (Actions)
        • Loop - Actions
          • Wait 1.00 seconds
          • Custom script: set udg_Temp_Unit = u
          • Unit - Cause (Triggering unit) to damage Temp_Unit, dealing 10.00 damage of attack type Spells and damage type Normal
          • Custom script: set udg_Temp_Integer = i
      • Custom script: set u = null

Now we have a MUI trigger which damages the target for 10 seconds. That's easy enough.

Now here's something more interesting: It's not just the Loop variable you can manipulate. All the three variables in a loop can be used to our advantage. Let's say I do not want the loop to end until the buff fades, accounting for dispel and stacking multiple casts.

For this we create another integer variable, "EndLoop".

Trigger:
  • Actions
    • Custom script: local integer i
    • Custom script: local integer e = 1
    • If (Increasing_Array Equal to 8192) then do (Set Increasing_Array = 0) else do (Set Increasing_Array = (Increasing_Array + 1))
    • Custom script: set i = udg_Increasing_Array
    • Custom script: set udg_Temp_Integer = i
    • Custom script: set udg_EndLoop = e
    • For each (Integer C[Temp_Integer]) from 1 to EndLoop, do (Actions)
      • Loop - Actions
        • Wait 0.00 seconds
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • Conditions are met
          • Then - Actions
            • Custom script: set e = ( e + 1 )
          • Else - Actions
        • Custom script: set udg_Temp_Integer = i
        • Custom script: set udg_EndLoop = e

This structure will now increase the loop until the conditions no longer match by increasing the end value by 1 before the end of the trigger. Let's try and apply this to the damage over time trigger:
Trigger:
  • Trigger
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Slow
    • Actions
      • Custom script: local integer i
      • Custom script: local integer e = 1
      • Custom script: local unit u = GetSpellTargetUnit()
      • If (Increasing_Array Equal to 8192) then do (Set Increasing_Array = 0) else do (Set Increasing_Array = (Increasing_Array + 1))
      • Custom script: set i = udg_Increasing_Array
      • Custom script: set udg_Temp_Integer = i
      • Custom script: set udg_EndLoop = e
      • For each (Integer C[Temp_Integer]) from 1 to EndLoop, do (Actions)
        • Loop - Actions
          • Wait 1.00 seconds
          • Custom Script: set udg_Temp_Unit = u
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • (Temp_Unit has buff Slow) Equal to True
            • Then - Actions
              • Unit - Cause (Triggering unit) to damage Temp_Unit, dealing 10.00 damage of attack type Spells and damage type Normal
              • Custom script: set e = ( e + 1 )
            • Else - Actions
          • Custom script: set udg_Temp_Integer = i
          • Custom script: set udg_EndLoop = e
      • Custom script: set u = null

Because we now end the loop first when the buff runs out, this allows dispels to end the damage effect all across the board and the stacking of damage. More conditions will allow you to further customize the effect, but that's not relevant to this tutorial.

Ideally you can use loops over periodic triggers using this method with greater flexibility in storing variables, however remember that the minimum wait action is 0.27 so it's not a solid method for creating moving effects.

Tips and Tricks
Here's some tips that makes creating local variables easier in GUI:

1. If you can't figure out the code for something then you can use an alternative method, which is to just leave the local variable with an empty value and then use a global variable to set its value. For instance:
Trigger:
  • Actions
    • Custom script: local real r
    • Set Temp_Real = (20.00 x (Real((Level of Mind Flay for (Triggering unit)))))
    • Custom script: set r = udg_Temp_Real

Sometimes it's also easier to do this way because it allows you to use the GUI to edit the values rather than finding the code names for abilities.

2. When using local groups, remember that you can't use the "set variable" action to update units in the group. Setting a unit group variable creates a new group variable, and you can only create group variables in the beginning of the trigger. Instead, you have to use the "Group Add Group" and "Clear Group" actions. The codes for these are:
call GroupAddGroup( <name of group being added>,<name of group you're adding to> )

call GroupClear( <group name> )
3. When you link point variables, they share the same data. For instance, if you set a local point in the beginning of the trigger "l" and later use the action "set udg_Temp_Point = l", then remove Temp_Point with the action "call RemoveLocation ( udg_Temp_Point )", you also remove l and cannot use it anymore in the trigger.

Click here to comment on this tutorial.
 
 
Blizzard Entertainment, Inc.
Silkroad Online Forums
Team Griffonrawl Trains Muay Thai and MMA fighters in Ohio.
Apex Steel Pipe - Buys and sells Steel Pipe.