Game Center for iOS: Building a Turn-Based Game

In this tutorial, you’ll learn about authentication with Game Center and how its turn-based mechanics work. In the end, you’ll have the foundations of how to integrate a multiplayer game with GameCenter. By Ryan Ackermann.

4.5 (12) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Finding an Online Match

Things are starting to come along. Now that the player is authenticated to Game Center, you need a way to start a new game. You’ll accomplish this using GKTurnBasedMatchmakerViewController.

Note: GKTurnBasedMatchmakerViewController doesn’t have a lot of options to customize how it appears. If that matters to your application you can manage the games programmatically, thus creating your own matchmaker.

Open GameCenterHelper.swift and create a new method to handle the presentation of the matchmaker view controller below init():

func presentMatchmaker() {
  // 1
  guard GKLocalPlayer.local.isAuthenticated else {
    return
  }
  
  // 2
  let request = GKMatchRequest()
  
  request.minPlayers = 2
  request.maxPlayers = 2
  // 3
  request.inviteMessage = "Would you like to play Nine Knights?"
  
  // 4
  let vc = GKTurnBasedMatchmakerViewController(matchRequest: request)
  viewController?.present(vc, animated: true)
}

Here’s what’s going on:

  1. Ensure the player is authenticated.
  2. Create a match request to send an invite using the matchmaker view controller.
  3. Customize the request. Set the invite message to personalize the invitation. Configure the number of players based on the rules of Nine Knights.
  4. Pass the request to the matchmaker view controller and present it.

You’re now able to make the online button functional by presenting the matchmaker.

Open MenuScene.swift and replace the print statement in the onlineButton node callback with:

GameCenterHelper.helper.presentMatchmaker()

Build and run, then select the online button.

The view once you select the online button.

Initially, the matchmaker view controller shows a new game view. If you try to cancel and dismiss the view or create a new game, the view controller doesn’t dismiss. This is because the matchmaker’s delegate isn’t set.

To fix this, first add the following extension at the bottom of GameCenterHelper.swift to conform the helper class to GKTurnBasedMatchmakerViewControllerDelegate:

extension GameCenterHelper: GKTurnBasedMatchmakerViewControllerDelegate {
  func turnBasedMatchmakerViewControllerWasCancelled(
    _ viewController: GKTurnBasedMatchmakerViewController) {
      viewController.dismiss(animated: true)
  }
  
  func turnBasedMatchmakerViewController(
    _ viewController: GKTurnBasedMatchmakerViewController, 
    didFailWithError error: Error) {
      print("Matchmaker vc did fail with error: \(error.localizedDescription).")
  }
}

Then, add this to assign the delegate in presentMatchmaker(), before presenting the view controller:

vc.turnBasedMatchmakerDelegate = self

You’ll notice that the matchmaker delegate consists of only two methods. There are others that manage finding and quitting a match, but they’re deprecated. The two methods you implemented log any errors that occur and dismiss the matchmaker.

Build and run.

Great job! You now can create new matches with Game Center. Right now it’s a little cumbersome. After creating a match, you have to back out of the view, close the matchmaker, then open the matchmaker back up. You’ll fix this when you implement handling for turn events.

Under the Hood of Game Center

The game is now in a state where you’re able to track authentication state changes and display Game Center’s matchmaker to manage games. The final piece to complete the online experience is handling the turns of the game.

Game Center deals with turn-based games by storing the players, the current player and the match data. It stores a number of other things, but they aren’t as important. Updating the game for a player’s turn consists of three things:

  1. Read the current game state and update the UI.
  2. Process player input, update the game model to reflect that input and convert the model to Data which you save to Game Center.
  3. Calculate the next player and send the turn.

All of this game data is represented by a GKTurnBasedMatch. Currently, you’re able to create the matches using the matchmaker, but you still need a way to receive a match.

Taking Nine Knights Online

To start receiving turn events right away when the player logs in, you need to register the local player.

Add the following extension at the end of the file:

extension GameCenterHelper: GKLocalPlayerListener {
  func player(_ player: GKPlayer, wantsToQuitMatch match: GKTurnBasedMatch) {
    // 1
    let activeOthers = match.others.filter { other in
      return other.status == .active
    }
    
    // 2
    match.currentParticipant?.matchOutcome = .lost
    activeOthers.forEach { participant in
      participant.matchOutcome = .won
    }
    
    // 3
    match.endMatchInTurn(
      withMatch: match.matchData ?? Data()
    )
  }
  
  func player(
    _ player: GKPlayer, 
    receivedTurnEventFor match: GKTurnBasedMatch, 
    didBecomeActive: Bool
  ) {
    // 4
  guard didBecomeActive else {
    return
  }
  
  NotificationCenter.default.post(name: .presentGame, object: match)
  }
}

Going through the code:

  1. When a player quits Nine Knights, you need to get the other active player in the game.
  2. Set the quitting player’s outcome to lost and each other active player’s outcome to win. For this game, there should only be one other player.
  3. Finally, end the match with the updated data.
  4. When the app receives a turn event, the event includes a Bool which indicates if the turn should present the game. You use one of the notifications you defined earlier to notify the menu scene.

To use this protocol implementation, change the print statement in the authentication block in init() to:

GKLocalPlayer.local.register(self)

If you try running this, you’ll still find that the matchmaker doesn’t dismiss when selecting a game. You can fix this by keeping track of the matchmaker view controller and dismissing it, if needed, in the turn event method.

Add the following property to the top of GameCenterHelper:

var currentMatchmakerVC: GKTurnBasedMatchmakerViewController?

Next, assign it right before presenting the view in presentMatchmaker():

currentMatchmakerVC = vc

Finally, add the following to the top of player(_:receivedTurnEventFor:didBecomeActive:):

if let vc = currentMatchmakerVC {
  currentMatchmakerVC = nil
  vc.dismiss(animated: true)
}

You’ll now see the matchmaker dismiss when you select a game.

Next, one of the most important things is to display the game. Open MenuScene.swift and add the following to the bottom of the class:

@objc private func presentGame(_ notification: Notification) {
  // 1
  guard let match = notification.object as? GKTurnBasedMatch else {
    return
  }
  
  loadAndDisplay(match: match)
}

// MARK: - Helpers

private func loadAndDisplay(match: GKTurnBasedMatch) {
  // 2
  match.loadMatchData { data, error in
    let model: GameModel
    
    if let data = data {
      do {
        // 3
        model = try JSONDecoder().decode(GameModel.self, from: data)
      } catch {
        model = GameModel()
      }
    } else {
      model = GameModel()
    }
    
    // 4
    self.view?.presentScene(GameScene(model: model), transition: self.transition)
  }
}

Here’s what’s happening:

  1. Ensure the object from the notification is the match object you expect.
  2. Request the match data from Game Center to ensure you have the most up to date information, the match data is requested from Game Center.
  3. Construct a game model from the match data. Nine Knights’ game model conforms to Codable so serialization is a breeze.
  4. Present the game scene with the model from Game Center.

For the final step, you must hook this up to listen for the notification to present the game.

Add the following below the other notification observer in didMove(to:):

NotificationCenter.default.addObserver(
  self,
  selector: #selector(presentGame(_:)),
  name: .presentGame,
  object: nil
)

Build and run.

Starting a new multiplayer game.

Great! Now you can see games. Except, you’ll notice that you can play the whole game like in the local mode. To prevent this, you need to add a few more things to the helper class.

Open GameCenterHelper.swift and add the following at the top of the helper class:

var currentMatch: GKTurnBasedMatch?

var canTakeTurnForCurrentMatch: Bool {
  guard let match = currentMatch else {
    return true
  }
  
  return match.isLocalPlayersTurn
}

enum GameCenterHelperError: Error {
  case matchNotFound
}

currentMatch keeps track of the game the player is currently viewing. To determine if the player can place a piece, you use the current match and check if it’s the local player’s turn.

Next, add the following helper methods at the end of the class:

func endTurn(_ model: GameModel, completion: @escaping CompletionBlock) {
  // 1
  guard let match = currentMatch else {
    completion(GameCenterHelperError.matchNotFound)
    return
  }
  
  do {
    match.message = model.messageToDisplay
    
    // 2
    match.endTurn(
      withNextParticipants: match.others,
      turnTimeout: GKExchangeTimeoutDefault,
      match: try JSONEncoder().encode(model),
      completionHandler: completion
    )
  } catch {
    completion(error)
  }
}

func win(completion: @escaping CompletionBlock) {
  guard let match = currentMatch else {
    completion(GameCenterHelperError.matchNotFound)
    return
  }
  
  // 3
  match.currentParticipant?.matchOutcome = .won
  match.others.forEach { other in
    other.matchOutcome = .lost
  }
  
  match.endMatchInTurn(
    withMatch: match.matchData ?? Data(),
    completionHandler: completion
  )
}

Here’s what the important methods do:

  1. Ensure there’s a current match set.
  2. End the turn after serializing the updated game model and modifying the match. Being able to use Codable protocol to handle serialization vastly simplifies this logic.
  3. Handle winning in a manner similar to when a player quits. Update the players’ outcomes are updated and end the game.

Open MenuScene.swift and add the following to the bottom of loadAndDisplay(match:) before presenting the game scene:

GameCenterHelper.helper.currentMatch = match

Inside didMove(to:), after feedbackGenerator.prepare(), add:

GameCenterHelper.helper.currentMatch = nil

Now that you’re reliably managing the current match, you can accurately calculate when it’s the player’s turn.

Open Scenes/GameScene.swift and add this guard statement to the top of handleTouch(_:):

guard !isSendingTurn && GameCenterHelper.helper.canTakeTurnForCurrentMatch else {
    return
}

This statement blocks players’ input when it’s not their turn as well as when they’re sending the turn update to Game Center.

Finally, to start sending the player’s moves, add the following to the bottom of processGameUpdate() inside the else beneath feedbackGenerator.prepare():

isSendingTurn = true

if model.winner != nil {
  GameCenterHelper.helper.win { error in
    defer {
      self.isSendingTurn = false
    }
    
    if let e = error {
      print("Error winning match: \(e.localizedDescription)")
      return
    }
    
    self.returnToMenu()
  }
} else {
  GameCenterHelper.helper.endTurn(model) { error in
    defer {
      self.isSendingTurn = false
    }

    if let e = error {
      print("Error ending turn: \(e.localizedDescription)")
      return
    }
    
    self.returnToMenu()
  }
}

This logic checks if there’s a winner for the game. If there is, it ends the game. Otherwise, the turn ends. It also updates isSendingTurn so the player cannot accidentally, or maliciously, take more than one turn.

Build and run, and enjoy the game:

The turn-based game in its completion.

Congratulations, you can now play Nine Knights with your friends!