Unit Groups and Enumerations

Back to lessons

Unit Groups

In this lesson, we'll learn about unit groups. Unit groups are one of the absolute most useful objects in WC3. In this tutorial we'll use them to program a spell that causes damage in an area.

Unit groups handle a bit differently from GUI to vjass, but the concept behind them is the same. A unit group contains a list of units that can be enumerated over and performed actions upon. A unit group is an object, so we'll need to initialize it just like we did for the trigger. When we initialized a trigger, we used the CreateTrigger() function. For groups, we will use CreateGroup().

The first thing I'm going to do is create this simple ability and the following trigger code:

scope VJassAreaSpell initializer onInit

    globals

    endglobals

    function onCast takes nothing returns nothing
        local unit source = GetTriggerUnit()
        call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl",GetUnitX(source),GetUnitY(source)))
        set source = null
    endfunction

    function onCheck takes nothing returns boolean
        return(GetSpellAbilityId() == 'A001')
    endfunction

    function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t,Condition(function onCheck))
        call TriggerAddAction(t,function onCast)
        set t = null
    endfunction

endscope


This is just basic code - nothing we haven't written in the previous tutorials. The difference between this spell and the ones written before is that for this one, we need to deal damage in an area. The UnitDamageTarget function is only used for damaging a single unit. There is a function called UnitDamageArea, but it hurts both allies and enemies, so it is rarely used. In cases like these, we need to use unit groups.

- Get all nearby units, and add them to a unit group
- For each unit in selected group, damage unit
- Destroy group to get rid of leaks.


This function takes a unit group reference, the x and y coordinates to get units from, a radius, and a boolean expression. To begin with, we're going to write a custom filter function as our boolean expression. In serious code, this is rarely done but is useful for the experience. Before we even write the GroupEnum call, let's write a filter function. The purpose of this function is to decide which units do and do not go into the unit group. By default, GroupEnumUnitsInRange selects all units in range, but we can customize this (as in this case) to select only enemies who we want to damage. So let's write the filter function definition:

function onFilter takes nothing returns boolean

endfunction


Remember that a boolean expression is anything that evaluates to true or false. This function returns a boolean - which is necessarily either true or false. Now in the body, we want to write code to check whether the selected unit is an enemy of the caster. This poses a problem, since we have no "caster" variable in this code. It was only a local in the other function. To get around this, we're going to make it a variable in the globals block:

globals
    unit caster
endglobals


And assign it inside the onCast function:

set caster = source

Then use this variable to check whether the selected unit is an enemy. In order to get the unit currently being processed by the filter function, we use GetFilterUnit().

return IsUnitEnemy(GetFilterUnit(),GetOwningPlayer(caster))

This will return true (i.e. unit will be added to group) for enemies of caster, and false (unit will not be added to group) for allies and the caster himself.

This is my full code at this time:

scope VJassAreaSpell initializer onInit

    globals
        unit caster
    endglobals

    function onFilter takes nothing returns boolean
        return IsUnitEnemy(GetFilterUnit(),GetOwningPlayer(caster))
    endfunction

    function onCast takes nothing returns nothing
        local unit source = GetTriggerUnit()
        local group g = CreateGroup()
        set caster = source
        call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl",GetUnitX(source),GetUnitY(source)))
        set source = null
    endfunction

    function onCheck takes nothing returns boolean
        return(GetSpellAbilityId() == 'A001')
    endfunction

    function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t,Condition(function onCheck))
        call TriggerAddAction(t,function onCast)
        set t = null
    endfunction

endscope


Finally now that we have the filter function written, we can add in the code to actually populate the group. As a reminder, this is the function we will use:

native GroupEnumUnitsInRange takes group whichGroup, real x, real y, real radius, boolexpr filter returns nothing

We created the group as a local. We know the x and y to get units from. For this example, let's take 200 AoE to deal damage. We made the filter function. So we have all the parts:

call GroupEnumUnitsInRange(g,GetUnitX(source),GetUnitY(source),200.,Filter(function onFilter))

Note the special syntax while referring to the filter function. First, we need Filter(). Then inside the parenthesis goes how we usually refer to a function (function name)

Now we have a unit group full of units. But it's not doing anything just sitting there - we have to use it to deal some damage. And for this, we need another function called an enumerator function. This function will run once for each unit in the unit group, allowing us to refer to the unit being processed with GetEnumUnit(). Let's write the definition first:

function onEnum takes nothing returns nothing

endfunction


In here, we only need to do one thing: damage the unit. We've seen this code before. Remember to use the global block variable caster for the source!

call UnitDamageTarget(caster,GetEnumUnit(),150.,true,false,ATTACK_TYPE_CHAOS,DAMAGE_TYPE_ENHANCED,WEAPON_TYPE_WHOKNOWS)

Now we need to actually call this enumerator function to make it process the group. We can use this function to do so:

native ForGroup takes group whichGroup, code callback returns nothing

We have a group. We have the code (in this case, it's called "callback" but this is the enumerator). So let' write it:

call ForGroup(g,function onEnum)

Finally, get rid of the memory leaks.

call DestroyGroup(g)
set g = null


And that's all. Here is my final code:

scope VJassAreaSpell initializer onInit

    globals
        unit caster
    endglobals

    function onFilter takes nothing returns boolean
        return IsUnitEnemy(GetFilterUnit(),GetOwningPlayer(caster))
    endfunction

    function onEnum takes nothing returns nothing
        call UnitDamageTarget(caster,GetEnumUnit(),150.,true,false,ATTACK_TYPE_CHAOS,DAMAGE_TYPE_ENHANCED,WEAPON_TYPE_WHOKNOWS)
    endfunction

    function onCast takes nothing returns nothing
        local unit source = GetTriggerUnit()
        local group g = CreateGroup()
        set caster = source
        call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl",GetUnitX(source),GetUnitY(source)))
        call GroupEnumUnitsInRange(g,GetUnitX(source),GetUnitY(source),200.,Filter(function onFilter))
        call ForGroup(g,function onEnum)
        call DestroyGroup(g)
        set g = null
        set source = null
    endfunction

    function onCheck takes nothing returns boolean
        return(GetSpellAbilityId() == 'A001')
    endfunction

    function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t,Condition(function onCheck))
        call TriggerAddAction(t,function onCast)
        set t = null
    endfunction

endscope


Review

This tutorial may be very overwhelming if you are used to GUI unit groups which do not require you to code separate functions for filtering and enumerating. Think of using unit groups as these steps:

1. Create a group
    - local group g = CreateGroup()
2. Add units to the group
    - Write a filter to decide whether or not a specific unit should be added
    - Write an appropriate GroupEnumUnits... call
    - Pass in your filter as Filter(function onFilter)
3. Do something to the group
    - Write an enumerator to decide what to do with a unit in the group
    - Write a ForGroup - i.e. call ForGroup(g,function onEnum)
4. Delete the group
    - call DestroyGroup(g)
    - set g = null

You should be able to go through each step here and write a working unit group use. As stated before, unit groups are usually used differently in serious code. Advanced methods like the FoG Loop will be covered later, but it is absolutely necessary that you master this way of writing groups too, as it can be useful in some cases.

Back to lessons