How to create MUI loops with waits in GUI

Bogrim

y hello thar
Reaction score
154
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:

  1. 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.
  2. 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:

  1. Global, created in the GUI and can be used by any trigger.
  2. 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:
JASS:
function Trig_Untitled_Trigger_001_Actions takes nothing returns nothing
    set udg_Temp_Unit = GetSpellTargetUnit()
endfunction

//===========================================================================
function InitTrig_Untitled_Trigger_001 takes nothing returns nothing
    set gg_trg_Untitled_Trigger_001 = CreateTrigger(  )
    call TriggerAddAction( gg_trg_Untitled_Trigger_001, function Trig_Untitled_Trigger_001_Actions )
endfunction

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:
JASS:
    set udg_Temp_Unit = GetSpellTargetUnit()

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:

  • 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.
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.
 
Have you verified your first fact? Other event responses ARE local, just not for a very long time:
Trigger:
  • Untitled Trigger 001
    • Events
      • Unit - A unit Is issued an order targeting a point
    • Conditions
    • Actions
      • Wait 0.50 seconds
      • Special Effect - Create a special effect attached to the overhead of (Ordered unit) using Abilities\Spells\Other\TalkToMe\TalkToMe.mdl


Try this. It works.
 
Have you verified your first fact? Other event responses ARE local, just not for a very long time:
The other event responses are global, but I can't determine the cause of why or when they lose their data.

You can try and run a test: Place 2 creeps and 2 Sorceress units on a map and run this trigger:
Trigger:
  • Test
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
    • Actions
      • Wait 0.50 seconds
      • Unit - Kill (Target unit of ability being cast)

Slow both creeps almost immediately. The second target will get killed 0.5 seconds after your first cast of Slow, and the first target won't get killed at all. That is the standard error when using a global variable for MUI. I wouldn't ever recommend using the other event responses with a wait action, your trigger will only work by chance.
 
The other event responses are global, but I can't determine the cause of why or when they lose their data.

You can try and run a test: Place 2 creeps and 2 Sorceress units on a map and run this trigger:
Trigger:
  • Test
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
    • Actions
      • Wait 0.50 seconds
      • Unit - Kill (Target unit of ability being cast)

Slow both creeps almost immediately. The second target will get killed 0.5 seconds after your first cast of Slow, and the first target won't get killed at all. That is the standard error when using a global variable for MUI. I wouldn't ever recommend using the other event responses with a wait action, your trigger will only work by chance.

Chance? What do you mean chance? It works perfectly fine! I'll give you a review.

HydraRancher's Brand New Tutorial Reviews!
Anyway, lets get straight to the point.
Pros n Cons.
Pros -
[O]Great info, I never actually knew how to use locals before, good job!
[O]Well Presented, no huge text, lots of examples.
[O]Excellent explanation of Global and Local variables

Cons -
[X]Not a very strong con, but it lacks a title and contents. Maybe a bit more order.

Neutral -
[-]Maybe a stronger explanation for those who don't know JASS or are new? Such as "Converting to Custom Script", some dont know how.


Overall Score:
9.75/10

Comments:
Very good tutorial, maybe more content? Highly recommended! Fix that tiny con, and get a 10/10!

Rating:
5 Star.
 
By "chance", I mean that there's a chance the trigger will bug because another unit casts a spell in the short time span - a coincidence.

I will be happy to update the tutorial with any suggestions. :)
 
By "chance", I mean that there's a chance the trigger will bug because another unit casts a spell in the short time span - a coincidence.

I will be happy to update the tutorial with any suggestions. :)

It seems that your spell trigger example isn't MUI, but my first order trigger is alright. Strange.
 
Guys, This is unrelated to this tutorial, but, I just have a quick question. What is the difference between "begins casting an ability" and "starts the effect of an ability" .. I can't find the difference.
 
Begins casting: Casting time starts, cooldown not fired, spell can be stopped/cancelled.
Start the effect: Casting time ended, cooldown fired, spell cannot be stopped/cancelled.

Use starts the effect to avoid abuses.

On topic:

Hashtables are probably much better for this than local variables since most decent GUI coders (oxymoron?) will use timers/periodic events for actions over time, and local variables won't stand up through things like that.
 
Great tutorial, nice work explaining some of the more complicated things, such as array indexes to contain instance data. :thup:

Hashtables are probably much better for this than local variables since most decent GUI coders (oxymoron?) will use timers/periodic events for actions over time, and local variables won't stand up through things like that.

He's right, local variables won't work for periodic events in GUI or JASS because of their scope being limited to a specific function call. In JASS we use timer systems or stacks of some sort, while in GUI you can't use timer systems.

I would recommend explaining that global arrays are more complicated, but also more useful when it comes to periodic events, and/or explaining the use of hashtables.
 
Hashtables are certainly the preferred way of making things MUI in GUI.
 
The only advantage this method has over using Hashtables is that you can often contain the spell in a single trigger, where periodic triggers usually require at least two.

However, the real reason for I wrote this tutorial is because I often see people posting bad loops and I wanted to shred some light on why loops break and how to fix them. After all, there's no shortage of Hashtable tutorials. ;-)
 
> The only advantage this method has over using Hashtables is that you can often contain the spell in a single trigger, where periodic triggers usually require at least two.
Periodic triggers in GUI will always require two :p. Unless you use a loop with waits, but that's inaccurate, and hashtables can still be used in it.
 
Great tut, I always wondered how people claim MUI can be done in GUI without using complex JASS-ing. I know now. +rep
 
General chit-chat
Help Users
  • No one is chatting at the moment.
  • Varine Varine:
    +1
  • Varine Varine:
    I've been really into this
  • Varine Varine:
    It's a musical retelling of The Odyssey
  • Varine Varine:
    Also if you want mixed drinks without alcohol, there's a handful of non-alcoholic 'spirits'. Ritual Zero and Free Spirit are the two brands I've tried and they are alright. They don't have a TON of options, but they have some gin and whiskey alternatives that are fun to play with.
  • Varine Varine:
    I got a couple bottles to make some mixy drinks around holidays when I mostly want to drink, they aren't exact replacements but they are surprisingly close.
  • Varine Varine:
    I ended up with some hop water things that I really like instead for when I want to feel like I'm participating
  • Varine Varine:
    I just got moved to unsupervised probation, so I can get away with drinking a bit now. I technically am not supposed to, but I don't think I get checked anymore. I really want to smoke weed but it scares me
  • The Helper The Helper:
    Happy Wednesday! I am not feeling it today, my teeth are bothering me, or the fragments that were left behind are coming up and it is extra painful today. Don't ever let them tell you that getting full dental implants is easy. They have to pull all your teeth to do those and apparently they are not very good at getting all the teeth out all the time.
    +1
  • The Helper The Helper:
    That is true with Dentures too. I am taking the day off and working from home. The big difference is though, I am not drinking anymore so I have a nice tea and coffee regimen set up :)
  • Varine Varine:
    I wish i had a 3D scanner
  • Varine Varine:
    That would make what I'm doing SO much easier
  • Varine Varine:
    I guess I could just take a picture of it and then use that as a reference. I have a circuit board with some weird holes I can't get measured right
  • Varine Varine:
    Oh I got a Klipper mod for my big printer! It comes with a bunch of parts and I don't totally understand what it does but apparently it's way better than Marlin, which is I think what it currently runs
    +1
  • The Helper The Helper:
    Nice!
  • Varine Varine:
    You're people in Florida safe? The only couple I know are safe but I think they evacuated.
  • The Helper The Helper:
    They are without power but safe. Flood damage and stuff alot of tornados hit east Florida. No fatalities or injuries to any of my people that I know of
  • The Helper The Helper:
    They have been through all the Florida hurricanes they are like me I have been through every texas hurricane for the last 50 years.
  • The Helper The Helper:
    Never evacuated.
  • The Helper The Helper:
    Check out this bad ass Chicken Noodle soup recipe - https://www.thehelper.net/threads/crack-chicken-noodle-soup.196687/
  • The Helper The Helper:
    Happy Saturday!
    +1

      The Helper Discord

      Members online

      No members online now.

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top