Welcome to another fantastic SceneKit tutorial!
Nowadays, 3D games are pushing the realism factor to a whole new level, giving it that slight visual edge over basic 2D games. Life-like materials and 3D assets, combined with realistic lighting and physics simulation pushes your games to that next level of realism for an immersive gaming experience.
In this tutorial, you’ll learn how easy it is to create your own version of Can Knockdown using SceneKit and Swift.
You’ll learn about the following topics:
- Building a basic 3D scene using the SceneKit editor.
- Loading and presenting a 3D scene programmatically.
- Setting up realistic physics and how to applying forces.
- Interacting with objects in a 3D scene through touch.
- Designing and implementing basic collision detection.
This tutorial assumes you have working knowledge of SceneKit and Swift. If you’re new to the “scene”, be sure to check out our beginner SceneKit tutorials and also our 3D iOS Games by Tutorials book. For a swift introduction to Swift, there’s a beginner Swift tutorial available too.
Getting Started
Before you continue, download the starter project first.
Open the project and take a moment to check out what’s included. You’ll find the assets for the ball and can, as well as a GameHelper file that provides you some additional helper functions that you’ll use while creating this game.
Build and run, to see what it looks like:
Stare into the black screen of endless possibilities! :]
Don’t distress, this is merely a clean slate for you to begin with. You’re now ready to get this ball rolling!
Setting Up and Presenting the Menu
Before you can start knocking some cans around, you’ll have to add the menu screen for the game first. Open GameViewController.swift and add a new property to GameViewController
:
// Scene properties var menuScene = SCNScene(named: "resources.scnassets/Menu.scn")! |
This loads the menu scene. You will also be able to use menuScene
as a reference to jump back and forth between the menu and the level scenes later on.
To present the menu scene, add the following below viewDidLoad()
:
// MARK: - Helpers func presentMenu() { let hudNode = menuScene.rootNode.childNode(withName: "hud", recursively: true)! hudNode.geometry?.materials = [helper.menuHUDMaterial] hudNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Float(M_PI)) helper.state = .tapToPlay let transition = SKTransition.crossFade(withDuration: 1.0) scnView.present( menuScene, with: transition, incomingPointOfView: nil, completionHandler: nil ) } |
This function configures the heads-up-display (HUD) node in the menu scene, and then presents it with a cross-fade transition using present(scene:with:incomingPointOfView:completionHandler:)
from SCNView
.
Add the call to presentMenu()
at the bottom of viewDidLoad()
:
override func viewDidLoad() { super.viewDidLoad() presentMenu() } |
Build and run – you should now see the menu scene:
Building the Level With the SceneKit Editor
Now that the menu is loaded and looking great, it’s time to build the level scene for the game. Open up the empty resources.scnassets/Level.scn scene:
Start by dragging a Floor node from the Object Library into the scene:
In the Attributes Inspector change Reflectivity to 0.05
so that the floor is slightly reflective.
Select the Material Inspector and set wood-floor.jpg as the Diffuse texture. Expand the Diffuse heading to see additional properties. Set the Offset to (x: 0, y: 0.2)
and the Scale to (x: 15, y: 15)
, this shifts the texture slightly forward and shrinks it down to size. Finally, set the Rotation to 90
degrees:
Now that the floor is in place, you’ll add a brick wall as a background. The wall’s geometry is already configured for you in the Wall.scn scene. To add this wall to the level scene you’ll use it as a Reference Node.
While still inside the Level.scn scene, drag and drop a Wall reference node from the Media Library into the scene.
Inside the Node Inspector set the name of the node to wall and its position to (x: 0, y: 0, z: -5)
.
Next, you’ll need a spot to stack the cans. Drag and drop a Box from the Object Library and name it shelf, then set its position to (x: 0.0, y: 2.25, z: -2.25)
, placing it slightly in-front of the wall.
In the Attributes Inspector set the Width to 10
and the Height to 0.25
. Finally, in the Material Inspector, set the Diffuse to wood-table.png and under additional properties, set both WrapS and WrapT to Repeat, and Scale to (x: 2, y: 2)
. That will make sure the texture fills the entire box, making it look like a proper shelf.
To complete the level, you just need to add a couple of lights and a camera. Start off by dragging in a Spot light from the Object Library, then set its Position to (x: 8.3, y: 13.5, z: 15.0)
and the Euler to (x: -40, y: 28, z: 0)
. This places the spot light high up in the air, then aims it downwards to highlight the focal point of the game, the shelf, where the cans will be stacked.
In the Attributes Inspector, set Inner Angle to 35
and Outer Angle to 85
. This softens the light somehwat and also widens the spot light cone, spreading the light to more parts of the scene.
Finally, under Shadow, set Sample radius to 4
and Sample count to 1
and set the Color to black with 50%
opacity. This will allow the spot light to cast a soft shadow:
Just to break those black shadows, add some ambient lighting by dragging and dropping an Ambient light into the scene. The default settings are all that you need.
Last but not least, you need to add a camera to the scene to give the game some perspective. Drag a Camera into the scene. Position it at (x: 0.0, y: 2.5, z: 14.0)
with a Rotation of (x: -5, y:0 , z:0)
. In the Attributes Inspector, change the Y fov to 45
.
Great job! That completes the design of the level. You should now have a level looking like this:
Loading and Presenting the Level
You have a level for your game set up in Level.scn, but how do you see your creation on your device?
In GameViewController
add the following right below the menuScene
property:
var levelScene = SCNScene(named: "resources.scnassets/Level.scn")! |
This loads the scene, and will also grant you access to all the nodes you just added to the level.
Now to present the level scene, add the following function below presentMenu()
:
func presentLevel() { helper.state = .playing let transition = SKTransition.crossFade(withDuration: 1.0) scnView.present( levelScene, with: transition, incomingPointOfView: nil, completionHandler: nil ) } |
The function sets the game state to .playing
, then presents the level scene with a cross-fade transition effect, pretty much the same way as you did with the menu scene.
Add the following to the bottom of touchesBegan(_:with:)
:
if helper.state == .tapToPlay { presentLevel() } |
This will start the game when you touch the menu scene.
Build and run, then tap on the menu scene to see the level you just designed fade in:
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.
Adding the Ball
At the moment you aren’t able to interact with your game; you’re just left to stare at those pesky cans until they rust. Time to do something about that!
Add the following to the top of the file along with the other node properties just below baseCanNode
:
var currentBallNode: SCNNode? |
This will keep track of the current ball the player is interacting with.
Next add the following new function right after createLevelsFrom(baseNode:)
:
func dispenseNewBall() { // 1 let ballScene = SCNScene(named: "resources.scnassets/Ball.scn")! let ballNode = ballScene.rootNode.childNode(withName: "sphere", recursively: true)! ballNode.name = "ball" let ballPhysicsBody = SCNPhysicsBody( type: .dynamic, shape: SCNPhysicsShape(geometry: SCNSphere(radius: 0.35)) ) ballPhysicsBody.mass = 3 ballPhysicsBody.friction = 2 ballPhysicsBody.contactTestBitMask = 1 ballNode.physicsBody = ballPhysicsBody ballNode.position = SCNVector3(x: -1.75, y: 1.75, z: 8.0) ballNode.physicsBody?.applyForce(SCNVector3(x: 0.825, y: 0, z: 0), asImpulse: true) // 2 currentBallNode = ballNode levelScene.rootNode.addChildNode(ballNode) } |
In this function:
- You create a ball from Ball.scn and configure it with a physics body that simulates a baseball.
- After the ball is positioned, you apply an initial force to launch the ball into view from the left.
To use this new function, add the following to the end of setupNextLevel()
:
// Delay the ball creation on level change let waitAction = SCNAction.wait(duration: 1.0) let blockAction = SCNAction.run { _ in self.dispenseNewBall() } let sequenceAction = SCNAction.sequence([waitAction, blockAction]) levelScene.rootNode.runAction(sequenceAction) |
This will dispense the first ball after a short delay while the level sets itself up.
There’s a tiny problem with the physics in the level. Build and run to see it in action:
Tap the menu; you’ll see the the ball fall into view, then fall off the screen. Whoops!
The floor doesn’t have a physics body yet, so the ball doesn’t know that it should bounce off the floor and instead falls into oblivion.
Instead of adding the physics for the floor through code, you can add it in the SceneKit editor. This way with only a few clicks, you’ll get the ball bouncing as it should.
Adding Physics Using the SceneKit Editor
Head over to resources.scnassets/Level.scn and click on the floor node. Select the Physics Inspector and change Type to Static, then change the Category mask to 5
.
That’s it for adding a physics body in the SceneKit Editor! The other settings can be tweaked to offer different behaviors, but the defaults are perfect for your game.
Build and run to see the ball bounce and roll right to the center, ready to be thrown:
Repeat the same steps to add a physics body to the wall as well, since you don’t want the ball disappearing through the back wall and off into the sunset.
Throwing the Ball
It’s now time to start bashing those cans around. Start off by adding the following properties to GameViewController
:
// Ball throwing mechanics var startTouchTime: TimeInterval! var endTouchTime: TimeInterval! var startTouch: UITouch? var endTouch: UITouch? |
The start and end touch times will help determine how quickly the player moved their finger across the screen. This lets you figure out how hard to thrust the ball towards those poor cans. The location of the touches are important as well so the the ball flies off in the right direction.
Next add the following function right after dispenseNewBall()
:
func throwBall() { guard let ballNode = currentBallNode else { return } guard let endingTouch = endTouch else { return } // 1 let firstTouchResult = scnView.hitTest( endingTouch.location(in: view), options: nil ).filter({ $0.node == touchCatchingPlaneNode }).first guard let touchResult = firstTouchResult else { return } // 2 levelScene.rootNode.runAction( SCNAction.playAudio( helper.whooshAudioSource, waitForCompletion: false ) ) // 3 let timeDifference = endTouchTime - startTouchTime let velocityComponent = Float(min(max(1 - timeDifference, 0.1), 1.0)) // 4 let impulseVector = SCNVector3( x: touchResult.localCoordinates.x, y: touchResult.localCoordinates.y * velocityComponent * 3, z: shelfNode.position.z * velocityComponent * 15 ) ballNode.physicsBody?.applyForce(impulseVector, asImpulse: true) helper.ballNodes.append(ballNode) // 5 currentBallNode = nil startTouchTime = nil endTouchTime = nil startTouch = nil endTouch = nil } |
In this function:
- First, you apply a hit test to get the touch result located on the touch catching node you created earlier.
- Next, you play a whooshing sound effect to provide some audible feedback.
- You calculate the velocity from the time difference from the start and end of the player’s touch.
- Then you create a vector from the local coordinate of the touch result to the shelf’s location. You use the velocity to elongate the vector.
- Finally you clear out the throw’s properties for the next throw.
In order for that function to work, you need to modify the touch handling in the game.
Replace the entire touchesBegan(_:with:)
with:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesBegan(touches, with: event) if helper.state == .tapToPlay { presentLevel() } else { guard let firstTouch = touches.first else { return } let point = firstTouch.location(in: scnView) let hitResults = scnView.hitTest(point, options: [:]) if hitResults.first?.node == currentBallNode { startTouch = touches.first startTouchTime = Date().timeIntervalSince1970 } } } |
At the beginning of a touch, if the game is in the playing state and the touch is on the current ball, then you record the beginning of a touch.
Next, replace touchesEnded(_: with:)
to:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesEnded(touches, with: event) guard startTouch != nil else { return } endTouch = touches.first endTouchTime = Date().timeIntervalSince1970 throwBall() } |
When the player lifts their finger off the screen, you’ll need to hold on to the end touch as well as the time since those are critical to throwing the ball in the right direction.
Build and run and try to bully those cans to the ground:
Collision Detection
You can throw the ball at the cans, and if your aim is any good, they’ll tumble to the ground. That’s great, but you can’t tell if all cans hit the ground so that you can advance to the next level.
SceneKit makes it really easy to handle this type of collision detection. The SCNPhysicsContactDelegate
protocol defines a few useful collision handling functions:
physicsWorld(_:didBegin:)
: This gets called when two physics bodies come into contact with each other.physicsWorld(_:didUpdate:)
: This gets triggered after contact has begun and provides additional information about an ongoing collision between two bodies.physicsWorld(_:didEnd:)
: This gets called when the contact between bodies comes to an end.
While all are useful, physicsWorld(_:didBeginContact:)
is really the only function you’ll have to worry about in your game.
Adding Collision Detection
When the ball collides with other nodes in the level, you typically want to play sounds based on types of nodes participating in the collision. Also, when a can hits the floor you need to increase the score.
First, add the following property to GameViewController
:
var bashedCanNames: [String] = [] |
You will use this to keep track of cans that have been hit.
To get started on handling collisions, add the following extension to the bottom of GameViewController.swift:
extension GameViewController: SCNPhysicsContactDelegate { // MARK: SCNPhysicsContactDelegate func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) { guard let nodeNameA = contact.nodeA.name else { return } guard let nodeNameB = contact.nodeB.name else { return } // 1 var ballFloorContactNode: SCNNode? if nodeNameA == "ball" && nodeNameB == "floor" { ballFloorContactNode = contact.nodeA } else if nodeNameB == "ball" && nodeNameA == "floor" { ballFloorContactNode = contact.nodeB } if let ballNode = ballFloorContactNode { // 2 guard ballNode.action(forKey: GameHelper.ballFloorCollisionAudioKey) == nil else { return } ballNode.runAction( SCNAction.playAudio( helper.ballFloorAudioSource, waitForCompletion: true ), forKey: GameHelper.ballFloorCollisionAudioKey ) return } // 3 var ballCanContactNode: SCNNode? if nodeNameA.contains("Can") && nodeNameB == "ball" { ballCanContactNode = contact.nodeA } else if nodeNameB.contains("Can") && nodeNameA == "ball" { ballCanContactNode = contact.nodeB } if let canNode = ballCanContactNode { guard canNode.action(forKey: GameHelper.ballCanCollisionAudioKey) == nil else { return } canNode.runAction( SCNAction.playAudio( helper.ballCanAudioSource, waitForCompletion: true ), forKey: GameHelper.ballCanCollisionAudioKey ) return } // 4 if bashedCanNames.contains(nodeNameA) || bashedCanNames.contains(nodeNameB) { return } // 5 var canNodeWithContact: SCNNode? if nodeNameA.contains("Can") && nodeNameB == "floor" { canNodeWithContact = contact.nodeA } else if nodeNameB.contains("Can") && nodeNameA == "floor" { canNodeWithContact = contact.nodeB } // 6 if let bashedCan = canNodeWithContact { bashedCan.runAction( SCNAction.playAudio( helper.canFloorAudioSource, waitForCompletion: false ) ) bashedCanNames.append(bashedCan.name!) helper.score += 1 } } } |
There’s a lot going on above, so let’s unpack what’s happening:
- First you check to see if the contact was between the ball and the floor.
- You play a sound effect if the ball hits the floor.
- If the ball didn’t make contact with the floor, then you check to see if the ball contacted a can. If so, you also play an appropriate sound effect.
- If the can has already collided with the floor, simply bail because you’ve already resolved this collison.
- You now check if a can hit the floor.
- If the can contacted the floor, you keep track of the can’s name so you only handle this collision once. You also increment the score when a new can hits the floor.
There are a lot of collisions going on — and a lot to handle! But now that you now know when collisions occur, you can add in one of the best parts of a game — winning! :]
Add the following to the bottom of physicsWorld(_:didBegin:)
:
// 1 if bashedCanNames.count == helper.canNodes.count { // 2 if levelScene.rootNode.action(forKey: GameHelper.gameEndActionKey) != nil { levelScene.rootNode.removeAction(forKey: GameHelper.gameEndActionKey) } let maxLevelIndex = helper.levels.count - 1 // 3 if helper.currentLevel == maxLevelIndex { helper.currentLevel = 0 } else { helper.currentLevel += 1 } // 4 let waitAction = SCNAction.wait(duration: 1.0) let blockAction = SCNAction.run { _ in self.setupNextLevel() } let sequenceAction = SCNAction.sequence([waitAction, blockAction]) levelScene.rootNode.runAction(sequenceAction) } |
Here’s what’s going on above:
- If the number of bashed cans is the same as the number of cans in the level, we advance to the next level.
- This removes the old game end action
- Once the last level is complete, loop through the levels again since the game is based on getting the highest score.
- Load the next level after a short delay.
To get the contact delegate working for your level scene, add the following at the top of createScene()
:
levelScene.physicsWorld.contactDelegate = self |
Finally add the following right after presentLevel()
:
func resetLevel() { // 1 currentBallNode?.removeFromParentNode() // 2 bashedCanNames.removeAll() // 3 for canNode in helper.canNodes { canNode.removeFromParentNode() } helper.canNodes.removeAll() // 4 for ballNode in helper.ballNodes { ballNode.removeFromParentNode() } } |
This helps to clear out the state being tracked while the player is in the middle of playing through a level. Here’s what’s going on:
- If there is a current ball, remove it.
- Remove all of the bashed can names used in the contact delegate.
- Loop through the can nodes and remove each can from its parent, then clear out the array.
- Remove each ball node from the scene.
You’ll need to call this function in a couple places. Add the following code at the top of presentLevel()
:
resetLevel() |
Replace the blockAction
used to move on to the next level inside physicsWorld(_:didBegin:)
with the following:
let blockAction = SCNAction.run { _ in self.resetLevel() self.setupNextLevel() } |
Build and run your game; you can finally play through the game! Well, that is, if you can beat each level in just one throw:
You can’t really expect every player to have the skill to finish a level with one ball. Your next job is to implement a HUD so the player will be able to see their score and remaining balls.
Improving the Gameplay
Add the following at the end of createScene()
:
levelScene.rootNode.addChildNode(helper.hudNode) |
Now the player can see their score and track the remaining balls. You still need a way of checking whether you should dispense another ball, or end the game.
Add the following at the end of throwBall()
:
if helper.ballNodes.count == GameHelper.maxBallNodes { let waitAction = SCNAction.wait(duration: 3) let blockAction = SCNAction.run { _ in self.resetLevel() self.helper.ballNodes.removeAll() self.helper.currentLevel = 0 self.helper.score = 0 self.presentMenu() } let sequenceAction = SCNAction.sequence([waitAction, blockAction]) levelScene.rootNode.runAction(sequenceAction, forKey: GameHelper.gameEndActionKey) } else { let waitAction = SCNAction.wait(duration: 0.5) let blockAction = SCNAction.run { _ in self.dispenseNewBall() } let sequenceAction = SCNAction.sequence([waitAction, blockAction]) levelScene.rootNode.runAction(sequenceAction) } |
This if
statement handles the case of the player throwing their last ball. It gives them a grace period of three seconds so the final can or two can stubbornly roll off the shelf. Otherwise, once the player has thrown the ball, you dispense a new ball after a short delay to give them another chance at bashing some more cans! :]
One final improvement is to also show the player their highest score so they can brag about it to their friends!
Add the following to presentMenu()
, right after helper.state = .tapToPlay
:
helper.menuLabelNode.text = "Highscore: \(helper.highScore)" |
That piece of code refreshes the menu’s HUD so that the player can view their highest score!
You’re all done! Build and run and see if you can beat your own high-score? :]
Where to Go From Here?
The final project for this tutorial can be found here.
You can take the SceneKit knowledge from this tutorial and add some rather clever and realistic throwing mechanics into your own game.
To further your knowledge of SceneKit, check out Apple’s developer videos covering SceneKit and its advances over time.
If you’d like to learn more about making 3D games with SceneKit and Swift, check out our book 3D iOS Games by Tutorials. The book teaches you everything you need to know to make 3D iOS games, by making a series of mini-games like this one, including games like Breakout, Marble Madness, and even Crossy Road. Check out the epic trailer below:
As an extra challenge, we encourage you to try and build some of your own levels. Try adding more than four cans and creating more challenging layouts. You simply need to add or edit the levels declared in createLevelsFrom(baseNode:)
.
We’d love you see what you can come up with — join the discussion below to comment, ask questions or share your ideas for improving this game. Feel free to post some bragging screenshots of those high-scores on Twitter too!
The post How to Make a Game Like Can Knockdown appeared first on Ray Wenderlich.