Search code examples
iosswiftscenekit

How to add a pause/wait in the middle of a nested SCNTransaction chain


This question must have a very simple answer, but I couldn't find it after searching for hours. Unless I just need to manually add some timer to wait.

Basically, I have an animation sequence to move a node to a certain place, here I want to wait 5 seconds and then I have another animation sequence to bring the node back to its original position.

Initially, I used SCNActions, grouping some actions and then sequencing them all. Here I just added an SCNAction.wait(duration: 5) and that did the trick. However, one of the actions is to move the node 90 degrees around the X-axis and I need to simultaneously rotate around another axis as well. The result is incorrect and I have a feeling I've run into the gimbal lock issue.

So instead of using SCNAction.rotate(by: ) I decided to rotate using quaternions which don't have the gimbal lock problem, but then I needed to switch to using an SCNTransaction. I'm nesting these transaction in the completionBlock of the previous SCNTransaction. For the pause between the 2 animations, I don't have a wait action here, but I thought that just adding a transaction with a duration that does nothing will at least wait for as long. But it doesn't. The transaction immediately skips to the 3rd step.

So is there a simple command to tell the animation transaction to wait? I did try to add DispatchQueue.main.asyncAfter(deadline: .now() + 5) { <3rd SCNTransaction here> } and that worked, but is that really the way to do it? I have a feeling there's something else that I just don't know that replaces SCNAction.wait().

Here's my current code which does the animations correctly, except it doesn't wait where I want it to (this is an excerpt of the relevant code only):

[...]
        let moveSideQuat = simd_quatf(angle: -currentYRotation, axis: simd_float3(0, 0, 1))
        let moveAwaySimd = simd_float3(-20 * sinf(currentYRotation), 0, -20 * cosf(currentYRotation))
        let moveUpQuat = simd_quatf(angle: .pi/2, axis: simd_float3(1, 0, 0))
        
        let moveDownQuat = simd_quatf(angle: -.pi/2, axis: simd_float3(1, 0, 0))
        let moveTowardsQuat = simd_float3(0, 0, pivotZLocation)
        let moveBackSideQuat = simd_quatf(angle: currentYRotation, axis: simd_float3(0, 0, 1))

        SCNTransaction.begin()
        SCNTransaction.animationDuration = 1
        baseNode?.simdOrientation = moveSideQuat * baseNode!.simdOrientation
        baseNode?.simdPosition = moveAwaySimd
        baseNode?.simdOrientation = moveUpQuat * baseNode!.simdOrientation
        
        SCNTransaction.completionBlock = {
            
            SCNTransaction.begin()
            SCNTransaction.animationDuration = 5  // <<<--- I WANT TO WAIT HERE
            
            SCNTransaction.completionBlock = {

                SCNTransaction.begin()
                SCNTransaction.animationDuration = 1

                self.baseNode?.simdOrientation = moveDownQuat * self.baseNode!.simdOrientation
                self.baseNode?.simdPosition = moveTowardsQuat
                self.baseNode?.simdOrientation = moveBackSideQuat * self.baseNode!.simdOrientation


                SCNTransaction.commit()
            }
            
            SCNTransaction.commit()
        }
        
        SCNTransaction.commit()
[...]

In the 2nd completion block, after the animationDuration = 5 I even tried to add a simple assignment for the node's simdPosition, basically leaving it in the same position in the hope to trick the sequence to take 5 seconds to basically do nothing, but it still skipped to the next completionBlock.

So is the asyncAfter actually the way to go or am I missing something else? Thanks!


Solution

  • Using asyncAfter to wait is fine. That is the purpose of this method, after all.

    Creating an empty transaction with the duration of 5 seconds doesn't work because SceneKit can easily see that you haven't changed anything, and knows that the animation can complete immediately. You can hide a node somewhere in the scene and animate that node for 5 seconds, but that feels way more like a hack.

    If you want, you can still use SCNActions for the rest of your animations, and just use SCNTransaction for the ones that need the quaternions. For example:

    let step1 = SCNAction.group([
        SCNAction.run {
            SCNTransaction.begin()
            SCNTransaction.animationDuration = 1
            // do rotations here with quaternions...
            SCNTransaction.commit()
        },
        SCNAction.move(to: /* destination */, duration: 1)
    ])
    
    let step2 = SCNAction.wait(duration: 5)
    
    let step3 = SCNAction.group([
        SCNAction.run {
            SCNTransaction.begin()
            SCNTransaction.animationDuration = 1
            // rotate back...
            SCNTransaction.commit()
        },
        SCNAction.move(to: /* origin */, duration: 1)
    ])
    
    let entireAction = SCNAction.sequence([
        step1, step2, step3
    ])