• 🏆 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!

LoopEvent

Status
Not open for further replies.

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
I based this work from the idea I found here which uses one universal timer for all 0.03125 period codes. Basically, what mine does is somewhat similar to the previous one but the difference is that it allows you to specify other periods aside from 0.03125 while still using a single timer for all registered codes.

However, this system works best for codes with periods divisible by 0.03125. Although any values greater than 0.03125 will work, it will cause the code execution interval not to be even. What does that mean? For example call RegisterLoopEvent(function Test, 0.05)
So after 1 tick of the timer, the code's time becomes 0.03125 and won't run yet. Next, code's time will be 0.0625 and then it runs, leaving a remainder time of (0.0625 - 0.05) = 0.0125. So that remainder time will be added to the next tick and it will cause the code for example, to run two consecutive ticks then skips one tick then run another two consecutive ticks and so on. BUT this may not be much of a problem for codes with low frequency period such as those with timeouts greater than 2 seconds.

Please give your feedbacks on what to improve on or just feel free to comment =)


SCRIPT:
JASS:
library LoopEvent uses Table


    //! novjass
    |=====|
    | API |
    |=====|

    function RegisterLoopEvent takes code c, real timeout returns boolean/*
    - Registers a code to run every <timeout> seconds and returns a boolean value depending
      on the success of the operation

  */function RemoveLoopEvent takes code c returns boolean/*
    - Unregisters a code from the loop and returns a boolean value depending
      on the success of the operation

  *///! endnovjass

    //======================================================================================

    globals
        private constant real TIMEOUT = 0.03125
    endglobals

    //======================================================================================

    globals
        private Table hash
        private timer Timer = CreateTimer()
        private trigger array trig
        private integer count = 0
        private real array codeTimeout
        private real array elapsed
    endglobals

    static if DEBUG_MODE then
        private function Debug takes string msg returns nothing
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60, "|CFFFFCC00[LoopEvent] :|R" + msg)
        endfunction
    endif

    private function RunLoop takes nothing returns nothing
        local integer i = 0
        loop
            set i = i + 1
            set elapsed[i] = elapsed[i] + TIMEOUT
            if elapsed[i] >= codeTimeout[i] then
                call TriggerEvaluate(trig[i])
                set elapsed[i] = elapsed[i] - codeTimeout[i]
            endif
            exitwhen i == count
        endloop
    endfunction

    function RegisterLoopEvent takes code c, real timeout returns boolean
        local integer i = GetHandleId(Filter(c))
        if timeout < TIMEOUT then
            debug call Debug("ERROR: Entered code execution timeout is less than the minimum value (" + R2S(TIMEOUT) + ")")
        elseif trig[hash.integer[i]] == null then
            debug if timeout - (timeout/TIMEOUT)*TIMEOUT > 0.00 then
                debug call Debug("WARNING: Entered code timeout is not divisible by " + R2S(TIMEOUT) + ", this code's execution interval will not be even")
            debug endif
            set count = count + 1
            set elapsed[count] = 0.00
            set codeTimeout[count] = timeout
            set trig[count] = CreateTrigger()
            call TriggerAddCondition(trig[count], Filter(c))
            set hash.integer[i] = count
            if count == 1 then
                call TimerStart(Timer, TIMEOUT, true, function RunLoop)
                debug call Debug("There is one code instance registered, starting to run timer")
            endif
            return true
        debug else
            debug call Debug("ERROR: Attempt to double register a code")
        endif
        return false
    endfunction

    function RemoveLoopEvent takes code c returns boolean
        local integer i = hash.integer[GetHandleId(Filter(c))]
        if trig[i] != null then
            debug call Debug("Removing a code from the loop")
            call TriggerClearConditions(trig[i])
            call DestroyTrigger(trig[i])
            set count = count - 1
            if count == 0 then
                call TimerStart(Timer, 0, false, null)
                debug call Debug("There are no code instances running, stopping timer")
            else
                loop
                    set i = i + 1
                    exitwhen i > count + 1
                    set trig[i - 1] = trig[i]
                    set codeTimeout[i - 1] = codeTimeout[i]
                endloop
            endif
            return true
        endif
        debug call Debug("ERROR: Attempt to remove a null or an already removed code")
        return false
    endfunction

    private module M
        static method onInit takes nothing returns nothing
            set hash = Table.create()
        endmethod
    endmodule

    private struct S extends array
        implement M
    endstruct


endlibrary
 
Level 29
Joined
Jul 29, 2007
Messages
5,174
This is actually how update loops of real programs that desire a constant (on average) FPS are designed, so you are fine.
And by this I mean using a remainder, and sometimes something doesn't run, sometimes it runs once, sometimes it runs twice, etc.
The important thing is that on average you do end up getting consistent results (this is even less relevant in WC3, where your accuracy for anything is pretty bad in the first place, and reality doesn't quite match with the numbers you input either way).
 
Level 13
Joined
Nov 7, 2014
Messages
571
I based this work from the idea I found here which uses one universal timer for all 0.03125 period codes.

Timer32 implements a fully optimised timer loop for a struct.

Well... according to the Jass Benchmarking Results, TriggerEvaluate, which is used by Timer32 and your verison, is
~87% slower than calling a function directly so they are not the best choice for performance critical scripts (i.e those that need to support as many "entities" as possible).

And about using a single timer... why so stingy with timers? It's not like they are very expensive or anything...
 
Level 14
Joined
Jul 1, 2008
Messages
1,314
while I like the idea of having a single timer, that manages all different things in the game, I see one critical point. If that periodic system crashes, then it impacts a lot. I had some problems lately with some timers stopping for unknown reasons .. ´probably the did hit the op limit or what. I dont know, but did you exclude, that things like this may affect your timing? Sry, if this is noober rubbish ..
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
while I like the idea of having a single timer, that manages all different things in the game, I see one critical point. If that periodic system crashes, then it impacts a lot. I had some problems lately with some timers stopping for unknown reasons .. ´probably the did hit the op limit or what. I dont know, but did you exclude, that things like this may affect your timing? Sry, if this is noober rubbish ..
Well as what I know, the TriggerEvaluate runs codes on a different thread so unless you have so much code registered, perhaps hundreds, it might crash.

Well... according to the Jass Benchmarking Results,
TriggerEvaluate
, which is used by Timer32 and your verison, is
~87% slower than calling a function directly so they are not the best choice for performance critical scripts (i.e those that need to support as many "entities" as possible).

And about using a single timer... why so stingy with timers? It's not like they are very expensive or anything...
This is actually one thing I was thinking about lately, I'm not sure if overall its a good idea or not.

EDIT:
Thanks for the feedback btw, and I'm still encouraging more feedbacks from others or from you guys.
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
Wc3 is slow to make dynamic invocations, so a frequent timer would require static code. But stuffing different scopes into that one timer would create immense coupling (one thread, op limit, ...). This is why I rather use the approach of only stringing instances of the same scope together, like when you scale units over time with some library, use one timer for all unit scalings but only for this local purpose.
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Or maybe at least use multiple triggerconditions on one trigger instead of creating a trigger for every registration.
Using multiple triggerconditions on one trigger is not good I think as all trigger conditions are forced to be evaluated every 0.03125 seconds. I used the same principle as the SpellEffectEvent where only the code who meets the check is executed by using a separate trigger for each instead of just using RegisterAnyPlayerUnitEvent( EVENT_PLAYER_UNIT_SPELL_EFFECT, function OnCast ) where all triggerconditions in that trigger is being evaluated each time a spell is cast.
 
Last edited:
Status
Not open for further replies.
Top