9.
The Scene Graph
Written by Caroline Begbie
In this chapter, you’ll modify your rendering engine and begin architecting your game engine by abstracting rendering code away from scene management. Game engines can include features such as physics engines and sound. The game engine you’ll produce in this chapter doesn’t have any high-end features, but it’ll help you understand how to integrate other components as well as give you the foundation needed to add complexity later.
For this exercise, you’ll create a scene with a drivable car and a controllable player. This should whet your appetite and provide inspiration for making your own games, which is, after all, what this book is all about!
Scenes
A scene can consist of one or more cameras, lights and models. In the scene you’ll be building, each model can be a prop or a character.
Of course, you can add these objects in your Renderer, but what happens when you want to add some game logic or have them interact with other things? One option is to add this interaction in update(deltaTime:). However, doing so tends to get more impractical as additional interactions are needed. A better way is to create a Scene class and abstract the game logic from the rendering code.
To accomplish this, you’ll first recreate the starter project’s initial render using a Scene class. Once you have that, you’ll be able to move the skeleton around and drive the car without worrying about what Metal and the GPU are doing behind the curtain.
The starter project
Open the starter project for this chapter. For the most part, this project is similar to the previous chapter’s project; however, there are a few changes.
- The main addition is an input controller located in the Utility group.
- The storyboard’s
MTKViewis subclassed to be aGameView, which captures key presses. -
GameViewpasses the user interaction toInputController(more on that later!), which then handles the key presses. Because you’ll be dealing with the keyboard in this chapter, the iOS target is not included, however, there is a combined-targets sample project that uses Core Motion for steering in iOS.
The other significant change is in Model. In the previous chapter, you were only able to play one animation. With a few small changes, you can now play, pause and stop named animations, of which skeleton.usda has four: walk, wave, sit and idle.
Build and run the starter project. If all goes well, you’ll see the ground and car, as well as the skeleton running an idle animation. (This “animation” is a still pose.)
The Scene class
To understand how everything will work together, examine the following diagram:
GameView will process all of the inputs and pass them along to InputController. Scene will have an opportunity to intercept and process touch, mouse and keyboard events. Scene will also hold references to all Models and Cameras. Renderer will hold a reference to the current Scene. For each frame, Renderer will ask Scene to update its models’ transforms, and then Renderer will process each model, which will render itself.
In your starter project, create a new Swift file named Scene.swift. This will be a base class that you can subclass for your specific game scenes.
In the base class, you need to include the elements that all subclasses of Scenes will require:
- A default
Camera. You’ll create an array of cameras from which the player can choose. - A reference to
InputController. - A list of
Renderables in the scene. - Stub methods that your
Scenesubclasses can override. These methods will be used to alter the setup and the updating on each frame. - Finally, because the
Scenecontrols the cameras, it will also need to holdUniforms.
In Scene.swift, add this code:
class Scene {
var sceneSize: CGSize
init(sceneSize: CGSize) {
self.sceneSize = sceneSize
setupScene()
}
func setupScene() {
// override this to add objects to the scene
}
}
This initializes the scene with the size of the metal view that you’ll pass in from ViewController. When you create a new scene subclass, you’ll override setupScene() to add the objects for each specific game scene.
In Scene, add the following:
var cameras = [Camera()]
var currentCameraIndex = 0
var camera: Camera {
return cameras[currentCameraIndex]
}
This creates an array of cameras and sets one as the default. For the moment, you only have one camera, but later you’ll set up another camera with an alternative viewing position.
The scene graph
In your game, you may want to have trees, houses and animated characters. Some of these objects may depend upon other objects. For example, a car may contain a driver and its passengers. As the car moves, its occupants should move along at the same speed, as if they were inside the car.
While it’s possible to move each individual object, you can (and in most cases, should) only move the car, getting the people to automatically follow along. The scene graph is a transform hierarchy that defines the spatial relationships of objects in the scene.
Any children of a parent will move with the parent; however, children can still have their own independent movement within the parent.
The scene you’re about to create will have a scene graph that looks like this:
Scene will hold a rootNode, which is the top of the hierarchy. The objects, Model and Camera, are all derived from the Node class, which you already have. Most nodes are Renderable, meaning, you’ll render a prop and a character, but not a camera.
Using this tree structure, you’ll be able to group Nodes together.
A group is a non-renderable Node that lets you move the group, and all of its children along with it, while still letting you move each node independently within the group.
Note: To better understand groups and their movement, let’s say you create a swarm of killer bees and want each to have its own flight pattern within the swarm. With scene graphs and groups, you can do exactly that.
In the following image, on the left-hand side, the skeleton and the car move independently of one another.
On the right, as the car moves, so does the skeleton. However, as in the bee example, the skeleton inside the car still has independent movement (and should be able to duck when those killer bees attack!).
To transform Node into a hierarchical tree, you need to add parent and child properties.
Open Node.swift and add these properties to Node:
var parent: Node?
var children: [Node] = []
Each Node can now have a single parent Node and multiple child Nodes.
Next, add the following methods to handle adding and removing nodes from the tree structure:
final func add(childNode: Node) {
children.append(childNode)
childNode.parent = self
}
final func remove(childNode: Node) {
for child in childNode.children {
child.parent = self
children.append(child)
}
childNode.children = []
guard let index = (children.firstIndex {
$0 === childNode
}) else { return }
children.remove(at: index)
childNode.parent = nil
}
The first method, add(childNode:), adds the node to the array of children and sets the child node’s parent to the current node.
The second method, remove(childNode:), has to adjust the parent/child relationship before removing the node. It does this by adding any children of the removed node to the children of the current node. When that’s all done, it sets the parent of these nodes to the current node.
Note: You can read up on tree structures at https://www.raywenderlich.com/138190/swift-algorithm-club-swift-tree-data-structure
You can now create a hierarchy of Nodes in your Scene.
While the transform hierarchy is a parent-child relationship, rendering is not. Later on, you may want to render objects in a particular order — keeping them in a flat array makes this task easier and more flexible.
In Scene.swift, add these properties to Scene:
let rootNode = Node()
var renderables: [Renderable] = []
var uniforms = Uniforms()
var fragmentUniforms = FragmentUniforms()
It’s time to replace the current update(deltaTime:) in Renderer with one you’ll add to Scene. These new update methods will handle updating the nodes before they are rendered, which requires traversing through the hierarchy.
Still in Scene.swift, add the following methods to handle updating the nodes:
final func update(deltaTime: Float) {
uniforms.projectionMatrix = camera.projectionMatrix
uniforms.viewMatrix = camera.viewMatrix
fragmentUniforms.cameraPosition = camera.position
updateScene(deltaTime: deltaTime)
update(nodes: rootNode.children, deltaTime: deltaTime)
}
private func update(nodes: [Node], deltaTime: Float) {
nodes.forEach { node in
node.update(deltaTime: deltaTime)
update(nodes: node.children, deltaTime: deltaTime)
}
}
func updateScene(deltaTime: Float) {
// override this to update your scene
}
Here, you update all of the node transforms, recursively. You also create updateScene(deltaTime:) as an overridable method for your subclassed game scenes.
You also need methods to add and remove objects from the scene.
Add the following code to Scene:
final func add(node: Node, parent: Node? = nil,
render: Bool = true) {
if let parent = parent {
parent.add(childNode: node)
} else {
rootNode.add(childNode: node)
}
guard render == true,
let renderable = node as? Renderable else {
return
}
renderables.append(renderable)
}
This method adds a node to the scene. When adding objects to the scene, you can optionally choose the parent of the object and also whether the object is renderable. Calling add(node:parent:render:), without the two optional parameters, will add the object to the scene and render it as you did previously in Renderer.
Now, create the removal method:
final func remove(node: Node) {
if let parent = node.parent {
parent.remove(childNode: node)
} else {
for child in node.children {
child.parent = nil
}
node.children = []
}
guard node is Renderable,
let index = (renderables.firstIndex {
$0 as? Node === node
}) else { return }
renderables.remove(at: index)
}
This calls the Node’s removal method to remove it from the node hierarchy. It also removes the node from the array of Renderables.
The scene is now able to add and update scene objects. The next step is to modify Renderer to use Scene.
In Renderer.swift, add a scene property to Renderer:
var scene: Scene?
In draw(in:), add the following to the initial guard statement to ensure that an instance of Scene exists:
let scene = scene,
Next, since the scene will be handling all the models, you need to replace this:
for model in models {
model.update(deltaTime: deltaTime)
}
With this:
scene.update(deltaTime: deltaTime)
And finally, replace:
for model in models {
renderEncoder.pushDebugGroup(model.name)
model.render(renderEncoder: renderEncoder,
uniforms: uniforms,
fragmentUniforms: fragmentUniforms)
renderEncoder.popDebugGroup()
}
With:
for renderable in scene.renderables {
renderEncoder.pushDebugGroup(renderable.name)
renderable.render(renderEncoder: renderEncoder,
uniforms: scene.uniforms,
fragmentUniforms: scene.fragmentUniforms)
renderEncoder.popDebugGroup()
}
You’re now processing each Renderable in Scene instead of models held on Renderer.
Because you’ve given Scene control of the cameras, you need to update the projection matrix when the screen is resized.
In Scene.swift, at the end of Scene, add this:
func sceneSizeWillChange(to size: CGSize) {
for camera in cameras {
camera.aspect = Float(size.width / size.height)
}
sceneSize = size
}
To properly initialize the scene, call this method at the end of init(sceneSize:):
sceneSizeWillChange(to: sceneSize)
In Renderer.swift, replace mtkView(_:drawableSizeWillChange:) with this:
func mtkView(_ view: MTKView,
drawableSizeWillChange size: CGSize) {
scene?.sceneSizeWillChange(to: size)
}
Excellent! You shifted the responsibility of updating models and cameras to the Scene, and you created a basic game engine.
To create a game scene, all you have to do is subclass Scene and add the nodes and the scene logic. For different game levels, you can create different Scene subclasses.
If you wish, you can clean up Renderer by removing the camera, models and uniforms properties and assignments. You can also delete the lines from Renderer that no longer compile after you’ve removed these objects.
In ViewControllerExtension.swift, in handlePan(gesture:), replace:
renderer?.camera.rotate(delta: delta)
With:
renderer?.scene?.camera.rotate(delta: delta)
Create a new Swift file named GameScene.swift, and then add the following code to it:
class GameScene: Scene {
let ground = Model(name: "ground.obj")
let car = Model(name: "racing-car.obj")
let skeleton = Model(name: "skeleton.usda")
}
This instantiates the three models you’ll be adding to your game scene. Override setupScene(), and add these models to your scene in their correct positions:
override func setupScene() {
ground.tiling = 32
add(node: ground)
car.rotation = [0, .pi / 2, 0]
car.position = [-0.8, 0, 0]
add(node: car)
skeleton.position = [1.6, 0, 0]
skeleton.rotation = [0, .pi, 0]
add(node: skeleton)
skeleton.runAnimation(name: "idle")
camera.position = [0, 1.2, -4]
}
This code adds the models to the scene in the same position they were located at initially in Renderer.
In ViewController.swift, create this new scene at the end of viewDidLoad():
let scene = GameScene(sceneSize: metalView.bounds.size)
renderer?.scene = scene
Build and run, and you’ll see much the same render as start of the chapter. The difference, however, is that you have abstracted the scene logic from the rendering logic, making it easier to set up new scenes with different models and different game logic. You can no longer move the camera, as you’ve also changed the default camera class, so that it is no longer an ArcballCamera.
Grouping nodes
To get an idea of how to update the scene logic, you’re going to have the skeleton drive the car off to the right. But there’s a hitch! You’re only going to move the car model each frame, not the skeleton.
Start by adding this to GameScene:
override func updateScene(deltaTime: Float) {
car.position.x += 0.02
}
This overrides the method that Scene calls on every frame, which allows its subclasses to update models further. Build and run the project.
Oh no! This scene must be haunted; the car’s driving off on its own, leaving its skeleton driver behind.
To solve this problem (and rid yourself of this haunting), you can make the car a parent of the skeleton so that whenever the car moves, the skeleton automatically follows along.
In GameScene, in setupScene(), change:
add(node: skeleton)
skeleton.runAnimation(name: "idle")
To:
add(node: skeleton, parent: car)
skeleton.runAnimation(name: "sit")
This adds the skeleton to the car as a child of the node and changes the animation so the skeleton doesn’t look like he’s walking when he’s supposed to be driving.
Next, you need to update the hierarchy of node transforms, recursively, so that any change to the base node will update all of the transforms of the child nodes.
Add this to Node, in Node.swift:
var worldTransform: float4x4 {
if let parent = parent {
return parent.worldTransform * self.modelMatrix
}
return modelMatrix
}
This computed property goes up through the node hierarchy, recursively, and returns the final matrix for the model.
In Model, in render(renderEncoder:uniforms:fragmentUniforms:), change:
uniforms.modelMatrix = modelMatrix * currentLocalTransform
To:
uniforms.modelMatrix = worldTransform * currentLocalTransform
Build and run.
Well that’s progress. Your skeleton now travels with the car, but he’s not positioned correctly.
When you initially placed the skeleton in the scene, you placed him relative to the scene coordinates, but now his position in the scene needs to be relative to the car instead.
In GameScene, in setupScene(), change the skeleton’s position and rotation to:
skeleton.position = [-0.35, -0.2, -0.35]
skeleton.rotation = [0, 0, 0]
The axes are not the scene’s axes, but rather the car’s axes. So when you move the skeleton negatively on the x-axis, you’re effectively moving the skeleton further left in the car.
Build and run, and the skeleton is now driving the car, sitting properly in the driver’s seat.
First-person camera
In this section, you’ll create a first-person camera which places you in the driving seat. Once inside the car, you’ll be able to drive around the scene using the traditional W-A-S-D keys. Before you begin, take a moment to review some important files.
The main view class in Main.storyboard is a GameView, which is a subclass of MTKView. Located in the SceneGraph-macOS group, open GameView.swift. Notice it has events for key presses and mouse movement, and it forwards these events to an instance of InputController.
The keys you’re dealing with are:
- W: Move forward
- A: Strafe left
- S: Move backward
- D: Strafe right
- Right and left arrows (or Q and E): Rotate left and right
In the Utility group, locate and open InputController.swift. Close to the bottom of the file you’ll find InputState. This is an enum for the state of the key and is used to track the keypress status. There’s also KeyboardControl, which contains a list of valid keys.
With the file review out of the way, it’s time to add a player into the scene.
Your games will have only one player which the input controller will update using the detected key presses.
In InputController.swift, create a property in InputController:
var player: Node?
player can be any Node type - a Model or even a Camera.
Next, you need to integrate the input controller into your base Scene class.
Add this property to Scene in Scene.swift:
let inputController = InputController()
In InputController.swift, create a new method to update the player:
public func updatePlayer(deltaTime: Float) {
guard let player = player else { return }
}
This method is where you’ll calculate how to update the player’s transform. Xcode displays a compiler warning until after you’ve done that.
On every frame, the scene will tell the input controller to update the player transforms according to which keys are pressed.
In Scene.swift, create a new method to update the player. This is a separate method as this process of updating the player will get more complex:
private func updatePlayer(deltaTime: Float) {
inputController.updatePlayer(deltaTime: deltaTime)
}
Add this at the beginning of update(deltaTime:):
updatePlayer(deltaTime: deltaTime)
You still need to connect the GameView input with the Scene in ViewController. So, in ViewController.swift, add this to the end of viewDidLoad():
if let gameView = metalView as? GameView {
gameView.inputController = scene.inputController
}
The first player that you’ll move around the scene will be the camera. Here’s what to expect:
-
When you move through the scene, you’ll be moving along the x and z axes.
-
Your camera will have a direction vector, and when the W key is pressed, you’ll move along the z-axis in a positive direction.
-
If you have the W and D keys pressed simultaneously, you’ll move diagonally.
-
When you press the left and right arrow keys (or Q and E), you’ll rotate in that direction.
In Node.swift, create a forward vector computed property based on the current rotation of the node:
var forwardVector: float3 {
return normalize([sin(rotation.y), 0, cos(rotation.y)])
}
This is an example of forward vectors when rotation.y is 0º and 45º:
It’s useful to have a transformed right direction vector too, so add this:
var rightVector: float3 {
return [forwardVector.z, forwardVector.y, -forwardVector.x]
}
This vector points 90º to the right of the node.
Open InputController.swift and add the default properties for translation and rotation speed to InputController:
var translationSpeed: Float = 2.0
var rotationSpeed: Float = 1.0
These values are per second, so you’ll use deltaTime to calculate the correct value for the current frame.
At the end of updatePlayer(deltaTime:), add this to create a desired direction vector from the current keys pressed.
let translationSpeed = deltaTime * self.translationSpeed
let rotationSpeed = deltaTime * self.rotationSpeed
var direction: float3 = [0, 0, 0]
for key in directionKeysDown {
switch key {
case .w:
direction.z += 1
case .a:
direction.x -= 1
case.s:
direction.z -= 1
case .d:
direction.x += 1
case .left, .q:
player.rotation.y -= rotationSpeed
case .right, .e:
player.rotation.y += rotationSpeed
default:
break
}
}
This code processes each depressed key and creates a final desired direction vector. For instance, if the game player presses W and A, she wants to go diagonally forward and left. The final direction vector ends up as [-1, 0, 1].
Add this after the previous code:
if direction != [0, 0, 0] {
direction = normalize(direction)
player.position +=
(direction.z * player.forwardVector
+ direction.x * player.rightVector)
* translationSpeed
}
Here, you’re calculating the player’s final position from the player’s forward and right vectors and the desired direction.
In GameScene.swift, add this to the end of setupScene():
inputController.player = camera
This sets the camera as the player the input controller will update.
Build and run, and you can now roam about your scene using the W-A-S-D keys. You can rotate using the left and right arrow keys (Q and E also do the rotation).
Follow the animated car off the edge of the scene!
Intercepting key presses
That’s cool, but what if you want to drive the car in place of the skeleton? By setting up the C key as a special key, you’ll be able to jump into the car at any time. But first, you need to remove the driving code that you created earlier.
In GameScene.swift, remove the following line of code from updateScene(deltaTime:):
car.position.x += 0.02
InputController is extensible, meaning you can add keys to the enum and capture the input, which is what you’re about to do.
Open InputController.swift, locate KeyboardControl at the bottom of the file, and add this to the end of the enum:
case c = 8
8 is the ASCII value of the C key.
In GameScene.swift, add the following property:
var inCar = false
This tracks the C key and toggles whether you’re driving the car or not. Now you need to set up GameScene to be InputController’s keyboard delegate.
Add this to the end of GameScene.swift:
extension GameScene: KeyboardDelegate {
func keyPressed(key: KeyboardControl,
state: InputState) -> Bool {
return true
}
}
Returning false from this method stops InputController from updating direction keys, which you don’t want, so you’ll return true.
Add this to the start of setupScene():
inputController.keyboardDelegate = self
You can now intercept key presses and optionally prevent default movement from taking place.
In keyPressed(key:state:), add this before the return:
switch key {
case .c where state == .ended:
let camera = cameras[0]
if !inCar {
remove(node: skeleton)
remove(node: car)
add(node: car, parent: camera)
car.position = [0.35, -1, 0.1]
car.rotation = [0, 0, 0]
inputController.translationSpeed = 10.0
}
inCar = !inCar
return false
default:
break
}
This code, when you press the C key and let go, temporarily removes the car and the skeleton from the scene. It then adds the car back to the scene but as a child of the camera. Finally, you set the position of the car so that it looks as if you’re in the driving seat, and you also set the speed of the camera to move faster.
Build and run, and you can now drive and explore the entire scene. Sure, there’s currently not much to see, but you can add several treefir.obj models to the scene if you’d like.
Note: In first-person shooter games, the player carries around a weapon. You now know how to substitute the car model for a gun model, with the gun parented to the camera, to implement your own first-person shooter.
To step out of the car, in keyPressed(key:state:), change this:
if !inCar {
To:
if inCar {
remove(node: car)
add(node: car)
car.position = camera.position + (camera.rightVector * 1.3)
car.position.y = 0
car.rotation = camera.rotation
inputController.translationSpeed = 2.0
} else {
This code checks to see if you’re in the car. If so, it removes the car from the scene and then adds it back, but as a child of the scene, not the camera. It also sets the car’s position to be near the camera’s position and decreases the player speed to simulate walking.
Build and run, and you can now drive the car around or get out and walk.
Full games tend to have a lot more code, but with this simple prototype, you made some cool stuff happen without even thinking about rendering in Metal.
But there’s still more you can do with the camera — like creating a top-down view.
Orthographic projection
Sometimes it’s a little tricky to see what’s happening in a scene. To help, you can build a top-down camera that shows you the whole scene without any perspective distortion, otherwise known as orthographic projection.
Orthographic projection flattens three dimensions to two dimensions without any perspective distortion.
MathLibrary.swift contains a method to return an orthographic matrix of the size of a specified Rectangle.
To create a camera with orthographic projection, add this to the end of Camera.swift:
class OrthographicCamera: Camera {
var rect = Rectangle(left: 10, right: 10,
top: 10, bottom: 10)
override init() {
super.init()
}
init(rect: Rectangle, near: Float, far: Float) {
super.init()
self.rect = rect
self.near = near
self.far = far
}
override var projectionMatrix: float4x4 {
return float4x4(orthographic: rect, near: near, far: far)
}
}
You can find Rectangle and float4x4(orthographic:near:far:) defined in MathLibrary.swift.
This creates a camera similar to the perspective camera that you generally use, but the projection matrix is an orthographic one. To get the most out of this camera, you’ll set up your scene so that you can switch to this camera by pressing the 1 key on your keyboard. To switch back to the original first-person camera, you’ll use the 0 key.
In GameScene.swift, set up a new property in GameScene:
let orthoCamera = OrthographicCamera()
Add this to the end of setupScene():
orthoCamera.position = [0, 2, 0]
orthoCamera.rotation.x = .pi / 2
cameras.append(orthoCamera)
Here, you add an orthographic camera that’s 2 units up in the air and pointing straight down.
To set the proper size of the camera, you need to override sceneSizeWillChange(to:). Add this to the end of GameScene:
override func sceneSizeWillChange(to size: CGSize) {
super.sceneSizeWillChange(to: size)
let cameraSize: Float = 10
let ratio = Float(sceneSize.width / sceneSize.height)
let rect = Rectangle(left: -cameraSize * ratio,
right: cameraSize * ratio,
top: cameraSize,
bottom: -cameraSize)
orthoCamera.rect = rect
}
This sets the rectangle for the orthographic projection matrix. It also sets the rotation of the camera to point straight down at the scene and increases its size, so you can show more of the scene.
In keyPressed(key:state:), add this to the switch statement:
case .key0:
currentCameraIndex = 0
case .key1:
currentCameraIndex = 1
This changes the current camera index into the scene’s camera array. Key 0 will change to the original camera, and key 1 will take you to top-down view. (That’s the numbers above the letters, not the numbers on the numeric keypad. Those keys have different key addresses.)
Build and run, and press 1 to go into top view. Press the C key so that the first-person camera becomes the object controlled by the input controller, and the input keys will work to move the car. You can now see what’s happening in the overall scene as you drive.
With overhead cameras, you can make things like 2D top-down car racing games.
Note: Notice how you can make the car strafe sideways by using the A and D keys; this isn’t very realistic. If you want to disable these keys, return
falsefromkeyPressed(key:state:)when the A and D keys are pressed.
Third-person camera
You wrote the skeleton out of the game, but what if you want to play as the skeleton? You still want to be in first-person while driving the car, but when you’re out of the car, the skeleton should walk about the scene, and the camera should follow.
In GameScene.swift, in setupScene(), change the skeleton position, rotation and setup code to:
skeleton.position = [1.6, 0, 0]
skeleton.rotation = [0, .pi, 0]
add(node: skeleton)
skeleton.runAnimation(name: "idle")
This makes the skeleton a child of the scene rather than the car.
Still in setupScene(), change:
inputController.player = camera
To:
inputController.player = skeleton
Build and run, and you can move your skeleton around the scene.
The camera is currently fixed, but you can go into top-down view by pressing the 1 key and see the skeleton move.
Speaking of fixed cameras and moving, a third-person camera should really follow the skeleton around. You can fix that now.
Add a new camera class to Camera.swift:
class ThirdPersonCamera: Camera {
var focus: Node
var focusDistance: Float = 3
var focusHeight: Float = 1.2
init(focus: Node) {
self.focus = focus
super.init()
}
}
This sets the third-person camera to focus on the player node, positioned by default 3 units back and 1.2 units up.
To calculate the position and rotation of the camera from the position and rotation of the player, all you need to do is override the view matrix property. Add this to ThirdPersonCamera:
override var viewMatrix: float4x4 {
position = focus.position - focusDistance
* focus.forwardVector
position.y = focusHeight
rotation.y = focus.rotation.y
return super.viewMatrix
}
With this code, whenever the renderer asks for the current camera’s viewMatrix, you update the position of the camera.
In GameScene.swift, add this to the end of setupScene():
let tpCamera = ThirdPersonCamera(focus: skeleton)
cameras.append(tpCamera)
currentCameraIndex = 2
Here, you set the third-person camera as the default rendering camera.
Now build and run the project, and follow your skeleton around the scene.
Note: Pressing the C and 1 keys now do weird things as you’ve been changing the camera. As a mini-challenge, see if you can make the C key go from the first-person camera (
currentCameraIndex= 0) to the third-person camera (currentCameraIndex= 2), and update the positions of all the nodes correctly.
Animating the player
It’s a good idea to animate the skeleton while you’re in control of him. skeleton.usda includes a walking animation, which is what you’ll use. The plan is to animate the skeleton when a key is pressed and freeze him when he’s standing still.
Note: In an ideal world, the walk animation would blend into the idle animation, but this is beyond the scope of this chapter.
In GameScene.swift, in setupScene(), replace:
skeleton.runAnimation(name: "idle")
With:
skeleton.runAnimation(name: "walk")
skeleton.currentAnimation?.speed = 2.0
skeleton.pauseAnimation()
The walk animation is a bit slow, so you speed it up to twice its normal speed.
Now to add the start and stop calls based on the key press status.
Add this code to the switch statement in keyPressed():
case .w, .s, .a, .d:
if state == .began {
skeleton.resumeAnimation()
}
if state == .ended {
skeleton.pauseAnimation()
}
Build and run and test out your animation.
Simple collisions
An essential element of games is collision detection. Imagine how boring it would be if platformer games didn’t let you collect power-ups like gems and oil cans?
Creating a complete physics engine would take a whole book to describe, so instead, you’ll create a simple physics controller so you can understand how to integrate game physics into your engine. This physics controller will test for a simple collision.
The previous chapter touched upon axis-aligned bounding boxes used to check the height of the bouncing ball; these are generally what you use for testing collisions.
Depending on the complexity of your game, you can do several types of collision detection. The most common are:
- Sphere-to-sphere: The simplest volume collision test where you add the radius of each sphere and check that the total is greater than the distance between the objects’ positions. This is fast and efficient. However, as you’ll see when colliding the car, when you have rectangular objects, the collision is not very accurate.
- AABB: Axis-aligned bounding box. The AABB is the smallest cubic volume that contains the object. AABB checking is also fast, but if you have many odd rotated shapes, it’s not too accurate.
- OBB: Oriented bounding box. This is where the bounding volume rotates with the character. It’s much more difficult to check the collision.
You’ll be doing spherical collision using the bounding box provided by Model I/O. The bounding sphere will have a diameter of the widest side.
If you have many objects in your scene and test collision for every object against every other object, the calculation can seriously overwhelm your processing allowance. If you have this scenario, you would generally separate out your game objects into sections using a space partitioning algorithm such as an octree. You’d set this up ahead of time so that you’d only check collisions for objects in the same sections.
In your engine, you’ll set only one dynamic object which you’ll check against every other object. All other objects will be static and don’t need to be checked against each other.
You’ll use a new game scene for this which has a few objects already in the scene. Locate CarScene.swift and add it to the macOS target. This is a subclass of Scene that contains some oil cans and trees as well as the car.
In ViewController.swift, change:
let scene = GameScene(sceneSize: metalView.bounds.size)
To:
let scene = CarScene(sceneSize: metalView.bounds.size)
In the Utility folder, open PhysicsController.swift. At the top of the file, you’ll see this:
let debugRenderBoundingBox = false
Model has some extra code to render the bounding boxes so that you can visualize them. Change debugRenderBoundingBox to true to switch on this code.
Build and run your new scene and check out the bounding boxes being rendered around the objects:
Note: The bounding boxes are axis-aligned bounding boxes whereas you’ll be using a sphere. In top view (key press 1) you can visualize the sphere radius as the largest of the width or height.
In Scene.swift, add this property to Scene:
let physicsController = PhysicsController()
Open PhysicsController.swift. PhysicsController currently has a property for the dynamic body and a property to hold all the static bodies. It also has add and remove methods for the static bodies. What it doesn’t have is a method for collision detection.
Add a method to check the collision:
func checkCollisions() -> Bool {
return false
}
At the moment, you’re returning false to indicate that there isn’t a collision. At the top of this method add the following code:
guard let node = dynamicBody else { return false }
let nodeRadius = max((node.size.x / 2), (node.size.z / 2))
let nodePosition = node.worldTransform.columns.3.xyz
Here, you set up the radius for the bounding sphere and the calculated position. Remember that the dynamic node might be a child of another node, so make sure you calculate the true world position.
After the previous code, check the dynamic node against all the static bodies:
for body in staticBodies {
let bodyRadius = max((body.size.x / 2), (body.size.z / 2))
let bodyPosition = body.worldTransform.columns.3.xyz
let d = distance(nodePosition, bodyPosition)
if d < (nodeRadius + bodyRadius) {
// There’s a hit
return true
}
}
This calculates each static body’s radius and world position and checks the distance between the body and the dynamic node. If the distance is less than the combined radii, the spheres overlap and there is a collision, so you return true. You return out of the method on the first collision you find so as not to check all of the other collisions. (In a moment, you’ll add an option to check all of the bodies and save the collided ones.)
The base scene class will be responsible for checking for collisions during player transform update. In Scene.swift, replace updatePlayer(deltaTime:) with this:
private func updatePlayer(deltaTime: Float) {
guard let node = inputController.player else { return }
let holdPosition = node.position
let holdRotation = node.rotation
inputController.updatePlayer(deltaTime: deltaTime)
if physicsController.checkCollisions() {
node.position = holdPosition
node.rotation = holdRotation
}
}
Here, you hold the position before the update. If the player collides with an object, you want to be able to restore the position, so the player doesn’t move forward.
In CarScene.swift, at the end of setupScene(), add the following to set up the physics bodies:
physicsController.dynamicBody = car
for body in bodies {
physicsController.addStaticBody(node: body)
}
Build and run and check out your collisions. (Hint: It’s easier to see them in top view.)
With the oil cans, you can check for collisions and create power-ups in your game. In this case, you’ll remove the oil can from the scene to indicate the can’s been collected.
In Scene.swift, add a new method in Scene that your game scene can override:
func updateCollidedPlayer() -> Bool {
// override this
return false
}
You’ll be able to override this in your subclassed game scene and return true if player movement should still take place.
In updatePlayer(deltaTime:), change:
if physicsController.checkCollisions() {
To:
if physicsController.checkCollisions()
&& !updateCollidedPlayer() {
If the subclassed game scene returns false from updateCollidedPlayer(), you’ll restore the held positions, and the player won’t update.
In PhysicsController.swift, add these new properties to PhysicsController:
var holdAllCollided = false
var collidedBodies: [Node] = []
This gives you the option to do a quick collision test rather than store the collided bodies. If you hit one object and want to exit the collision testing immediately, set holdAllCollided to false; otherwise, set it to true and checkCollisions() will store all of the collided bodies.
In checkCollisions(), in the distance test conditional, replace:
return true
With:
if holdAllCollided {
collidedBodies.append(body)
} else {
return true
}
At the end of checkCollisions(), replace:
return false
With:
return collidedBodies.count != 0
At the start of checkCollisions(), initialize collidedBodies:
collidedBodies = []
You’ll now be able to check each body in CarScene to see whether the car has hit an oilcan or a tree.
In CarScene.swift, at the end of setupScene(), add this
physicsController.holdAllCollided = true
Override updateCollidedPlayer() so that you can check if you’ve collected an oil can.
override func updateCollidedPlayer() -> Bool {
for body in physicsController.collidedBodies {
if body.name == "oilcan.obj" {
print("power-up")
remove(node: body)
physicsController.removeBody(node: body)
return true
}
}
return false
}
Build and run. When you run over the oil cans, you collect a power-up; when you hit the trees, you stop.
Where to go from here?
In this chapter, you created a simple game engine with an input controller and a physics controller. There are many different architecture choices you can make. The one presented here is overly simplified and only touched the surface of collision detection and physics engines. However, it should give you an idea of how to separate your game code from your Metal rendering code.
In the resources folder for this chapter, you’ll find references.markdown. In it, you’ll find some further reading on how to improve your collision detection using the axis aligned bounding box.
Don’t forget to check out the combined-targets project. You’ll be able to drive your car on your iPhone using an accelerator pedal and steering by tilting your device!