Alternative to animation(forKey:) (now deprecated)?
Asked Answered
S

4

13

I am working with iOS 11 (for ARKit) and while many point to a sample app for SceneKit from Apple with a Fox, I am having problem with the extension it uses in that sample project (file) to add animations:

extension CAAnimation {
    class func animationWithSceneNamed(_ name: String) -> CAAnimation? {
        var animation: CAAnimation?
        if let scene = SCNScene(named: name) {
            scene.rootNode.enumerateChildNodes({ (child, stop) in
                if child.animationKeys.count > 0 {
                    animation = child.animation(forKey: child.animationKeys.first!)
                    stop.initialize(to: true)
                }
            })
        }
        return animation
    }
}

It seems that this extension is very handy but I am not sure how to migrate this now that it is deprecated? Is it built into SceneKit by default now?

The documentation didn't really show much info on why it was deprecated or where to go from here.

Thanks

Spillage answered 1/9, 2017 at 23:17 Comment(0)
M
8

TL;DR: examples of how to use new APIs can be found in Apple's sample game (search for SCNAnimationPlayer)


Even though animation(forKey:) and its sister methods that work with CAAnimation have been deprecated in iOS11, you can continue using them – everything will work.

But if you want to use new APIs and don't care about backwards compatibility (which you wouldn't need in the case of ARKit anyway, because it's only available since iOS11), read on.

The newly introduced SCNAnimationPlayer provides a more convenient API compared to its predecessors. It is now easier to work with animations in real time. This video from WWDC2017 would be a good starting point to learn about it.

As a quick summary: SCNAnimationPlayer allows you to change animation's speed on the fly. It provides a more intuitive interface for animation playback using methods such as play() and stop() compared to adding and removing CAAnimations. You also can blend two animations together which, for example, can be used to make smooth transitions between them.

You can find examples of how to use all of this in the Fox 2 game by Apple.


Here's the extension you've posted adapted to use SCNAnimationPlayer (which you can find in the Character class in the Fox 2 sample project):

extension SCNAnimationPlayer {
    class func loadAnimation(fromSceneNamed sceneName: String) -> SCNAnimationPlayer {
        let scene = SCNScene( named: sceneName )!
        // find top level animation
        var animationPlayer: SCNAnimationPlayer! = nil
        scene.rootNode.enumerateChildNodes { (child, stop) in
            if !child.animationKeys.isEmpty {
                animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
                stop.pointee = true
            }
        }
        return animationPlayer
    }
}

You can use it as follows:

  1. Load the animation and add it to the corresponding node

    let jumpAnimation = SCNAnimationPlayer.loadAnimation(fromSceneNamed: "jump.scn")
    jumpAnimation.stop() // stop it for now so that we can use it later when it's appropriate 
    model.addAnimationPlayer(jumpAnimation, forKey: "jump")
    
  2. Use it!

    model.animationPlayer(forKey: "jump")?.play()
    
Mormon answered 2/9, 2017 at 1:30 Comment(1)
Many thanks for this. I've followed your link and done exactly that, replacing my CAAnimation code with SCNAnimationPlayer but I cannot get it to play for the life of me. The mesh jumps to the first frame of the animation so I know it's loading it. I can log the animation and see the right number of frames, but play() does nothing. The same animation using CAAnimation works fine. Anything I can test for?Dialectal
C
2

Lësha Turkowski's answer without force unwraps.

extension SCNAnimationPlayer {

    class func loadAnimationPlayer(from sceneName: String) -> SCNAnimationPlayer? {
        var animationPlayer: SCNAnimationPlayer?

        if let scene = SCNScene(named: sceneName) {
            scene.rootNode.enumerateChildNodes { (child, stop) in
                if !child.animationKeys.isEmpty {
                    animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
                    stop.pointee = true
                }
            }
        }

        return animationPlayer
    }
}
Cosset answered 6/1, 2020 at 6:22 Comment(0)
A
1

Here's an example of SwiftUI and SceneKit

import SwiftUI
import SceneKit

struct ScenekitView : UIViewRepresentable {
@Binding var isPlayingAnimation: Bool

let scene = SCNScene(named: "art.scnassets/TestScene.scn")!

func makeUIView(context: Context) -> SCNView {
    // create and add a camera to the scene
    let cameraNode = SCNNode()
    cameraNode.camera = SCNCamera()
    scene.rootNode.addChildNode(cameraNode)

    let scnView = SCNView()
    return scnView
}

func updateUIView(_ scnView: SCNView, context: Context) {
    scnView.scene = scene

    // allows the user to manipulate the camera
    scnView.allowsCameraControl = true

    controlAnimation(isAnimating: isPlayingAnimation, nodeName: "TestNode", animationName: "TestAnimationName")
}

func controlAnimation(isAnimating: Bool, nodeName: String, animationName: String) {
    guard let node = scene.rootNode.childNode(withName: nodeName, recursively: true) else {  return }
    guard let animationPlayer: SCNAnimationPlayer = node.animationPlayer(forKey: animationName) else {  return }
    if isAnimating {
        print("Play Animation")
        animationPlayer.play()
    } else {
        print("Stop Animation")
        animationPlayer.stop()
    }
}
}

struct DogAnimation_Previews: PreviewProvider {
    static var previews: some View {
        ScenekitView(isPlayingAnimation: .constant(true))
    }
}
Adamek answered 2/4, 2020 at 19:26 Comment(0)
G
0

A 2023 example.

I load typical animations like this:

func simpleLoadAnim(filename: String) -> SCNAnimationPlayer {
    let s = SCNScene(named: filename)!
    let n = s.rootNode.childNodes.filter({!$0.animationKeys.isEmpty}).first!
    return n.animationPlayer(forKey: n.animationKeys.first!)!
}

So,

laugh = simpleLoadAnim(filename: "animeLaugh") // animeLaugh.dae
giggle = simpleLoadAnim(filename: "animeGiggle")

You then, step one, have to add them to the character:

sally.addAnimationPlayer(laugh, forKey: "laugh")
sally.addAnimationPlayer(giggle, forKey: "giggle")

very typically you would have only one going at a time. So set the weights, step two.

laugh.blendFactor = 1
giggle.blendFactor = 0

to play or stop an SCNAnimationPlayer it's just step three

laugh.play()
giggle.stop()

Almost certainly, you will have (100s of lines) of your own code to blend between animations (which might take only a short time, 0.1 secs, or may take a second or so). To do so, you would use SCNAction.customAction.

If you prefer you can access the animation, on, the character (sally) using the keys. But really the "whole point" is you can just start, stop, etc etc the SCNAnimationPlayer.

You will also have lots of code to set up the SCNAnimationPlayer how you like (speed, looping, mirrored, etc etc etc etc etc etc etc etc)

You will need THIS very critical answer to get collada files (must be separate character / anim files) working properly https://mcmap.net/q/245407/-how-to-get-a-ordinary-mixamo-character-animation-working-in-scenekit

Once you have the various SCNAnimationPlayer animations working properly, it is quite easy to use, run, blend etc animes.

The essential sequence is

  1. each anime must be in its own .dae file
  2. load each anime files in to a SCNAnimationPlayer
  3. "add" all the animes to the character in question
  4. program the blends
  5. then simply play() or stop() the actual SCNAnimationPlayer items (don't bother using the keys on the character, it's a bit pointless)
Gentianella answered 17/1, 2023 at 0:25 Comment(1)
Do you have any questions regarding this article?Barramunda

© 2022 - 2024 — McMap. All rights reserved.