Vincent
Vincent

Reputation: 31

How to make a delay in a loop in SpriteKit?

I have made some search here and there but I didn't find (or understand) how to make a delay in a loop in SpriteKit.

This is the idea : I have some SKSpriteNodes sorted in an array and I want to display them on the screen, one every second. The problem is... I simply don't manage to do it (sorry, I'm quite new to this).

       let sprite1 = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
    sprite1.position = CGPoint(x: 100, y: 100)

    let sprite2 = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
    sprite2.position = CGPoint(x: 100, y: 300)

    let sprite3 = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
    sprite3.position = CGPoint(x: 300, y: 100)

    let sprite4 = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
    sprite4.position = CGPoint(x: 300, y: 300)

    let allSprites : [SKSpriteNode] = [sprite1, sprite2, sprite3, sprite4]
    let delay = 1.0
    var i = 0
    while i <= 3
    {
        let oneSprite = allSprites[i]
        self.addChild(oneSprite)


       DispatchQueue.main.asyncAfter(deadline : .now() + delay) {
            i = i + 1
        }
    }

If you're asking : this doesn't work at all. It seems that what is inside the DispatchQueue.main.asyncAfter isn't read.

So, if you can help me to understand why, that would be great. I'm not picky, I can't take the answer with a "for" loop.

Regards,

Upvotes: 3

Views: 1055

Answers (1)

rickster
rickster

Reputation: 126107

This is a common beginner misunderstanding of event-driven / run-loop-based / GUI programming systems. Some key points to help get on the right track:

  • SpriteKit is trying to render the scene 60 (or so) times per second.
  • That means SpriteKit internally has a loop where, once per frame, it calls your code asking what's new (update), then runs its own code to draw the changes (render).
  • Setup code, or things that happen in response to events (clicks/taps/buttons) are external to this loop, but feed into it: events change state, which the render loop reacts to.

So, if you have a loop in an update method, an event handler, or initial setup code, everything that happens in that loop will happen before SpriteKit gets a chance to draw any of it. That is, if we ignore for a moment the "delay" part of your question, a loop like this...

var i = 0
while i <= 3 {
    let oneSprite = allSprites[i]
    self.addChild(oneSprite)
}

... will result in none of the sprites being visible right before the loop starts, and all of the sprites being visible after it completes. No amount of introducing delays within the loop will change that, because SpriteKit doesn't get its first chance to draw until after your loop is finished.

Fixing the problem

The best way to do animations and delays in a system like SpriteKit is to make use of the tools it gives you for treating animation declaratively. That is, you tell SpriteKit what you want to have happen over the next several (hundred) frames, and SpriteKit makes that happen — each time it goes through that update/render loop, SpriteKit determines what changes need to be made in the scene to accomplish your animation. For example, if you're running at 60 fps, and you ask for a fade-out animation on some sprite lasting one second, then each frame it'll reduce that sprite's opacity by 1/60.

SpriteKit's tool for declarative animations is SKAction. There are actions for most of the things you'd want to animate, and actions for composing other actions into groups and sequences. In your case, you probably want some combination of the wait and unhide and run(_:onChildWithName) actions:

  1. Give each of your sprites a unique name:

    let sprite1 = // ...
    sprite1.name = "sprite1"
    // etc
    
  2. Add all the nodes to your scene, but keep the ones you don't want visible yet hidden:

    let allSprites : [SKSpriteNode] = [sprite1, sprite2, sprite3, sprite4]
    for sprite in allSprites {
        sprite.isHidden = true
        self.addChild(sprite)
    }
    
  3. Create a sequence action that alternates delays with telling each node to unhide, and run that action on your scene:

    let action = SKAction.sequence([
        run(.unhide(), onChildNodeWithName: "sprite1"),
        wait(forDuration: 1.0),
        run(.unhide(), onChildNodeWithName: "sprite2"),
        wait(forDuration: 1.0),
        // etc
    ])
    self.run(action)
    

That should accomplish what you're looking for. (Disclaimer: code written in web browser, may require tweaking.) Read on if you want to better understand the situation...

Other alternatives

If you really want to get your head around how the update/render loop works, you could participate directly in that loop. I wouldn't recommend it in general, because it's a lot more code and bookkeeping, but it's useful to know.

  1. Keep a queue (array) of nodes that you want to add or unhide.
  2. In you scene's update method, keep track of how much time has passed.
  3. If it's been at least 1.0 second (or whatever delay you're after) since you last showed a node, add the first one in the array, and remove it from the array.
  4. When the array is empty, you're done.

About DispatchQueue.asyncAfter...

This isn't essential to completing your task, but helpful to understand so you don't get into similar problem later: the word "async" in the call you were using to try delaying things is short for "asynchronous". Asynchronous means that the code you're writing doesn't execute in the order you write it in.

A simplified explanation: Remember that modern CPUs have multiple cores? When CPU 0 is executing your while loop, it gets to the asyncAfter call and passes a note to CPU 1 saying to wait one second and then execute the closure body attached to that asyncAfter call. As soon as it passes that note, CPU 0 continues merrily on its way — it doesn't wait for CPU 1 to receive the node and complete the work that note specifies.

Just from reading the code, I'm not 100% clear on how this approach fails, but fail it certainly does. Either you have an infinite loop, because the while condition never changes, because the work to update i is happening elsewhere and not propagating back to local scope. Or the the changes to i do propagate back, but only after the loop spins an indeterminate number of times waiting for the four asyncAfter calls to finish waiting and execute.

Upvotes: 8

Related Questions