• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Convenient Unit Group Filtering in GUI

Level 37
Joined
Jul 22, 2015
Messages
3,485
Convenient Unit Group Filtering in GUI




Introduction

This short tutorial will show you the best and most efficient way to filter out units enumerated from a unit group function. It will not show you what usage the function offers (excluding the example trigger), nor how to use them. With that in mind, I recommend that you have moderate knowledge and experience with the trigger editor before reading on.​

The unit group function is probably one of the most useful functions in the Trigger Editor. A handful of the submitted spells and systems would simply not exist without it. Being an extremely common function, I can guarantee that any trigger that involves the manipulation of multiple units will use it.​

NOTE: The example triggers you will see throughout this tutorial will all achieve to do the same thing: deal 100 damage to enemy units, that are alive, within 300 range of the triggering unit.​



Common Structure

As someone who lurks the Triggers & Scripts and World Editor Help Zone forums, I see a lot of people filtering units for a unit group like this:

  • Set Caster = (Triggering unit)
  • Set Player = (Triggering player)
  • Set CasterLoc = (Position of Caster)
  • -------- --------
  • Set UnitGroup = (Units within 300.00 of CasterLoc matching ((((Matching unit) is alive) Equal to True) and (((Matching unit) belongs to an enemy of Player) Equal to True)))
  • Unit Group - Pick every unit in UnitGroup and do (Actions)
    • Loop - Actions
      • Set TempUnit = (Picked unit)
      • Unit - Cause Caster to damage TempUnit, dealing 100.00 damage of attack type Spells and damage type Normal
  • Custom script: call DestroyGroup(udg_UnitGroup)
  • Custom script: call RemoveLocation(udg_CasterLoc)
  • Set Caster = (Triggering unit)
  • Set Player = (Triggering player)
  • Set CasterLoc = (Position of Caster)
  • -------- --------
  • Custom script: set bj_wantDestroyGroup = true
  • Unit Group - Pick every unit in (Units within 300.00 of CasterLoc matching ((((Matching unit) is alive) Equal to True) and (((Matching unit) belongs to an enemy of Player) Equal to True))) and do (Actions)
    • Loop - Actions
      • Set TempUnit = (Picked unit)
      • Unit - Cause Caster to damage TempUnit, dealing 100.00 damage of attack type Spells and damage type Normal
  • Custom script: call RemoveLocation(udg_CasterLoc)

Both will achieve the same thing, as explained in the introduction. All though there is nothing entirely wrong with it, there are a few issues that I
personally have with it:
  • (Matching unit) will constantly call the function GetFilterUnit(), which means that the execution will be slower.
  • They can get excessively long when you have multiple filters (ex: magic immune, not a structure), making it a hassle to read, and when copy-pasted as text it will actually cut off at a certain point:
    • Set UnitGroup = (Units within 300.00 of CasterLoc matching (((((Matching unit) is A structure) Equal to False) and (((Matching unit) is Magic Immune) Equal to False)) and ((((Matching unit) is alive) Equal to True) and (((Matching unit) belongs to an enemy of Player) Equal t
  • It is a complete pain in the a** to add, change, and or remove filters:
    cringeunitgroupfiltering-gif.242113

Recommended Structure

If that GIF traumatized you from ever using unit groups again, fret not, for now I will show a much better way of doing it. Instead of using the (Matching unit) function, use an If/Then/Else, Multiple Actions function that directly compares the (Picked unit) to a filter:

  • Set Caster = (Triggering unit)
  • Set Player = (Triggering player)
  • Set CasterLoc = (Position of Caster)
  • -------- --------
  • Set UnitGroup = (Units within 300.00 of CasterLoc)
  • Unit Group - Pick every unit in UnitGroup and do (Actions)
    • Loop - Actions
      • Set TempUnit = (Picked unit)
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (TempUnit is alive) Equal to True
          • (TempUnit belongs to an enemy of Player) Equal to True
        • Then - Actions
          • Unit - Cause Caster to damage TempUnit, dealing 100.00 damage of attack type Spells and damage type Normal
        • Else - Actions
  • Custom script: call DestroyGroup(udg_UnitGroup)
  • Custom script: call RemoveLocation(udg_CasterLoc)
  • Set Caster = (Triggering unit)
  • Set Player = (Triggering player)
  • Set CasterLoc = (Position of Caster)
  • -------- --------
  • Custom script: set bj_wantDestroyGroup = true
  • Unit Group - Pick every unit in (Units within 300.00 of CasterLoc) and do (Actions)
    • Loop - Actions
      • Set TempUnit = (Picked unit)
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (TempUnit is alive) Equal to True
          • (TempUnit belongs to an enemy of Player) Equal to True
        • Then - Actions
          • Unit - Cause Caster to damage TempUnit, dealing 100.00 damage of attack type Spells and damage type Normal
        • Else - Actions
  • Custom script: call RemoveLocation(udg_CasterLoc)



Conclusion

Voila! Your unit group loop is now efficient, extremely easy to read, and requires little to no effort when you want to add and or remove filters. You also get the added benefit of me not scolding you in the Triggers & Scripts or Spell Section!
 
Last edited:
Level 21
Joined
Dec 4, 2007
Messages
1,473
Good to know, just something:

Isn't it advisable, to use (matching unit) as a first and quick overall outsourcing, especially in multiple if/then/else filters?

It would seem rather odd to use if/then/else clauses exlusively, always adding (TempUnit is alive) Equal to True, instead of outsourcing it once via matching unit?!

Would like some insight on this.
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
A]mun;2833529 said:
Isn't it advisable, to use (matching unit) as a first and quick overall outsourcing, especially in multiple if/then/else filters?
It would seem rather odd to use if/then/else clauses exlusively, always adding (TempUnit is alive) Equal to True, instead of outsourcing it once via matching unit?!
"Outsourcing" doesn't really make any sense. Whether or not you use an If/Then/Else with (Picked unit) or (Matching unit), they will all be checked the same exact way. Every single unit enumerated by the unit group will all be checked to each filter you have one at a time. There is no outsourcing.

This is what the JASS code will look like when the GUI is compiled:
  • Set TempLoc = (Center of (Playable map area))
  • Custom script: set bj_wantDestroyGroup = true
  • Unit Group - Pick every unit in (Units within 100.00 of TempLoc matching ((((Matching unit) is A structure) Equal to True) and (((Matching unit) is alive) Equal to True))) and do (Actions)
    • Loop - Actions
      • -------- do stuff --------
  • Custom script: call RemoveLocation(udg_TempLoc)
JASS:
//Using (Matching unit)
function Filter1 takes nothing returns boolean
    return ( IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) == true )
endfunction

function Filter2 takes nothing returns boolean
    return ( IsUnitAliveBJ(GetFilterUnit()) == true )
endfunction

function FilterResult takes nothing returns boolean
    return GetBooleanAnd( Filter1(), Filter2() )
endfunction

function UnitPassesFilters takes nothing returns nothing
    // do stuff
endfunction

function Actions takes nothing returns nothing
    set udg_TempLoc = GetRectCenter(GetPlayableMapRect())

    set bj_wantDestroyGroup = true
    call ForGroupBJ( GetUnitsInRangeOfLocMatching(100.00, udg_TempLoc, Condition(function Filter1)), function Filter2 )
    call RemoveLocation(udg_TempLoc)
endfunction

  • Set TempLoc = (Center of (Playable map area))
  • Custom script: set bj_wantDestroyGroup = true
  • Unit Group - Pick every unit in (Units within 100.00 of TempLoc) and do (Actions)
    • Loop - Actions
      • Set TempUnit = (Picked unit)
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (TempUnit is A structure) Equal to True
          • (TempUnit is alive) Equal to True
        • Then - Actions
          • -------- do stuff --------
        • Else - Actions
  • Custom script: call RemoveLocation(udg_TempLoc)
JASS:
//Using an If/Then/Else with (Picked unit) stored into a variable
function Filters takes nothing returns boolean
    if ( not ( IsUnitType(udg_TempUnit, UNIT_TYPE_STRUCTURE) == true ) ) then
        return false
    endif
    if ( not ( IsUnitAliveBJ(udg_TempUnit) == true ) ) then
        return false
    endif
    return true
endfunction

function UnitGroupLoop takes nothing returns nothing
    set udg_TempUnit = GetEnumUnit()
    if ( Filters() ) then
        // do stuff
    else
    endif
endfunction

function Actions takes nothing returns nothing
    set udg_TempLoc = GetRectCenter(GetPlayableMapRect())

    set bj_wantDestroyGroup = true
    call ForGroupBJ( GetUnitsInRangeOfLocAll(100.00, udg_TempLoc), function UnitGroupLoop )
    call RemoveLocation(udg_TempLoc)
endfunction

To repeat, you don't magically filter out the units you want to in an instant using (Matching unit). They individually get checked by each filter you have, and if they don't pass, they are removed from the group. So it is done the same exact way as an If/Then/Else, but with ugly results as I describe in the tutorial.
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,180
Might be worth mentioning that some standard unit group functions leak due to the LDLHVRCLOR (Local Declared Local Handle Variable Reference Counter Leak On Return) bug.

These include...
JASS:
function EnumUnitsSelected takes player whichPlayer,boolexpr enumFilter,code enumAction returns nothing
function GetRandomSubGroup takes integer count,group sourceGroup returns group
function GetUnitsInRangeOfLocMatching takes real radius,location whichLocation,boolexpr filter returns group
function GetUnitsInRectMatching takes rect r,boolexpr filter returns group
function GetUnitsInRectOfPlayer takes rect r,player whichPlayer returns group
function GetUnitsOfPlayerAndTypeId takes player whichPlayer,integer unitid returns group
function GetUnitsOfPlayerMatching takes player whichPlayer,boolexpr filter returns group
function GetUnitsOfTypeIdAll takes integer unitid returns group
function GetUnitsSelectedAll takes player whichPlayer returns group
The standard implementation of the above functions can leak handle indices. Either custom implementations must be used, or alternatives. This is yet another reason why GUI in WC3 is so terrible.
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
As I explained in this post, they are filtered the same exact way. Each enumerated unit goes through each individual filter, one by one. There is no "outsourcing" as A[mun described it.

I see no benefit in ever using (Matching unit) other than having hate to how the GUI If/Then/Else looks. A fair enough excuse to not use it, but what kind of sane person would ever think this looks any better:
  • Set UnitGroup = (Units within 300.00 of CasterLoc matching (((((Matching unit) is A structure) Equal to False) and (((Matching unit) is Magic Immune) Equal to False)) and ((((Matching unit) is alive) Equal to True) and (((Matching unit) belongs to an enemy of Player) Equal t
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
Tutorial Title: Proper Unit Group Filtering in GUI
...Proper Unit Group Filtering in GUI
...Unit Group Filtering in GUI
...Filtering in GUI
...in GUI


The tutorial is for making GUI triggers look clean, not code in general. Regardless, if you wanted good looking code, you would use vJASS. JASS is too damn raw to look good.
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,180
Except what you are teaching might not be able to be used because a lot of the GUI group functions leak internally...

At least add a footnote warning of the leaks. The more aware people are about them the fewer leak related problems will surface on the message boards.
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
What you are talking about is irrelevant to what the tutorial is aimed to do.
This short tutorial will show you the best and most efficient way to filter out units enumerated from a unit group function. It will not show you
what usage the function offers (excluding the example trigger), nor how to use them.

As far as I know, the bug is not game breaking, and no mod has ever enforced such a miniscule issue to GUI users. I will consider adding a footnote when I see it as an issue to worry about. A user would not search for and or read a tutorial called "Proper Unit Group Filtering" if they want to figure out if the unit group function leaks; they would go here, where the exact issue is addressed in the thread.
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,180
People read roughly around what they are looking for. What you are saying is that if someone would look for "how to bake a cake" they are not at all interested in the sort of oven to use despite the sort of oven being very important to the quality of the result. All the "efficiency" in the world does not matter if it still leaks as eventually it can still lead to a crash or poor performance.
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
Efficiency is such a small reason to do what I suggested in the tutorial. I highly doubt one will be able to recognize the faster execution time from storing (Picked unit) into a variable rather than constantly calling GetFilterUnit(). I only threw it in as a reason to not using (Matching unit) because three reasons are better than two.

The main points I really wanted to emphasize on is readability and the ease of configuring.
 
The recommented way is also more efficent in question of performance.

Cause the matching And/Or which you can't avoid when using matching, will test each insert condition regardless, if the result is already final.
While the if conditions will skip further conditions if the result is final.

Proof in attached map.
 

Attachments

  • Test ifs.w3x
    17.4 KB · Views: 118
Does nobody want to recommend the two trigger solution version? :p

At least that would be useful for Spell Section.
Some time ago I also played around with this second trigger as condition Idea it can be a nice structed approach, if one is capable of such structed triggering. But I personaly had 2 problems with it.
The first problem is it floots the trigger variableslist(probably also a problem of spell system), this problem can be weakened/countered by having a good naming for everything.

You can't use all filters in any situation in one you check for friendship of trigger Unit to matching unit in the other you need attacking <> picked unit. This problem now requires either 2 filterTriggers or by introtrucing an unit variable. That same thing expands to you compared currently to picking or matching unit. Both require a different filter

But after you found a good structed way to create all that your filters in the group filters are quite small and not as redundant as normaly.

An example when casting holy light near organic non spell immune allies also should be healed by 100 Life.
This is the filter for types it does not include the ally test so it can be used inside any matching filter:
  • Filter matching not Struct not Mech not SpellImmune
    • Events
    • Conditions
      • ((Matching unit) is A structure) Equal to False
      • ((Matching unit) is Mechanical) Equal to False
      • ((Matching unit) is Magic Immune) Equal to False
    • Actions
Reuse the matching and check for allies
  • Filter Spell Holy Light
    • Events
    • Conditions
      • ((Matching unit) belongs to an ally of (Triggering player)) Equal to True
      • (Evaluate Filter matching not Struct not Mech not SpellImmune <gen> conditions) Equal to True
    • Actions
The spellcast Trigger
  • SpellCast Holy Light
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Holy Light
    • Actions
      • Set Loc = (Position of (Target unit of ability being cast))
      • Set Group = (Units within 512.00 of Loc matching ((Evaluate Filter Spell Holy Light <gen> conditions) Equal to True))
      • Unit Group - Remove (Target unit of ability being cast) from Group
      • Unit Group - Pick every unit in Group and do (Actions)
        • Loop - Actions
          • Unit - Set life of (Picked unit) to ((Life of (Picked unit)) + 100.00)
      • Custom script: call RemoveLocation(udg_Loc)
      • Custom script: call DestroyGroup(udg_Group)
I think it looks pretty nice when one is capable of doing it and can handle so many triggers.

Performance wise it is slower then the other 2 versions (tested using Lua os.clock() with 100 executions), although that does not matter much if it does not happen dozens of times a second.
 
Last edited:
Top