Let's build a timer and scheduler for Sprite Kit using Swift

This post assumes you have some know how working with Sprite Kit and Swift, and are fairly confident about what a game loop's timestep is.

The demo project in GitHub provides a Scheduler class that allows you to write code like the following in your Scene:

scheduler
  .every(1.0) // every one second
  .perform( self=>GameScene.updateElapsedTimeLabel ) // update the elapsed time label
  .end()
        
scheduler
  .every(0.5) // every half second
  .perform( self=>GameScene.createRandomSprite ) // randomly place a sprite on the scene
  .end()

 

But first, some ramblings about time step and Sprite Kit

To make a time based scheduler, we need to know the elapsed time in the system. At any point in time, we need to know exactly how much seconds has passed.

In a game, the update loop is called once per frame. The timestep or delta time (dt) is the time it takes between frames. Sprite Kit tried to maintain a 60FPS mark, so the update loop gets called every 1/60th of a second, making dt = .016666667 (well, theoretically anyway).

The default signature of Sprite Kit's update method is as follows:

func update(_ currentTime: NSTimeInterval)

The currentTime parameter is the current system time. So you could measure dt by keeping track on the previous system time and subtract that from the current time every frame. Hold on to that thought, because we need to define another aspect of measuring time in relation to a game.

Game Time vs Real World Time

Real world time is just that, the time you see when you look at a watch, or clock, or... you get the idea. Game Time may be the same as real world time - 5 mins in the real world is 5 mins of elapsed gameplay time. If you have slow motion effects, you may need to slow down time; if your game has controlling time as a core mechanic (like Braid) - well things can get complicated. So you see, game time does not always map directly to the real world time. Another aspect is how should game time elapse when you pause the game? Should it proceed ahead in the background? Or should time be paused as well? E.g. if it takes 30 seconds to make a resource, if you pause the game midway after 15 seconds, and get back to the game after an hour, should the resource be made already (as real world time an hour has passed) or should the resource take the remaining 15 seconds to finish making the resource? The answer to this question depends very much on the kind of game you want to make.

Now, using Sprite Kit's currentTime method to calculate dt every frame has slight problem - if you pause, wait an hour and then resume the game, the dt in the very first update loop after unpausing will be very very large. (It would equal 3600 seconds rather than 1/60th of a second, give or take a few).

 // just print currentTime in update
...
...
spritekit time:545123.830644708
// pause the game for some time / press the home button
// and then get back to the game after a while
// notice how the time jumps ahead in relation
// to real world time. If we use currentTime
// to calculate the timestep, then dt=10.666769 seconds
spritekit time:545134.497413708
...
...


So we need a better way to calculate dt - something that is flexible in taking into account the paused state of a game when required, and ignoring it when it is not required (depending on the nature of game you are trying to make).

We need a timer component that can advance time in the correct increment and calculate dt reliably. If we need game time to halt when the game is paused, dt should be zero; when the game is unpaused, we need to dt to again be set correctly to the difference between two frames.

The following function in the Timer class achieves this:

    private func advance(currentTime:CFTimeInterval, paused:Bool) {
        if paused {
            shouldCorrectAfterPause = true
            dt = 0
        }
        else  {
            if shouldCorrectAfterPause {
                dt = 0
                shouldCorrectAfterPause = false
            }
            else {
                dt = currentTime - previousTime
            }
            previousTime = currentTime
        }
    }

If you need game time to pause when your game is paused, the paused parameter value should reflect the state of your game scene. If game time progresses independently of whether the game scene is paused or not, always set the paused parameter in the advance function to false. Have a look at Timer.swift in the demo project for the full implementation.


 

UPDATE AUG 15  Another approach to is to limit the time step.

dt = currentTime - previousTime
if dt > 0.02 { 
    dt = 0.02 // something convenient
}

This way you can ignore pausing the timer.


Ok, time step sorted, now how about that scheduler?

A time based scheduler is actually easy to make. If you know the elapsed time (which is the accumulated value of dt), you know exactly how much time has elapsed.

Think of scheduled events as pieces of code that needs to be executed at a specific time. We need to abstract away the piece of code that needs to be executed, and the time at which the code's execution needs to be triggered. Pieces of code can be abstracted in terms of blocks, closures etc. (By having code abstracted away I mean we need to store a reference to a piece of executable code in a variable that can be invoked later on).

In the very least, a scheduled event should look like:

final class SchedulerEvent {
    var trigger:Double = 0.0
    var callback:TargetAction? = nil
    func fire() {
        if let c = callback {
            c.performAction()
        }
    }
}

We have the the time at which the event should be triggered, and something called a TargetAction. A TargetAction is a technique you can represent a block of code based on the fact that instance methods are curried functions in Swift. Seriously, read that post. So rather than use blocks or closures, we will use TargetActions.

When you create a scheduled event and pass it on to the scheduler, it gets inserted into a priority queue.  

In every frame update, the scheduler will update the elapsed time by adding dt to it, and then scan the priority queue for matching events that need to be run. If the top item in the queue has a trigger time that is less than the current elapsed time, it means that that item needs to be run. The item is popped off the queue, and its corresponding TargetAction is invoked.

If you want recurring events, we just update the trigger time and re-insert the event back to the queue; it will get picked up by the scheduler in the next appropriate update loop.

The demo project

Over at GitHub, you will find a demo implementation of a game scene that does two things:

  • Show how many seconds the game scene has been displayed; updates every second
  • Twice a second, a sprite is placed on a random position on the scene 

It uses the scheduler and timer components outlined above.