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!
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 due to the simulated effect of gravity and disappeared into the abyss.
Although the effect is neat, it would be so much cooler to spawn multiple objects that collide with each other. That would 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 chapters, Scene Kit 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, and 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 Scene Kit takes to render 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:
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, these steps run… you guessed it… 60 times a second.
The steps always execute in the following order, which lets you inject your game logic exactly where it’s needed:
-
Update: The view calls
renderer(_: updateAtTime:)
on its delegate. This is a good spot to put basic scene update logic. - Execute Actions & Animations: Scene Kit executes all actions and performs all attached animations to the nodes in the scene graph.
-
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. - Simulates Physics: Scene Kit applies a single step of physics simulation to all the physics bodies in the scene.
-
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. - Evaluates Constraints: Scene Kit evaluates and applies constraints, which are rules you can configure to make Scene Kit automatically adjust the transformation of a node.
-
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. - Renders Scene In View: Scene Kit renders the scene in the view.
-
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 to spawnShape()
– your job is to decide where to inject the spawn logic.
Adding 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: NSTimeInterval) { // 3 spawnShape() } } |
Taking a closer look at the code above:
-
This adds an extension to
GameViewController
for protocol conformance and lets you maintain code protocol methods in separate blocks of code. -
This adds an implemention of the
renderer(_: updateAtTime:)
protocol method. -
Finally, you call
spawnShape()
to create a new shape inside the delegate method.
This give you your first hook into Scene Kit’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 Scene Kit 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 function inside the render loop now.
Build and run; unleash the spawning fury of your render loop! :]
The game starts and spawns an insane amount of objects, resulting in a moshpit 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.
Adding a Spawn Timer
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:NSTimeInterval = 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 + NSTimeInterval(Float.random(min: 0.2, max: 1.5)) } |
Taking each commented line in turn:
-
You check if
time
(the current system time) is greater thanspawnTime
. If so, spawn a new shape; otherwise, do nothing. -
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. SinceNSTimeInterval
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:
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?
Cleaning up Your Scene
spawnShape()
continuously adds new child nodes into the scene – but they’re never removed, even after they fall out of sight. Scene Kit 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.presentationNode.position.y < -2 { // 3 node.removeFromParentNode() } } } |
Here’s what’s going on in the code above:
-
Here you simply create a little
for
loop that steps through all available child nodes within the root node of the scene. -
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. Scene Kit 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! - 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, Scene Kit enters into a “paused” state if there aren’t any 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.playing = true |
This forces the Scene Kit 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:
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 iPhone. :]
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 iOS 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!
The post Scene Kit Tutorial with Swift Part 4: Render Loop appeared first on Ray Wenderlich.