How to Make a Game Like Stack

In this tutorial, you’ll learn how to make a game like Stack using SceneKit and Swift. By Brody Eller.

4 (1) · 1 Review

Save for later
Share

Have you ever seen one of those amazing 3D games that uses slick simplistic graphics and requires a steady timed tapping finger to create a fun and immersive gaming experience? With the power of SceneKit, it’s amazingly easy to create those types of games yourself!

Here’s what you’ll learn:

  • Visually creating a 3D scene.
  • Programmatically adding nodes to a scene.
  • Using physics bodies with nodes.
  • Combining UIKit and SceneKit.
  • Playing audio in SceneKit games.

This tutorial assumes you’re familiar with SceneKit and Swift. If you are beginning your SceneKit journey, check out our SceneKit tutorials as well as our beginning Swift tutorials.

Getting Started

Start by downloading the starter project.

Inside, you’ll find a SceneKit catalog filled with some audio and a scene file. In addition, there are some SCNVector3 extensions to perform simple arithmetic operations on vectors and a gradient image. You’ll also notice the App Icon has already been added for you! :] Take some time to look around and get familiar with the project.

You’ll be creating a game similar to Stack. The goal of stack is to place blocks one on top of the other. Be careful though: Placing a block even slightly off will cause it to become sliced. Miss entirely, and it’s game over!

Setting up the Scene

You’ll begin by setting up your game scene. Open GameScene.scn.

game_scene

Drag a new camera into your scene, then select the Node Inspector and rename the node to Main Camera. Set the Position to X: 3, Y: 4.5, Z: 3 and the Rotation to X: -40, Y: 45, Z:0:

camera_node_inspector

Now switch to the Attributes Inspector and change the camera’s Projection type to Orthographic.

Next, you’ll add some light to the scene.

Drag a new directional light from the object library into the scene and rename it to Directional Light. Since the camera is viewing the scene from one side, you don’t have to light the side you don’t see. Back in the Attributes Inspector, set the Position to X: 0, Y: 0, Z: 0 and the Rotation to X: -65, Y: 20, Z:-30:

directional_light

Fantastic. It’s lit!

Now onto the tower. You’ll need a base block to support the tower as the player builds it up. Drag a new box into the scene and apply the following properties to it:

  • In the Node Inspector, change the name to Base Block and set the position to X:0, Y:-4, Z:0.
  • In the Attributes Inspector, change the size to Width: 1, Height: 8, Length: 1.
  • In the Material Inspector, change the diffuse hex color to #434343.

base_block_diffuse_color

You need to add a physics body to the base block, so switch to the Physics Inspector and change the physics body type to Static.

Now let’s spice it up with a cool background! With the base block still selected, switch to the Scene Inspector and drag the file Gradient.png to the background slot like this:

scene_background

You need a way to show the player how high they’ve stacked their tower. Open Main.storyboard; you’ll notice it already has a SCNView. Add a label on top of the SCNView and set its text to 0. Then add a constraint that aligns the label to the center, like so:

Add another constraint that pins the top of the label to the top of the screen.

Now switch to the Attributes Inspector and change the font to Custom, Thonburi, Regular, 50.

label_text_settings

Then use the assistant editor (hint: it’s the one with the two overlapping circles located at the top right of Xcode’s window) to add an outlet from the label to the view controller and name it scoreLabel:

Build and run to see what you have so far.

build_and_run_1

Adding Your First Block

Do you know what makes a tower taller and taller? Yep! Building blocks.

You’re going to add some properties to help you keep track of the blocks in play. To do this, open ViewController.swift() and add the following variables right above viewDidLoad():

//1
var direction = true
var height = 0

//2
var previousSize = SCNVector3(1, 0.2, 1)
var previousPosition = SCNVector3(0, 0.1, 0)
var currentSize = SCNVector3(1, 0.2, 1)
var currentPosition = SCNVector3Zero

//3
var offset = SCNVector3Zero
var absoluteOffset = SCNVector3Zero
var newSize = SCNVector3Zero

//4
var perfectMatches = 0

Here’s what this code does:

  1. The direction will track whether the block’s position is increasing or decreasing, and the height variable will contain how high the tower is.
  2. The previousSize and previousPosition variables contain the size and position of the previous layer. The currentSize and currentPosition variables contain the size and position of the current layer.
  3. You will use the offset, absoluteOffset, and newSize variables to calculate the size of the new layer.
  4. The perfectMatches keeps track of how many times the player has perfectly matched the previous layer in a row.

With this in place, it’s time to add a block to your scene. Add this at the bottom of viewDidLoad():

//1
let blockNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
blockNode.position.z = -1.25
blockNode.position.y = 0.1
blockNode.name = "Block\(height)"
    
//2
blockNode.geometry?.firstMaterial?.diffuse.contents =
      UIColor(colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
scnScene.rootNode.addChildNode(blockNode)

Here’s the play-by-play:

  1. Here you create a new block using a box-shaped SCNNode, position it on the Z and Y axis, and name it according to its place in the tower using the height property.
  2. You calculate a new number for the diffuse color’s red value so that it increases with the height. Finally, you add the node to the scene.

Build and run, and you should now see your new block show up on the screen!

build_and_run_2

Moving the Blocks

Excellent! Now you have a brand new block ready to play. However, I think we can all agree that blocks are more fun when they’re moving.

You will accomplish this movement by setting the view controller as the scene renderer delegate and implementing the required methods on the SCNSceneRendererDelegate protocol.

Add this extension at the bottom of the class:

extension ViewController: SCNSceneRendererDelegate {
  func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {

  }
}

Here you implement the SCNSceneRendererDelegate protocol and add renderer(_:updateAtTime:).

Add the following inside renderer(_:updateAtTime:):

// 1
if let currentNode = scnScene.rootNode.childNode(withName: "Block\(height)", recursively: false) {
      // 2
      if height % 2 == 0 {
        // 3
        if currentNode.position.z >= 1.25 {
          direction = false
        } else if currentNode.position.z <= -1.25 {
          direction = true
        }
        
        // 4
        switch direction {
        case true:
          currentNode.position.z += 0.03
        case false:
          currentNode.position.z -= 0.03
        }
      // 5
      } else {
        if currentNode.position.x >= 1.25 {
          direction = false
        } else if currentNode.position.x <= -1.25 {
          direction = true
        }
       
        switch direction {
        case true:
          currentNode.position.x += 0.03
        case false:
          currentNode.position.x -= 0.03
        }
      }
    }

Here's what's happening:

  1. You use the name of the block specified earlier to find it in the scene.
  2. You need to move the block on the X or Z axis, depending on the layer it's in. Even layers move on the Z axis, while odd layers move on the X axis. If the layer height divided by 2 has a remainder of 0, the layer height is even. You use the modulo operator (%) to find the remainder.
  3. If the position of the box hits either 1.25 or -1.25, you change its direction and start moving it the other way.
  4. Depending on the direction, the box is either moving forward or backward along the Z axis.
  5. You repeat the same code as before, but for the X axis.

By default, SceneKit pauses the scene. To be able to see moving objects in your scene, add this at the bottom of viewDidLoad():

scnView.isPlaying = true
scnView.delegate = self

In this code you also set this view controller as the scene's renderer delegate, so you can execute the code you wrote above.

Build and run to see some movement!