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 this second part of the series, you’ll get started making your game, learning about Scene Kit nodes along the way.
Let’s dive back in!
Getting Started
In the previous tutorial, you learned that SceneKit organizes the components of your game into a hierarchy known as the scene graph.
Each element of your game — such as lights, cameras, geometry and particle emitters — are called nodes, and nodes are stored in this tree-like structure.
To illustrate how this works, think back to a childhood nursery rhyme you might have heard…
The hip bone’s connected to the back bone The back bone’s connected to the shoulder bone…
You’re right! It’s the classic song Dem Dry Bones. Bonus points if you know a classic video game that makes particularly good use of this concept. ;]
With those lyrics in mind, take a look at the following anatomically-correct structure of a rare four-fingered skeleton:
To help illustrate how you can construct a node-based hierarchy from this skeleton, think of each bone in the skeleton as a node.
As the song points out, the shoulder bone’s connected to the back bone. So consider the back bone as the parent node of the shoulder bone, and the shoulder bone as the child node of the back bone.
To add the shoulder bone to the scene, you add it as a child of the back bone. You can continue to construct the whole arm in this way, adding child bones to parent bones, right up to the little pinky.
To position a bone, you position it relative to its parent. For example, to wave the skeleton’s left arm, you simply rotate the shoulder node back and forth as indicated by the little blue arrow. All child nodes of the shoulder node will rotate along with their parent.
Congratulations! You just passed skeleton anatomy 101! :]
From a technical perspective, a single node is represented by the SCNNode
class and represents a position in 3D space relative to its parent node. A node on its own has no visible content, therefore, it’s invisible when rendered as part of a scene. To create visible content, you have to add other components such as lights, cameras or geometries (like bones) to the node.
The scene graph contains a special node which forms the foundation of your node-based hierarchy: the root node. To construct your scene, you add your nodes either as child nodes of the root node or as a child of one of the root node’s descendants.
In this tutorial, you’ll start to work with a few simple nodes in SceneKit, such as camera and geometry nodes. By the end of the tutorial, you’ll have rendered a simple 3D cube to the screen!
Asset Catalogs
Once you’re a successful and rich 3D game designer, you’ll have enough money to hire your very own graphics artist and sound engineer, which will free you up to focus on the game code alone. :] The SceneKit asset catalog has been designed specifically to help you manage your game assets separately from the code.
An asset catalog lets you manage your game assets in a single folder. To use it, simply add a folder with the .scnassets extension to your project and save all your game assets in that folder. Xcode will copy everything in your catalog to your app bundle at build time. Xcode preserves your assets folder hierarchy; this gives you full control over the folder structure.
By sharing your assets folder with your artists, they can quickly fix any issues, such as a not-quite-so-scary cross-eyed monster and have it ready for the next build, without having to copy the changed assets back into the project.
Adding an Asset Catalog
Now that you understand what the asset catalog is all about, you’ll add one to Geometry Fighter.
Drag and drop the GeometryFighter.scnassets folder from the resources folder into your game project in Xcode. In the popup that appears, make sure that Copy items if needed, Create Groups and your GeometryFighter target are all checked, then click Finish.
Select the GeometryFighter.scnassets folder in the project navigator. Notice the additional settings unique to the asset catalog in the right hand panel. Expand the GeometryFighter.scnassets folder and sub-folders to see more detail about your assets:
There are two folders inside the asset catalog:
- Sounds: Contains all of the sound assets you’ll need for your game.
- Textures: Contains all of the images you’ll need.
Feel free to take a sneak peek at what’s inside each folder.
Adding a Launch Screen
Now that you’ve imported the asset catalog, you’ll take care of some basic housekeeping steps, including adding a proper image to the launch screen.
First, click Assets.xcassets in the project navigator. Drag and drop GeometryFighter.scnassets/Textures/Logo_Diffuse.png into the assets, below the AppIcon.
Next, click LaunchScreen.storyboard in the project navigator. Select the main view and set the Background property to a dark blue:
Next, drag the Logo_Diffuse image from the Media Library into the center of the view. Set the Content Mode property of your new image to Aspect Fit:
You’re almost done with your launch screen. All you need to do is add some constraints so the splash image works on all devices. Click the Pin button at the bottom, toggle the constraints on for all four edges and click Add 4 Constraints as shown below:
You’re done setting up your launch screen! Build and run your app; you’ll see your shiny new launch screen appear:
Adding a Background Image
Once your splash screen disappears, you’re dumped back to the blank screen of opportunity. Time to add a nice clean background so you don’t feel like you’re staring into a black hole.
To do this, add the following line of code to the bottom of setupScene()
in GameViewController.swift:
scnScene.background.contents = "GeometryFighter.scnassets/Textures/Background_Diffuse.png" |
This line of code instructs the scene to load the Background_Diffuse.png image from the asset catalog and use it as the material property of the scene’s background.
Build and run; you should now see a blue background image once the game starts:
That’s it! You’ve finished all the basic housekeeping tasks for your project. Your game now has a flashy app icon, a splash screen and a pretty background that’s all ready to display the nodes you’re about to add to the scene.
The SceneKit Coordinate System
Before you can start adding nodes to the scene, you first need to understand how SceneKit’s coordinate system works so you can position your nodes where you want them.
In a 2D system such as UIKit or SpriteKit, you use a point to describe the position of a view or a sprite on the x and y axes. To place an object in 3D space, you also need to describe the depth of the object’s position on the z-axis.
Consider the following simple illustration:
SceneKit uses this three-axis system to represent position in 3D space. The red blocks are placed along the x-axis, the green blocks along the y-axis and the blue blocks along the z-axis. The grey cube in the very center of the axis indicates the origin, which has coordinates of (x:0, y:0, z:0)
.
SceneKit uses the SCNVector3
data type to represent coordinates in three dimensions as a three-component vector. Here’s how you create a vector in code:
let position = SCNVector3(x: 0, y: 5, z: 10) |
This declares the variable position
with a vector of (x:0, y:5, z:10)
. You can easily access individual properties of the vector like so:
let x = position.x let y = position.y let z = position.z |
If you’ve worked with CGPoint
before, you can easily draw comparisons between it and SCNVector3
.
(x:0, y:0, z:0)
, which is always relative to the parent node. To place a node at the desired location, you need to adjust the position of the node relative to its parent (local coordinates) — not the origin (world coordinates).
Cameras
Now that you understand how to position nodes in SceneKit, you’re probably wondering how to actually display something onscreen. Think back to the analogy of the movie set from Part 1 of this tutorial series. To shoot a scene, you’d position a camera looking at the scene and the resulting image of that scene would be from the camera’s perspective.
SceneKit works in a similar fashion; the position of the node that contains the camera determines the point of view from which you view the scene.
The following illustration demonstrates how a camera works in SceneKit:
There are a few key points in the previous diagram:
- The camera’s direction of view is always along the negative z-axis of the node that contains the camera.
- The field of view is the limiting angle of the viewable area of your camera. A tight angle provides a narrow view, while a wide angle provides a wide view.
- The viewing frustum determines the visible depth of your camera. Anything outside this area — that is, too close or too far from the camera — will be clipped and won’t appear on the screen.
A SceneKit camera is represented by SCNCamera
, whose xPov
and yPov
properties let you adjust the field of view, while zNear
and zFar
let you adjust the viewing frustum.
One key point to remember is that a camera by itself won’t do anything unless it’s a part of the node hierarchy.
Adding a Camera
Time to try this out. Open GameViewController.swift and add the following property below scnScene
:
var cameraNode: SCNNode! |
Next, add the following method below setupScene()
:
func setupCamera() { // 1 cameraNode = SCNNode() // 2 cameraNode.camera = SCNCamera() // 3 cameraNode.position = SCNVector3(x: 0, y: 0, z: 10) // 4 scnScene.rootNode.addChildNode(cameraNode) } |
Taking a closer look at the code:
-
First, you create an empty
SCNNode
and assign it tocameraNode
. -
Next, you create a new
SCNCamera
object and assign it to thecamera
property ofcameraNode
. -
Then, you set the position of the camera at
(x:0, y:0, z:10)
. -
Finally, you add
cameraNode
to the scene as a child node of the scene’s root node.
Finish things off by calling the method you just added in viewDidLoad()
, right below setupScene()
:
setupCamera() |
There is no need to build and run. Even though you just added a camera to the scene, there’s still nothing to look at, which means you won’t see anything different. But that’s about to change!
Geometry
In order to create visible content, you need to add a geometry object to a node. A geometry object represents a three-dimensional shape and is created out of many points known as vertices that defines polygons.
Additionally, a geometry object can contain material objects that modify the appearance of a geometry’s surface. Materials let you specify information such as the color and texture of the geometry’s surface, and how the geometry should respond to light along with other visual effects. A collection of vertices and materials is known as a model or a mesh.
SceneKit includes the following built-in geometric shapes:
In the front row, starting from the left, you have a cone, a torus, a capsule and a tube. In the back row, again starting from the left, you have a pyramid, a box, a sphere and a cylinder.
Adding ShapeTypes
Before you start adding geometric shapes to the scene, create a new Swift file to define a ShapeType
enum for the different shapes you’ll use in the game.
Right-click on the GeometryFighter group and select New File…. Select the iOS/Source/Swift File template and click Next:
Name the file ShapeType.swift, make sure it’s included in your project, then click Create.
Once the file’s been created, open ShapeType.swift and replace its contents with the following:
import Foundation // 1 enum ShapeType:Int { case box = 0 case sphere case pyramid case torus case capsule case cylinder case cone case tube // 2 static func random() -> ShapeType { let maxValue = tube.rawValue let rand = arc4random_uniform(UInt32(maxValue+1)) return ShapeType(rawValue: Int(rand))! } } |
The code above is relatively straightforward:
-
You create a new enum named
ShapeType
that enumerates the various shapes. -
You also define a static method named
random()
that generates a randomShapeType
. This feature will come in handy later on in your game.
Adding a Geometry Node
Your next task is to create a method that spawns the various random shapes defined in ShapeType.
Add the following method to GameViewController.swift, right below setupCamera()
:
func spawnShape() { // 1 var geometry:SCNGeometry // 2 switch ShapeType.random() { default: // 3 geometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0) } // 4 let geometryNode = SCNNode(geometry: geometry) // 5 scnScene.rootNode.addChildNode(geometryNode) } |
Taking each numbered comment in turn:
-
First, you create a placeholder
geometry
variable for use a bit later on. -
Next, you define a
switch
statement to handle the returned shape fromShapeType.random()
. It’s incomplete at the moment, and only creates a box shape; you’ll add more to it in the challenge at the end of this tutorial. -
Then, you create an
SCNBox
object and store it ingeometry
. You specify the width, height and length, along with the chamfer radius (which is a fancy way of saying rounded corners). -
Here, you create an instance of
SCNNode
namedgeometryNode
. This time, you make use of theSCNNode
initializer which takes ageometry
parameter to create a node and automatically attach the supplied geometry. - Finally, you add the node as a child of the scene’s root node.
Now you need to call this method. Add the following line to viewDidLoad()
below setupCamera()
:
spawnShape() |
Build and run; you’ll see a white square displayed onscreen:
There are a few things to observe here:
-
The box node is the default shape from
spawnShape()
, and it sits at(x:0, y:0, z:0)
in the scene. -
You’re viewing the scene through your
cameraNode
. Since the camera node lives at(x:0, y:0: z:10)
, the box is smack dab in the center of the camera’s viewable area.
OK, it’s not very exciting, and it’s hardly three-dimensional — but fear not… the next section changes all of that!
Built-in View Features
SCNView
comes with a few out-of-the-box features to help make your life easier.
Add the following lines to setupView()
in GameViewController.swift, just below the current implementation:
// 1 scnView.showsStatistics = true // 2 scnView.allowsCameraControl = true // 3 scnView.autoenablesDefaultLighting = true |
Here’s an explanation of the code above:
-
showStatistics
enables a real-time statistics panel at the bottom of your scene. -
allowsCameraControl
lets you manually control the active camera through simple gestures. -
autoenablesDefaultLighting
creates a generic omnidirectional light in your scene so you don’t have to worry about adding your own light sources.
Build and run; things should look a little more exciting this time around!
You can use the following gestures to control the active camera in your scene:
- Single finger swipe: Rotates your active camera around the contents of the scene.
- Two finger swipe: Moves, or pans your camera left, right, up or down in the scene.
- Two finger pinch: Zooms the camera in and out of the scene.
- Double-tap: If you have more than one camera, this switches between the cameras in your scene. Of course, since you have only one camera in the scene, this doesn’t do that. However, it also has the effect of resetting the camera to its original position and settings.
Working with Scene Statistics
Find the statistics panel at the bottom of the screen:
Here’s a quick breakdown of what each element means:
- fps: Stands for frames per second. This a measurement of the total amount of consecutive frame redraws done in one-second. The lower this amount, the more poorly your game is performing. You typically want your game to run at 60fps, which will make your game look and feel smooth.
- ◆: Stands for total draw calls per frame. This is typically the total amount of visible objects drawn per single frame. Lights affecting objects can also increase the amount of draw calls of an object. The lower this amount, the better.
- ▲: Stands for total polygons per frame. This the total amount of polygons used to draw a single frame for all the visible geometry. The lower this amount, the better.
- ✸: Stands for total visible light sources. This is the total amount of light sources currently affecting visible objects. The SceneKit guidelines recommend not using more than 3 light sources at a time.
Click on the + button to expand the panel and reveal more detail:
This panel provides you with the following information:
- Frame time: This is the total amount of time it took to draw a single frame. A frame time of 16.7ms is required to achieve a frame rate of 60fps.
- The color chart: This provides you with a rough frame time percentage breakdown per component within the SceneKit rendering pipeline.
From this example, you now know that it took 22.3ms to draw a single frame of which ±75% was used for Rendering, and ±25% was used for GL Flush.
You can click the – button to minimize the panel again.
Isn’t it great that all these features are built-in? :]
Challenges
It’s important for you to practice what you’ve learned, on your own, so many parts of this series have one or more challenges associated with them.
I highly recommend trying all of the challenges. While following a step-by-step tutorial is educational, you’ll learn a lot more by solving a problem by yourself.
If you get stuck, you can find solutions in the resources for the tutorial — but to get the most from this series, give it your best shot before you look!
Your First Challenge
There’s only one challenge in this tutorial, but it’s a fun one.
Your challenge is to improve the switch
statement inside spawnShape()
to handle the remaining shapes in the enumerator.
Use Apple’s official SceneKit documentation (http://apple.co/2aDBgtH) as a guide to the various geometric shapes. Also, take a look at the ShapeType
enum to see which shapes are left to create; their names should give you a good idea of where to start.
Don’t worry too much about the sizes to use; just try to make them about the same relative size as the box you made earlier.
After this challenge, you’ll have a firm grasp on some of the most fundamental concepts in SceneKit! :]
Where to Go From Here?
Here is the completed project from this part of the tutorial series.
At this point, you should keep reading to the third part of this tutorial series, where you’ll learn how to make the geometry move via Scene Kit physics.
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!
The post Scene Kit Tutorial with Swift Part 2: Nodes appeared first on Ray Wenderlich.