Triggers - Heal All: A tale of ancient GUI and modern JASS

Tutorial By AceHart

While roaming through ancient maps in a pointless quest for inspiration,
I came upon this beauty:

Trigger:
  • Heal All
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Heal All
    • Actions
      • Unit Group - Pick every unit in (Units owned by (Owner of (Triggering unit)) matching (((Matching unit) is alive) Equal to True)) and do (Actions)
        • Loop - Actions
          • Unit - Set life of (Picked unit) to (Max life of (Picked unit))


It waits for a unit to cast "Heal All", then takes all units of that player, provided it's alive, and sets its health back to max.

Rather simple, right?
Right.

Then again, it's somewhat too simple:
- it leaks
- it doesn't show anything


Also, why wait for "any unit" to do it?
The spell, most likely, will only ever be used by players, not Creeps for example.

I.e. we can "optimize" this by setting the event only for players this actually applies to:

Trigger:
  • Heal All
    • Events
      • Unit - A unit owned by Player 1 (Red) Starts the effect of an ability
      • Unit - A unit owned by Player 2 (Blue) Starts the effect of an ability
      • Unit - A unit owned by Player 3 (Teal) Starts the effect of an ability
      • Unit - A unit owned by Player 4 (Purple) Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Heal All
    • Actions
      • Unit Group - Pick every unit in (Units owned by (Owner of (Triggering unit)) matching (((Matching unit) is alive) Equal to True)) and do (Actions)
        • Loop - Actions
          • Unit - Set life of (Picked unit) to (Max life of (Picked unit))



More importantly though, "Units owned by" creates a unit group.
A new one on every run.
And, once the loop has been used... well, that group is lost.
Unfortunately, it still exists somewhere in memory.
Only, we have no way to get back to it.
It's lost.
A memory leak.

Depending on how many, or how often, or how big, or all of those, your map runs slower and slower over time.
Even to the point of serious lag.

To prevent that, we store the group in a variable, and destroy it later.

Trigger:
  • Heal All
    • Events
      • Unit - A unit owned by Player 1 (Red) Starts the effect of an ability
      • Unit - A unit owned by Player 2 (Blue) Starts the effect of an ability
      • Unit - A unit owned by Player 3 (Teal) Starts the effect of an ability
      • Unit - A unit owned by Player 4 (Purple) Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Heal All
    • Actions
      • Set UnitGroup = (Units owned by (Owner of (Triggering unit)) matching (((Matching unit) is alive) Equal to True))
      • Unit Group - Pick every unit in UnitGroup and do (Actions)
        • Loop - Actions
          • Unit - Set life of (Picked unit) to (Max life of (Picked unit))
      • Custom script: call DestroyGroup( udg_UnitGroup )


Now the group stays in the variable "UnitGroup".
After the loop, we destroy it with a line of custom script (i.e. a JASS line).

Note the "udg_" in front of the variable name.
That's the way the variable editor creates those, when seen from JASS.

So, the trigger works.

It's also fully multi-unit (MUI), multi-player (MPI), multi-everything (WTH) and then some.

Though it's still a bit simple.
It heals the units all right, but we don't really see anything.

More importantly perhaps, our opponent, in the middle of a fight, is not seeing anything either.

In short, it's boring, so let's add some simple eye-candy: a special effect:
Special Effect - Create a special effect attached to the "overhead" of <unit> using <some effect>

I'm going to use the effect "Chest of Gold / Lumber".
It's some golden flame-like thing.
Shown above the head of the units.

Additionally, that effect also needs to be destroyed.
Same reason as above, it would be a (small) memory leak otherwise.


Trigger:
  • Heal All
    • Events
      • Unit - A unit owned by Player 1 (Red) Starts the effect of an ability
      • Unit - A unit owned by Player 2 (Blue) Starts the effect of an ability
      • Unit - A unit owned by Player 3 (Teal) Starts the effect of an ability
      • Unit - A unit owned by Player 4 (Purple) Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Heal All
    • Actions
      • Set UnitGroup = (Units owned by (Owner of (Triggering unit)) matching (((Matching unit) is alive) Equal to True))
      • Unit Group - Pick every unit in UnitGroup and do (Actions)
        • Loop - Actions
          • Unit - Set life of (Picked unit) to (Max life of (Picked unit))
          • Special Effect - Create a special effect attached to the overhead of (Picked unit) using Abilities\Spells\Items\ResourceItems\ResourceEffectTarget.mdl
          • Special Effect - Destroy (Last created special effect)
      • Custom script: call DestroyGroup( udg_UnitGroup )


There we go, a nice GUI spell, with effect, no leak, MUI and it even works.


What more can you ask for?

Well, that's where JASS comes into play.
We want that thing to be optimized.


Basic rules of optimization:
- Don't do anything that doesn't need to be done.
- Don't do anything twice.


Sounds simple enough.

Let's see what that gives us with menu: Edit / Convert to custom script:





Ugly as all hell :P

"Trig_Heal_All_Func001002002"? Nice name...
Plenty of functions here with "BJ" in the name... we'll come to that soon enough.

Given we're going JASS here, we're also going to use the latest available, i.e. what follows requires NewGen.

For starters, we need better function names.

Like this for example:



Makes a lot more sense already.
The "init" function prepares the trigger, adds a "condition", and the "actions".
The Actions function gets a group that matches "IsAlive" which at least tells what it is checking,
and the units are healed in a function called Heal.
Easy to read and find.

Though, well, if you have several spells in your map, or some imported ones,
chances are those names are already used.

Which is why the people over at wc3campaigns introduced "scopes".
Function names inside a scope, assuming the function is private, are, well, private to that scope.
I.e. it's no problem to have another scope with the same names.


Current version:




The "init" function also assumes the existence of a global variable, of type trigger, called "gg_trg_Heal_All".
Not very flexible...

We want to be able to simply copy this over to a map, change a couple settings and be done with it.
Anything else if too much work :P

That's where we're going to start to "make it better" (for some definition of "better").

We replace that weird InitTrig_Heal_All with an initialzer called from the scope,
and provide our own trigger:



Slightly better.

But, let's stay here some more and try to find out what "TriggerRegisterPlayerUnitEventSimple" is doing.
It's a BJ, it should be in Blizzard.j...

Turns out it is:


Well, with simply adding the extra "null" parameter, we can save some function calls.



This is now doing as little as possible to get it working for those 4 players.

Does t actually need to be nulled?
Well... not really. Reason: that trigger will stay around forever anyway.


Let's continue with more interesting things, the condition for example:


The actual test is this: "GetSpellAbilityId() == 'A042'".
It returns true or false, depending on whether the spell cast was or not... 'A042'???
That's the object id of the spell.
Can be seen in the object editor with "ctrl + D".

Only, it's not very readable.

You probably know it now from looking it up, but, in three weeks from now?

We better give it a name: "GetSpellAbilityId() == SPELL_HEAL_ALL".
That's easy to read and write.

All we need for this is a new global variable...
Fortunately, that's also possible.
Even private to the scope it's in.



"private" just in case there's some other spell in the map called "spell_heal_all".
"constant" because it's never going to change anyway.
"integer" is the type.

Back to the real stuff, what is that condition doing?

If the ability test is true (it was our spell), then "if not true then return false, otherwise true".
Not true being false, the "if" will fail and we return true.
I.e. if it is true, it will return true.

If the ability test is false (it wasn't out spell), then "if not false then return false, otherwise true".
Not false being true, the "if" passes and it returns false.
I.e. if it is false, it will return false.

In short, it returns the result of that test...

Which is something we can do directly:




So far, we have this:



Things to note:
- "scope" around
- all functions are private
- there's an easy to find "configuration option" to set the Object ID of the spell
- "Init" is as basic as it gets
- the condition is down to one line...


The next part would be the "IsAlive" function.


The outer () aren't needed:


"GetFilterUnit()" is called "Matching unit" in GUI.

"IsUnitAliveBJ" can, as before, be found in Blizzard.j:


I.e. it calls a function that checks if the unit is not dead...



We're getting closer.
Dead means you have no life anymore... (who'd have guessed that :P)

Hence the following:


One function call. Not a call to a call that returns a result that is negated...

Why 0.5?
Because it works better than 0.
Yes, even though GUI is using it.

Well, some recommend 0.405 (or was that 0.407?).
If you feel adventurous, try to locate the thread over at campaigns... Good luck though, it's ages old.

0.5 seems to work.
It's also rather safe to assume a unit with less is "dead enough" for what we need it here.

That one too would then be down to one line, that gets the result immediately.


Latest version:



Next would be the function "Heal".


Looks simple, set the "picked unit"'s life to its possible maximum.

Still, SetUnitLifeBJ is this:


Yet another function that calls a function to do the work.
Additionally, it also makes sure we never set the health to a negative value.
We are rather sure though the max possible health of a unit is not negative.

That's also one of the reasons those BJs exist: call the native, unless you need to... test something for example.
Anyway, the "newValue" we pass here is the unit's max life from: "GetUnitStateSwap(UNIT_STATE_MAX_LIFE, GetEnumUnit())".
You just never know what people might throw at it.
Here, we do it ourselves, we know what's going on (famous last words).

Another BJ:


So, we plug that one back into the original:


One more replace with a native:


Another 4 (3? 5? :P) function calls reduced to 1.


On to the special effect:


For starters, these long effect strings are not readable in code.
And, if you need to change it, you must actually go and look for them...

A better way to organize that is... yet another global constant:


Which goes to the top, is easy to find later, and gives it a name that is much easier to use and read.


"AddSpecialEffectTargetUnitBJ", as you may have guessed, is yet another function that calls a function:


It creates the effect, stores it in a global variable, and returns that value.

Quick look at "DestroyEffectBJ":


And, finally, "GetLastCreatedEffectBJ()":


Ok...

So, the effect is created, stored in the global variable bj_lastCreatedEffect.
That variable is asked for, and used, when destroying it.

Which means we can save a couple steps by combining this all together:



Somewhat better.
But, the only point of that variable is to have something to pass to the destroy function.
Which we could also do all at once:




All those parts give us our new "Heal" function:



Most recent incarnation:



We're coming to the "Actions":



The usual look in Blizzard.j:


Create a new group,
put all units inside,
destroy the filter ???
return the group.

Well, we can take that part out, which eliminates one function call already.
And the need for a global variable that needs to be created from the variable editor...


Then it tries to use the group:


This is also where the famous "bj_wantDestroyGroup" is coming from.
I strongly recommend to avoid it.

The interesting part is inside: call ForGroup(whichGroup, callback)


Putting it all back in:



What happened to destroying the boolexpr (the condition function)?
That's not needed.
It seems the game keeps them around, and returns the very same one next time you use it.
And a couple other details...
In short, no need to destroy it.


Our function could look like this:


There's really no reason to create and destroy a new group on every cast.
If this were a global that we simply keep around...

A new "scope global":
group g = CreateGroup()

And an extra call before using it to be sure any "old" mess is gone:
call GroupClear(g)


All in all, the current spell looks like this:



This already looks like production code.
It's clean, easy and simple.

And yet, there's one more step we can do to simplify this:
We're running a condition over a bunch of units, remember all of them, then do something to them.

However, by the time we tell the "enum" function to remember this unit,
we already know we're going to do something to that unit.
So, why not do it immediately?
Then we wouldn't even need to remember it.
We also wouldn't need to loop over the units a second time.

Just do it all at once in the condition.

I.e. instead of "GetUnitState(whichUnit, UNIT_STATE_LIFE) > 0.5",
we put an "if" around, do the healing actions as needed,
and return false at the end.

Why false?
Because we don't need the game to collect any units in the group.
We're already done with them...

Combining "IsAlive" and "Heal" leads to something like this:


Note the change from "Picked unit" to "Matching unit".
And the change from "returns nothing" to "returns boolean" as it is now a condition function.


And a somewhat simpler "Actions" function:



The vastly improved script:




Are we there yet?

Nearly... :P

"Don't do stuff more than once".
Our heal function uses GetFilterUnit() rather often.
Assuming a player has more living units than dead units, the actions inside the if will run more often than not.

As such, that could use a local variable, to reduce the calls to one.


Based on the same, there's also "Condition(function Heal)".
Every cast, turn that into a boolexpr, find out you already have it, return the same one.
We can save some micro-work here too by storing it once, and using that variable instead.



Final script:





And, finally, how to put this into your own map as a spell?
Well, copy&paste the script, either to some existing JASS trigger, or convert one, or the map header.
Edit the spell ID as needed to match your ability.
Done.


Remember what it is supposed to do?
Take all units of the caster and fully heal them.


The core activity is this:


Which does:
Take all units of the caster, and fully heal them.
With special effect.


In three lines.
Natives only.
JASS.
Pure.


Yours,
AceHart

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.