HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Optimizing a system...

01-23-2007, 03:25 AM#1
rain9441
I'll start by saying I'm looking for conceptual ideas to solve my problem, not actual code.


I have an autocast system. Units with certain abilities will automatically cast their abilties when appropriate. The system is fairly complex and 100% JASS. It is on its way to being fully optimized. Its really quite the sight too, I can give a unit 6-7 abilities and it'll just fire away. Healing, Debuffs, channeled spells, AOE's, hell even mana burn and all its variations. Unfortunately I hit a snag on dispel'ish type effects.

Lets first just assume the ability in question is Abolish Magic. Yes, I know, abolish magic HAS an autocast, but custom abilities that do similar things (yet not exactly the same) may not. The basic underlying fact here is that the spell being casted is infact targetable on both enemies and allies, and has some girth to it (checking for buffs, etc).

Currently, what happens is, any unit with the abolish magic ability is automatically created its own trigger. The trigger says, when any unit enters a certain range (700) of the abolish magic casting unit, it is automatically added to group "UnitsInRange". UnitsInRange is of course 'attached' to the unit with the ability using a combonation of *seldom use* of handlevars and the great structs of JASSHelper (+1 pt for Vexorian). The unit also has a temporary group attached to it, because it cannot modify the UnitsInRange group (Units can have multiple autocastable abilities, all of which use UnitsInRange). The unit is also associated with a timer, which is executed every 1.19 seconds. The timer will do the following things:

if "unit is dead", kill timer, cleanup ,etc.
if "unit is channeling" do nothing.
if "last cast time + cooldown < current time" do nothing.
if "mana cost > mana" do nothing.

if temporarygroup is empty, add UnitsInRange to temporary group.
call ForGroup(temporarygroup, filter). [details below]

Now the ForGroup filter is complicated. What it does is this: If the enum unit is dead, no longer in range, has more positive than negative buffs (for allies, or vice versa if enemy), or has no buffs at all, then REMOVE it from the temporary group... Otherwise, set a global variable (target) to the unit. If the global variable (target) is not null, every unit after is then ignored (not removed from the group nor even check range etc). That check is what prevents repetative dumping of UnitsInRange to the temporary group over.. and over... and over again.

After the ForGroup call, the global variable target will hold the 'target' we should cast the spell on. If target is null, no valid target was found, so we do nothing and wait for the next time the timer fires.


Heres where it is having problems:

If I have one unit with abolish magic and 100 surrounding him, it runs smooth. It'll dispel a unit almost instantly after its hit with a negative buff. BUT, If I have 100 units, all with abolish magic, then it goes haywire. See, every unit is adding groups of 100 to another group, then looking through all 100 units, checking to see if they need to be dispelled. That looks like O(N^2) (100 units = 10000 checks) to me (worst case scenario). Worst case scenario happens to be when theres a large bunch of units who can cast dispel standing next to eachother when not one has a single buff. This case is extremely common. Fortunately for me, even if I have 30-40 or even 50 units with abolish magic standing next to eachother it runs semismooth, its just when you hit the 120 mark or so when it noticably bogs down. Do I anticipate someone making 100 units with dispel magic all next to eachother? Not really, but I hate it when maps lag due to heavy jass execution.

If there is any better way to approach this problem, I'm all ears.

Heres some alternatives I know don't work:


GroupEnumUnits... Those functions leak and are slow. They cannot be used at all. Having a trigger store all units that come into range into a group and re-using the group is much better than constantly fetching all nearby units.

Increasing the timer firing frequency: Not really fixing the problem, its just making the casting less efficient, and instead of slowing down at the 120 mark, it'll slow down at the 130 or 140 mark.

Jass code
Collapse JASS:

function Autocast_ActDispelSingleFilter takes nothing returns nothing
    local integer iPositiveBuffCount
    local integer iNegativeBuffCount

    if ( Autocast_FilterTarget != null ) then
        return
    endif

    set Autocast_FilterEnemy = GetEnumUnit()
    if ( GetUnitState(Autocast_FilterEnemy, UNIT_STATE_LIFE) <= 0.5 ) then
        call GroupRemoveUnit(Autocast_FilterGroup, Autocast_FilterEnemy)
        return
    endif

    if ( IsUnitInRange(Autocast_FilterCaster, Autocast_FilterEnemy, Autocast_FilterRange) == FALSE ) then
        call GroupRemoveUnit(Autocast_FilterGroup, Autocast_FilterEnemy)
        return
    endif

    set iPositiveBuffCount =  UnitCountBuffsEx(Autocast_FilterEnemy, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE)
    set iNegativeBuffCount =  UnitCountBuffsEx(Autocast_FilterEnemy, FALSE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE)

    if ( IsPlayerAlly(GetOwningPlayer(Autocast_FilterCaster), GetOwningPlayer(Autocast_FilterEnemy)) == TRUE ) then
        // allies, don't dispel if they got alot of good buffs
        if ( iPositiveBuffCount > iNegativeBuffCount ) then
            call GroupRemoveUnit(Autocast_FilterGroup, Autocast_FilterEnemy)
            return
        endif
    else
        // unlike allies, don't dispel enemies with lots of negative buffs
        if ( iPositiveBuffCount < iNegativeBuffCount ) then
            call GroupRemoveUnit(Autocast_FilterGroup, Autocast_FilterEnemy)
            return
        endif
        // hook summoners
        if ( IsUnitType(Autocast_FilterEnemy, UNIT_TYPE_SUMMONED) == TRUE ) then
            // bad programming, but whatever.
            set Autocast_FilterTarget = Autocast_FilterEnemy
            return
        endif
    endif

    if ( iPositiveBuffCount == 0 and iNegativeBuffCount == 0 ) then
        call GroupRemoveUnit(Autocast_FilterGroup, Autocast_FilterEnemy)
        return
    endif

    //call BJDebugMsg("Found one: " + I2S(iPositiveBuffCount) + "," + I2S(iNegativeBuffCount) + " " + GetUnitName(uTarget) + " of " + GetPlayerName(GetOwningPlayer(uTarget)))
    set Autocast_FilterTarget = Autocast_FilterEnemy
    return
endfunction
    
// returns boolean whether or not it casted...
function Autocast_ActDispelSingle takes ACAbility ab, ACUnit au, ACInstance ai returns boolean

    if ( FirstOfGroup(ai.gTemp) == null ) then
        call GroupAddGroup(au.gEnemiesInRange, ai.gTemp)
        call GroupAddGroup(au.gFriendsInRange, ai.gTemp)
    endif

    call ForGroup(ai.gTemp, function Autocast_ActDispelSingleFilter)
    if ( Autocast_FilterTarget == null ) then
        return FALSE
    endif

    if ( ab.strOrder == "id" ) then
        return IssueTargetOrderById(au.u, ab.iOrderId, Autocast_FilterTarget)
    else
        return IssueTargetOrder(au.u, ab.strOrder, Autocast_FilterTarget)
    endif

endfunction

01-23-2007, 04:58 AM#2
PipeDream
I shudder to think about doing it in JASS, but a quadtree might be the answer. GroupEnum is unavoidable unless you figure a way to tile the plane with circles :P but if you use a permanent group and a filter (which returns true) it won't leak. Plus they'd be cached, and in fact you'd only need one, since you do the subrectangles in JASS. Then, instead of looping through 100 units that need dispelling, you would just check within the square until you find one unit that is close enough, since I assume a unit can only cast once per moment. Ought to get you to constant time for finding the dispel target in linear units, and another linear in units step for the grid construction.

Edit: You wouldn't actually need a quadtree, just placing units on a square 90x90 grid in an array would be good enough I bet.

---
Well I ended up just writing it since it seemed like a fun problem. Untested, and probably won't work. The unit lists should be replaced with just a group. To pass the filter in I would recommend subclassing an interface.
Collapse JASS:
//! library UnitInRange

struct unitpair    
    unit u
    unitpair next
    static method Create takes unit u returns unitpair
        local unitpair p = unitpair.create()
        set p.u = u
        set p.next = 0
        return p
    endmethod
endstruct

globals
    private group g
    private unitpair array grid
    private integer array active    //determines if units were added this round
    private integer activecolor = 0
    private integer n = 45            // max is 90
    private real minx
    private real maxx
    private real miny
    private real maxy
    private real width
    private real height
    private real hx
    private real hy
    private rect maprect
    private boolexpr truebe
endglobals

private function truefunc takes nothing returns boolean
    return true
endfunction

function UIR_Init takes nothing returns nothing
    set maprect = GetWorldBounds()
    set minx = GetRectMinX(maprect)
    set miny = GetRectMinY(maprect)
    set maxx = GetRectMaxX(maprect)
    set maxy = GetRectMaxY(maprect)
    set width = maxx - minx
    set height = maxy - miny
    set hx = n / width        //with of a grid cell
    set hy = n / height

    set g = CreateGroup()

    set truebe = Filter(function truefunc)
endfunction

function destroylist takes unitpair p returns nothing
    local unitpair next
    loop
        exitwhen p == 0
        set next = p.next
        call unitpair.destroy(p)
        set p = p.next
    endloop
endfunction

function AddUnitToCell takes integer i, unit u returns nothing
    local unitpair p
    if active[i] == activecolor then
        set p = active[i]
        loop
            exitwhen p.next == 0
            set p = p.next
        endloop
        set p.next = unitpair.Create(u)
    else
        set active[i] = activecolor
        if grid[i] != 0 then
            call destroylist(grid[i])
        endif
        set grid[i] = unitpair.Create(u)
    endif
endfunction

private function GetRow takes real x returns integer
    return R2I(hx * (x - minx))
endfunction
private function GetCol takes real y returns integer
    return R2I(hy * (y - miny))
endfunction

function UIR_BuildCache takes nothing returns nothing
    local real x
    local real y
    local integer i
    local integer j
    local unit u
    set activecolor = activecolor + 1
    call GroupEnumUnitsInRect(g,maprect,truebe)
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null

        set x = GetUnitX(u)
        set y = GetUnitY(u)
    
        set i = R2I(hx * (x - minx))
        set j = R2I(hy * (y - miny))

        call AddUnitToCell(i*n + j,u)

        call GroupRemoveUnit(g,u)
    endloop
endfunction

//Must build cache before running
//Think about how to add the filter cleanly
function UIR_GroupEnumUnitsInRangeCounted takes group g, real x, real y, real r, integer count returns nothing
    local unitpair p
    local real rr = r*r
    local real dx
    local real dy
    // Draw bounding square around circle and take all cells that overlap with square
    local integer mini = GetRow(x-r)
    local integer maxi = GetRow(x+r)
    local integer minj = GetCol(y-r)
    local integer maxj = GetRow(y+r)

    local integer i = mini
    local integer j
    local integer offset
    loop
        exitwhen i > maxi
        set j = minj
        loop
            exitwhen j > maxj
            set offset = i*n+j
            if active[offset] == activecolor then
                set p = grid[offset]
                loop
                    exitwhen p == 0
                    set dx = GetUnitX(p.u) - x
                    set dy = GetUnitY(p.u) - y
                    if dx*dx + dy*dy < rr then        //Include collision circle (?)
                        call GroupAddUnit(g,p.u)
                        set count = count - 1
                        if count == 0 then
                            return
                        endif
                    endif
                    set p = p.next
                endloop
            endif
            set j = j + 1
        endloop
        set i = i + 1
    endloop
endfunction

//! endlibrary

I bet that the native GroupEnumUnitsInRangeCounted will just work though.
01-23-2007, 02:04 PM#3
rain9441
Hmmmm interesting.

So you're proposal says to divide the map into 2025 rectangles, and track the units inside the rectangles. So instead of 100 units all having 100 groups filled with all 100 units, you'll have maybe 2, 3 or possibly 4 groups filled with 25 each (and 2020 empty ones). I can have simple OnUnitEntersRect and onUnitLeavesRect to manage the units as well, if they move around and such.

Interesting way to approach the problem.

What about determining who to cast dispel on? If I have to find all units in range (Constant time), then iterate through them to find buffs, I'll have O(n).

100 units in 1 grid cell, all determining whether or not each other 100 units have buffs.... 100*100 = 10,000 (we're still at O(N^2)).

I think I may have an idea with your system though, store information on each cell, and reuse it. If there's nobody in cell (50,10) with a negative buff, we don't bother looking for more until some time duration expires.

EG. Instead of each unit going through the list of units in the 4 nearby rects, have the rects look. Maybe cell(50,10) has 100 units in it, 0 with buffs. I'll look through the 100 units and decide that there are no buffs, then store that information in the grid. Now all 100 units in cell(50,10) are looking for units to dispel so they say "hey, cell(50,10), got any dispellable targets?" And the cell will say look at it's hand see that it has no dispellable targets, so it will respond.. "Go fish". Almost instantaneously the next guy in the circle comes out and askss, "hey, cell(50,10), got any dispellable targets?" Well duh, don't even need to look at the hand to know that one.

You're example shows 45 as the # of times to disect a rect. The biggest map area I have in my map is ~13000x13000. Giving a rect size of 288. It could *easily* be bumped to a rect size of 1000 or 1200 even without issues, giving a ~12x12 = 144total groups. Not bad.

Sounds doable to me.

P.S. To add to a single-linked list, don't add to the tail... set added.next = head; set head = added. Much faster than looping.
01-23-2007, 08:07 PM#4
PipeDream
Quote:
To add to a single-linked list, don't add to the tail... set added.next = head; set head = added. Much faster than looping.
it was late ^_^ and better to abandon the lists and use groups, as you'll quickly run out of pairs if you have as many units as you say and they're so strongly concentrate.

If you only want units with buffs, then build the cache only with units with buffs. If you need to do this for many different conditions, build separate caches for each conditions or couple conditions. Change the private singleton class style stuff to a proper class.

Shrinking the number of squares down would be really good.