• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[System] GroupTools

Level 6
Joined
Jun 20, 2011
Messages
249
JASS:
library GroupTools uses /*

*/  Table  /*  thehelper.net/forums/showthread.php/162582-Table
*/  Alloc  /*  thehelper.net/forums/showthread.php/163457-Alloc

//  A system that provides optimal speed for group handling instead of 
//  using slow natives such as ForGroup or FirstOfGroup. Also provides
//  multiple tools for grouping that Wc3 doesn't have internalized. This
//  system is entirely dedicated to speed freaks and advanced JASSers.

//  Sadly there is no ForGroup call, and adding one would kill all the
//  performance this system provides. How to enumerate units through the
//  group then? An example is provided:

    function Enumerate takes nothing returns nothing
        local UnitGroup SomeGroup=UnitGroup.create() //Allocates a new
        //group, this group is currently empty
        local integer i=0 //This is needed to go through all units inside
        //the group using a loop
        local unit u //This variable is a must. NextOfGroup always returns
        //a different unit from the group, so be sure to use it only once
        //inside each loop.
        call SomeGroup.enumUnitsInRange(0,0,600) //Adds EVERY unit within
        //the specified range, there's no way to filter these units, but
        //there is a way to prevent actios to be done to those you do not
        //want.
        loop
            exitwhen i==SomeGroup.count //The loop ends once i reachs the
            //number of units inside the group
            set u=SomeGroup.NextOfGroup //Use the variable u to handle the
            //curent unit
            if GetUnitTypeId(u)='hfoo' then //Checks if u is a footman
                call KillUnit(u) //If the unit passes the filter actions
                //are fired
            endif
            set i=i+1 //Iterates the loop
        endloop
        //A great advantage of doing this is that the group isn't empty now,
        //If you were using the common FirstOfGroup loop, the group would
        //be right now, so you couldn't save those units for later use
        call SomeGroup.destroy() //Deallocates the group, deleting units
        //inside of it
        set u=null
    endfunction

//  This system also provides the SimpleGroup struct, it works similar
//  to UnitGroup, it's faster, but doesn't have some of the tools it
//  provides.
***********************************************************************

    API for the UnitGroup struct:
    
        -Allocation Methods:
    
    static method create takes nothing returns thistype
//      Allocates a group, this group is initially empty.

    method destroy takes nothing returns nothing
//      Deallocates a group and all of the units inside of it.

    method addUnit takes unit whichUnit returns nothing
//      Adds an unit to the group.

    method removeUnit takes unit whichUnit returns nothing
//      Removes an unit from the group.

    method hasUnit takes unit whichUnit returns boolean
//      Returns true if the group has the given unit.

    method clear takes nothing returns nothing
//      Removes every unit from the group.

***********************************************************************

    API for the SimpleGroup struct:
    
    static method create takes nothing returns thistype
//      Allocates a group, this group is initially empty.

    method destroy takes nothing returns nothing
//      Deallocates a group and all of the units inside of it.

    method addUnit takes unit whichUnit returns nothing
//      Adds an unit to the group.

    method clear takes nothing returns nothing
//      Removes every unit from the group.

***********************************************************************

    API of the Enumeration methods:

    method enumUnitsInRange takes real x, real y, real radius returns nothing
//      Picks all units within a radius and adds them to the group.

    method enumUnitsInRing takes real x, real y, real innerRadius, real outerRadius returns nothing
//      Only adds the units inside the given parameters.

    method enumUnitsInCone takes real x, real y, real radius, real facing, real coneAngle returns nothing
//      Only adds the units inside the given parameters.

    method enumUnitsInRect takes real x1, real y1, real x2, real y2 returns nothing
//      Only adds the units inside the given parameters.

***********************************************************************

        -Group Handling:

    method isEmpty takes nothing returns boolean
//      Returns true if the group is empty.
//      Using "0==this.count" gives the same result.

***********************************************************************

        -Additional Tools:

    method closestToLoc takes real x, real y returns unit
//      Returns the closest unit from the group to the given location.

    method farthestToLoc takes real x, real y returns unit
//      Returns the farthest unit from the group to the given location.

***********************************************************************

    VARIABLES:
    
        -Globals provided
        
    group ENUM_GROUP
//      Initialized group.

        -Non Static Members (this.Variable)

    readonly integer count
//      The amount of units inside the group (Non modifiable).

    readonly unit FirstOfGroup
//      The first unit of the group, when another is added or removed it changes (Non modifiable).

    readonly unit NextOfGroup
//      Basically cycles between units from the group everytime it's used (Non modifiable).

    integer data
//      This variable exists for those who wants to attach instances to the group.


**********************************************************************/


//  Basically this is how the OOP works:
//      A group is allocated, this group has record of 1 random unit from an UnitList, this unit knows the next and previous
//      instance from it's list. In the example below the group G has 6 units A - B - C - D - E - F

//            B - C
//           /     \
//      G - A      D
//          \     /
//           F - E

//      If the unit "Z" is added to the group then it would look like this

//            B - C - D
//           /         \
//      G - Z          E
//          \         /
//           A   -   F

//      If the unit "D" is removed from the group then it would look like this

//            F - A
//           /     \
//      G - E      Z
//          \     /
//           C - B

//      Notice how the unit the group points to changed from A to Z and now is E. It doesn't really matter which unit the group
//      points to, as long as it belongs to the same unit list the group will remain complete. The reason of why it changed from
//      being "Z" to "E" is explained below.

    globals
        group ENUM_GROUP=CreateGroup()
    endglobals

    private struct UnitList extends array
        implement Alloc
        //The way this system keeps track of units it's similar to a linked list, only that there's no head. UnitList only keeps
        //track of the next and prev units from an unit. In a list where units "A" "B" and "C" are linked it's representation would be
        //like this:
        //  A - B - C - A - B...
        //If this were a normal linked list, "A" would be the head, but instead is just another echelon.
        
        thistype next
        thistype prev
        unit unit
    endstruct
    
    struct UnitGroup extends array
        implement Alloc
        //the "first variable works like a pointer the group has towards any unit allocated inside UnitList, and because this
        //list keeps track units that follow it, there's no need for the group to know all of them.
        private UnitList first
        //keeps track of how many units are inside the group.
        readonly integer count
        //For those who want to attach instances to the group
        integer data

        //Table is used almost exclusively for the Group.hasUnit method.
        private Table lookUp
        
        method addUnit takes unit whichUnit returns nothing
            local UnitList thisUnit
            local UnitList start
            local integer id=GetHandleId(whichUnit)
            
            //prevent adding the same unit to the group twice
            if this.lookUp.has(id) then
                return
            endif
            set thisUnit=UnitList.allocate()
            
            //This is for creating a LinkedList without a head.
            if 0==this.count then
                set start=thisUnit
                //It dosn't really matter which UnitList is stored in the "first" variable
                set this.first=thisUnit
            else
                set start=this.first
            endif
            
            set start.next.prev=thisUnit
            set thisUnit.next=start.next
            set start.next=thisUnit
            set thisUnit.prev=start
            
            set thisUnit.unit=whichUnit
            set this.lookUp[GetHandleId(thisUnit.unit)]=thisUnit
            set this.count=this.count+1
        endmethod
        
        method removeUnit takes unit whichUnit returns nothing
            local integer h=GetHandleId(whichUnit)
            local UnitList thisUnit=this.lookUp[h]
            set thisUnit.next.prev=thisUnit.prev
            set thisUnit.prev.next=thisUnit.next
            
            //This would be only necessary if the deallocated unit IS the "first" of the group, but to avoid useless checks
            //it's performed everytime an unit is deallocated. Again, it dosn't matter which UnitList is the "first" variable
            //unless it's an unit from outside the group.
            set this.first=thisUnit.next
            
            call thisUnit.deallocate()
            call this.lookUp.remove(h)
            set this.count=this.count-1
        endmethod
        
        //Safety method to remove Non-existant units from the unit group
        private method safeIteration takes nothing returns nothing
            loop
                exitwhen not(0==GetUnitTypeId(this.first.unit)) or (0==this.count)
                call this.removeUnit(this.first.unit)
            endloop
        endmethod
        
        method operator FirstOfGroup takes nothing returns unit
            call this.safeIteration()
            return this.first.unit
        endmethod
        
        //This method always returns a different unit from the group, won't return the same unit until it goes through all
        //the units inside the group.
        method operator NextOfGroup takes nothing returns unit
            set this.first=this.first.next
            call this.safeIteration()
            return this.first.unit
        endmethod
        
        //Destroys the whole list looping through it once.
        method destroy takes nothing returns nothing
            local UnitList next
            loop
                exitwhen 0==this.count
                set next=this.first.next
                call this.lookUp.remove(GetHandleId(this.first.unit))
                call this.first.deallocate()
                set this.first=next
                set this.count=this.count-1
            endloop
            call this.deallocate()
        endmethod
        
        method clear takes nothing returns nothing
            local UnitList next
            loop
                exitwhen 0==this.count
                set next=this.first.next
                call this.lookUp.remove(GetHandleId(this.first.unit))
                call this.first.deallocate()
                set this.first=next
                set this.count=this.count-1
            endloop
        endmethod

        method isEmpty takes nothing returns boolean
            return (0==this.count)
        endmethod
        
        method hasUnit takes unit whichUnit returns boolean
            return this.lookUp.has(GetHandleId(whichUnit))
        endmethod
        
        //! runtextmacro GroupTools_GroupEnumerationTools()
        
        static method create takes nothing returns thistype
            local thistype this=thistype.allocate()
            set this.lookUp=Table.create()
            set this.count=0
            return this
        endmethod
    endstruct
    
    struct SimpleGroup extends array
        implement Alloc
        //This one works like a number stack, it's a lot faster for unit groups that doesn't need
        //removing units or checking for units inside of it.
        
        private UnitList first
        readonly integer count
        integer data
        
        private method safeIteration takes nothing returns nothing
            local integer i=this.count
            loop
                exitwhen not(0==GetUnitTypeId(this.first.unit)) or 0==i
                set this.first=this.first.next
                set i=i-1
            endloop
        endmethod

        method operator FirstOfGroup takes nothing returns unit
            call this.safeIteration()
            return this.first.unit
        endmethod
        
        method operator NextOfGroup takes nothing returns unit
            set this.first=this.first.next
            call this.safeIteration()
            return this.first.unit
        endmethod
        
        method isEmpty takes nothing returns boolean
            return (0==this.count)
        endmethod
        
        method addUnit takes unit whichUnit returns nothing
            local UnitList thisUnit=UnitList.allocate()
            local UnitList start
            
            if 0==this.count then
                set start=thisUnit
                set this.first=thisUnit
            else
                set start=this.first
            endif
            
            set thisUnit.next=start.next
            set start.next=thisUnit
            
            set thisUnit.unit=whichUnit
            set this.count=this.count+1
        endmethod
        
        method clear takes nothing returns nothing
            local UnitList next
            loop
                exitwhen 0==this.count
                set next=this.first.next
                call this.first.deallocate()
                set this.first=next
                set this.count=this.count-1
            endloop
        endmethod
        
        method destroy takes nothing returns nothing
            local UnitList next
            loop
                exitwhen 0==this.count
                set next=this.first.next
                call first.deallocate()
                set first=next
                set this.count=this.count-1
            endloop
            call this.deallocate()
        endmethod
        
        //! runtextmacro GroupTools_GroupEnumerationTools()
        
        static method create takes nothing returns thistype
            local thistype this=thistype.allocate()
            set this.count=0
            return this
        endmethod
    endstruct
    
    //! textmacro GroupTools_GroupEnumerationTools
    
        //These two methods use Selection Sort, i can't think of a faster method to compare units
        method farthestToLoc takes real x, real y returns unit
            local integer i=0
            local unit u=this.first.unit
            local real od
            local real ox=GetUnitX(u)-x
            local real oy=GetUnitY(u)-y
            local real min=SquareRoot(ox*ox+oy*oy)
            local unit r=u
            loop
                exitwhen i==this.count
                set this.first=this.first.next
                call this.safeIteration()
                set u=this.first.unit
                set ox=GetUnitX(u)-x
                set oy=GetUnitY(u)-y
                set od=SquareRoot(ox*ox+oy*oy)
                if od>min then
                    set r=u
                    set min=od
                endif
                set i=i+1
            endloop
            set u=null
            return r
        endmethod
        
        method closestToLoc takes real x, real y returns unit
            local integer i=0
            local unit u=this.first.unit
            local real od
            local real ox=GetUnitX(u)-x
            local real oy=GetUnitY(u)-y
            local real min=SquareRoot(ox*ox+oy*oy)
            local unit r=u
            loop
                exitwhen i==this.count
                set this.first=this.first.next
                call this.safeIteration()
                set u=this.first.unit
                set ox=GetUnitX(u)-x
                set oy=GetUnitY(u)-y
                set od=SquareRoot(ox*ox+oy*oy)
                if od<min then
                    set r=u
                    set min=od
                endif
                set i=i+1
            endloop
            set u=null
            return r
        endmethod
        
        private static thistype enumGroup
        
        private static method addTo takes nothing returns boolean
            call enumGroup.addUnit(GetFilterUnit())
            return false
        endmethod
        
        method enumUnitsInRange takes real x, real y, real radius returns nothing
            set enumGroup=this
            call GroupEnumUnitsInRange(ENUM_GROUP,x,y,radius,Filter(function thistype.addTo))
        endmethod

        private static real tempX
        private static real tempY
        private static real tempN
        private static real tempM

        private static method addConeTo takes nothing returns boolean
            local unit u=GetFilterUnit()
            local real a=Atan2(GetUnitY(u)-tempY,GetUnitX(u)-tempX)
            if a<0 then
                set a=a+bj_PI*2
            endif
            if (a>=tempN-tempM) and (a<=tempN+tempM) then
                call enumGroup.addUnit(u)
            endif
            set u=null
            return false
        endmethod

        method enumUnitsInCone takes real x, real y, real radius, real facing, real coneAngle returns nothing
            if 0==coneAngle then
                debug call BJDebugMsg("the given cone angle was 0, group won't pick anything")
                return
            endif
            set enumGroup=this
            set tempX=x
            set tempY=y
            set tempN=facing
            set tempM=coneAngle/2
            call GroupEnumUnitsInRange(ENUM_GROUP,x,y,radius,Filter(function thistype.addConeTo))
        endmethod
        
        private static method addRingTo takes nothing returns boolean
            local unit u=GetFilterUnit()
            local real x=GetUnitX(u)-tempX
            local real y=GetUnitY(u)-tempY
            local real d=SquareRoot(x*x+y*y)
            if d>=tempN then
                call enumGroup.addUnit(u)
            endif
            set u=null
            return false
        endmethod
        
        method enumUnitsInRing takes real x, real y, real innerRadius, real outerRadius returns nothing
            if innerRadius>=outerRadius then
                debug call BJDebugMsg("You can't have a ring with an inner radius bigger than the outer radius")
                return
            endif
            set enumGroup=this
            set tempX=x
            set tempY=y
            set tempN=innerRadius
            call GroupEnumUnitsInRange(ENUM_GROUP,x,y,outerRadius,Filter(function thistype.addRingTo))
        endmethod
        
        method enumUnitsInRect takes rect r returns nothing
            set enumGroup=this
            call GroupEnumUnitsInRect(ENUM_GROUP,r,Filter(function thistype.addTo))
        endmethod
        
    //! endtextmacro
endlibrary
 
Your linked lists could be sped up lots, for example when clearing you
could skip setting the first unit in the group over and over and just set
the local thistype this, instead.

Does this empty the group as you iterate through it? There wouldn't
really be a point in doing that if so.

Using your built in enumerators for range/rect are incredibly slow
because you open a new thread for each unit during the filter, and
then we have to use your unit group utility then to iterate through.
Instead you should just have an option that does "convert group to
UnitGroup", and loop through using FirstOfGroup.

Currently there is too much overhead to see too much use in this.

Tonight I must upload my vJass LinkedList utility because it is really
useful for things like this, I don't know why I've kept neglecting the
upload.
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
Remember that's an old script, i don't really like bitching other ressources but AutoIndex is really cumbersome and many parts have so much overheads.

And about Nestharus, well i won't flame him, i keep my thoughts for me, it will be the first and last time that i will "talk" about it.
Consider me as a jealous noob or whatever else, i don't care.
 
Level 6
Joined
Jun 20, 2011
Messages
249
@Bribe
Your linked lists could be sped up lots, for example when clearing you
could skip setting the first unit in the group over and over and just set
the local thistype this, instead.
I'll look into this, I remember doing this because if the "first" unit was deleted then it would point towards nothing, if adding a boolean check is faster, then i'll do that

Does this empty the group as you iterate through it? There wouldn't
really be a point in doing that if so.
No, one of the cool things about this.

Using your built in enumerators for range/rect are incredibly slow
because you open a new thread for each unit during the filter, and
then we have to use your unit group utility then to iterate through.
Instead you should just have an option that does "convert group to
UnitGroup", and loop through using FirstOfGroup.
I like the idea of converting groups to UnitGroups and around.

@Troll-Brain
The first time i developed this system i did it in 2 very different flavors: this one, and one similar to yours, they both did the exact same thing except one difference, arrays are a lot faster than tables, so i ended up deleting the other one. If your system was faster for more than 100 units, this might be faster for more than 10. But still throgh some benchmarks i've done, this system proves equal to natives.

Remember the SimpleGroup struct, i added it to improve speed for groups that doesn't need that many options, it uses only arrays instead of a Table to look up for units.
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
But you can't really get rid of hashtable if you want to add the same feature as me :
Units are automatically removed from an "unitList" when the unit is removed of the game.
And for me, this feature is a requirement, not an option.
I use the hastable only to link units and unitLists, then it's a simple double linked list (array)

My benchmarks are truly outdated, since i got rid of the module autodestroy which is really inefficient, and improved the code.
I wouldn't be suprised if it is = to native group with a script optimized, at least on iterating (but i'm not saying it is, just a though), the other operations would be several times slower.
But like i've said the efficiency was not the point of it.

I like also the boolean returned by the methods, natives functions related to group should have the same behavior.
I recommend to read the whole thread in thehelper.

I keep the relative order of the units inside an unitList because it can be useful for a custom spell or even a system.

Anyway good luck with it, i can't force your point of view about your own resource.
 
Level 6
Joined
Jun 20, 2011
Messages
249
JASS:
scope Tests
    // 24 - 27 fps
    private struct Main extends array
        private static method period takes nothing returns nothing
            local group g
            local unit u
            local integer i=150
            loop
                exitwhen 0==i
                set g=CreateGroup()
                call GroupEnumUnitsInRange(g,0,0,600,null)
                loop
                    set u=FirstOfGroup(g)
                    exitwhen null==u
                    call GroupRemoveUnit(g,u)
                endloop
                call DestroyGroup(g)
                set i=i-1
            endloop
            set g=null
            set u=null
        endmethod
        private static method onInit takes nothing returns nothing
            call TimerStart(CreateTimer(),0.005,true,function thistype.period)
        endmethod
    endstruct
endscope
JASS:
scope Tests
    // crash
    private struct Main extends array
        private static method period takes nothing returns nothing
            local SimpleGroup g
            local integer i=150
            local integer n
            loop
                exitwhen 0==i
                set g=SimpleGroup.create()
                call g.enumUnitsInRange(0,0,600)
                set n=0
                loop
                    exitwhen n==g.count
                    set n=n+1
                endloop
                call g.destroy()
                set i=i-1
            endloop
        endmethod
        private static method onInit takes nothing returns nothing
            call TimerStart(CreateTimer(),0.005,true,function thistype.period)
        endmethod
    endstruct
endscope
JASS:
scope Tests
    // 5 fps - crashes
    private struct Main extends array
        private static method enum takes nothing returns boolean
            return false
        endmethod
        
        private static method period takes nothing returns nothing
            local group g
            local integer i=150
            loop
                exitwhen 0==i
                set g=CreateGroup()
                call GroupEnumUnitsInRange(g,0,0,600,Filter(function thistype.enum))
                call DestroyGroup(g)
                set i=i-1
            endloop
            set g=null
        endmethod
        private static method onInit takes nothing returns nothing
            call TimerStart(CreateTimer(),0.005,true,function thistype.period)
        endmethod
    endstruct
endscope
I even ran a bench mark that runs through nestharus's UnitList as "all units in map" to add units to an array group, still a lot slower, but i deleted id.
 
Level 6
Joined
Jun 20, 2011
Messages
249
The thing is that the sole act of "adding" an unit to a linked list is very slow (my benchmarks prove that) enumerating through them is actually fast. So, whats the point of having a group if you cant add units to it? The fastest way to adds unit to a group is through native enumeration functions, any other way is slower. IMO you lose more speed by trying to loop quickly to the group than adding units to it by another method than the already existant.
 
Top