SceneKit Tutorial with Swift Part 4: Render Loop

Chris Language

Thumb

Welcome back to our Scene Kit Tutorial with Swift series!

This tutorial series will show you how to create your first game with Scene Kit, Apple’s built-in 3D game framework.

In the first part of the series, you learned how to make an empty Scene Kit project as a good starting point.

In the second part of the series, you started making your game, learning about Scene Kit nodes along the way.

In the third part of the series, you learned how to make your geometry move through the power of Scene Kit physics.

In this fourth part of the series, you’ll learn how to make your geometry spawn over time through the Scene Kit render loop.

Let’s dive back in!

Note: This tutorial begins where the previous tutorial left off. If you didn’t follow along, no sweat – you can simply use the starter project for this tutorial.

Getting Started

In the previous tutorial, you enabled basic physics for your spawned object and applied an impulse to kick it up into the air. Eventually, the object fell back down and vanished into the abyss due to the simulated effect of gravity.

Although the effect is neat, it would be so much cooler to spawn multiple objects that collide with each other. This will certainly push the excitement factor up a notch!

Right now, your game calls spawnShape() just once. To spawn multiple objects you’ll need to call spawnShape() repeatedly. Introducing… the render loop!

As you learned in previous tutorials, SceneKit renders the contents of your scene using an SCNView object. SCNView has a delegate property that you can set to an object that conforms to the SCNSceneRendererDelegate protocol; SCNView will then call methods on that delegate when certain events occur within the animation and rendering process of each frame.

In this way, you can tap into the steps SceneKit takes as it renders each frame of a scene. These rendering steps are what make up the render loop.

So — what exactly are these steps? Well, here’s a quick breakdown of the render loop:

RenderLoop

Is this Wheel of Fortune? :] No, it’s simply a depiction of the nine steps of the render loop. In a game that runs at 60 fps, all these steps run — you guessed it — 60 times per second, in sequence.

The steps always execute in the following order, which lets you inject your game logic exactly where it’s needed:

  1. Update: The view calls renderer(_: updateAtTime:) on its delegate. This is a good spot to put basic scene update logic.
  2. Execute Actions & Animations: SceneKit executes all actions and performs all attached animations to the nodes in the scene graph.
  3. Did Apply Animations: The view calls its delegate’s renderer(_: didApplyAnimationsAtTime:). At this point, all the nodes in the scene have completed one single frame’s worth of animation, based on the applied actions and animations.
  4. Simulates Physics: SceneKit applies a single step of physics simulation to all the physics bodies in the scene.
  5. Did Simulate Physics: The view calls renderer(_: didSimulatePhysicsAtTime:) on its delegate. At this point, the physics simulation step has completed, and you can add in any logic dependent on the physics applied above.
  6. Evaluates Constraints: SceneKit evaluates and applies constraints, which are rules you can configure to make SceneKit automatically adjust the transformation of a node.
  7. Will Render Scene: The view calls renderer(_: willRenderScene: atTime:) on its delegate. At this point, the view is about to render the scene, so any last minute changes should be performed here.
  8. Renders Scene In View: SceneKit renders the scene in the view.
  9. Did Render Scene: The final step is for the view to call its delegate’s renderer(_: didRenderScene: atTime:). This marks the end of one cycle of the render loop; you can put any game logic in here that needs to execute before the process starts anew.

Because the render loop is, well, a loop, it’s the perfect place to call spawnShape(). Your job is to decide where to inject the spawn logic.

The Renderer Delegate

It’s time to put this cool feature to use in your game.

First, make the GameViewController class conform to the SCNSceneRendererDelegate protocol by adding the following to the bottom of GameViewController.swift:

// 1
extension GameViewController: SCNSceneRendererDelegate {
  // 2
  func renderer(_ renderer: SCNSceneRenderer, 
    updateAtTime time: TimeInterval) {
    // 3
    spawnShape()
  }
}

Taking a closer look at the code above:

  1. This adds an extension to GameViewController for protocol conformance and lets you maintain protocol methods in separate blocks of code.
  2. This adds an implementation of the renderer(_: updateAtTime:) protocol method.
  3. Finally, you call spawnShape() to create a new shape inside the delegate method.

This give you your first hook into SceneKit’s render loop. Before the view can call this delegate method, it first needs to know that GameViewController will act as the delegate for the view.

Do this by adding the following line to the bottom of setupView():

scnView.delegate = self

This sets the delegate of the SceneKit view to self. Now the view can call the delegate methods you implement in GameViewController when the render loop runs.

Finally, clean up your code a little by removing the single call to spawnShape() inside viewDidLoad(); it’s no longer needed since you’re calling the method inside the render loop now.

Build and run; unleash the spawning fury of your render loop! :]

BuildAndRun0

The game starts and spawns an insane amount of objects, resulting in a mosh pit of colliding objects — awesome! :]

So what’s happening here? Since you’re calling spawnShape() in every update step of the render loop, you’ll spawn 60 objects per second — if the device you’re running on can support your game at 60 fps. But less-powerful devices, which includes the simulator, can’t support that frame rate.

As the game runs, you’ll notice a rapid decrease in the frame rate. Not only does the graphics processor have to deal with increasing amounts of geometry, the physics engine has to deal with an increasing number of collisions, which also negatively affects your frame rate.

Things are a bit out of control at the moment, as your game won’t perform terribly well on all devices.

Spawn Timers

To make the gaming experience consistent across devices, you need to make use of time. No, I don’t mean taking more time to write your game! :] Rather, you need to use the passage of time as the one constant across devices; this lets you animate at a consistent rate, regardless of the frame rate the device can support.

Timers are a common technique in many games. Remember the updateAtTime parameter passed into the update delegate method? That parameter represents the current system time. If you monitor this parameter, you can calculate things like the elapsed time of your game, or spawn a new object every three seconds instead of as fast as possible.

Geometry Fighter will use a simple timer to spawn objects at randomly timed interval that any processor should be able to handle.

Add the following property to GameViewController below cameraNode:

var spawnTime: TimeInterval = 0

You’ll use this to determine time interval until you spawn another shape.

To fix the continuous spawning, replace the entire body of renderer(_: updateAtTime:) with the following:

// 1
if time > spawnTime {
  spawnShape()
 
  // 2
  spawnTime = time + TimeInterval(Float.random(min: 0.2, max: 1.5))
}

Taking each commented line in turn:

  1. You check if time (the current system time) is greater than spawnTime. If so, spawn a new shape; otherwise, do nothing.
  2. After you spawn an object, update spawnTime with the next time to spawn a new object. The next spawn time is simply the current time incremented by a random amount. Since TimeInterval is in seconds, you spawn the next object between 0.2 seconds and 1.5 seconds after the current time.

Build and run; check out the difference your timer makes:

BuildAndRun1

Mesmerizing, eh?

Things look a bit more manageable, and the shapes are spawning randomly. But aren’t you curious about what happens to all those objects after they fall out of sight?

Removing Child Nodes

spawnShape() continuously adds new child nodes into the scene — but they’re never removed, even after they fall out of sight. SceneKit does an awesome job of keeping things running smoothly for as long as possible, but that doesn’t mean you can forget about your children. What kind of parent are you? :]

To run at an optimal performance level and frame rate, you’ll have to remove objects that fall out of sight. And what better place to do this than — that’s right, the render loop! Handy thing, isn’t it?

Once an object reaches the limits of its bounds, you should remove it from the scene.

Add the following to the end of your GameViewController class, right below spawnShape():

func cleanScene() {
  // 1
  for node in scnScene.rootNode.childNodes {
    // 2
    if node.presentation.position.y < -2 {
      // 3
      node.removeFromParentNode()
    }
  }
}

Here’s what’s going on in the code above:

  1. First, you simply create a little for loop that steps through all available child nodes within the root node of the scene.
  2. Since the physics simulation is in play at this point, you can’t simply look at the object’s position as this reflects the position before the animation started. SceneKit maintains a copy of the object during the animation and plays it out until the animation is done. It’s a strange concept to understand at first, but you’ll see how this works before long. To get the actual position of an object while it’s animating, you leverage the presentationNode property. This is purely read-only — don’t attempt to modify any values on this property!
  3. This line of code makes an object blink out of existence; it seems cruel to do this to your children, but hey, that’s just tough love.

To use your method above, add the following line to call cleanScene() just after the if statement inside renderer(_: updatedAtTime:):

cleanScene()

There’s one last thing to add. By default, SceneKit enters into a “paused” state if there are no animations to play out. To prevent this from happening, you have to enable the playing property on your SCNView instance.

Add the following line of code to the bottom of setupView():

scnView.isPlaying = true

This forces the SceneKit view into an endless playing mode.

Build and run; as your objects start to fall, pinch to zoom out and see where they disappear into nothingness:

BuildAndRun2

Objects that fall past the lower y-bound (noted by the red line in the screenshot above), are removed from the scene. That’s better than having all those objects lying around the dark recesses of your device. :]

Where to Go From Here?

Here is the example code from this Scene Kit tutorial with Swift.

At this point, you should keep reading to the fifth and final part of this tutorial series, where you’ll add some particle systems and wrap up the game.

If you’d like to learn more, you should check out our book 3D Apple Games by Tutorials. The book teaches you everything you need to know to make 3D iOS games, by making a series of mini-games like this one, including a games like Breakout, Marble Madness, and even Crossy Road.

In the meantime, if you have any questions or comments about this tutorial, please join the forum discussion below!

Chris Language

Chris is a seasoned coder with 20+ years of experience. He has fond memories of his childhood and his Commodore 64; more recently he started adding more good memories of life with all his iOS devices. At day, he fights for survival in the corporate jungle of Johannesburg, South Africa. At night he fights demons, dragons and angry little potty-mouth kids online. For relaxation he codes.

You can find Chris on Twitter.

Forever Coder, Artist, Musician, Gamer and Dreamer.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

Swift Team

... 15 total!

iOS Team

... 33 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 15 total!