October 20, 2021, 10:13:17

Author Topic: Low resolution timers  (Read 234 times)

Offline Scaremonger

  • Full Member
  • ***
  • Posts: 233
    • ITSpeedway - Ramblings of a geek!
Low resolution timers
« on: May 13, 2021, 17:18:05 »
I wrote this as more of an experiment, but thought I should post it here in case anyone may need or want to improve on it.

It all started whilst I was looking at a simple animation in Javascript and I wondered how setTimeout() and setInterval() were implemented because there didn't seem to be any limit on the number you could create. In Windows, there is a limit of just 16 timers so you need some careful coding if you want to create lots of them (There was a post about this on the old Blitzbasic forum, but I cannot find it).

Behind the scene; Javascript and Node both use a list to hold active timers which is sorted by the time that the timer will be called. The event loop simply checks the current time against the header of the list and if it has expired, the callback is executed. setInterval() does exactly the same except it adds itself back into the list after activation.

A Timeout will wait for the requested time and then run.
An Interval will repeat on the duration you provide.

A Timeout or an Interval can be created to call a Handler function (like Javascript) or send an Event:

Using a handler
Code: [Select]
setInterval( 500, fire )
setTimeout( 1000, bang )

Function bang( context:Object )
Print "BANG!"
End Function
Function fire( context:Object )
Print "FIRE!"
End Function

Using an event
Code: [Select]
AddHook( EmitEventHook, eventhook )

setInterval( 500 )
setTimeout( 1000 )

Function eventhook:Object( id:Int, data:Object, context:Object )
Local event:TEvent = TEvent( data )
If event.id=TIMERTICK Print event.tostring()
Return data
End Function

You can also pass an object or string to the timer as per these examples and they will be passed to your handler or event in the context variable. You simply need to typecast this to a string or your object type.

Sending context data
Code: [Select]
' Timers with Handlers
setInterval( 500, fire, "MyString" )
setTimeout( 1000, bang, MyObject )

' Timers with events
setInterval( 500, "MyString" )
setTimeout( 1000, MyObject )

If you save the result of the setTimer() or setInterval() into a variable, you have the option of cancelling the timer:

Create and Cancel
Code: [Select]
local myTimeout:TTimer = setInterval( 500 )
local myInterval:TTimer = setTimeout( 1000 )

clearTimeout( myTimeout )
clearInterval( myInterval )

clearTimeout() and clearInterval() are identical; they can be interchanged and can also be replaced with the following:

Code: [Select]
local myTimeout:TTimer = setInterval( 500 )
myTimeout.cancel()

To integrate it into your application, you can either call update() within your game loop or attach it to the fliphook:

Calling the update in your game loop
Code: [Select]
include "timers.bmx"
Graphics 300,200
Repeat
Cls
TTimer.update()      'Update Timers
Flip
Until KeyHit( KEY_ESCAPE ) Or AppTerminate()

Or you can attach it to the Flip Hook, which cleans up your game loop a little:

Attaching to the flip hook
Code: [Select]
include "timers.bmx"
TTimer.UseFlipHook()      'Attach to Flip Hook
Graphics 300,200
Repeat
Cls
Flip
Until KeyHit( KEY_ESCAPE ) Or AppTerminate()

Lastly: This is the include file...

timers.bmx
Code: [Select]
' Timers for Blitzmax
' Public Domain, Supplied without warranty
' Author Si Dunford, May 2021

Rem CHANGE LOG

V1.0 - Created setTimeout(), setInterval(), TTimer (Using events)
V1.1 - Added useflip as option instead of update() plus onexit to detach fliphook
V1.2 - Expanded settimeout() and setinterval() to use handlers
V1.3 - Replaced list sort with insert that maintains sort order (10*faster)

End Rem

Global TIMERTICK:Int = AllocUserEventId( "TimerTick" )

Function setTimeout:TTimer( duration:Int, context:Object = Null )
Return New TTimer( duration, 0, Null, context )
End Function

Function setTimeout:TTimer( duration:Int, handler( context:Object ), context:Object = Null )
Return New TTimer( duration, 0, handler, context )
End Function

Function setInterval:TTimer( frequency:Int, context:Object = Null )
Return New TTimer( frequency, frequency, Null, context )
End Function

Function setInterval:TTimer( frequency:Int, handler( context:Object ), context:Object = Null )
Return New TTimer( frequency, frequency, handler, context )
End Function

Function clearTimeout( timer:TTimer )
If timer timer.cancel()
End Function

Function clearInterval( timer:TTimer )
If timer timer.cancel()
End Function

Type TTimer

Private

Global list:TList = New TList
Global initialised:Int = False
Global useflip:Int = False

Field link:TLink

Field timeout:Int ' When to activate
Field frequency:Int ' Repeating interval
Field handler( context:Object ) ' Handler function

Public

Field context:Object ' Object context

Method New( timer:Int, frequency:Int, handler( context:Object ), context:Object )
If Not initialised initialise()
Self.timeout   = MilliSecs() + timer
Self.frequency = frequency
Self.handler   = handler
Self.context   = context
Self.link      = insert( Self )
End Method

Method cancel()
link.remove()
End Method

' Manual method
Function update()
For Local timer:TTimer = EachIn list
If timer.timeout>MilliSecs() Exit
timer.activate()
Next
End Function

' Add "On-Flip" event handler
Function UseFlipHook()
If Not initialised initialise()
If Not useflip AddHook( FlipHook, TTimer.hook )
useflip = True
End Function

Private

' Add event handler and exit function
Function initialise()
If Not initialised OnEnd( TTimer.depart )
initialised = True
End Function

' Insert into list maintaining sort order
Function insert:TLink( timer:TTimer )
Local t:TTimer
Local link:TLink = TTimer.list.firstlink()
While link
t = TTimer(link.value())
If t.timeout > timer.timeout Return list.insertBeforeLink( timer, link )
link = link.nextlink()
Wend
' Not found, add to end
Return ListAddLast( list, timer )
End Function

Method activate()
link.remove() ' Kill existing timer
' Repeat timer
If frequency>0
timeout = MilliSecs() + frequency
'SortList( list, True, sorter )
link = insert( Self )
End If

If handler
handler( context )
Else
Local event:TEvent = CreateEvent( TIMERTICK, Self, 0, 0, 0, 0,context )
EmitEvent( event )
End If
End Method

' Flip hook
Function hook:Object( id:Int, data:Object, context:Object )
TTimer.update()
Return data
End Function

' Exit function
Function depart()
If useflip RemoveHook( FlipHook, hook )
useflip = False
End Function

End Type

I hope you guys find a use for it.

Si...

Offline Scaremonger

  • Full Member
  • ***
  • Posts: 233
    • ITSpeedway - Ramblings of a geek!
Re: Low resolution timers
« Reply #1 on: May 13, 2021, 17:44:36 »
Here is a little example that uses 4800 setInterval() timers to animate individual "things":

Code: [Select]
include "timers.bmx"
Graphics 800,600
TTimer.UseFlipHook() ' Use Flip Hook

' Create some things
For Local x:Int = 0 Until GraphicsWidth() Step 10
For Local y:Int = 0 Until GraphicsHeight() Step 10
New TThing( x, y )
Next
Next

Type TThing
Global list:TList = New TList()
Field x:Int,y:Int,angle:Int=0
Method New( x:Int, y:Int )
Self.x=x+5
Self.y=y+5
angle = Rand(0,360)
setInterval( Rand(100,1000), fn_rotate, Self )
ListAddLast( list, Self )
End Method
Method rotate()
angle = (angle + 10) Mod 360
End Method
Method render()
DrawLine( x,y, x+5*Cos(angle), y+5*Sin(angle) )
End Method
Function fn_rotate( context:Object )
TThing( context ).rotate()
End Function
End Type

Repeat
Cls
For Local thing:TThing = EachIn TThing.list
thing.render()
Next
Flip
Until KeyHit( KEY_ESCAPE ) Or AppTerminate()

Offline Midimaster

  • Sr. Member
  • ****
  • Posts: 363
    • Midimaster Music Education Software
Re: Low resolution timers
« Reply #2 on: May 13, 2021, 18:17:56 »
But will this be accurate enough? As you connect your timers to the flip the minimum accuracy will be 16msec. Together with the wrong re-setting of the timestamp you use:
Code: [Select]
...
If frequency>0
timeout = MilliSecs() + frequency
...

...this will add more inaccuracy to the time. did you check with a simple
Code: [Select]
...
print (Millisecs()-LastTime)
LastTime=Millisecs()
...
inside a Method rotate() how accurate the intervals really are?

I expect a setting of "100msec" wil produce 108msec in avarage with a values from 100 to 115

I suggest to calculate it like this:
Code: [Select]
...
If frequency>0
timeout = timeout + frequency
...

to get better intervals. Sometimes the action will be a little to late because of the fliptimer, but the next time it will come a little earlier instead



And I use the already existing event-timer with
Code: [Select]
CreateTimer 200
....
While WaitEvent()

   Select EventID()
...
  Case EVENT_TIMERTICK
TimerTickt()
....
Wend

Function TimerTickt()
Global MidiInTimer:INT
If (MidiInTimer<MilliSecs())
                        If MidiInTimer=0 then MidiInTimer=Millisecs()
MidiInTimer=MidiInTimer+10
Midi.Send
.....

Now with this a setting of "100msec" will produce  exact 100msec in avarage with a spread from 100 to 104
See my current project on PlayStore: 20Tracks-Audio-Player https://play.google.com/store/apps/details?id=midimaster.twentytrackd

Offline Scaremonger

  • Full Member
  • ***
  • Posts: 233
    • ITSpeedway - Ramblings of a geek!
Re: Low resolution timers
« Reply #3 on: May 13, 2021, 18:26:07 »
JavaScript uses this type of timer and they are well known as being inaccurate (especially if you use events) which is why I called the thread Low resolution timers.

There are situations where you don't need a perfect timer and in those cases you can create thousands of them with this.

Saying that, your suggestion did improve it quite a bit.
« Last Edit: May 13, 2021, 18:27:43 by Scaremonger »

 

SimplePortal 2.3.6 © 2008-2014, SimplePortal