简体   繁体   中英

SceneKit: how to animate multiple SCNNodes together then call completion block once

The goal is to animate multiple SCNNodes at the same time then call a completion block once all the animations complete. The parallel animations have the same duration so will complete at the same time if started together.

This SO answer suggested using the group function for Sprite Kit, but there is no analog in Scene Kit because the SCNScene class lacks a runAction .

One option is run all the actions individually against each node and have each one call the same completion function, which must maintain a flag to ensure it's only called once.

Another option is to avoid the completion handler and call the completion code after a delay matched to the animation duration. This creates race conditions during testing, however, since sometimes the animations get held up before completing.

This seems clunky, though. What's the right way to group the animation of multiple nodes in SceneKit then invoke a completion handler?

The way I initially approached this was, since all the initial animations have the same duration, to apply the completion handler to just one of the actions. But, on occasion, the animations would hang-up ( SCNAction completion handler awaits gesture to execute ).

My current, successful solution is to not use the completion handler in conjunction with an SCNAction but with a delay:

func delay(delay:Double, closure:()->()) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), closure)
}

An examlpe of invocation:

delay(0.95) {
     self.scaleNode_2.runAction(moveGlucoseBack)
     self.fixedNode_2.runAction(moveGlucoseBack)
     self.scaleNode_3.hidden = true
     self.fixedNode_3.hidden = true
}

I doubt this can be called "the right way" but it works well for my uses and eliminates the random hang-ups I experienced trying to run animations on multiple nodes with completion handlers.

I haven't thought this through completely but I'll post it in hopes of being useful.

The general problem, do something after the last of a set of actions completes, is what GCD's dispatch_barrier is about. Submit all of the blocks to a private concurrent queue, then submit the Grand Finale completion block with dispatch_barrier . Grand Finale runs after all previous blocks have finished.

What I don't see right away is how to integrate these GCD calls with SceneKit calls and completion handlers.

Maybe dispatch_group is a better approach.

Edits and comments welcome!

Try something like this:

private class CountMonitor {
    var completed: Int = 0
    let total: Int
    let then: ()->Void

    init(for total: Int, then: @escaping(()->Void)) {
        self.total = total
        self.then = then
    }

    func didOne() {
        completed += 1
        if completed == total {
            then()  // Generally you should dispatch this off the main thread though
        }
    }
}

Then creating the actions looks something like:

private func test() {
    // for context of types
    let nodes: [SCNNode] = []
    let complexActionsToRun: SCNAction = .fadeIn(duration: 100)

    // Set up the monitor so it knows how many 'didOne' calls it should get, and what to do when they are all done ...
    let monitor = CountMonitor(for: nodes.count) { () in
        // do whatever you want at the end here
        print("Done!")
    }
    for node in nodes {
        node.runAction( complexActionsToRun ) { () in
            monitor.didOne()
        }
    }
}

Note you should also account for the nodes array being empty (you might still want to do whatever you wanted to do at the end, just immediately in that case).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM