HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Thoughts on my timer system?

04-28-2009, 04:51 AM#1
Tossrock
I'm sure it's small beans compared to the big productions out there, but I needed something lightweight for my map so I worked this up. I'd appreciate any feedback on improving efficiency or general critiques. It's a single timer system that uses a linked list of objects and a few globals to run all timed effects.

Collapse JASS:
globals
    timeHandler MAIN_TIME
    real TIME_STEP = .03
    real SLOW_STEP = 1.00
    real TIME = 0
endglobals

//And in the init

set MAIN_TIME = timeHandler.create()


The setup


Collapse JASS:
library timeHandler requires Handles

    function mainStepCaller takes nothing returns nothing
        call MAIN_TIME.mainStep()
    endfunction

    struct timeHandler
        real time
        timer handler
        boolean slow = true
        TMLinkedList objects
        
        static method create takes nothing returns timeHandler
            local timeHandler th = timeHandler.allocate()
            set th.handler = CreateTimer()
            set th.objects = TMLinkedList.create()
            call TimerStart(th.handler, SLOW_STEP, true, function mainStepCaller)
            return th
        endmethod
        
        method addObj takes timedObj obj returns boolean
            if(obj!=null) then
                call .objects.add(obj)
                //if(LIST) then
                //    call BJDebugMsg("Added "+obj.name+", current timed objs = "+I2S(MAIN_TIME.objects.size))
                //endif
                if .slow then
                    call PauseTimer(MAIN_TIME.handler)
                    call TimerStart(MAIN_TIME.handler, TIME_STEP, true, function mainStepCaller)
                    set .slow = false
                endif
                return true
            else
                //if(LIST) then
                //  call BJDebugMsg("Attempted to add null to list")
                //endif
            endif
            return false
        endmethod
        
        method removeObj takes timedObj obj returns nothing
            call .objects.removeObj(obj)
            //if(LIST) then
            //    call BJDebugMsg("Removed "+obj.name+", current timed objs = "+I2S(MAIN_TIME.objects.size))
            //endif
            if .objects.size == 0 then
                call PauseTimer(MAIN_TIME.handler)
                call TimerStart(MAIN_TIME.handler, SLOW_STEP, true, function mainStepCaller)
                set .slow = true
            endif
        endmethod
        
        method mainStep takes nothing returns nothing
            local timedObj head = .objects.head.obj
            local TMNode pointer = .objects.end
            local timedObj curr
            //if DEBUG then
            //    call BJDebugMsg("main step run: "+R2S(TIME))
            //endif
            if .objects.size > 0 then
                loop
                    set curr = pointer.obj
                    //if NAMES then
                    //    call BJDebugMsg(curr.name + " in list")
                    //endif
                    if curr.shouldStep() then
                        call curr.step()
                    endif
                    set curr.duration = curr.duration + TIME_STEP
                    exitwhen curr == head
                    set pointer = pointer.prev
                endloop
            else
                if not(.slow) then
                    call PauseTimer(.handler)
                    call TimerStart(.handler, SLOW_STEP, true, function mainStepCaller)
                endif
            endif
            if .slow then
                set TIME = TIME + SLOW_STEP
            else
                set TIME = TIME + TIME_STEP
            endif
        endmethod
    
    endstruct

endlibrary

This is the primary object that does all the work. It has a list of timedObj structs which it cycles through and updates for every timer period, taking the need to manage timers away from the spells. They just add themselves to the timeHandler and get taken care of.

Collapse JASS:
library timedObj requires Handles

    interface timedObj
        string name
        real duration = 0.0
        method step takes nothing returns nothing
        method shouldStep takes nothing returns boolean
    endinterface

endlibrary

This is just the interface that spell structs extend

Collapse JASS:
library LinkedList
//! textmacro LinkedList takes TYPENAME, TYPE
    struct $TYPENAME$LinkedList
        $TYPENAME$Node head
        $TYPENAME$Node end
        integer size = 0

        static method create takes nothing returns $TYPENAME$LinkedList
            local $TYPENAME$LinkedList list = $TYPENAME$LinkedList.allocate()
            return list
        endmethod

        method add takes $TYPE$ obj returns nothing
            if this.size == 0 then
                set this.head = $TYPENAME$Node.create(obj)
                set this.end = this.head
            elseif this.size == 1 then
                set this.head.next = $TYPENAME$Node.create(obj)
                set this.head.next.prev = this.head
                set this.end = this.head.next
            else
                set this.end.next = $TYPENAME$Node.create(obj)
                set this.end.next.prev = this.end
                set this.end = this.end.next
            endif
            set this.size = this.size+1
        endmethod

        method removeEnd takes nothing returns $TYPE$
            local $TYPENAME$Node objnode = this.end
            local $TYPE$ obj
            set this.end = objnode.prev
            set this.end.next = 0
            set this.size = this.size - 1
            set obj = objnode.obj
            call $TYPENAME$Node.destroy(objnode)
            return obj
        endmethod

        method removeHead takes nothing returns $TYPE$
            local $TYPENAME$Node objnode = this.head
            local $TYPE$ obj
            set this.head = objnode.next
            set this.head.prev = 0
            set this.size = this.size - 1
            set obj = objnode.obj
            call $TYPENAME$Node.destroy(objnode)
            return obj
        endmethod

        method removeNode takes $TYPENAME$Node match returns boolean
            local $TYPENAME$Node node = this.head
            loop
                if node == match then
                    set match.prev.next = match.next
                    set match.next.prev = match.prev
                    call $TYPENAME$Node.destroy(match)
                    set this.size = this.size - 1
                    
                    if match == this.head then
                        set this.head = match.next
                    elseif match == this.end then
                        set this.end = match.prev
                    endif
                    return true                  
                else
                    if node == this.end then
                        call BJDebugMsg("Couldn't remove object attached to node number "+I2S(match))
                        return false
                    else
                        set node = node.next
                    endif
                endif
            endloop
            return false
        endmethod

        method removeObj takes $TYPE$ match returns boolean
            local $TYPENAME$Node objnode = this.head
            local $TYPE$ obj = objnode.obj
            loop
                if obj == match then
                    return .removeNode(objnode)
                else
                    if objnode == this.end then
                        return false
                    else
                        set objnode = objnode.next
                        set obj = objnode.obj
                    endif
                endif
            endloop
            return false
        endmethod

        method onDestroy takes nothing returns nothing
            loop
                exitwhen this.size == 0
                call this.removeEnd()
            endloop
        endmethod

    endstruct

    struct $TYPENAME$Node
        $TYPENAME$Node next = 0
        $TYPENAME$Node prev = 0
        $TYPE$ obj

        static method create takes $TYPE$ obj returns $TYPENAME$Node
            local $TYPENAME$Node node = $TYPENAME$Node.allocate()
            set node.obj = obj
            return node
        endmethod

    endstruct
//! endtextmacro

//! runtextmacro LinkedList("TM","timedObj")
//! runtextmacro LinkedList("UN","unit")

endlibrary

This is a generic linked list implementation using text macros. I know the removeObj is wonky in that it calls removeNode instead of just removing the node itself, but I was having issues that it seemed to resolve. I think they may have been unrelated though, and I'll probably try changing it back in the future.

Collapse JASS:
library ImageTrail requires MapFunctions

    globals
        //Image trail stuff
        //integer IMAGETRAILSPAWNTIME = 4
        //integer IMAGETRAILFADEINTERVAL = 7
        real IMAGEDURATION = 1.0928
        real IMAGESEPERATION = .1
    endglobals
   
    struct ImageTrail extends timedObj
        unit caster
        boolean on
        UNLinkedList list
        real x
        real y
        real maxtime
        player p
        integer constraint = 0
        integer spawntime
        integer fadeinterval
        effect blur
        
        static method create takes real ImgDur, real ImgSep, real MaxTime, unit u returns ImageTrail
            local ImageTrail td = ImageTrail.allocate()
            set td.maxtime = MaxTime
            set td.spawntime = R2I(ImgSep/TIME_STEP)
            set td.fadeinterval = R2I((255/ImgDur)*TIME_STEP)
            set td.caster = u
            set td.list = UNLinkedList.create()
            set td.p = GetOwningPlayer(u)
            set td.on = true
            set td.x = GetUnitX(u)
            set td.y = GetUnitY(u)
            call MAIN_TIME.addObj(td)
            return td
        endmethod
        
        method shouldStep takes nothing returns boolean
            if(.list.size > 0 or (.on)) then
                return true
            else
                call MAIN_TIME.removeObj(this)
                call UNLinkedList.destroy(.list)
                call ImageTrail.destroy(this)
                return false
            endif
            return false
        endmethod
        
        method step takes nothing returns nothing
            local integer i = 0    
            local real x = GetUnitX(.caster)
            local real y = GetUnitY(.caster)
            local real distsquared = ( (x-.x)*(x-.x) ) + ( (y-.y)*(y-.y) )
            local unit trailimage
            local integer fade
            local UNNode node
            local UNNode temp
            
                if(.on and .constraint==0) then
                    if distsquared != 0 then
                        set trailimage = CreateUnit(.p,'hIMG',x,y,GetUnitFacing(.caster))
                        call .list.add(trailimage)
                        call SetUnitTimeScale(trailimage,.01)
                        call SetUnitX(trailimage,x)
                        call SetUnitY(trailimage,y)
                        call SetUnitFlyHeight(trailimage,GetUnitFlyHeight(.caster),100000)
                        call SetUnitUserData(trailimage,255)
                    endif
                    call DestroyEffect(.blur)
                    set .blur = AddSpecialEffectTarget("Abilities\\Spells\\Orc\\MirrorImage\\MirrorImageCaster.mdl", .caster, "origin")
                endif

                if .list.size > 0 then
                    set node = .list.head
                    loop
                        set trailimage = node.obj
                        set fade = GetUnitUserData(trailimage)
                        if (fade<=1) then
                            call .list.removeNode(node)
                            call RemoveUnit(trailimage)
                        else
                            call SetUnitVertexColor(trailimage,200,200,200,fade)
                            //call BJDebugMsg("Fading, interval = "+I2S(td.fadeinterval))
                            call SetUnitUserData(trailimage,fade-.fadeinterval)
                        endif 
                        exitwhen node == .list.end or .list.size == 0
                        set node = node.next
                    endloop
                endif
                if .duration >= .maxtime then
                    set .on = false
                endif
                set .x = x
                set .y = y
                if (.constraint == .spawntime) then
                    set .constraint = 0
                else
                    set .constraint = .constraint + 1
                endif
                
            set trailimage = null
        endmethod

    endstruct

endlibrary

And this is an example spell for the system (it creates a trail of images following the unit. I could change it to take the unit type as a parameter, but for my purposes hardcoding was sufficient).

The basic idea of the system is to have only one timer running at a time, that loops through a list of objects implementing a "timedObj" interface, and stepping the ones that declare they should be stepped. It's worked pretty well for me so far, but it can get strained by many copies of some of my more extravagant spells running at once.

Anyway, thoughts, critiques, scathing criticisms?
04-28-2009, 11:28 AM#2
Anitarf
Quote:
The basic idea of the system is to have only one timer running at a time, that loops through a list of objects implementing a "timedObj" interface, and stepping the ones that declare they should be stepped. It's worked pretty well for me so far, but it can get strained by many copies of some of my more extravagant spells running at once.
See, that's the problem with this: in pursuit of "optimizing" it down to using only a single timer, you made it much much slower. All those interface method calls have to use .evaluate which is considerably slower than regular function calls which you get if you use something like Periodic Struct.
05-13-2009, 01:56 AM#3
Tossrock
Interesting, I didn't know interface calls were slow. Do all calls to methods on interfaced objects use .evaluate? Or only when the object is in interface scope?

ie
Collapse JASS:
struct SpecificObject implements InterfaceObject
    method step takes nothing returns nothing
        call BJDebugMsg("foobar")
    endmethod
endstruct

interface InterfaceObject
    method step takes nothing returns nothing
endinterface

function SlowOrNot takes nothing returns nothing
    local SpecificObject specObj = SpecificObject.allocate()
    local InterfaceObject interObj = specObj
    //1
    call interObj.step()

    //2
    call specObj.step()
endfunction

do 1 and 2 both use .evaluate? Or only 1?

In any case, it wouldn't be too difficult to have a timer for each struct type if that'd eliminate the .evaluate slowness. Then again, PeriodicStructs seem to do pretty much the same thing, so maybe I'll just adapt those.
05-13-2009, 01:33 PM#4
Anitarf
Only interface calls use .evaluate (well, that and method calls if the method is declared below the point from where you're calling it), otherwise struct methods are functions and are called as such.