How to Make a Game Like Can Knockdown
Learn how to make a game like Can Knockdown using SceneKit and Swift. By Ryan Ackermann.
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
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
How to Make a Game Like Can Knockdown
35 mins
- Getting Started
- Setting Up and Presenting the Menu
- Building the Level With the SceneKit Editor
- Loading and Presenting the Level
- Physics in SceneKit
- Dynamically Adding Physics to the Level
- Creating the Cans
- Adding the Ball
- Adding Physics Using the SceneKit Editor
- Throwing the Ball
- Collision Detection
- Adding Collision Detection
- Improving the Gameplay
- Where to Go From Here?
Physics in SceneKit
A huge benefit to creating games in SceneKit is being able to leverage the built-in physics engine to implement realistic physics very easily.
To enable physics on a node, you simply attach a physics body to it and configure its properties. There are various factors you can tweak to simulate a real world object; the most common properties you will work with are shape, mass, friction, damping and restitution.
In this game, you will use physics and forces to launch balls at the cans. The cans will have physics bodies that make them behave like empty aluminum cans. Your baseballs will feel more heavy and will bash through the light cans and lump together on the floor.
Dynamically Adding Physics to the Level
Before you can add physics to the game, you need a way of accessing the nodes you created in the SceneKit editor. To do this, add the following below the scene properties in GameViewController
:
// Node properties
var cameraNode: SCNNode!
var shelfNode: SCNNode!
var baseCanNode: SCNNode!
You will need these nodes to layout the cans, configure physics bodies, and position other nodes in the scene.
Next, add the following below the scnView
computed property:
// Node that intercept touches in the scene
lazy var touchCatchingPlaneNode: SCNNode = {
let node = SCNNode(geometry: SCNPlane(width: 40, height: 40))
node.opacity = 0.001
node.castsShadow = false
return node
}()
This is a lazy property for an invisible node that you’ll use later on when handling touches in the scene.
Now you’re ready to start wiring up the physics in the level. Add the following function after presentLevel()
:
// MARK: - Creation
func createScene() {
// 1
cameraNode = levelScene.rootNode.childNode(withName: "camera", recursively: true)!
shelfNode = levelScene.rootNode.childNode(withName: "shelf", recursively: true)!
// 2
guard let canScene = SCNScene(named: "resources.scnassets/Can.scn") else { return }
baseCanNode = canScene.rootNode.childNode(withName: "can", recursively: true)!
// 3
let shelfPhysicsBody = SCNPhysicsBody(
type: .static,
shape: SCNPhysicsShape(geometry: shelfNode.geometry!)
)
shelfPhysicsBody.isAffectedByGravity = false
shelfNode.physicsBody = shelfPhysicsBody
// 4
levelScene.rootNode.addChildNode(touchCatchingPlaneNode)
touchCatchingPlaneNode.position = SCNVector3(x: 0, y: 0, z: shelfNode.position.z)
touchCatchingPlaneNode.eulerAngles = cameraNode.eulerAngles
}
Here’s what’s going on in the code above:
- You first find the nodes you created in the editor and assign them to the
camera
andshelf
properties. - Next you assign
baseCanNode
to a node from a pre-built can scene for you to use later when creating the cans. - Here you create a static physics body with the shape of the shelf and attach it to
shelfNode
. - Finally you position and angle the invisible touch catching node towards the scene’s camera.
To put this new function to use, call it right after presentMenu()
in viewDidLoad()
:
createScene()
The new physics properties you added won’t have any visual effect on the game yet, so now you’ll move on to adding the cans to the level.
Creating the Cans
In the game, there will be varying arrangements of the cans to make the game difficult, yet interesting. To accomplish this, you’ll need a reusable way of creating the cans, configuring their physics properties and adding them to the level.
Start off by adding the following function after presentLevel()
:
func setupNextLevel() {
// 1
if helper.ballNodes.count > 0 {
helper.ballNodes.removeLast()
}
// 2
let level = helper.levels[helper.currentLevel]
for idx in 0..<level.canPositions.count {
let canNode = baseCanNode.clone()
canNode.geometry = baseCanNode.geometry?.copy() as? SCNGeometry
canNode.geometry?.firstMaterial = baseCanNode.geometry?.firstMaterial?.copy() as? SCNMaterial
// 3
let shouldCreateBaseVariation = GKRandomSource.sharedRandom().nextInt() % 2 == 0
canNode.eulerAngles = SCNVector3(x: 0, y: shouldCreateBaseVariation ? -110 : 55, z: 0)
canNode.name = "Can #\(idx)"
if let materials = canNode.geometry?.materials {
for material in materials where material.multiply.contents != nil {
if shouldCreateBaseVariation {
material.multiply.contents = "resources.scnassets/Can_Diffuse-2.png"
} else {
material.multiply.contents = "resources.scnassets/Can_Diffuse-1.png"
}
}
}
let canPhysicsBody = SCNPhysicsBody(
type: .dynamic,
shape: SCNPhysicsShape(geometry: SCNCylinder(radius: 0.33, height: 1.125), options: nil)
)
canPhysicsBody.mass = 0.75
canPhysicsBody.contactTestBitMask = 1
canNode.physicsBody = canPhysicsBody
// 4
canNode.position = level.canPositions[idx]
levelScene.rootNode.addChildNode(canNode)
helper.canNodes.append(canNode)
}
}
In the code above:
- If the player completed the previous level, meaning they have balls remaining, then they’ll receive a ball as a reward.
- You loop over each can position in the current level and create and configure a can by cloning
baseCanNode
. You’ll find out what can positions are in the next step. - Here you create a random bool that decides which texture and rotation the can will have.
- The positioning of each can will be defined by the level data stored in
canPositions
.
With that in place, you are almost ready to see some cans in the level. Before you can see them though, you’ll need to create some levels first.
In GameHelper.swift, you’ll find is a GameLevel
struct that contains a single property representing an array of 3D coordinates for each of the cans in that level. There is also an array of levels where you’ll store the levels you create.
To populate the levels
array add the following back in GameViewController
below setupNextLevel()
:
func createLevelsFrom(baseNode: SCNNode) {
// Level 1
let levelOneCanOne = SCNVector3(
x: baseNode.position.x - 0.5,
y: baseNode.position.y + 0.62,
z: baseNode.position.z
)
let levelOneCanTwo = SCNVector3(
x: baseNode.position.x + 0.5,
y: baseNode.position.y + 0.62,
z: baseNode.position.z
)
let levelOneCanThree = SCNVector3(
x: baseNode.position.x,
y: baseNode.position.y + 1.75,
z: baseNode.position.z
)
let levelOne = GameLevel(
canPositions: [
levelOneCanOne,
levelOneCanTwo,
levelOneCanThree
]
)
// Level 2
let levelTwoCanOne = SCNVector3(
x: baseNode.position.x - 0.65,
y: baseNode.position.y + 0.62,
z: baseNode.position.z
)
let levelTwoCanTwo = SCNVector3(
x: baseNode.position.x - 0.65,
y: baseNode.position.y + 1.75,
z: baseNode.position.z
)
let levelTwoCanThree = SCNVector3(
x: baseNode.position.x + 0.65,
y: baseNode.position.y + 0.62,
z: baseNode.position.z
)
let levelTwoCanFour = SCNVector3(
x: baseNode.position.x + 0.65,
y: baseNode.position.y + 1.75,
z: baseNode.position.z
)
let levelTwo = GameLevel(
canPositions: [
levelTwoCanOne,
levelTwoCanTwo,
levelTwoCanThree,
levelTwoCanFour
]
)
helper.levels = [levelOne, levelTwo]
}
That function simply creates positions for various numbers of cans and stores it in the helper class’ levels
array.
To see your progress, add the following to the bottom of createScene()
:
createLevelsFrom(baseNode: shelfNode)
Finally add this to the top of presentLevel()
:
setupNextLevel()
Build and run, then tap the menu to see the cans stacked up like this:
Great job! :] You now have an efficient and reusable way of loading levels of varying layouts in the game. It’s now time to add in the ball and start bashing away.