How To Make a Breakout Game with SpriteKit and Swift: Part 2

Learn how to make a breakout game for iOS with Sprite Kit and Swift via a fun hands-on tutorial. By Michael Briscoe.

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

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 didMove(to:) method:

let gameMessage = SKSpriteNode(imageNamed: "TapToPlay")
gameMessage.name = GameMessageName
gameMessage.position = CGPoint(x: frame.midX, y: frame.midY)
gameMessage.zPosition = 4
gameMessage.setScale(0.0)
addChild(gameMessage)
    
gameState.enter(WaitingForTap.self)

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 didMove(to:) 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 didEnter(from:) and willExit(to:) with this code:

override func didEnter(from previousState: GKState?) {
  let scale = SKAction.scale(to: 1.0, duration: 0.25)
  scene.childNode(withName: GameMessageName)!.run(scale)
}
  
override func willExit(to nextState: GKState) {
  if nextState is Playing {
    let scale = SKAction.scale(to: 0, duration: 0.4)
    scene.childNode(withName: GameMessageName)!.run(scale)
  }
}

When the game enters the WaitingForTap state, the didEnter(from:) 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 willExit(to:) 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: 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 didEnter(from:):

if previousState is WaitingForTap {
  let ball = scene.childNode(withName: 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 update(deltaTime:) method:

let ball = scene.childNode(withName: 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 update(deltaTime:) 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(_:with:) with this new implementation:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
  switch gameState.currentState {
  case is WaitingForTap:
    gameState.enter(Playing.self)
    isFingerOnPaddle = true
      
  case is Playing:
    let touch = touches.first
    let touchLocation = touch!.location(in: self)
      
    if let body = physicsWorld.body(at: 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: TimeInterval) {
  gameState.update(deltaTime: currentTime)
}

The update(_:) method is called before each frame is rendered. This is where you call the Playing state's update(deltaTime:) 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 didEnter(from:):

if previousState is Playing {
  let ball = scene.childNode(withName: BallCategoryName) as! SKSpriteNode
  ball.physicsBody!.linearDamping = 1.0
  scene.physicsWorld.gravity = CGVector(dx: 0.0, dy: -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.enumerateChildNodes(withName: 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 = childNode(withName: GameMessageName) as! SKSpriteNode
    let textureName = gameWon ? "YouWon" : "GameOver"
    let texture = SKTexture(imageNamed: textureName)
    let actionSequence = SKAction.sequence([SKAction.setTexture(texture), 
      SKAction.scale(to: 1.0, duration: 0.25)])
      
    gameOver.run(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 didBegin(_:) method as follows:

First, add this line to the very top of didBegin(_:):

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.enter(GameOver.self)
gameWon = false

Now when the ball hits the bottom of the screen the game is over.

And replace the // TODO: with:

if isGameWon() {
  gameState.enter(GameOver.self)
  gameWon = true
}

When all the blocks are broken you win!

Finally, add this code to touchesBegan(_:with:) just above default:

case is GameOver:
  let newScene = GameScene(fileNamed:"GameScene")
  newScene!.scaleMode = .aspectFit
  let reveal = SKTransition.flipHorizontal(withDuration: 0.5)
  self.view?.presentScene(newScene!, transition: reveal)

Your game is now complete! Give it a build and run!

You Won

Michael Briscoe

Contributors

Michael Briscoe

Author

Over 300 content creators. Join our team.