Timers in Sprite Kit
I made a demo for a simple scheduler for use with Sprite Kit in Swift.
NSTimers are difficult to manage because of the app's background-foreground cycle, and SKActions may not really be suited for this (for one - creating scheduled events as an SKAction is a pain and not very readable in the long run + it does not care about the SKScene
's paused state).
The approach I took was to roll out a custom scheduler which allows you to write code like:
Schedule a recurring event
scheduler
.every(1.0) // every one second
.perform( self=>GameScene.updateElapsedTimeLabel ) // update the elapsed time label
.end()
Schedule an event for a specific time
scheduler
.at(10.0) // ten seconds after game starts
.perform( self=>GameScene.createRandomSprite ) // randomly place a sprite on the scene
.end()
Schedule an event for later, and repeat 5 times
scheduler
.after(10.0) // ten seconds from now
.perform( self=>GameScene.createRandomSprite ) // randomly place a sprite on the scene
.repeat(5) // repeat 5 times
.end()
How does it work?
In a nutshell, the scheduler is a class that holds a priority queue of scheduler events. The scheduler maintains a variable that represents the elapsed time in a game. On every frame update:
- The scheduler updates its elapsed time reference
- Checks against the priority queue if any items in the queue need to be run; if it finds them, pops them out of the queue and runs the corresponding action. If this was a recurring event, its next trigger time will be updated and pushed back into the queue
- The scheduler runs forever, unless you stop it explicitly
Since the scheduler works by maintaining an elapsed time counter, it uses yet another roll your own Timer component. The default timing value in Sprite Kit's update
method is not useful when backgrounding/foregrounding an app, so we need to roll out a Timer component as well - this will allow us to calculate a correct time step for our game loop.
I go to some length in explaining the gotchas with finding your timestep further in a blog article.
Summary
NSTimer / GCD based dispatch async methods do not honour your game's notion of elapsed time, and does not integrate with Sprite Kit well. It will not work correctly in all cases (based on your game's logic), and will lead to hard-to-identify timing bugs.
Sprite Kit's
SKAction
s are awesome for running pre-defined actions like applying transformations on nodes, because its built in and respects a scene's paused state. But for scheduling blocks/closures and maintaining control over its execution, its a tough call. Expressing your intent is difficult.SKAction.runBlock
will pause your running block when the scene state ispaused
Roll your own / use a library. This approach gives you the most control and allows you to integrate with a scene's paused state and game's notion of elapsed time. This may seem daunting at first, but if you are already having a mechanism to calculate the time step of your game, making a scheduler on top of that is straightforward. The demo project I shared should provide some info on how to achieve this if rolling out your own / using those components outright if you are on Swift.
To achieve functionality similar to cocos scheduler you can use SKAction.
For example for the to achieve something like this
[self schedule:@selector(fireMethod:) interval:0.5];
using SKAction You would write this
SKAction *wait = [SKAction waitForDuration:0.5];
SKAction *performSelector = [SKAction performSelector:@selector(fireMethod:) onTarget:self];
SKAction *sequence = [SKAction sequence:@[performSelector, wait]];
SKAction *repeat = [SKAction repeatActionForever:sequence];
[self runAction:repeat];
It isn't best looking, and lacks some flexibility of CCScheduler, but it will pause upon backgrounding, pausing scene/view etc. + it is like playing with LEGOs :)