Quick RealityKit Tutorial 2: Looping Animations and Gestures

Dennis Ippel
3 min readApr 2, 2021

--

In part 1 we looked at a basic non-AR setup where we added an environment map and hooked into the render loop to create a simple camera animation.

This time we’ll look at two things that aren’t documented very well:

  • Creating a basic transform animation that loops.
  • Adding gestures to an entity and detect when these gestures start and end.

Basic Setup

Lets start by creating a setup similar to the one in the first tutorial:

arView = ARView(frame: view.frame, 
cameraMode: .nonAR,
automaticallyConfigureSession: false)
view.addSubview(arView)
let skyboxResource = try! EnvironmentResource.load(named: "decor_shop_2k")
arView.environment.lighting.resource = skyboxResource
arView.environment.background = .skybox(skyboxResource)
let cubeMaterial = SimpleMaterial(color: .blue, isMetallic: true)
let cubeEntity = ModelEntity(mesh: .generateBox(size: 1),
materials: [cubeMaterial])
let cubeAnchor = AnchorEntity(world: .zero)
cubeAnchor.addChild(cubeEntity)
arView.scene.anchors.append(cubeAnchor)

Creating A Looping Animation

Creating a non-looping simple transform animation is well documented. Looping such an animation isn’t. It would be convenient if the move(to:relativeTo:duration:timingFunction:) method had a loop parameter but obviously it hasn’t got one. We need to do a bit more work ourselves.

The key is to subscribe to the AnimationEvents.PlaybackCompleted event. You can subscribe to this event by using the Scene.subscribe(to:on:_:) method.

Now we can create a method that animates the entity and subscribes to the PlaybackCompleted event once:

// This needs to be an instance variable, otherwise it'll
// get deallocated immediately and the animation won't start.
var animUpdateSubscription: Cancellable?
func animate(entity: HasTransform,
angle: Float,
axis: SIMD3<Float>,
duration: TimeInterval,
loop: Bool)
{
var transform = entity.transform
// additive rotation
transform.rotation *= simd_quatf(angle: angle, axis: axis)
entity.move(to: transform,
relativeTo: entity.parent,
duration: duration)
// Checks if we should loop and if we have already subscribed
// to the AnimationEvent. We only need to do this once.
guard loop,
animUpdateSubscription == nil
else { return }
animUpdateSubscription =
arView.scene.subscribe(to: AnimationEvents.PlaybackCompleted.self,
on: entity,
{ _ in
self.animate(entity: entity,
angle: angle,
axis: axis,
duration: duration,
loop: loop)
})
}

The animUpdateSubscription variable is a Cancellable which is defined in the Combine framework. To use this we need to add the corresponding import:

import Combine

This variable must be an instance variable. Otherwise it’ll be deallocated immediately and the animation won’t play.

Notice how the animation is started again each time the animation is completed.

Adding Gestures

This part is pretty simple and well documented. We need to generate collision shapes and then install the gestures:

cubeEntity.generateCollisionShapes(recursive: false)
arView.installGestures(.all, for: cubeEntity)

When you try this you’ll notice that it doesn’t work very well. This is because both the animation loop and the gesture recognizer are modifying the cube entity’s transform at the same time. This is why we need to take a step back and animate the cubeAnchor instead of the cubeEntity :

animate(entity: cubeAnchor,
angle: .pi,
axis: [1, 0, 0],
duration: 4,
loop: true)

The tricky and not-so-well documented part is getting notified when the gestures begin and end.

Calling installGestures(:for:) returns an array with EntityGestureRecognizer instances. You can loop through this array and add a target to each recognizer:

arView.installGestures(.all, for: entity).forEach {    
gestureRecognizer in
gestureRecognizer.addTarget(self, action: #selector(handleGesture(_:)))
}

Then we can add a gesture handler that checks for the appropriate gesture recognizer, inspects its .state property and acts accordingly:

@objc private func handleGesture(_ recognizer: UIGestureRecognizer) {
guard let rotationGesture = recognizer as? EntityRotationGestureRecognizer else { return }
switch rotationGesture.state {
case .began:
// do something
case .ended:
// do something
default:
break
}
}

In the full example below, the color of the cube changes to orange when the gesture begins and to white when the gesture ends.

Putting It Together

Here’s the complete UIViewController where everything comes together:

--

--