0

Calling an SCNAction from the completion handler of RunAction seems to hang SceneKit.

A touch event or rotating the device seems to unblock the hang.

To reproduce:

1) Take the default SceneKit project you get on startup with the rotating spaceship.

2) Replace the animation code:

ship.RunAction(SCNAction.RepeatActionForever(SCNAction.RotateBy(0, 2, 0, 1)));

with:

        ship.RunAction(SCNAction.RotateBy(0, 2, 0, durationInSeconds: 3.0f), delegate
        {
            Console.WriteLine("DONE ROTATE");
            ship.RunAction(SCNAction.MoveBy(1, 0, 0, durationInSeconds: 3.0f), delegate
            {
                Console.WriteLine("DONE MOVEBY");
            });
        });

3) Run on the simulator or real device (the problem is the same on both)

4) The results is:

  • Spaceship rotates OK

  • DONE ROTATE is printed out OK

  • Now it's hung

  • Tap the screen (or rotate the device to landscape) and then the move happens OK and DONE MOVEBY is printed out.

I'm using C# and Visual Studio for Mac, but I suspect it happens using Xcode too.

Is this a bug in SceneKit? How can a workaround be done?

Maybe this is the same issue as described here:

SCNAction completion handler awaits gesture to execute

Bbx
  • 3,184
  • 3
  • 22
  • 33
  • Instead of running a new SCNAction in the completion handler, have you tried the SCNAction.sequence([SCNAction]) function. It may help but I’m not sure – Brandon Jun 02 '19 at 23:01
  • Thanks for the idea, but I have now discovered the underlying cause of the problem which is the callback is not on the main thread(!). Also using Sequence with Run to execute a block also calls the block on a different thread. So a solution is to invoke the callback on the main thread using eg: SynchronizationContext Post... – Bbx Jul 04 '19 at 11:42

3 Answers3

3

This happens because by default SceneKit is not rendering continuously. When tapping the screen the scene is changed and a new frame will be rendered. That's why the moveBy action is not triggered immediately after the rotateBy action.

Try setting SCNView's renderContinuously property to true like so:

scnView.rendersContinuously = true
ship.runAction(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 3.0)) {
    print("DONE ROTATE")
    ship.runAction(SCNAction.moveBy(x: 1, y: 0, z: 0, duration: 3.0), completionHandler: {
        print("DONE MOVEBY")
        scnView.rendersContinuously = false
    })
}
MasDennis
  • 726
  • 4
  • 9
  • Thanks for that explanation. It does indeed fix the above example, but I think it will be bad for battery life (my game does not need continuous rendering) and rendersContinuously is iOS 11+ only. I have also discovered that using SCNTransaction or CABasicAnimation work fine (I will add an answer showing how). Why do you think they work, but RunAction doesn't? – Bbx May 23 '19 at 08:26
1

Using SCNTransaction with a completionBlock does not suffer from the same problem, so this works fine:

        SCNTransaction.Begin();
        SCNTransaction.AnimationDuration = 3.0f;
        SCNTransaction.SetCompletionBlock(() =>
        {
            Console.WriteLine("DONE ROTATE");
            SCNTransaction.Begin();
            SCNTransaction.AnimationDuration = 3.0f;
            SCNTransaction.SetCompletionBlock(() =>
            {
                Console.WriteLine("DONE MOVEBY");
            });
            ship.Position = new SCNVector3(1.0f, 0.0f, 0.0f);
            SCNTransaction.Commit();
        });
        ship.EulerAngles = new SCNVector3(0.0f, (float)Math.PI / 2, 0.0f);
        SCNTransaction.Commit();

(Also using CABasicAnimation with CAAnimationDelegate to do the callback works OK.)

Since SCNTransaction and CABasicAnimation work, but RunAction doesn't, it really looks like an Apple bug in RunAction.

Bbx
  • 3,184
  • 3
  • 22
  • 33
0

My solution is to use DispatchQueue to lunch inner actions:

piece.runAction(action0, completionHandler: {
    DispatchQueue.main.asyncAfter(deadline: .now()) {
       ship.runAction(action1)
    }
}
Tony
  • 1,551
  • 20
  • 21