CC.
CC.

Reputation: 2928

SpriteKit multiple SKActions for multiple SKSpriteNode - wait for all to complete

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.

Upvotes: 3

Views: 1280

Answers (2)

Luca Angeletti
Luca Angeletti

Reputation: 59516

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")
}

Upvotes: 3

user8606263
user8606263

Reputation:

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 :)

Upvotes: 6

Related Questions