25 min read

So, this is it! Finally, we move from the 2D world to 3D. With SceneKit, we can make 3D games quite easily, especially since the syntax for SceneKit is quite similar to SpriteKit.

When we say 3D games, we don’t mean that you get to put on your 3D glasses to make the game. In 2D games, we mostly work in the x and y coordinates. In 3D games, we deal with all three axes x, y, and z.

Additionally, in 3D games, we have different types of lights that we can use. Also, SceneKit has an inbuilt physics engine that will take care of forces such as gravity and will also aid collision detection.

We can also use SpriteKit in SceneKit for GUI and buttons so that we can add scores and interactivity to the game. So, there is a lot to cover in this article. Let’s get started.

The topics covered in this article by Siddharth Shekar, the author of Learning iOS 8 Game Development Using Swift, are as follows:

  • Creating a scene with SCNScene
  • Adding objects to a scene
  • Importing scenes from external 3D applications
  • Adding physics to the scene
  • Adding an enemy

(For more resources related to this topic, see here.)

Creating a scene with SCNScene

First, we create a new SceneKit project. It is very similar to creating other projects. Only this time, make sure you select SceneKit from the Game Technology drop-down list. Don’t forget to select Swift for the language field. Choose iPad as the device and click on Next to create the project in the selected directory, as shown in the following screenshot:

Learning iOS 8 Game Development Using Swift

Once the project is created, open it. Click on the GameViewController class, and delete all the contents in the viewDidLoad function, delete the handleTap function, as we will be creating a separate class, and add touch behavior.

Create a new class called GameSCNScene and import the following headers. Inherit from the SCNScene class and add an init function that takes in a parameter called view of type SCNView:

import Foundation
import UIKit
import SceneKit
 
class GameSCNScene: SCNScene{
 
   let scnView: SCNView!
   let _size:CGSize!
   var scene: SCNScene!
  
       required init(coder aDecoder: NSCoder) {
       fatalError("init(coder:) has not been implemented")
   }
  
   init(currentview view: SCNView) {
      
       super.init()
   }
}

Also, create two new constants scnView and _size of type SCNView and CGSize, respectively. Also, add a variable called scene of type SCNScene.

Since we are making a SceneKit game, we have to get the current view, which is the type SCNView, similar to how we got the view in SpriteKit where we typecasted the current view in SpriteKit to SKView.

We create a _size constant to get the current size of the view. We then create a new variable scene of type SCNScene. SCNScene is the class used to make scenes in SceneKit, similar to how we would use SKScene to create scenes in SpriteKit.

Swift would automatically ask to create the required init function, so we might as well include it in the class.

Now, move to the GameViewController class and create a global variable called gameSCNScene of type GameSCNScene and assign it in the viewDidLoad function, as follows:

class GameViewController: UIViewController {
var gameSCNScene:GameSCNScene!
   override func viewDidLoad() {
     super.viewDidLoad()
     let scnView = view as SCNView
     gameSCNScene = GameSCNScene(currentview: scnView)
   }
}// UIViewController Class

Great! Now we can add objects in the GameSCNScene class. It is better to move all the code to a single class so that we can keep the GameSceneController class clean.

In the init function of GameSCNScene, add the following after the super.init function:

scnView = view
_size = scnView.bounds.size                      
 
// retrieve the SCNView
scene = SCNScene()
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.showsStatistics = true
scnView.backgroundColor = UIColor.yellowColor()

Here, we first assign the current view to the scnView constant. Next, we set the _size constant to the dimensions of the current view.

Next we initialize the scene variable. Then, assign the scene to the scene of scnView. Next, enable allowCameraControls and showStatistics. This will enable us to control the camera and move it around to have a better look at the scene. Also, with statistics enabled, we will see the performance of the game to make sure that the FPS is maintained.

The backgroundColor property of scnView enables us to set the color of the view. I have set it to yellow so that objects are easily visible in the scene, as shown in the following screenshot. With all this set we can run the scene.

Learning iOS 8 Game Development Using Swift

Well, it is not all that awesome yet. One thing to notice is that we have still not added a camera or a light, but we still see the yellow scene. This is because while we have not added anything to the scene yet, SceneKit automatically provides a default light and camera for the scene created.

Adding objects to a scene

Let us next add geometry to the scene. We can create some basic geometry such as spheres, boxes, cones, tori, and so on in SceneKit with ease. Let us create a sphere first and add it to the scene.

Adding a sphere to the scene

Create a function called addGeometryNode in the class and add the following code in it:

func addGeometryNode(){
 
   let sphereGeometry = SCNSphere(radius: 1.0)
   sphereGeometry.firstMaterial?.diffuse.contents = UIColor.orangeColor()
      
   let sphereNode = SCNNode(geometry: sphereGeometry)
   sphereNode.position = SCNVector3Make(0.0, 0.0, 0.0)
   scene.rootNode.addChildNode(sphereNode)      
}

For creating geometry, we use the SCNSphere class to create a sphere shape. We can also call SCNBox, SCNCone, SCNTorus, and so on to create box, cone, or torus shapes respectively.

While creating the sphere, we have to provide the radius as a parameter, which will determine the size of the sphere. Although to place the shape, we have to attach it to a node so that we can place and add it to the scene.

So, create a new constant called sphereNode of type SCNNode and pass in the sphere geometry as a parameter. For positioning the node, we have to use the SCNvector3Make property to place our object in 3D space by providing the values for x, y, and z.

Finally, to add the node to the scene, we have to call scene.rootNode to add the sphereNode to scene, unlike SpriteKit where we would simply use addChild to add objects to the scene.

With the sphere added, let us run the scene. Don’t forget to add self.addGeometryNode() in the init function.

We did add a sphere, so why are we getting a circle (shown in the following screenshot)? Well, the basic light source used by SceneKit just enables to us to see objects in the scene. If we want to see the actual sphere, we have to improve the light source of the scene.

Learning iOS 8 Game Development Using Swift

Adding light sources

Let us create a new function called addLightSourceNode as follows so that we can add custom lights to our scene:

func addLightSourceNode(){
      
   let lightNode = SCNNode()
   lightNode.light = SCNLight()
   lightNode.light!.type = SCNLightTypeOmni
   lightNode.position = SCNVector3(x: 10, y: 10, z: 10)
   scene.rootNode.addChildNode(lightNode)
      
 let ambientLightNode = SCNNode()
   ambientLightNode.light = SCNLight()
   ambientLightNode.light!.type = SCNLightTypeAmbient
   ambientLightNode.light!.color = UIColor.darkGrayColor()
   scene.rootNode.addChildNode(ambientLightNode)
}

We can add some light sources to see some depth in our sphere object. Here we add two types of light source. The first is an omni light. Omni lights start at a point and then the light is scattered equally in all directions. We also add an ambient light source. An ambient light is the light that is reflected by other objects, such as moonlight.

There are two more types of light sources: directional and spotlight. Spotlight is easy to understand, and we usually use it if a certain object needs to be brought to attention like a singer on a stage. Directional lights are used if you want light to go in a single direction, such as sunlight. The Sun is so far from the Earth that the light rays are almost parallel to each other when we see them.

For creating a light source, we create a node called lightNode of type SCNNode. We then assign SCNLight to the light property of lightNode. We assign the omni light type to be the type of the light. We assign position of the light source to be at 10 in all three x, y, and z coordinates. Then, we add it to the rootnode of the scene.

Next we add an ambient light to the scene. The first two steps of the process are the same as for creating any light source:

  1. For the type of light we have to assign SCNLightTypeAmbient to assign an ambient type light source. Since we don’t want the light source to be very strong, as it is reflected, we assign a darkGrayColor to the color.
  2. Finally, we add the light source to the scene.

There is no need to add the ambient light source to the scene but it will make the scene have softer shadows. You can remove the ambient light source to see the difference.

Call the addLightSourceNode function in the init function. Now, build and run the scene to see an actual sphere with proper lighting, as shown in the following screenshot:

Learning iOS 8 Game Development Using Swift

You can place a finger on the screen and move it to rotate the cameras as we have enabled camera control. You can use two fingers to pan the camera and you can double tap to reset the camera to its original position and direction.

Adding a camera to the scene

Next let us add a camera to the scene, as the default camera is very close. Create a new function called addCameraNode to the class and add the following code in it:

func addCameraNode(){
      
let cameraNode = SCNNode()
   cameraNode.camera = SCNCamera()
   cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
   scene.rootNode.addChildNode(cameraNode)      
}

Here, again we create an empty node called cameraNode. We assign SCNCamera to the camera property of cameraNode. Next we position the camera such that we keep the x and y values at zero and move the camera back in the z direction by 15 units. Then we add the camera to the rootnode of the scene. Call the addCameraNode at the bottom of the init function.

In this scene, the origin is at the center of the scene, unlike SpriteKit where the origin of a scene is always at bottom right of the scene. Here the positive x and y are to the right and up from the center. The positive z direction is toward you.

Learning iOS 8 Game Development Using Swift

We didn’t move the sphere back or reduce its size here. This is purely because we brought the camera backward in the scene.

Let us next create a floor so that we can have a better understanding of the depth in the scene. Also, in this way, we will learn how to create floors in the scene.

Adding a floor

In the class, add a new function called addFloorNode and add the following code:

func addFloorNode(){
            
     var floorNode = SCNNode()
     floorNode.geometry = SCNFloor()
     floorNode.position.y = -1.0
     scene.rootNode.addChildNode(floorNode)
}

For creating a floor, we create a variable called floorNode of type SCNNode. We then assign SCNFloor to the geometry property of floorNode. For the position, we assign the y value to -1 as we want the sphere to appear above the floor. At the end, as usual, we assign the floorNode to the root node of the scene.

In the following screenshot, I have rotated the camera to show the scene in full action. Here we can see the floor is gray in color and the sphere is casting its reflection on the floor, and we can also see the bright omni light at the top left of the sphere.

Learning iOS 8 Game Development Using Swift

Importing scenes from external 3D applications

Although we can add objects, cameras, and lights through code, it will become very tedious and confusing when we have a lot of objects added to the scene. In SceneKit, this problem can be easily overcome by importing scenes prebuilt in other 3D applications.

All 3D applications such as 3D StudioMax, Maya, Cheetah 3D, and Blender have the ability to export scenes in Collada (.dae) and Alembic (.abc) format. We can import these scenes with lighting, camera, and textured objects into SceneKit directly, without the need for setting up the scene.

In this section, we will import a Collada file into the scene. Drag this file into the current project.

Learning iOS 8 Game Development Using Swift

Along with the DAE file, also add the monster.png file to the project, otherwise you will see only the untextured monster mesh in the scene.

Click on the monsterScene.DAE file. If the textured monster is not automatically loaded, drag the monster.png file from the project into the monster mesh in the preview window. Release the mouse button once you see a (+) sign while over the monster mesh. Now you will be able to see the monster properly textured.

The panel on the left shows the entities in the scene. Below the entities, the scene graph is shown and the view on the right is the preview pane.

Entities show all the objects in the scene and the scene graph shows the relation between these entities. If you have certain objects that are children to other objects, the scene graph will show them as a tree. For example, if you open the triangle next to CATRigHub001, you will see all the child objects under it.

You can use the scene graph to move and rotate objects in the scene to fine-tune your scene. You can also add nodes, which can be accessed by code. You can see that we already have a camera and a spotlight in the scene. You can select each object and move it around using the arrow at the pivot point of the object.

You can also rotate the scene to get a better view by clicking and dragging the left mouse button on the preview scene. For zooming, scroll your mouse wheel up and down. To pan, hold the Alt button on the keyboard and left-click and drag on the preview pane.

One thing to note is that rotating, zooming, and panning in the preview pane won’t actually move your camera. The camera is still at the same position and angle. To view from the camera, again select the Camera001 option from the drop-down list in the preview pane and the view will reset to the camera view.

At the bottom of the preview window, we can either choose to see the view through the camera or spotlight, or click-and-drag to rotate the free camera. If you have more than one camera in your scene, then you will have Camera002, Camera003, and so on in the drop-down list.

Below the view selection dropdown in the preview panel you also have a play button. If you click on the play button, you can look at the default animation of the monster getting played in the preview window.

The preview panel is just that; it is just to aid you in having a better understanding of the objects in the scene. In no way is it a replacement for a regular 3D package such as 3DSMax, Maya, or Blender.

You can create cameras, lights, and empty nodes in the scene graph, but you can’t add geometry such as boxes and spheres. You can add an empty node and position it in the scene graph, and then add geometry in code and attach it to the node.

Now that we have an understanding of the scene graph, let us see how we can run this scene in SceneKit.

In the init function, delete the line where we initialized the scene and add the following line instead. Also delete the objects, light, and camera we added earlier.

init(currentview view:SCNView){
  
super.init()
   scnView = view
   _size = scnView.bounds.size
  
   //retrieve the SCNView
   //scene = SCNScene()
  
scene = SCNScene(named: "monsterScene.DAE")
  
   scnView.scene = scene
   scnView.allowsCameraControl = true
   scnView.showsStatistics = true
   scnView.backgroundColor = UIColor.yellowColor()
  
//   self.addGeometryNode()
//   self.addLightSourceNode()
//   self.addCameraNode()
//   self.addFloorNode()
//  
}

Build and run the game to see the following screenshot:

Learning iOS 8 Game Development Using Swift

You will see the monster running and the yellow background that we initially assigned to the scene. While exporting the scene, if you export the animations as well, once the scene loads in SceneKit the animation starts playing automatically.

Also, you will notice that we have deleted the camera and light in the scene. So, how come the default camera and the light aren’t loaded in the scene?

What is happening here is that while I exported the file, I inserted a camera in the scene and also added a spotlight. So, when we imported the file into the scene, SceneKit automatically understood that there is a camera already present, so it will use the camera as its default camera. Similarly, a spotlight is already added in the scene, which is taken as the default light source, and lighting is calculated accordingly.

Adding objects and physics to the scene

Let us now see how we can access each of the objects in the scene graph and add gravity to the monster.

Accessing the hero object and adding a physics body

So, create a new function called addColladaObjects and call an addHero function in it. Create a global variable called heroNode of type SCNNode. We will use this node to access the hero object in the scene. In the addHero function, add the following code:

init(currentview view:SCNView){
   super.init()
   scnView = view
   _size = scnView.bounds.size
  
   //retrieve the SCNView
   //scene = SCNScene()
   scene = SCNScene(named: "monster.scnassets/monsterScene.DAE")
  
   scnView.scene = scene
   scnView.allowsCameraControl = true
   scnView.showsStatistics = true
   scnView.backgroundColor = UIColor.yellowColor()
  
   self.addColladaObjects()
  
//   self.addGeometryNode()
//   self.addLightSourceNode()
//   self.addCameraNode()
//   self.addFloorNode()
  
}
 
func addHero(){
 
   heroNode = SCNNode()
      
var monsterNode = scene.rootNode.childNodeWithName(
"CATRigHub001", recursively: false)
   heroNode.addChildNode(monsterNode!)
heroNode.position = SCNVector3Make(0, 0, 0)              
  
   let collisionBox = SCNBox(width: 10.0, height: 10.0,
           length: 10.0, chamferRadius: 0)
 
   heroNode.physicsBody?.physicsShape =
SCNPhysicsShape(geometry: collisionBox, options: nil)
  
heroNode.physicsBody = SCNPhysicsBody.dynamicBody()  
   heroNode.physicsBody?.mass = 20
   heroNode.physicsBody?.angularVelocityFactor = SCNVector3Zero
heroNode.name = "hero"
      
   scene.rootNode.addChildNode(heroNode)
}

First, we call the addColladaObjects function in the init function, as highlighted. Then we create the addHero function. In it we initiate the heroNode. Then, to actually move the monster, we need access to the CatRibHub001 node to move the monster. We gain access to it through the ChildWithName property of scene.rootNode. For each object that we wish to gain access to through code, we will have to use the ChildWithName property of the rootNode of the scene and pass in the name of
the object.

If recursively is set to true, to get said object, SceneKit will go through all the child nodes to get access to the specific node. Since the node that we are looking for is right on top, we said false to save processing time.

We create a temporary variable called monsterNode. In the next step, we add the monsterNode variable to heroNode. We then set the position of the hero node to
the origin.

For heroNode to interact with other physics bodies in the scene, we have to assign a shape to the physics body of heroNode. We could use the mesh of the monster, but the shape might not be calculated properly and a box is a much simpler shape than the mesh of the monster. For creating a box collider, we create a new box geometry roughly the width, height, and depth of the monster.

Then, using the physicsBody.physicsShape property of the heroNode, we assign the shape of the collisionBox we created for it. Since we want the body to be affected by gravity, we assign the physics body type to be dynamic. Later we will
see other body types.

Since we want the body to be highly responsive to gravity, we assign a value of 20 to the mass of the body. In the next step, we set the angularVelocityFactor to 0 in all three directions, as we want the body to move straight up and down when a vertical force is applied. If we don’t do this, the body will flip-flop around.

We also assign the name hero to the monster to check if the collided object is the hero or not. This will come in handy when we check for collision with other objects.

Finally, we add heroNode to the scene.

Add the addColladaObjects to the init function and comment or delete the self.addGeometryNode, self.addLightSourceNode, self.addCameraNode, and self.addFloorNode functions if you haven’t already. Then, run the game to see the monster slowly falling through.

We will create a small patch of ground right underneath the monster so that it doesn’t fall down.

Adding ground

Create a new function called addGround and add the following:

func addGround(){
      
   let groundBox = SCNBox(width: 10, height: 2,
                           length: 10, chamferRadius: 0)
 
   let groundNode = SCNNode(geometry: groundBox)
      
   groundNode.position = SCNVector3Make(0, -1.01, 0)
   groundNode.physicsBody = SCNPhysicsBody.staticBody()
   groundNode.physicsBody?.restitution = 0.0
 
   scene.rootNode.addChildNode(groundNode)
}

We create a new constant called groundBox of type SCNBox, with a width and length of 10, and height of 2. Chamfer is the rounding of the edges of the box. Since we didn’t want any rounding of the corners, it is set to 0.

Next we create a SCNNode called groundNode and assign groundBox to it. We place it slightly below the origin. Since the height of the box is 2, we place it at –1.01 so that heroNode will be (0, 0, 0) when the monster rests on the ground.

Next we assign the physics body of type static body. Also, since we don’t want the hero to bounce off the ground when he falls on it, we set the restitution to 0. Finally, we then add the ground to the scene’s rootnode.

The reason we made this body static instead of dynamic is because a dynamic body gets affected by gravity and other forces but a static one doesn’t. So, in this scene, even though gravity is acting downward, the hero will fall but groundBox won’t as it is a static body.

You will see that the physics syntax is very similar to SpriteKit with static bodies and dynamic bodies, gravity, and so on. And once again, similar to SpriteKit, the physics simulation is automatically turned on when we run the scene.

Add the addGround function in the addColladaObjects functions and run the game to see the monster getting affected by gravity and stopping after coming in touch with the ground.

Learning iOS 8 Game Development Using Swift

Adding an enemy node

To check collision in SceneKit, we can check for collision between the hero and the ground. But let us make it a little more interesting and also learn a new kind of body type: the kinematic body.

For this, we will create a new box called enemy and make it move and collide with the hero. Create a new global SCNNode called enemyNode as follows:

let scnView: SCNView!
let _size:CGSize!
var scene: SCNScene!
var heroNode:SCNNode!
var enemyNode:SCNNode!

Also, create a new function called addEnemy to the class and add the following in it:

func addEnemy(){
      
   let geo = SCNBox(width: 4.0,
height: 4.0,
length: 4.0,
chamferRadius: 0.0)
      
   geo.firstMaterial?.diffuse.contents = UIColor.yellowColor()
      
   enemyNode = SCNNode(geometry: geo)
   enemyNode.position = SCNVector3Make(0, 20.0 , 60.0)
   enemyNode.physicsBody = SCNPhysicsBody.kinematicBody()
   scene.rootNode.addChildNode(enemyNode)
      
   enemyNode.name = "enemy"
}

Nothing too fancy here! Just as when adding the groundNode, we have created a cube with all its sides four units long. We have also added a yellow color to its material. We then initialize enemyNode in the function. We position the node along the x, y, and z axes. Assign the body type as kinematic instead of static or dynamic. Then we add the body to the scene and finally name the enemyNode as enemy, which we will be needing while checking for collision. Before we forget, call the addEnemy function in the addColladaObjects function after where we called the addHero function.

The difference between the kinematic body and other body types is that, like static, external forces cannot act on the body, but we can apply a force to a kinematic body to move it.

In the case of a static body, we saw that it is not affected by gravity and even if we apply a force to it, the body just won’t move.

Here we won’t be applying any force to move the enemy block but will simply move the object like we moved the enemy in the SpriteKit game. So, it is like making the same game, but in 3D instead of 2D, so that you can see that although we have a third dimension, the same principles of game development can be applied to both.

For moving the enemy, we need an update function for the enemy. So, let us add it to the scene by creating an updateEnemy function and adding the following to it:

func updateEnemy(){
  
     enemyNode.position.z += -0.9
      
     if((enemyNode.position.z - 5.0) < -40){
          
       var factor = arc4random_uniform(2) + 1
          
       if( factor == 1 ){
           enemyNode.position = SCNVector3Make(0, 2.0 , 60.0)
       }else{
           enemyNode.position = SCNVector3Make(0, 15.0 , 60.0)
       }
   }
}

In the update function, similar to how we moved the enemy in the SpriteKit game, we increment the Z position of the enemy node by 0.9. The difference being that we are moving the z direction.

Once the enemy has gone beyond –40 in the z direction, we reset the position of the enemy. To create an additional challenge to the player, when the enemy resets, a random number is chosen between 1 and 2. If it is 1, then the enemy is placed closer to the ground, otherwise it is placed at 15 units from the ground.

Later, we will add a jump mechanic to the hero. So, when the enemy is closer to the ground, the hero has to jump over the enemy box, but when the enemy is spawned at a height, the hero shouldn’t jump. If he jumps and hits the enemy box, then it is game over. Later we will also add a scoring mechanism to keep score.

For updating the enemy, we actually need an update function to add the enemyUpdate function to so that the enemy moves and his position resets. So, create a function called update in the class and call the updateEnemy function in it as follows:

   func update(){
  
       updateEnemy()
   }

Summary

This article has given insight on how to create a scene with SCNScene, how to add objects to a scene, how to import scenes from external 3D applications, how to adding physics to the scene, and how to add an enemy.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here