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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Handling Taps

Now that you've got the block moving, you need to add a new block and resize the old block whenever the player taps the screen. Switch to Main.storyboard and add a tap gesture recognizer to the SCNView like this:

Now create an action and name it handleTap inside the view controller using the assistant editor.

tap_action

Switch back to the Standard Editor and open ViewController.swift, then place this inside handleTap(_:):

if let currentBoxNode = scnScene.rootNode.childNode(
  withName: "Block\(height)", recursively: false) {
      currentPosition = currentBoxNode.presentation.position
      let boundsMin = currentBoxNode.boundingBox.min
      let boundsMax = currentBoxNode.boundingBox.max
      currentSize = boundsMax - boundsMin
      
      offset = previousPosition - currentPosition
      absoluteOffset = offset.absoluteValue()
      newSize = currentSize - absoluteOffset
      
      currentBoxNode.geometry = SCNBox(width: CGFloat(newSize.x), height: 0.2, 
        length: CGFloat(newSize.z), chamferRadius: 0)
      currentBoxNode.position = SCNVector3Make(currentPosition.x + (offset.x/2),
        currentPosition.y, currentPosition.z + (offset.z/2))
      currentBoxNode.physicsBody = SCNPhysicsBody(type: .static, 
        shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
}

Here you retrieve the currentBoxNode from the scene. Then you calculate the offset and new size of the block. From there you change the size and position of the block and give it a static physics body.

The offset is equal to the difference in position between the previous layer and the current layer. By subtracting the absolute value of the offset from the current size, you get the new size.

You'll notice that by setting the position of the current node to the offset divided by two, the block's edge matches perfectly with the previous layer's edge. This gives the illusion of chopping the block.

Next, you need a method to create the next block in the tower. Add this under handleTap(_:):

func addNewBlock(_ currentBoxNode: SCNNode) {
  let newBoxNode = SCNNode(geometry: currentBoxNode.geometry)
  newBoxNode.position = SCNVector3Make(currentBoxNode.position.x, 
    currentPosition.y + 0.2, currentBoxNode.position.z)
  newBoxNode.name = "Block\(height+1)"
  newBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(
    colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
    
  if height % 2 == 0 {
    newBoxNode.position.x = -1.25
  } else {
    newBoxNode.position.z = -1.25
  }
    
  scnScene.rootNode.addChildNode(newBoxNode)
}

Here you create a new node with the same size as the current block. You position it above the current block and change its X or Z position depending on the layer height. Finally, you change its diffuse color and add it to the scene.

You will use handleTap(_:) to keep all your properties up to date. Add this to the end of handleTap(_:) inside the if let statement:

addNewBlock(currentBoxNode)

if height >= 5 {
  let moveUpAction = SCNAction.move(by: SCNVector3Make(0.0, 0.2, 0.0), duration: 0.2)
  let mainCamera = scnScene.rootNode.childNode(withName: "Main Camera", recursively: false)!
  mainCamera.runAction(moveUpAction)
}
      
scoreLabel.text = "\(height+1)"
      
previousSize = SCNVector3Make(newSize.x, 0.2, newSize.z)
previousPosition = currentBoxNode.position
height += 1

The first thing you do is call addNewBlock(_:). If the tower size is greater than or equal to 5, you move the camera up.

You also update the score label, set the previous size and position equal to the current size and position. You can use newSize because you set the current box node's size to newSize. Then you increment the height.

Build and run. Things are stacking up nicely! :]

Implementing Physics

The game resizes the blocks correctly, but it would be cool if the chopped block would fall down the tower.

Define the following new method under addNewBlock(_:):

func addBrokenBlock(_ currentBoxNode: SCNNode) {
    let brokenBoxNode = SCNNode()
    brokenBoxNode.name = "Broken \(height)"
    
    if height % 2 == 0 && absoluteOffset.z > 0 {
      // 1
      brokenBoxNode.geometry = SCNBox(width: CGFloat(currentSize.x), 
        height: 0.2, length: CGFloat(absoluteOffset.z), chamferRadius: 0)
      
      // 2
      if offset.z > 0 {
        brokenBoxNode.position.z = currentBoxNode.position.z - 
          (offset.z/2) - ((currentSize - offset).z/2)
      } else {
        brokenBoxNode.position.z = currentBoxNode.position.z - 
          (offset.z/2) + ((currentSize + offset).z/2)
      }
      brokenBoxNode.position.x = currentBoxNode.position.x
      brokenBoxNode.position.y = currentPosition.y
      
      // 3
      brokenBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
        shape: SCNPhysicsShape(geometry: brokenBoxNode.geometry!, options: nil))
      brokenBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(colorLiteralRed: 0.01 * 
        Float(height), green: 0, blue: 1, alpha: 1)
      scnScene.rootNode.addChildNode(brokenBoxNode)

    // 4
    } else if height % 2 != 0 && absoluteOffset.x > 0 {
      brokenBoxNode.geometry = SCNBox(width: CGFloat(absoluteOffset.x), height: 0.2, 
        length: CGFloat(currentSize.z), chamferRadius: 0)
      
      if offset.x > 0 {
        brokenBoxNode.position.x = currentBoxNode.position.x - (offset.x/2) - 
          ((currentSize - offset).x/2)
      } else {
        brokenBoxNode.position.x = currentBoxNode.position.x - (offset.x/2) + 
          ((currentSize + offset).x/2)
      }
      brokenBoxNode.position.y = currentPosition.y
      brokenBoxNode.position.z = currentBoxNode.position.z
      
      brokenBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
        shape: SCNPhysicsShape(geometry: brokenBoxNode.geometry!, options: nil))
      brokenBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(
        colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
      scnScene.rootNode.addChildNode(brokenBoxNode)
    }
  }

Here you create a new node and name it using the height variable. You use anif statement to determine the axis and make sure the offset is greater than 0, because if it is equal to zero then you shouldn't spawn a broken block!

Breaking down the rest:

  1. Earlier, you subtracted the offset to find the new size. Here, you don't need to subtract anything, as the correct size is equal to the offset.
  2. You change the position of the broken block.
  3. You add a physics body to the broken block so it will fall. You also change its color and add it to the scene.
  4. You do the same for the X axis as you did for the Z.

You find the position of the broken block by subtracting half the offset from the current position. Then, depending on whether the block is in a positive or negative position, you add or subtract half the current size minus the offset.

Add a call to this method right before you call addNewBlock(_:) in handleTap(_:):

addBrokenBlock(currentBoxNode)

When the broken node falls out of view, it doesn't get destroyed: It continues falling infinitely. Add this inside renderer(_:updateAtTime:), right at the top:

for node in scnScene.rootNode.childNodes {
  if node.presentation.position.y <= -20 {
    node.removeFromParentNode()
  }
}

This code deletes any node whose Y position is less than -20.

Build and run to see some sliced blocks!

Finishing Touches

Now that you've finished the core game mechanics, there are only a few loose ends to tie up. There should be a reward for the player if they match the previous layer perfectly. Also, there is no win/lose condition or any way to start a new game when you've lost! Finally, the game is devoid of sound, so you'll need to add some as well.