How to Implement a Spritekit Timer

How to implement a SpriteKit timer?

I would stick to SKActions for these kind of tasks in SpriteKit due to fact that NSTimer is not affected by scene's, or view's paused state, so it might lead you into troubles. Or at least, it will require from you to implement a pause feature in order to pause your timers in certain situations, like when user pause the scene, or receive a phone call etc. Read more here about SKAction vs NSTimer vs GCD for time related actions in SpriteKit.

import SpriteKit

class GameScene: SKScene {

var levelTimerLabel = SKLabelNode(fontNamed: "ArialMT")

//Immediately after leveTimerValue variable is set, update label's text
var levelTimerValue: Int = 500 {
didSet {
levelTimerLabel.text = "Time left: \(levelTimerValue)"
}
}

override func didMoveToView(view: SKView) {

levelTimerLabel.fontColor = SKColor.blackColor()
levelTimerLabel.fontSize = 40
levelTimerLabel.position = CGPoint(x: size.width/2, y: size.height/2 + 350)
levelTimerLabel.text = "Time left: \(levelTimerValue)"
addChild(levelTimerLabel)

let wait = SKAction.waitForDuration(0.5) //change countdown speed here
let block = SKAction.runBlock({
[unowned self] in

if self.levelTimerValue > 0{
self.levelTimerValue--
}else{
self.removeActionForKey("countdown")
}
})
let sequence = SKAction.sequence([wait,block])

runAction(SKAction.repeatActionForever(sequence), withKey: "countdown")
}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

//Stop the countdown action

if actionForKey("countdown") != nil {removeActionForKey("countdown")}
}
}

SpriteKit - Creating a timer

In Sprite Kit do not use NSTimer, performSelector:afterDelay: or Grand Central Dispatch (GCD, ie any dispatch_... method) because these timing methods ignore a node's, scene's or the view's paused state. Moreover you do not know at which point in the game loop they are executed which can cause a variety of issues depending on what your code actually does.

The only two sanctioned ways to perform something time-based in Sprite Kit is to either use the SKScene update: method and using the passed-in currentTime parameter to keep track of time.

Or more commonly you would just use an action sequence that starts with a wait action:

id wait = [SKAction waitForDuration:2.5];
id run = [SKAction runBlock:^{
// your code here ...
}];
[node runAction:[SKAction sequence:@[wait, run]]];

And to run the code repeatedly:

[node runAction:[SKAction repeatActionForever:[SKAction sequence:@[wait, run]]]];

Alternatively you can also use performSelector:onTarget: instead of runBlock: or perhaps use a customActionWithDuration:actionBlock: if you need to mimick the SKScene update: method and don't know how to forward it to the node or where forwarding would be inconvenient.

See SKAction reference for details.


UPDATE: Code examples using Swift

Swift 5

 run(SKAction.repeatForever(SKAction.sequence([
SKAction.run( /*code block or a func name to call*/ ),
SKAction.wait(forDuration: 2.5)
])))

Swift 3

let wait = SKAction.wait(forDuration:2.5)
let action = SKAction.run {
// your code here ...
}
run(SKAction.sequence([wait,action]))

Swift 2

let wait = SKAction.waitForDuration(2.5)
let run = SKAction.runBlock {
// your code here ...
}
runAction(SKAction.sequence([wait, run]))

And to run the code repeatedly:

runAction(SKAction.repeatActionForever(SKAction.sequence([wait, run])))

SpriteKit - How to transition to a new scene with a timer

You can create a Scheduled Timer and configure a function to call your method that's creates and present the new scene.

Example:

class LoadingScene: SKScene {
var timer = Timer()

override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: "")
background.position = CGPoint (x: self.size.width / 2, y: self.size.height / 2)
background.zPosition = -1
self.addChild(background)
//Create a Scheduled timer thats will fire a function after the timeInterval
timer = Timer.scheduledTimer(timeInterval: 5.0,
target: self,
selector: #selector(presentNewScene),
userInfo: nil, repeats: false)
}

@objc func presentNewScene() {
//Configure the new scene to be presented and then present.
let newScene = SKScene(size: .zero)
view?.presentScene(newScene)
}

deinit {
//Stops the timer.
timer.invalidate()
}
}

Using a timer vs update to run game SpriteKit

First I think you are taking the FPS problem the wrong way around. You cannot "force" a faster frame rate than the device can give you. If you are basing the movements in your game on the assumption that every frame will be consistent, you are doing it wrong. It's actually how they did in the early days because CPUs were so slow and the difference from one generation to the new one wasn't too bad at first. But running an old DOS game on younger hardware will be tricky because the framerate is so high that the whole game mechanic becomes unstable or simply too fast to be playable.

The concept here is to think "over time" and to scale down any action in relation with the time elapsed between two frames.

The update() method gives you that opportunity by providing the current system clock state every frame. By keeping track of the time on the last frame, you can calculate the time difference with the current frame and use that difference to scale down what you are doing.

Using a timer to get the update on a consistent frame rate is not recommended nor practical. You may be calling the update closure at a given time interval, but the code inside that closure is taking time to execute on its own, and depending on your game logic, it might even have different execution times. So maybe the pause timing is consistent, but the code running before and after that pause might not be consistent. Then what happens if you run your game on a slower CPU? The code speed will change even more, making your timing inaccurate.

Another point against using an SKAction for your game loop is simply what they are. An action is an object in memory, meany to be reused by multiple objects. If you are making a "jump" action, for example, it is recommended to store that action somewhere and to reuse the same object every time you need something that "jumps", no matter what node it is. Your game loop is meant to be executed every frame, but not by different objects. Actions are also disposable. Meaning that you can kill an action even while it's running. If you put your game loop in an action, it will probably be run by the SKScene. If you use another action on your scene it becomes a puzzle right away because there are only two ways of removing an action besides letting it come to term: removing all actions or creating the action with an identifier key and use that key to remove any action with that key. If you don't see it already, it then forces you to put identifiers on every action that will be run by the scene and remove them one by one. And them again it leave a door open for a mistake that will get rid of your game loop because, keep it in mind, actions are DISPOSABLE! Then there is also no guarantee that your action will get executed first every single frame.

Why use the update() method? Simply because it is built IN your scene. No matter what, every frame, update() gets called first. THEN, the actions get executed. You cannot flush the update() method accidentally like you can with an action. You don't have to be careful about strong/weak relationships causing memory leaks because you are referring to objects from inside a closure like you do with an action.

Suggested reads:

  1. SKAction API reference
  2. SKScene API reference : read about the frame processing in SpriteKit. It will help you understand how they put everything together at every frame.

I hope it makes things clearer.



Related Topics



Leave a reply



Submit