Update 05/11/2016: Updated for Xcode 7.3 by Michael Briscoe. Original post by Tutorial Team member Barbara Reichart.
Welcome back to our How To Create a Breakout Game with Sprite Kit and Swift tutorial series!
In the first part of the series, you added a moving paddle and ball to the game.
In this second and final part of the series, you’ll add the bricks to the game along with the rest of the gameplay logic.
This tutorial begins where the previous tutorial left off. If you don’t have it already, here’s the example project up to this point.
Bamboo Blocks
Now that you’ve got the ball bouncing around and making contact, let’s add some bamboo blocks to break. This is a breakout game after all!
Go to GameScene.swift and add the following code to didMoveToView(_:)
to introduce the blocks to the scene:
// 1 let numberOfBlocks = 8 let blockWidth = SKSpriteNode(imageNamed: "block").size.width let totalBlocksWidth = blockWidth * CGFloat(numberOfBlocks) // 2 let xOffset = (CGRectGetWidth(frame) - totalBlocksWidth) / 2 // 3 for i in 0..<numberOfBlocks { let block = SKSpriteNode(imageNamed: "block.png") block.position = CGPoint(x: xOffset + CGFloat(CGFloat(i) + 0.5) * blockWidth, y: CGRectGetHeight(frame) * 0.8) block.physicsBody = SKPhysicsBody(rectangleOfSize: block.frame.size) block.physicsBody!.allowsRotation = false block.physicsBody!.friction = 0.0 block.physicsBody!.affectedByGravity = false block.physicsBody!.dynamic = false block.name = BlockCategoryName block.physicsBody!.categoryBitMask = BlockCategory block.zPosition = 2 addChild(block) } |
This code creates eight blocks that are centered on the screen.
- Some useful constants like the number of blocks you want and their width.
- Here you calculate the x offset. This is the distance between the left border of the screen and the first block. You calculate it by subtracting the width of all the blocks from the screen width and then dividing it by two.
- Create the blocks, configure each with the proper physics properties, and position each one using blockWidth, and xOffset.
Build and run your game and check it out!
The blocks are now in place. But in order to listen to collisions between the ball and blocks, you must update the contactTestBitMask
of the ball. Still in GameScene.swift, edit the already existing line of code in didMoveToView(_:)
to add an extra category to it:
ball.physicsBody!.contactTestBitMask = BottomCategory | BlockCategory |
The above executes a bitwise OR
operation on BottomCategory
and BlockCategory
. The result is that the bits for those two particular categories are set to one while all other bits are still zero. Now, collisions between ball and floor as well as ball and blocks will be sent to to the delegate.
Breaking Bamboo
Now that you’re all set to detect collisions between the ball and blocks, let’s add a helper method to GameScene.swift to remove the blocks from the scene:
func breakBlock(node: SKNode) { let particles = SKEmitterNode(fileNamed: "BrokenPlatform")! particles.position = node.position particles.zPosition = 3 addChild(particles) particles.runAction(SKAction.sequence([SKAction.waitForDuration(1.0), SKAction.removeFromParent()])) node.removeFromParent() } |
This method takes an SKNode
. First, it creates an instance of SKEmitterNode
from the BrokenPlatform.sks file, then sets it’s position to the same position as the node. The emitter node’s zPosition
is set to 3, so that the particles appear above the remaining blocks. After the particles are added to the scene, the node (bamboo block) is removed.
The only thing left to do is to handle the delegate notifications accordingly. Add the following to the end of didBeginContact(_:)
:
if firstBody.categoryBitMask == BallCategory && secondBody.categoryBitMask == BlockCategory { breakBlock(secondBody.node!) //TODO: check if the game has been won } |
The above lines check whether the collision is between the ball and a block. If this is the case, you pass the node to the breakBlock(_:)
method and the block is removed from the scene with a particle animation flourish!
Build and run. Blocks should now break apart when the ball hits them.
Adding Gameplay
Now that you have all the elements of your breakout game set up, it’s time for the player to experience the the thrill of victory, or the agony of defeat.
Understanding State Machines
Most gameplay logic is governed by the current state of the game. For example, if the game is in the “main menu” state, the player shouldn’t move, but if the game is in the “play” state, it should.
A lot of simple games manage the state by using Boolean variables within the update loop. By using a state machine, you can better organize your code as your game becomes more complex.
A state machine manages a group of states, with a single current state and a set of rules for transitioning between states. As the state of the game changes, the state machine will run methods upon exiting the previous state and entering the next. These methods can be used to control gameplay from within each state. After a successful state change, the machine will then execute the current state’s update loop.
Apple introduced the GameplayKit framework in iOS 9, which has built-in support for state machines and makes working with them easy. GameplayKit is beyond the scope of this tutorial, but for now, you’ll use two of its many classes: the GKStateMachine
and GKState
classes.
Adding States
In Bamboo Breakout there are three game states:
- WaitingForTap: The game has loaded and is ready to play.
- Playing: The game is actively playing.
- GameOver: The game is over with either a win or loss.
To save time these three GKState
classes have already been added to your project (check out the Game States group if you’re curious). To create the state machine, first add the following import statement at the top of the GameScene.swift file:
import GameplayKit |
Next, insert this class variable just below var isFingerOnPaddle = false
:
lazy var gameState: GKStateMachine = GKStateMachine(states: [ WaitingForTap(scene: self), Playing(scene: self), GameOver(scene: self)]) |
By defining this variable, you’ve effectively created the state machine for Bamboo Breakout. Notice that you’re initializing GKStateMachine
with an array of GKState
subclasses.
WaitingForTap State
The WaitingForTap
state is when the game has loaded and is ready to begin. The player is prompted to “Tap to Play”, and the game waits for a touch event before entering the play state.
Start by adding the following code to the end of the didMoveToView(_:)
method:
let gameMessage = SKSpriteNode(imageNamed: "TapToPlay") gameMessage.name = GameMessageName gameMessage.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame)) gameMessage.zPosition = 4 gameMessage.setScale(0.0) addChild(gameMessage) gameState.enterState(WaitingForTap) |
This creates a sprite that displays the “Tap to Play” message, later it will also be used to display “Game Over”. You are also telling the state machine to enter the WaitingForTap
state.
While you are in didMoveToView(_:)
also remove the line that reads:
ball.physicsBody!.applyImpulse(CGVector(dx: 2.0, dy: -2.0)) // REMOVE |
You’ll move this line to the play state a little later in this tutorial.
Now, open the WaitingForTap.swift file located in the Game States group. Replace didEnterWithPreviousState(_:)
and willExitWithNextState(_:)
with this code:
override func didEnterWithPreviousState(previousState: GKState?) { let scale = SKAction.scaleTo(1.0, duration: 0.25) scene.childNodeWithName(GameMessageName)!.runAction(scale) } override func willExitWithNextState(nextState: GKState) { if nextState is Playing { let scale = SKAction.scaleTo(0, duration: 0.4) scene.childNodeWithName(GameMessageName)!.runAction(scale) } } |
When the game enters the WaitingForTap
state, the didEnterWithPreviousState(_:)
method is executed. This function simply scales up the “Tap to Play” sprite, prompting the player to begin.
When the game exits the WaitingForTap
state, and enters the Playing
state, then the willExitWithNextState(_:)
method is called, and the “Tap to Play” sprite is scaled back to 0.
Do a build and run, and tap to play!
Okay, so nothing happens when you tap the screen. That’s what the next game state is for!
Playing State
The Playing
state starts the game, and manages the balls velocity.
First, switch back to the GameScene.swift file and implement this helper method:
func randomFloat(from from:CGFloat, to:CGFloat) -> CGFloat { let rand:CGFloat = CGFloat(Float(arc4random()) / 0xFFFFFFFF) return (rand) * (to - from) + from } |
This handy function returns a random float between two passed in numbers. You’ll use it to add some variability to the balls starting direction.
Now, open the Playing.swift file located in the Game States group. First, add this helper method:
func randomDirection() -> CGFloat { let speedFactor: CGFloat = 3.0 if scene.randomFloat(from: 0.0, to: 100.0) >= 50 { return -speedFactor } else { return speedFactor } } |
This code just “flips a coin” and returns either a positive, or negative number. This adds a bit of randomness to the direction of the ball.
Next, add this code to didEnterWithPreviousState(_:)
:
if previousState is WaitingForTap { let ball = scene.childNodeWithName(BallCategoryName) as! SKSpriteNode ball.physicsBody!.applyImpulse(CGVector(dx: randomDirection(), dy: randomDirection())) } |
When the game enters the Playing
state, the ball sprite is retrieved and it’s applyImpulse(_:)
method is fired, setting it in motion.
Next, add this code to the updateWithDeltaTime(_:)
method:
let ball = scene.childNodeWithName(BallCategoryName) as! SKSpriteNode let maxSpeed: CGFloat = 400.0 let xSpeed = sqrt(ball.physicsBody!.velocity.dx * ball.physicsBody!.velocity.dx) let ySpeed = sqrt(ball.physicsBody!.velocity.dy * ball.physicsBody!.velocity.dy) let speed = sqrt(ball.physicsBody!.velocity.dx * ball.physicsBody!.velocity.dx + ball.physicsBody!.velocity.dy * ball.physicsBody!.velocity.dy) if xSpeed <= 10.0 { ball.physicsBody!.applyImpulse(CGVector(dx: randomDirection(), dy: 0.0)) } if ySpeed <= 10.0 { ball.physicsBody!.applyImpulse(CGVector(dx: 0.0, dy: randomDirection())) } if speed > maxSpeed { ball.physicsBody!.linearDamping = 0.4 } else { ball.physicsBody!.linearDamping = 0.0 } |
The updateWithDeltaTime(_:)
method will be called every frame while the game is in the Playing
state. You get the ball and check its velocity, essentially the movement speed. If the x or y velocity falls below a certain threshold, the ball could get stuck bouncing straight up and down, or side to side. If this happens another impulse is applied, kicking it back into an angular motion.
Also, the ball’s speed can increase as it’s bouncing around. If it’s too high, you increase the linear damping so that the ball will eventually slow down.
Now that the Playing state is set up, it’s time to add the code to start the game!
Back in GameScene.swift replace touchesBegan(_:withEvent:)
with this new implementation:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { switch gameState.currentState { case is WaitingForTap: gameState.enterState(Playing) isFingerOnPaddle = true case is Playing: let touch = touches.first let touchLocation = touch!.locationInNode(self) if let body = physicsWorld.bodyAtPoint(touchLocation) { if body.node!.name == PaddleCategoryName { isFingerOnPaddle = true } } default: break } } |
This enables the game to check the game’s current state, and change the state accordingly. Next, you need to override the update(_:)
method and implement it like so:
override func update(currentTime: NSTimeInterval) { gameState.updateWithDeltaTime(currentTime) } |
The update(_:)
method is called before each frame is rendered. This is where you call the Playing
state’s updateWithDeltaTime(_:)
method to manage the ball’s velocity.
Give the game a build and run, then tap the screen to see the state machine in action!
GameOver State
The GameOver
state occurs when the all the bamboo blocks are crushed, or the ball hits the bottom of the screen.
Open the GameOver.swift file located in the Game States group, and add these lines to didEnterWithPreviousState(_:)
:
if previousState is Playing { let ball = scene.childNodeWithName(BallCategoryName) as! SKSpriteNode ball.physicsBody!.linearDamping = 1.0 scene.physicsWorld.gravity = CGVectorMake(0, -9.8) } |
When the game enters the GameOver
state, linear damping is applied to the ball and gravity is restored, causing the ball to drop to the floor and slow down.
That’s it for the GameOver
state. Now it’s time to implement the code that determines if the player wins or loses the game!
You win some, you lose some
Now that you have the state machine all set, you have most of your game play finished. What you need now is a way to win or lose the game.
Start by opening GameScene.swift and adding this helper method:
func isGameWon() -> Bool { var numberOfBricks = 0 self.enumerateChildNodesWithName(BlockCategoryName) { node, stop in numberOfBricks = numberOfBricks + 1 } return numberOfBricks == 0 } |
This method checks to see how many bricks are left in the scene by going through all the scene’s children. For each child, it checks whether the child name is equal to BlockCategoryName
. If there are no bricks left, the player has won the game and the method returns true
.
Now, add this property to the top of the class, just below the gameState
property:
var gameWon : Bool = false { didSet { let gameOver = childNodeWithName(GameMessageName) as! SKSpriteNode let textureName = gameWon ? "YouWon" : "GameOver" let texture = SKTexture(imageNamed: textureName) let actionSequence = SKAction.sequence([SKAction.setTexture(texture), SKAction.scaleTo(1.0, duration: 0.25)]) gameOver.runAction(actionSequence) } } |
Here you create the gameWon
variable and attach the didSet
property observer to it. This allows you to observe changes in the value of a property and react accordingly. In this case, you change the texture of the game message sprite to reflect whether the game is won or lost, then display it on screen.
willSet
) or its old value (in didSet
) allowing value changes comparison right when it occurs. These parameters have default names if you do not provide your own, respectively newValue
and oldValue
. If you want to know more about this, check the Swift Programming Language documentation here: The Swift Programming Language: Declarations
Next, let’s edit the didBeginContact(_:)
method as follows:
First, add this line to the very top of didBeginContact(_:)
:
if gameState.currentState is Playing { // Previous code remains here... } // Don't forget to close the 'if' statement at the end of the method. |
This prevents any contact when the game is not in play.
Then replace this line:
print("Hit bottom. First contact has been made.") |
With the following:
gameState.enterState(GameOver) gameWon = false |
Now when the ball hits the bottom of the screen the game is over.
And replace the // TODO:
with:
if isGameWon() { gameState.enterState(GameOver) gameWon = true } |
When all the blocks are broken you win!
Finally, add this code to touchesBegan(_:withEvent:)
just above default
:
case is GameOver: let newScene = GameScene(fileNamed:"GameScene") newScene!.scaleMode = .AspectFit let reveal = SKTransition.flipHorizontalWithDuration(0.5) self.view?.presentScene(newScene!, transition: reveal) |
Your game is now complete! Give it a build and run!
Finishing Touches
Now that Bamboo Breakout is complete, let’s kick it up a notch by adding a little juice! You’ll do this by adding some sound effects whenever the ball makes contact, and when the blocks are broken. You’ll also add a quick blast of music when the game is over. Finally, you’ll add a particle emitter to the ball, so that it leaves a trail as it bounces around the screen!
Adding sound effects
To save time the project has the various sound files already imported. To start, make sure that GameScene.swift is open, then add the following constants to the top of the class, just after the gameWon
variable:
let blipSound = SKAction.playSoundFileNamed("pongblip", waitForCompletion: false) let blipPaddleSound = SKAction.playSoundFileNamed("paddleBlip", waitForCompletion: false) let bambooBreakSound = SKAction.playSoundFileNamed("BambooBreak", waitForCompletion: false) let gameWonSound = SKAction.playSoundFileNamed("game-won", waitForCompletion: false) let gameOverSound = SKAction.playSoundFileNamed("game-over", waitForCompletion: false) |
You define a series of SKAction constants, each of which will load and play a sound file. Because you define these actions before you need them, they are preloaded into memory, which prevents the game from stalling when you play the sounds for the first time.
Next, update the line that sets the ball’s contactTestBitMask
within the didMoveToView(_:)
method, to the following:
ball.physicsBody!.contactTestBitMask = BottomCategory | BlockCategory | BorderCategory | PaddleCategory |
Nothing new here, you’re just adding the BorderCategory
and PaddleCategory
to the ball’s contactTestBitMask
so that you can detect contact with the borders of the screen, and when the ball makes contact with the paddle.
Let’s edit didBeginContact(_:)
to include the sound effects by adding the following lines right after the if/else statement where you set up firstBody
and secondBody
accordingly:
// 1 if firstBody.categoryBitMask == BallCategory && secondBody.categoryBitMask == BorderCategory { runAction(blipSound) } // 2 if firstBody.categoryBitMask == BallCategory && secondBody.categoryBitMask == PaddleCategory { runAction(blipPaddleSound) } |
This code checks for two new collisions:
- React to ball bouncing off screen borders by playing
blipSound
. - React to paddle contact by playing
blipPaddleSound
.
Of course you want a satisfying crunching sound when the ball breaks the blocks, so add this line to the top of breakBlock(_:)
:
runAction(bambooBreakSound) |
Lastly, at the top of the class insert the following line within the didSet
property observer for the gameWon
variable:
runAction(gameWon ? gameWonSound : gameOverSound) |
One more thing…
Now let’s add a particle system to the ball, so that it leaves a flaming trail as it bounces around!
Add this code to didMoveToView(_:)
:
// 1 let trailNode = SKNode() trailNode.zPosition = 1 addChild(trailNode) // 2 let trail = SKEmitterNode(fileNamed: "BallTrail")! // 3 trail.targetNode = trailNode // 4 ball.addChild(trail) |
Let’s review this section by section:
- Create an
SKNode
to serve as thetargetNode
for the particle system. - Create an
SKEmitterNode
from the BallTrail.sks file. - Set the
targetNode
to thetrailNode
. This anchors the particles so that they leave a trail, otherwise they would follow the ball. - Attach the
SKEmitterNode
to the ball by adding it as a child node.
That’s it – you’re all done! Build and run to see how much more polished your game feels with a little juice:
Where To Go From Here?
You can download the final project for the Sprite Kit Breakout Game that you’ve made in this tutorial.
This is a simple implementation of Breakout. But now that you have this working, there’s a lot more you can do. You could add scoring, or you could extend this code to give the blocks hit points, have different types of blocks, and make the ball have to hit some of them (or all of them) a number of times before they are destroyed. You could add blocks which drop bonuses or power-ups, let the paddle shoot lasers toward the blocks, whatever you dream up!
If you want to learn more about Sprite Kit, you should check out our book 2D iOS & tvOS Games by Tutorials:
In this book we’ll teach you everything you need to know to make great games for iOS & tvOS – from physics, to tile maps, to particle systems, and even how to make your games “juicy” with polish and special effects.
I hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!
The post How To Create a Breakout Game with Sprite Kit and Swift: Part 2 appeared first on Ray Wenderlich.