3

I'm trying to create a function that starts and stops a timer. The starting is always on the click of a button, but the stopping can be due to the timer running down or the function being called again from another function.

This is what I have so far. Works perfect for what you see but I cannot figure out how to incorporate clearInterval() so that it stops when the game is won. The functioning calling timerCountdown is located in a different js file. I've read answers to similar questions but they are all seem to be doing it a little differently to where I can't make it work for my case

I do realize that I need to call clearInterval(count) but I don't know how to incorporate this into the function itself.

const timerCountdown = () => {                             
  let count = setInterval(() => {
    let timeLeft = timer.innerHTML
    if (timeLeft > 0) {
      timer.innerHTML = timeLeft - 1
    } else {
      gameOverMessage()
    }
  }, 1000) 
}
Jeff S
  • 109
  • 2
  • 13

3 Answers3

3

You need to push the interval id in a global variable. Like that you can use another function to stop the interval when you want.

Like

let intervalId; // define `count` globaly
let timer = document.getElementById('timer')

const timerStop = () => {
  clearInterval(intervalId)
}

const timerRestart = () => {
  timerStop()
  timer.innerHTML = 100;
}

const timerStart = () => {
  timerStop(); // Stop timer by security, e.g. if you call `timerStart()` multiple times
  intervalId = setInterval(() => {
    let timeLeft = timer.innerHTML;
    if (+timeLeft > 0) {
      timer.innerHTML = +timeLeft - 1
    } else {
      timerRestart();
      gameOverMessage()
    }
  }, 1000)
}
<div id="timer">100</div>

<div>
  <button onclick="timerStart()">Start</button>
  <button onclick="timerStop()">Pause</button>
  <button onclick="timerRestart()">Restart</button>
</div>
R3tep
  • 12,512
  • 10
  • 48
  • 75
2

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>
Ben Aston
  • 53,718
  • 65
  • 205
  • 331
1

You could take a global variable intervalId and clear the interval timer, if won or if no time is avaliable.

var intervalId;

const timerCountdown = () => {                             
    intervalId = setInterval(() => {
        let timeLeft = timer.innerHTML
        if (timeLeft > 0) {
            timer.innerHTML = timeLeft - 1
        } else {
            clearInterval(intervalId);
            gameOverMessage();
        }
    }, 1000) 
  },
  won = () => {
      clearInterval(intervalId);
      // additional code
  };
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392