(Keeps Hive Alive)
Go Back   The Hive Workshop - A Warcraft III Modding Site > Warcraft III Tutorials > Trigger (GUI) Editor Tutorials

Trigger (GUI) Editor Tutorials Contains tutorials concerning the usage of GUI features.
Read the Rules before posting.

Reply
 
LinkBack Thread Tools Display Modes
Old 05-26-2007, 07:56 PM   #1 (permalink)
Spell Moderator
 
wyrmlord's Avatar

Ubuntu User
 
Join Date: Oct 2005
Posts: 350

wyrmlord is a jewel in the rough (188)wyrmlord is a jewel in the rough (188)

Zephyr Challenge #2 Winner: Redemption Aura 

Multi-Instancible GUI Spell Making

Warning The first suggestion for local variables is incorrect. While the use of multiple local variables is demonstrated, in fact only ONE local variable can be used in GUI. Until the tutorial is updated, please keep this in mind and either use only one local variable in GUI or consider using JASS for spells.

I've been looking through the spells section lately, and what I see greatly disappoints me. Most of the spells I've looked at - specifically the GUI ones aren't multi-instancible. What this means is that they cannot be cast by more than one unit at a time and still work properly. There are many reasons to why they don't work, and I will go over some of the problems and why they don't work right. After, I will go over methods on how (with little extra trouble) you can make your spell multi-instancible.

First example - waits
Probably one of the most common errors I see in GUI spells involves setting a single global variable to a unit - let's say the caster. The spell then does a bunch of actions and then uses a large wait action. After waiting, the user refers to the specific global variable to do things. Let's say the wait was around 10 seconds. What would happen if another unit cast the spell within 10 seconds of the first unit? Here's an example I'll walk through:
bloodL
Events
Unit - A unit Begins channeling an ability
Conditions
(Ability being cast) Equal to Bloodrage
Actions
Set Luster = (Triggering unit)
Unit - Add speed to Luster
Special Effect - Create a special effect attached to the right,hand of Luster using Abilities\Spells\Other\BreathOfFire\BreathOfFireDamage.mdl
Set BLse1 = (Last created special effect)
Special Effect - Create a special effect attached to the left,hand of Luster using Abilities\Spells\Other\BreathOfFire\BreathOfFireDamage.mdl
Set BLse2 = (Last created special effect)
Special Effect - Create a special effect attached to the head of Luster using Abilities\Weapons\NecromancerMissile\NecromancerMissile.mdl
Set BLse3 = (Last created special effect)
Wait 15.00 seconds
Special Effect - Destroy BLse1
Special Effect - Destroy BLse2
Special Effect - Destroy BLse3
Unit - Remove speed from Luster
This is a spell from a recent map I've been looking at - a real example of someone's work I've seen. Now, in the test map the spell may work find or at least seem to, but if you look closely enough you should be able to easily find the error. The ability is added to a unit, and many effects are created as well. Each of these are set to their own variable, and after 15 seconds are referenced. Well, now what happens if 2 units cast it within 15 seconds of another? I'll tell you what, the effects and the ability from the first round of the spell still remain on the hero - not a good sign!

Second example - periodic events and multiple triggers
This one is another one I see all the time. People need to create a spell that does a certain thing - such as knocking back repetitively over and over again. These people use another trigger and a periodic event in order to accomplish this. Sure it works for a single unit, but what if someone wants all the units in a map able to cast the spell? Let me show you an example of this:
Flurry
Events
Unit - A unit Begins casting an ability
Conditions
(Ability being cast) Equal to Flurry
Actions
Set F_Archer = (Triggering unit)
Trigger - Turn on FlurryReg <gen>
Animation - Change F_Archer's animation speed to 200.00% of its original speed
Unit - Pause F_Archer
For each (Integer A) from 1 to 20, do (Actions)
Loop - Actions
Wait 0.01 seconds
Unit - Create 1 arow effect for (Owner of F_Archer) at (Position of F_Archer) facing (Facing of F_Archer) degrees
Unit - Order (Last created unit) to Move To (Center of Region 002 <gen>)
Unit - Move F_Archer instantly to (Position of F_Archer), facing ((Facing of F_Archer) + 18.00) degrees
Animation - Play F_Archer's attack animation
-------- Line removed due to taking up too much space --------
Animation - Change F_Archer's animation speed to 100.00% of its original speed
Unit - Unpause F_Archer
Selection - Select F_Archer for (Owner of F_Archer)
Set ArrowEffect[(Integer A)] = (Units of type arow effect)
Trigger - Turn off FlurryReg <gen>
Wait 0.50 seconds
Unit Group - Pick every unit in ArrowEffect[(Integer A)] and do (Unit - Remove (Picked unit) from the game)

(This trigger is initially disabled)
FlurryReg
Events
Time - Every 0.01 seconds of game time
Conditions
Actions
Region - Center Region 002 <gen> on ((Position of F_Archer) offset by 300.00 towards (Facing of F_Archer) degrees)
You could argue that this spell isn't coded very well, but that's not the point. As you can see, this spell turns on the disabled spell in the middle of the spell. The disabled spell moves a specific region to a unit's position every .01 seconds. The problem now is that this other trigger will be turned on for some time. If this spell were being cast again, the 'F_Archer' global variable would be changed to a different unit. The first and second trigger would then be referring to the second unit to cast the spell and not the first, this is a big problem!

Some Solutions
There is no single answer for fixing these kind of problems. Sometimes just better thinking can find you a better solution. If you're unable to find answers, here are some possible solutions:

GUI Local Variable Bug
Someone once found that local variables and global variables could share the same name, and that local variables are looked at before globals. Now, if you don't understand what I just said, here's the same thing in different terms. It means you can declare a local variable with the same name as a global variable, and then each time you refer to the global variable you will really be referencing the local variable. Here's an example of what I'm trying to say:
Untitled Trigger 001
Events
Conditions
Actions
Custom script: local unit udg_Caster
Set Caster = (Triggering unit)
Wait 60.00 seconds
Unit - Kill Caster
Now, everything should make sense to you except for the first line. The first line uses some JASS to declare a local variable with the same name as a particular global. This means that we can use the name of the global variable to really refer to the local variable. You're still probably wondering how more than one unit can use this at once and not cause problems I'm sure. If you were to create a global variable 'Caster' and test this code, you'd see that it would work without failing. Once again, you may be a bit confused so I'll explain how local variables work. Local variables are called local variables for a reason, they're only able to be accessed by the function they're in. "Functions, isn't that some kind of confusing JASS thing?" you might say. Let me tell you that GUI is converted to JASS when the map is saved (inefficient JASS, but still JASS nonetheless). All the actions inside a specific trigger are all part of one function for the most part. There are exceptions to this, but I'll review those later. Local variables being specific to each function may not make sense to you, so I'll word it in terms you may understand. Creating a local variable is like creating a completely new variable each time the spell starts. Basically, this makes it so we'll be able to refer to the caster no matter how many units cast it at once.

Now, let's fix up the first spell to use this method:
bloodL
Events
Unit - A unit Begins channeling an ability
Conditions
(Ability being cast) Equal to Bloodrage
Actions
Custom script: local unit udg_Luster
Custom script: local effect udg_BLes1
Custom script: local effect udg_BLes2
Custom script: local effect udg_BLes3
Set Luster = (Triggering unit)
Unit - Add speed to Luster
Special Effect - Create a special effect attached to the right,hand of Luster using Abilities\Spells\Other\BreathOfFire\BreathOfFireDamage.mdl
Set BLse1 = (Last created special effect)
Special Effect - Create a special effect attached to the left,hand of Luster using Abilities\Spells\Other\BreathOfFire\BreathOfFireDamage.mdl
Set BLse2 = (Last created special effect)
Special Effect - Create a special effect attached to the head of Luster using Abilities\Weapons\NecromancerMissile\NecromancerMissile.mdl
Set BLse3 = (Last created special effect)
Wait 15.00 seconds
Special Effect - Destroy BLse1
Special Effect - Destroy BLse2
Special Effect - Destroy BLse3
Unit - Remove speed from Luster
As you can see, we've added a local variable for each variable used after the wait in the spell. This spell is now MUI. Now, there's still some more explaining to do. All the global variables you make in the editor are renamed to "udg_Variable_Name" when the GUI is converted to JASS. So when you want to refer to a global variable in JASS, you need to add "udg_" to the front of the variable name and also replace any spaces with an underscore( the _ character).

I must mention one problem with using locals in GUI. The problem is that they cannot be used everywhere. For instance, you should not refer to local variables in if/then/else blocks and "pick every ..." blocks. The reason behind this is that if/then/else sometimes creates additional functions (even if they really are unnecessary) for the condition amongst other things. The "pick ever ..." blocks will always create an additional function that is called for each picked object. All of this information I just told you is hidden from you in GUI, which is why I'm warning you.

Implementing a Stack
Now, that solves one possible problem, but what if we need to use a separate trigger for periodic events? Local variables can't be used outside of the function they're created in. We'll have to be much cleverer to accomplish this task, but it's still possible. This method may be a bit confusing at first, but I'll try to explain it as best I can. I'll be showing you how you might make a knockback ability with this method.

This particular method I have in mind uses a "stack" of unique integers. When you need a unit to get knocked back, you "take" one of those integers off the stack and use it in conjunction with global arrays to put in data. The knockback trigger will then go through each index in use and do the proper actions. I realize what I said probably doesn't make sense, which is why I'll show you some triggers right now. Here is the trigger that would go off when the spell is cast:

Start
Events
Unit - A unit Starts the effect of an ability
Conditions
Actions
-------- Will increase the index, granting us a unique number --------
Trigger - Run NewIndex <gen> (ignoring conditions)
-------- Now we will set our variables --------
Set KnockbackUnit[CurrentIndex] = (Target unit of ability being cast)
Set KnockbackDistance[CurrentIndex] = 10.00
Set KnockbackTime[CurrentIndex] = 10.00
-------- Our loop trigger will handle the rest --------

And here's the "loop" trigger that would handle moving the units:

Loop
Events
Time - Every 0.04 seconds of game time
Conditions
Actions
-------- For each index in use --------
For each (Integer A) from 1 to 50, do (Actions)
Loop - Actions
If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
KnockbackUnit[(Integer A)] Not equal to No unit
Then - Actions
-------- Decrease the time in each index, when it reaches 0 the unit will stop moving --------
Set KnockbackTime[(Integer A)] = (KnockbackTime[(Integer A)] - 0.04)
If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
KnockbackTime[(Integer A)] Greater than 0.00
Then - Actions
-------- The unit is still being knocked back, move it --------
Unit - Move KnockbackUnit[(Integer A)] instantly to ((Position of KnockbackUnit[(Integer A)]) offset by KnockbackDistance[(Integer A)] towards 0.00 degrees)
Else - Actions
-------- The unit should no longer be knocked back, return the index for future use --------
Set CurrentIndex = (Integer A)
Trigger - Run ReturnIndex <gen> (ignoring conditions)
Else - Actions

Now, I've purposefully left out the other triggers being run so I can first explain what's going on here. The start trigger would go off whenever a unit started the effect of an ability. Next, we run the NewIndex trigger which will return a unique integer for use with this knockback spell. The unique integer is stored by the NewIndex function in the CurrentIndex variable. Now, we have also set up a bunch of global array variables. To be exact, there is one for each type of information we want to store. We have one for the unit being knocked back, one for how far the unit is moved each cycle, and how long the knockback should go on for. Next, we store the target unit, the distance we want, and the time in their respective global arrays with the index being the value in the CurrentIndex global variable (which is a unique) integer for this spell.

Next is the Loop trigger which handles the actual moving of the units. We're going to loop through all the indexes that are being used. To do this, you must first know that the amount of indexes being used is stored in the KnockbackIndex variable. We run a "For each integer A from 1 to 50" action which will allow us to go through each index in use and perform the necessary actions. 50 is the maximum number of indexes, so this code will run through the loop 50 times. Of course, we will need to check the current index to make sure that the unit stored in KnockbackUnit isn't equal to "no unit" (which would mean that index isn't being used). The rest should be self-explanatory because I've commented the code. For those who don't know, (Integer A) will refer to the current index being "looked at" by the trigger.

Now I will move on to what's happening in the background. We have a stack of unique integers you might say. If you're one of those people who learns better visually, you're in luck. Here's a visual representation of the stack:

Stack:
123...50

This stack is basically a container that starts out just holding the numbers 1 through 50. Now, when we request a new index, we "take" the value stored on the top of the stack and the next value will be treated as the top of the stack. This is what the stack would look like if we grabbed the value stored at the "top" of the stack.

Stack:
234...50

Doing things like this will ensure the same number cannot be taken twice, which grants us 50 unique numbers. Now that's great, but unless we return numbers to the stack we'll have nothing left when we get to 50. That would definitely be a problem, so we'll have another trigger to return indexes back to the stack. Let's say the indexes 1 through 5 were already in use. Here's what our stack would look like:

Stack:
678...50

Now, let's say the first index was done being used. We'll put it back on the stack to be able to be used in the future by our spell. The stack would look like so if we returned the first index:

Stack:
1678...50

Okay, I hope that gave you an understanding of how a stack works. If it didn't, that's too bad because I'm not going to really be covering it any longer. Next we need to implement this into trigger form. We obviously first need to have a stack before it can be used, so let's make an initializing trigger for it:

Init
Events
Map initialization
Conditions
Actions
-------- Fill up the array with 50 unique numbers --------
For each (Integer A) from 1 to 50, do (Actions)
Loop - Actions
Set KnockbackId[(Integer A)] = (Integer A)
Set KnockbackUnit[(Integer A)] = No unit

Now, the KnockbackId will contain each of our unique ids to be used with the spell. We've filled the first 50 indexes of it (we shouldn't need any more indexes) with the numbers 1 - 50. We're also going to set the unit stored in each index used in the KnockbackUnit array to "no unit" which means that the index isn't being used. I'd go into further detail on how this trigger works, only I'm not really sure that there's anything more I could say.

Moving on, let's see how we're going to get a new index from this stack:

NewIndex
Events
Conditions
Actions
Set KnockbackIndex = (KnockbackIndex + 1)
Set CurrentIndex = KnockbackId[KnockbackIndex]

KnockbackIndex originally is set to 0, meaning the current amount of used ids from the stack is 0. This variable is equivalent to how many ids are currently in use. First thing this trigger does is increment the KnockbackIndex variable so that it will accurately show how many ids are in use. Next, we set the CurrentIndex variable to the value stored in at the "top" of the stack. Since this isn't really a stack, its size will always remain at 50 which is why we have our KnockbackIndex variable. This variable is really used to tell us which index we need to take the id from when we want a new id. Here's a visual representation first when the stack is full, and second when we're taking a value:

Full Stack:
KnockbackIndex (0) ->
1
2
...
50

Stack After KnockbackIndex is incremented:
KnockbackIndex (1) ->1
2
3
...
5

Stack After Id Taken:
KnockbackIndex (1) ->
2
3
4
...
50

This should give you an idea of what's going on with this trigger. Now we will move onto returning an index back to the stack. I'm hoping you will be able to better understand this example after the previous trigger. Here it is:

ReturnIndex
Events
Conditions
Actions
Set KnockbackId[KnockbackIndex] = CurrentIndex
Set KnockbackIndex = (KnockbackIndex - 1)

From the previous example, you saw that KnockbackIndex would actually point 1 space above where the top of the stack was. First, why not just place the index being returned right there on top of the stack where it belongs? Good question, let's do so first. After the index is placed one space above the stack, KnockbackIndex is pointing directly at it when it should be pointing one space above it. So, let's fix it by setting KnockbackIndex to KnockbackIndex - 1. We of course need to set KnockbackUnit on the id to be released to "no unit" for safety measures with the Loop trigger.

Just to clear things up, CurrentIndex is equal to one of the ids that was taken from the stack while KnockbackIndex represents a position on the stack.

This basically concludes things for now. I might choose to explain how you can make a spell just multi-instancible per player for those who have a hard time understanding the stack idea, but that will have to wait. I hope this has helped you and that you will be able to use these techniques in future projects.

A member of Clan TDG - Quality mapmaking and playtesting.

Tired of boring old GUI? Want to learn JASS? Take a look at these tutorials.

Last edited by wyrmlord; 08-16-2008 at 12:09 AM. Reason: detached signature.
wyrmlord is offline   Reply With Quote
Old 05-26-2007, 08:59 PM   #2 (permalink)
Overall Site Manager
 
Wolverabid's Avatar

 
Join Date: Oct 2006
Posts: 8,756

Wolverabid has much of which to be proud (1208)Wolverabid has much of which to be proud (1208)

Respected User: This user has been given the respected user award. User of the Year: 2007 

Multi-Instancible GUI Spell Making seems very helpful to me wrymlord: if all spell makers would carefully consider your points you would have a lot fewer messes to clean up and our spell resource section would contain far better spells.

Try to force a line break into the action Unit - Cause F_Archer to damage circular area after 0.00 seconds of radius 300.00 at (Position of F_Archer), dealing 5.00 damage of attack type Pierce and damage type Normal to see if you can prevent that action from stretching the page.


Let's see what your fellow Spell Resource Moderators have to add: I'm sure that they can help you further develop it.
Wolverabid is offline   Reply With Quote
Old 05-28-2007, 12:03 PM   #3 (permalink)
 
paskovich's Avatar

I am ghost
 
Join Date: Jul 2005
Posts: 942

paskovich has a spectacular aura about (125)paskovich has a spectacular aura about (125)paskovich has a spectacular aura about (125)paskovich has a spectacular aura about (125)


This was extremely needed! Very good job!

I can't add or correct things in this tutorial, cause i stopped using GUI, and i don't mess with this. It's far harder than jass...

I can't see anything against not approving this, Wolve.
__________________
Jimmy Eat World - Let It Happen
"I'm the evil one who said.
Gonna let everything just happen
like my chest, my ears are proud
The collision is such an ugly sound."
Vote for the Hive without mercy!
paskovich is offline   Reply With Quote
Old 06-05-2007, 11:29 AM   #4 (permalink)
 
Katu's Avatar

Architect
 
Join Date: Apr 2007
Posts: 247

Katu has little to show at this moment (45)Katu has little to show at this moment (45)Katu has little to show at this moment (45)Katu has little to show at this moment (45)Katu has little to show at this moment (45)


Good job, I really needed this. +rep
__________________

I'm currently working on: Nothing, just trying things with GMAX. And planning a sh*tload of never-complete projects.
My userbars | My Aesthetics tutorial

Vote for the Hive!
| Play Against the Darkness!

Katu is offline   Reply With Quote
Old 06-22-2007, 03:45 AM   #5 (permalink)
Spell Moderator
 
wyrmlord's Avatar

Ubuntu User
 
Join Date: Oct 2005
Posts: 350

wyrmlord is a jewel in the rough (188)wyrmlord is a jewel in the rough (188)

Zephyr Challenge #2 Winner: Redemption Aura 

*Bump*

Just thought I'd bring this to the attention of the moderators since this thread hasn't gotten any new replies in a few weeks. I'd like to know sooner than later if something needs to be fixed as I won't have any time after tomorrow to fix this tutorial up.
__________________
A member of Clan TDG - Quality mapmaking and playtesting.

Tired of boring old GUI? Want to learn JASS? Take a look at these tutorials.
wyrmlord is offline   Reply With Quote
Old 06-22-2007, 04:51 AM   #6 (permalink)

iRawr
 
Join Date: Dec 2005
Posts: 8,908

PurplePoot is a name known to all (737)PurplePoot is a name known to all (737)PurplePoot is a name known to all (737)PurplePoot is a name known to all (737)PurplePoot is a name known to all (737)

Respected User: This user has been given the respected user award. Map Development Mini-Contest #1 Winner: Stand of the Elements 

You don't show ReturnIndex (the trigger)

Other than that, no complaints.

(oh, and nice pseudo-functions :P)
PurplePoot is offline   Reply With Quote
Old 06-22-2007, 05:58 AM   #7 (permalink)

User
 
Join Date: Jun 2007
Posts: 115

atropos007 is an unknown quantity at this point (0)


your triggers leak locations in PERIODIC triggers! baaaaaaad :>
atropos007 is offline   Reply With Quote
Old 06-22-2007, 06:23 AM   #8 (permalink)
Spell Moderator
 
wyrmlord's Avatar

Ubuntu User
 
Join Date: Oct 2005
Posts: 350

wyrmlord is a jewel in the rough (188)wyrmlord is a jewel in the rough (188)

Zephyr Challenge #2 Winner: Redemption Aura 

Quote:
Originally Posted by PurplePoot View Post
You don't show ReturnIndex (the trigger)

Other than that, no complaints.

(oh, and nice pseudo-functions :P)
Actually, it's there right at the bottom. :P I had debated on using JASS functions for this, but figured this way would be easier for GUI users.

Quote:
Originally Posted by atropos007 View Post
your triggers leak locations in PERIODIC triggers! baaaaaaad :>
This tutorial is just showing methods used for multi-instancible spells. I don't think it's really necessary to remove every last leak.
__________________
A member of Clan TDG - Quality mapmaking and playtesting.

Tired of boring old GUI? Want to learn JASS? Take a look at these tutorials.
wyrmlord is offline   Reply With Quote
Old 06-22-2007, 06:47 AM   #9 (permalink)
Overall Site Manager
 
Wolverabid's Avatar

 
Join Date: Oct 2006
Posts: 8,756

Wolverabid has much of which to be proud (1208)Wolverabid has much of which to be proud (1208)

Respected User: This user has been given the respected user award. User of the Year: 2007 

Sorry, wyrmlord, I just now noticed that you had bumped this.
  1. Approval status: almost certain.

  2. Please detach your signature from the tutorial.

  3. Would it be possible to create a simple map that includes all of your trigger techniques so that consumers will have more flexibility in their implementation?

  4. It's really up to you and the other spell mods to determine if the leak issues should be addressed. My opinion is that in tutorials, they should. If you guys don't think it's absolutely necessary to plug them up, please make a strong disclaimer in the tut. Thanks.
Wolverabid is offline   Reply With Quote
Old 06-22-2007, 10:23 AM   #10 (permalink)