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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
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.
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:
- 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.
- You change the position of the broken block.
- You add a physics body to the broken block so it will fall. You also change its color and add it to the scene.
- 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.