setInterval
makes a best effort to space the running of the callback according to the interval you specify. The thing is: in a game, what you actually want is the current state of the world to be printed to the screen in smooth and timely fashion. This is different to the behavior of setInterval
, which knows nothing about the screen and is blindly called repeatedly.
For example: if you kick-off setInterval(foo, 100)
for your game in a browser tab, and then navigate to another tab in your browser, wait ten seconds and then return to your game, your foo
callback will be invoked about a hundred times in rapid succession as the queued callbacks are "drained". It is highly unlikely you want this behavior.
requestAnimationFrame
is a better solution for this, because it is only called when (shortly before) your game is rendered - which is what you want.
In the following code a timer object is created by createTimer
. The timer has start
, stop
and toggle
methods.
The start
method records when it was invoked and triggers requestAnimationFrame
, supplying a callback called tick
. Every time a tick occurs, we run some logic to see which (if any) callback to invoke.
If the time elapsed is greater than or equal to the duration
of the timer, then the onTimeout
callback is invoked and the timer is stopped.
If the time elapsed is smaller than the duration
, but greater than or equal to the interval
period, then we update the lastInterval
and invoke the onInterval
callback.
Otherwise we simply cue up another tick of the timer.
The stop
method simply uses the request animation ID to cancel the timer with cancelAnimationFrame
.
function createTimer() {
let rafId = null
function start({duration = 10000, interval = 1000, onInterval, onTimeout, onStop, startTime=performance.now(), lastInterval = startTime}) {
function tick(now=performance.now()) {
const elapsed = now - startTime
if (elapsed >= duration) {
cancelAnimationFrame(rafId)
rafId = null
return onTimeout()
}
if ((now - lastInterval) >= interval) {
lastInterval = now
onInterval({
duration,
elapsed
})
}
rafId = requestAnimationFrame(tick)
}
rafId = requestAnimationFrame(tick)
}
function stop() {
cancelAnimationFrame(rafId)
rafId = null
return onStop()
}
function toggle(...args) {
rafId ? stop() : start(...args)
}
const timer = {
start,
stop,
toggle
}
return timer
}
const timer = createTimer()
const onInterval = ({duration, elapsed})=>console.log(`Remaining: ${((duration - elapsed)/1000).toFixed(0)}`)
const onTimeout = ()=>console.log('Timed out.')
const onStop = ()=>console.log('Manually stopped.')
document.getElementById('btn').addEventListener('click', () => timer.toggle({
onInterval,
onTimeout,
onStop
}))
<button id="btn">Toggle Timer</button>