How to Make a Game Like Can Knockdown

Learn how to make a game like Can Knockdown using SceneKit and Swift. By Ryan Ackermann.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

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:

  1. You first find the nodes you created in the editor and assign them to the camera and shelf properties.
  2. Next you assign baseCanNode to a node from a pre-built can scene for you to use later when creating the cans.
  3. Here you create a static physics body with the shape of the shelf and attach it to shelfNode.
  4. 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:

  1. If the player completed the previous level, meaning they have balls remaining, then they’ll receive a ball as a reward.
  2. 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.
  3. Here you create a random bool that decides which texture and rotation the can will have.
  4. 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:

Added can to the scene

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.