1

I’m looking for a way of chaining SceneKit animations.

I’m adding x number of nodes to a scene, I’d like to add the first node, make it fade in and once it’s visible then we go to the next node until all nodes are visible. So I need the animation on the first node to end before we start on the second.

Obviously I tried a for loop with SCNActions but the animation is batched together so all nodes appear at the same time.

I don’t know how many nodes will be added, so I can’t make a sequence.

What would be the best way to handle this?

Edit:

Okay, I've figured that if I add a sequence to the node before I add it to the paused scene (that includes an incremented wait interval)

let sequence = SCNAction.sequence([.wait(duration: delay), .fadeIn(duration: 0.5)])
node.runAction(sequence)

then I un-pause the scene once all the nodes are added it achieves the effect that I'm looking for. But it seems hacky.

Is there a better way?

Bwgan
  • 121
  • 11
  • 1
    You can use `runAction` with a `completionHandler` that's called when the action is finished. Would that help? https://developer.apple.com/documentation/scenekit/scnactionable/1524219-runaction – James P Sep 19 '19 at 09:27

2 Answers2

2

Pause/unpause - hmm, yeah it works, but just feels like that might cause problems down the road if you start doing more things.

I like the completionHandler route per (James P). You could set up multiple (and different) animations or movements with this method. Like move to (5,0,0), rotate, animate, and when it gets there, call it again and move to (10,0,0), etc.

You could do it with a timer if they all work the same way and that would give you some consistency if that's what you are looking for. If you go this route, please ensure to put timers in the main thread.

You can also create some pre-defined sequences, depending on your needs:

let heartBeat = SCNAction.sequence([
        SCNAction.move(to: SCNVector3(-0.5,  0.0, 0.80), duration: 0.4),
        SCNAction.unhide(),
        SCNAction.fadeIn(duration: 1.0),
        SCNAction.move(to: SCNVector3(-0.3,  0.0, 0.80), duration: 0.4),
        SCNAction.move(to: SCNVector3(-0.2,  0.2, 0.80), duration: 0.4),
        SCNAction.move(to: SCNVector3(-0.1, -0.2, 0.80), duration: 0.4),
        SCNAction.move(to: SCNVector3( 0.0,  0.0, 0.80), duration: 0.4),
        SCNAction.move(to: SCNVector3( 0.1,  0.5, 0.80), duration: 0.4),
        SCNAction.move(to: SCNVector3( 0.3, -0.5, 0.80), duration: 0.4),
        SCNAction.move(to: SCNVector3( 0.5,  0.0, 0.80), duration: 0.4),
        SCNAction.fadeOut(duration: 0.1),
        SCNAction.hide()
        ])

node.runAction(SCNAction.repeatForever(heartBeat))
Voltan
  • 1,170
  • 1
  • 7
  • 16
0

I ended up using a Timer, I did think about this but couldn't get it to work, probably because I wasn't doing it on the main thread.

Here's the code I'm using so far in case anyone else is in the same boat.

func animateNodesInTo(scene: SCNScene, withDuration: TimeInterval) {

        DispatchQueue.main.async {
            let nodes = scene.rootNode.childNodes
            let acion = SCNAction.fadeIn(duration: withDuration)

            Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
                let opaqueNode = nodes.first(where: {$0.opacity == 0})
                opaqueNode?.runAction(acion)

                if opaqueNode == nil {
                    timer.invalidate()
                }
            }
        }

    }
Bwgan
  • 121
  • 11