Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4387

How To Create a Breakout Game with Sprite Kit and Swift: Part 2

$
0
0

breakout

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.

  1. Some useful constants like the number of blocks you want and their width.
  2. 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.
  3. 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!

Bamboo Blocks

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.

Note: Emitter nodes are a special type of nodes that display particle systems created in the Scene Editor. To check it out for yourself and see how it’s configured, open BrokenPlatform.sks, which is a particle system I’ve created for you for this tutorial. To learn more about particle systems, check out our book 2D iOS & tvOS Games by Tutorials, which has a detailed chapter on the subject.

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.

Breaking Bamboo

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.

State Machine Flow

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!

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!

Game 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.

Note: Property Observers have a parameter that allows you to check the new value of the property (in 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!

You Won

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:

  1. React to ball bouncing off screen borders by playing blipSound.
  2. 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:

  1. Create an SKNode to serve as the targetNode for the particle system.
  2. Create an SKEmitterNode from the BallTrail.sks file.
  3. Set the targetNode to the trailNode. This anchors the particles so that they leave a trail, otherwise they would follow the ball.
  4. 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:

Flaming particle ball

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.


Viewing all articles
Browse latest Browse all 4387

Trending Articles