SpriteKit - Creating a timer
Asked Answered
H

5

33

How can I create a timer that fires every two seconds that will increment the score by one on a HUD I have on my screen? This is the code I have for the HUD:

    @implementation MyScene
{
    int counter;
    BOOL updateLabel;
    SKLabelNode *counterLabel;
}

-(id)initWithSize:(CGSize)size
{
    if (self = [super initWithSize:size])
    {
        counter = 0;

        updateLabel = false;

        counterLabel = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        counterLabel.name = @"myCounterLabel";
        counterLabel.text = @"0";
        counterLabel.fontSize = 20;
        counterLabel.fontColor = [SKColor yellowColor];
        counterLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeCenter;
        counterLabel.verticalAlignmentMode = SKLabelVerticalAlignmentModeBottom;
        counterLabel.position = CGPointMake(50,50); // change x,y to location you want
        counterLabel.zPosition = 900;
        [self addChild: counterLabel];
    }
}
Hebraism answered 1/6, 2014 at 8:56 Comment(1)
Why not overriding the update(currentTime) method ? "it is called exactly once per frame, so long as the scene is presented in a view and is not paused"Generatrix
G
62

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])))
Goingson answered 1/6, 2014 at 10:21 Comment(7)
@LearnCocos2D Works perfectly! Just to prevent error from other users, I suggest adding a closed bracket on the third line to stop syntax errors.Hebraism
fixed typo, added repeat exampleGoingson
How would I translate this into Swift?Juju
I added Swift code fragments. Haven't tested whether it compiles. Let me know if it doesn't.Goingson
Repeating the action did not help because it takes a big performance toll. I need to run the action every second, maybe its too intense. I recommend using the update method for that reason. Edit: I take it back, for some reason Product>Clean resolved the problem.Ingredient
What if you need to calculate something that takes x amount of time where x is more than a few frames? I don't think it's fair to say never use dispatch_async. We don't our game to hang while we wait for some loop to finish.Paulinepauling
High CPU usage occurs with SKAction.repeatForever in xCode 11.3.1, CPU Usage never reduces less than 50% and most around 77% of usage, Is there is some alternative way? As soon as i have stopped the animation, CPU usage reducing to 0-1%. I have tested on real device and due to high CPU usage, fps drops happening.Riproaring
F
5

In Swift usable:

var timescore = Int()  
var actionwait = SKAction.waitForDuration(0.5)
            var timesecond = Int()
            var actionrun = SKAction.runBlock({
                    timescore++
                    timesecond++
                    if timesecond == 60 {timesecond = 0}
                    scoreLabel.text = "Score Time: \(timescore/60):\(timesecond)"
                })
            scoreLabel.runAction(SKAction.repeatActionForever(SKAction.sequence([actionwait,actionrun])))
Faldstool answered 25/7, 2014 at 8:15 Comment(0)
C
5

I've taken the swift example above and added in leading zeros for the clock.

    func updateClock() {
    var leadingZero = ""
    var leadingZeroMin = ""
    var timeMin = Int()
    var actionwait = SKAction.waitForDuration(1.0)
    var timesecond = Int()
    var actionrun = SKAction.runBlock({
        timeMin++
        timesecond++
        if timesecond == 60 {timesecond = 0}
        if timeMin  / 60 <= 9 { leadingZeroMin = "0" } else { leadingZeroMin = "" }
        if timesecond <= 9 { leadingZero = "0" } else { leadingZero = "" }

        self.flyTimeText.text = "Flight Time [ \(leadingZeroMin)\(timeMin/60) : \(leadingZero)\(timesecond) ]"
    })
    self.flyTimeText.runAction(SKAction.repeatActionForever(SKAction.sequence([actionwait,actionrun])))
}
Cami answered 9/8, 2014 at 9:26 Comment(0)
B
2

Here's the full code to build a timer for SpriteKit with Xcode 9.3 and Swift 4.1

enter image description here

In our example the score label will be incrementd by 1 every 2 seconds. Here's final result

Good, let's start!

1) The score label

First of all we need a label

class GameScene: SKScene {
    private let label = SKLabelNode(text: "Score: 0")
}

2) The score label goes into the scene

class GameScene: SKScene {

    private let label = SKLabelNode(text: "Score: 0")

    override func didMove(to view: SKView) {
        self.label.fontSize = 60
        self.addChild(label)
    }
}

Now the label is at the center of the screen. Let's run the project to see it.

enter image description here

Please note that at this point the label is not being updated!

3) A counter

We also want to build a counter property which will hold the current value displayed by the label. We also want the label to be updated as soon as the counter property is changed so...

class GameScene: SKScene {

    private let label = SKLabelNode(text: "Score: 0")
    private var counter = 0 {
        didSet {
            self.label.text = "Score: \(self.counter)"
        }
    }

    override func didMove(to view: SKView) {
        self.label.fontSize = 60
        self.addChild(label)

        // let's test it!
        self.counter = 123
    }
}

enter image description here

4) The actions

Finally we want to build an action that every 2 seconds will increment counter

class GameScene: SKScene {

    private let label = SKLabelNode(text: "Score: 0")
    private var counter = 0 {
        didSet {
            self.label.text = "Score: \(self.counter)"
        }
    }

    override func didMove(to view: SKView) {
        self.label.fontSize = 60
        self.addChild(label)
        // 1 wait action
        let wait2Seconds = SKAction.wait(forDuration: 2)
        // 2 increment action
        let incrementCounter = SKAction.run { [weak self] in
            self?.counter += 1
        }
        // 3. wait + increment
        let sequence = SKAction.sequence([wait2Seconds, incrementCounter])
        // 4. (wait + increment) forever
        let repeatForever = SKAction.repeatForever(sequence)

        // run it!
        self.run(repeatForever)
    }
}
Berley answered 30/3, 2018 at 13:1 Comment(0)
F
-3

The following code creates a new thread and waits 2 seconds before doing something on the main thread:

BOOL continueIncrementingScore = YES;

dispatch_async(dispatch_queue_create("timer", NULL);, ^{
    while(continueIncrementingScore) {
        [NSThread sleepForTimeInterval:2];
        dispatch_async(dispatch_get_main_queue(), ^{
            // this is performed on the main thread - increment score here

        });
    }
});

Whenever you want to stop it - just set continueIncrementingScore to NO

Ferruginous answered 1/6, 2014 at 9:6 Comment(1)
bad example because there's dispatch_afterGoingson

© 2022 - 2024 — McMap. All rights reserved.