3

I'm working on a iOs game and I have a problem. I need to display x sprites (for each one I have a scale SKAction). I need to be able to wait until all SKAction sprites and then do something else. Each SKAction run in a separate thread. How I can wait ?

Here is a piece of code:

for tile in tiles {

            let randomNum:UInt32 = arc4random_uniform(20) // range is 0 to 99
            let randomTime:TimeInterval = TimeInterval(randomNum/10)
            let scale = SKAction.scale(by: 1, duration: 2, delay:randomTime , usingSpringWithDamping: 0.1, initialSpringVelocity: 5)

            tile.sprite = SKSpriteNode(imageNamed: tile.type.spriteName)
            tile.sprite?.size = CGSize(width: TileWidth, height: TileHeight)
            tile.sprite?.position = tile.position!

            tile.sprite?.scale(to: CGSize(width: 0, height: 0))
            cookiesLayer.addChild(tile.sprite!)
            tile.sprite?.run(scale)


        }
//TODO code to add to be executed after all SKActions

How can I do my TODO code to be executer after all SKActions ? I would like to run the SKAction in parallel or one after another.

Thanks.

CC.
  • 2,736
  • 11
  • 53
  • 70
  • Possible duplicate of [SKAction Completion Handlers; usage in Swift](https://stackoverflow.com/questions/29627613/skaction-completion-handlers-usage-in-swift) – Tamás Sengel Sep 23 '17 at 08:09

2 Answers2

6

You could do this very easily using a completion block with your run method.

Just for the sake of this example, say you have a SKSpriteNode named someSpriteNode and want to know when two actions (applyImpulse in that case) have finished running:

    // 1) Create your actions:

    let action1 = SKAction.applyImpulse(CGVector(dx: 1.0, dy: 0.0), duration: 2.0)
    let action2 = SKAction.applyImpulse(CGVector(dx: 6.0, dy: 2.0), duration: 1.0)

     // 2) Add them to a sequence:

    let actionSequence = SKAction.sequence([action1, action2])

     // 3) Run the sequence using a completion block:

    someSpriteNode?.run(actionSequence, completion: {

         // All your actions are now finished

         // Do whatever you want here :)
    })

UPDATE: Get notified when a group of actions got executed, where all the actions run on the same node

You might be looking for action groups then:

// Declare an empty array that will store all your actions:

var actions = [SKAction]()

// Iterate through your nodes:

for _ in 0..<6 {

    // ...

    // Generate your random scale, delay, or whatever you need:

    let randomScale = CGFloat(GKRandomDistribution(lowestValue: 0, highestValue: 10).nextInt())

    // Create your custom action

    let scaleAction = SKAction.scale(by: randomScale, duration: 2.0)

    // Append your action to the actions array:

    actions.append(scaleAction)
}


// Create an action group using the actions array:

let actionGroup = SKAction.group(actions)

// Run your action group, and do whatever you need inside the completion block:

self.run(actionGroup, completion: {

    // All your actions are now finished, no matter what node they were ran on.
})

Also, I would recommend you using GameplayKit to generate random numbers in your game, it will definitely make your life easier :)

UPDATE 2: Get notified when all actions got executed, in the case where all the actions run on different nodes

Using DispatchGroup :

    // Create a DispatchGroup:

    let dispatchGroup = DispatchGroup()

    for _ in 0..<6 {

        // ...

        let randomWait = Double(GKRandomDistribution(lowestValue: 1, highestValue: 12).nextInt())

        let waitAction = SKAction.wait(forDuration: randomWait)
        let fadeOutAction = SKAction.fadeOut(withDuration: 2.0)
        let fadeInAction = SKAction.fadeIn(withDuration: 2.0)
        let sequenceAction = SKAction.sequence([waitAction, fadeOutAction, fadeInAction])

        // Enter the DispatchGroup

        dispatchGroup.enter()

        colorSquares[i].run(sequenceAction, completion: {

             // Leave the DispatchGroup

             dispatchGroup.leave()
        })

    }

   // Get notified when all your actions left the DispatchGroup:

    dispatchGroup.notify(queue: DispatchQueue.main, execute: {

        // When this block is executed, all your actions are now finished
    })

I think this solution is way more elegant than a counter :)

  • Actually, I have one action per node. So I have n node, n actions. I don't have n actions for one single node. – CC. Sep 23 '17 at 09:17
  • I just updated my answer for you, please let me know if you have any question! –  Sep 23 '17 at 12:48
  • Hello @Alex, I am afraid the second solution will not work because *CC.* needs to run every action on a different node. – Luca Angeletti Sep 23 '17 at 13:32
  • Hmmm I see, let me think about it :) –  Sep 23 '17 at 13:57
  • Exactly. I have n actions for each one of my m sprites. – CC. Sep 23 '17 at 14:02
  • Well, something that would definitely work is, creating a count variable, and in your for loop, increment the count by one whenever the completion block is called on each specific node. Then in your Scene's update(_ currentTime:) method, check if the count is equal to the number of nodes. If yes, it means all your actions are now finished, so you can do whatever you need to do, and set the count to 0. –  Sep 23 '17 at 14:10
  • But I am thinking that we could maybe find a more elegant way of doing that. –  Sep 23 '17 at 14:10
  • 1
    I updated my answer with an other possible solution that looks way more elegant to me, DispatchGroup. I think it would be the best way to solve your problem! –  Sep 23 '17 at 14:41
  • 1
    Thanks for the solution. I was testing something like that. It's working bug I think it's not ideal. I have also tried with a counter, it works also, but with a huge code I think it is going to be messy. Thanks for the 2 solutions. At least I can do what I need. – CC. Sep 23 '17 at 15:31
  • You're very welcome @CC. I'm glad if it helped you anyway ! As you say, I also think that there is no ideal way of doing this just yet, so we unfortunately have to do with what he have here. While it may definitely not be their priority, hopefully Apple Engineers will consider offering us a better way of doing this in a near future :). –  Sep 23 '17 at 15:35
  • 1
    I just implemented your DispatchGroup suggestion and it worked beautifully - thanks. – AW101 Jan 14 '19 at 21:00
3

First of all you should always create a Minimum Verifiable Example. Remove the unneeded things from your question and make sure to include the everything we needed to test your code.

Premise

I assume you have a Tile class similar to this

class Tile {
    var sprite: SKSpriteNode?
}

and an array like this

let tiles:[Tile] = ...

Your objective

  1. You want to run an action of random duration on the sprite element of each tile in tiles.
  2. You want the actions to start at the same time
  3. You want to be able to run some code when all the actions are completed

Solution

// 0. create a maxDuration variable
var maxDuration:TimeInterval = 0

// 1. create all the actions
let actions = tiles.map { tile in
    return SKAction.run {
        let randomNum = arc4random_uniform(100)
        let randomTime = TimeInterval(randomNum / 10)
        let wait = SKAction.wait(forDuration: randomTime)
        let scale = SKAction.scale(by: 1, duration: 2)
        tile.sprite?.run(scale)
        maxDuration = max(maxDuration, randomTime + 2)
    }
}

// 2. create a wait action for the max duration
let wait = SKAction.wait(forDuration: maxDuration)

// 3. write inside this action the code to be executed after all the actions
let completion = SKAction.run {
    print("now all the actions are completed")
}
// 4. create a sequence of wait + completion
let sequence = SKAction.sequence([wait, completion])

// 5. create a group to run in parallel actions + sequence
let group = SKAction.group(actions + [sequence])

// 6. run the group on the node you prefer (it doesn't really matter which node since every inner action is tied to a specific node)
self.run(group)

Update (as suggested by @Alex)

var maxDuration:TimeInterval = 0

tiles.forEach { tile in
    let randomNum = arc4random_uniform(100)
    let randomTime = TimeInterval(randomNum / 10)
    let wait = SKAction.wait(forDuration: randomTime)
    let scale = SKAction.scale(by: 1, duration: 2)
    tile.sprite?.run(scale)
    maxDuration = max(maxDuration, randomTime + 2)
}

run(.wait(forDuration: maxDuration)) {
    print("now all the actions are completed")
}
Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
  • Well, doing this though, would be the same as waiting from the start for the max duration, and it doesn't actually wait for all the actions to be finished, but rather for the longest one to be finished. It's kind of more of an hardcoded solution. My approach with DispatchGroup fits perfectly the problem though, because you would get notified really when all the actions got executed. Of course, both approach would work (even an approach using a counter) but after all, it's all about a question of preferences/style –  Sep 23 '17 at 14:49
  • @Alex Good point! I updated my answer following your suggestion and now the code is much more clear. About `DispatchGroup` usually it is not a good practice using Grande Central Dispatch and SpriteKit togheter. – Luca Angeletti Sep 23 '17 at 14:56
  • Great! Concerning DispatchGroup, I have never heard that it would be a bad practice, also I used it several times and haven't got any issues related to it though. Could you please explain further where it would cause a problem here? –  Sep 23 '17 at 15:00
  • @Alex I think the problem with SpriteKit and GCD is that SpriteKit uses a Game Run Loop with very specific constraints about what kind of code should be executed at any given time. E.g. *Run actions, Update Physic, Apply Constraints, ...* On the other hand with `dispatchGroup.notify(queue: DispatchQueue.main` you run your code in the main thread which could interfere with the Game Run Loop phases. – Luca Angeletti Sep 23 '17 at 15:10
  • Interesting! But I have to say I would be surprised if it were to interfere though, because it would definitely be a different thread. Do you have any sources or links to the documentation or a WWDC in which they would be talking about this topic maybe, because I would really like to learn more about this? –  Sep 23 '17 at 15:16
  • Good news @Luca, after reading a few interesting answers [here](https://stackoverflow.com/questions/28748774/reusable-multithread-implementation-in-sprite-kit) and [there](https://stackoverflow.com/questions/20311653/does-sprite-kit-render-in-the-background-or-on-the-main-thread-how-does-it-wor), and going through [this](https://developer.apple.com/documentation/spritekit/skscene) in the Documentation (with this figure explaning how a scene processes frames of animation), it seems like it wouldn't be an issue to use GCD threading along with Sprite Kit! –  Sep 23 '17 at 15:40
  • While I may probably continue using GCD in a few (rare) situations in Sprite Kit like the one in this question, I am still going to do more research about it because I think it's a very engaging topic, isn't it :) –  Sep 23 '17 at 15:41
  • 1
    @Alex Very interesting. Specifically the first answer you [linked](https://stackoverflow.com/a/28751747/1761687) does explain how to properly move from a background thread to the SpriteKit Run Loop using the `update` method. Ok your answer has my upvote ;-) – Luca Angeletti Sep 23 '17 at 16:37
  • 1
    Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/155194/discussion-between-alex-and-luca-angeletti). –  Sep 24 '17 at 13:22