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.

Version

  • Swift 4.2, iOS 12, Xcode 10

In September 2010, Apple released the GameKit framework, which allows developers to create social multiplayer games without needing to develop a back end. Since its initial release, Apple has added turn-based gaming, player photos, friend suggestions, achievement points and challenges. Although Apple has since scaled back Game Center and its integration with iOS, many games still use some of its features.

In this tutorial, you’ll add a turn-based multiplayer system to an existing game. 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 Game Center.

The game used in this tutorial is a Nine Men’s Morris clone named Nine Knights. Nine Men’s Morris is a simple strategy game dating back to the Roman Empire. There are three phases of the game: placing, moving and flying. The average length of a game is 5-30 minutes depending on the players’ skills.

Getting Started

Start by downloading the starter project using the Download Materials link at the top or bottom of this tutorial. After unzipping the downloaded materials, open Nine Knights.xcodeproj.

The sample project is a complete local multiplayer game. Build and run to see the menu scene. It contains a single play button.

A menu scene with a single play button

The main sections of the game that you’ll be working with are Models/GameModel.swift and the scenes in the Scenes group. The game model represents the game’s state and determines the gameplay logic, such as move validation. The menu scene takes care of presenting the game scene for now. Finally, the game scene is where all of the player facing gameplay happens.

Connecting To Game Center

Playing against yourself is fun, but it’s time to get connected with other people. Before you can use Game Center, you have to configure some things in the project and App Store Connect.

To get started, open the project file in Xcode. With the Nine Knights target selected, choose a unique bundle identifier under the Identity group.

editing the bundle identifier

Next, select the Capabilities tab. You’ll need to turn on the Game Center switch and the Push Notifications switch.

Enabling Game Center and push notifications via the capabilities tab

Enabling the Game Center capability is obvious, but you enable the Push Notifications capability so that the game can receive notifications when it’s the player’s turn. Believe it or not, that’s all of the configuration you must do in the project.

Next, head over to appstoreconnect.apple.com. After logging in, select My Apps and create a new app by selecting the plus button in the top left. Fill out the information in the form. Make sure to select iOS as the platform, and choose the Bundle ID that you configured in Xcode.

Note: If you don’t see the Bundle ID you registered using Xcode, you can create one manually in the developer portal.

Creating a new app in appstoreconnect.

After App Store Connect creates the app, select it. Then, navigate to the Features tab and select Game Center.

Select the plus button next to the Leaderboard section title to create a new leaderboard.

Note: This step may seem odd since this tutorial doesn’t cover leaderboards with Game Center. However, without this step, Game Center won’t recognize the game when attempting to authenticate.The game is not recognized by Game Center.

Select the Single Leaderboard option and fill out the next form however you want. Since this is a hack to get Game Center working correctly, the information used to fill out the leaderboard isn’t important. Make sure to add a language as well so App Store Connect can create the leaderboard.

Creating a leaderboard.

Once that’s done, select the save button, and you should now have a leaderboard configured with the app. Game Center authentication is now ready for you to use!

Authenticating with Game Center

Now that you have an app configured for Game Center, it’s time to use it. For Nine Knights, it makes the most sense to authenticate to Game Center right away. In some games, it might be more appropriate to wait to authenticate until the player indicates a desire to play online.

You’ll use a helper class to consolidate the logic for Game Center. That’s where you’ll add the logic to authenticate.

To get started, open Helpers/GameCenterHelper.swift and add the following properties below the CompletionBlock declaration:

// 1
static let helper = GameCenterHelper()

// 2
var viewController: UIViewController?

Here’s how it works:

  1. You use a singleton to easily provide access to the Game Center logic from anywhere in the app.
  2. You use this view controller when you present the Game Center authentication view controller.

You want to attempt Game Center authentication when you first create this helper class. Add the following override to accomplish this:

override init() {
  super.init()
  
  GKLocalPlayer.local.authenticateHandler = { gcAuthVC, error in
    if GKLocalPlayer.local.isAuthenticated {
      print("Authenticated to Game Center!")
    } else if let vc = gcAuthVC {
      self.viewController?.present(vc, animated: true)
    }
    else {
      print("Error authentication to GameCenter: " +
        "\(error?.localizedDescription ?? "none")")
    }
  }
}

If this API is confusing to you, don’t worry. It’s not normal to kick off important logic like authenticating as a result of setting a variable. Even though this may seem strange, it’s reasonably simple to use.

You can determine if the user successfully authenticates in your completion block by evaluating GKLocalPlayer.local.isAuthenticated. If the user needs to sign in to Game Center, iOS passes a view controller in the completion block for you to present. Finally, if anything goes wrong, you can handle the error that is also returned.

To see this in action, add the following to the bottom of viewDidLoad() in ViewControllers/GameViewController.swift:

GameCenterHelper.helper.viewController = self

Build and run to see this in action.

The Sign In view for Game Center.

After logging in using your Apple ID, you’ll see the familiar success banner along with a message in the console.

Successfully signed into Game Center

Authenticating to Game Center is straightforward. It primarily consists of setting the authenticateHandler variable and Game Center takes care of most of the logic for you.

Telling the App About Authentication

Right now, Game Center knows if the player is logged in or not, but the rest of the app does not. You’ll use NotificationCenter to alert other parts of the app when the player authenticates.

To accomplish this, add the following to the bottom of GameCenterHelper.swift:

extension Notification.Name {
  static let presentGame = Notification.Name(rawValue: "presentGame")
  static let authenticationChanged = Notification.Name(rawValue: "authenticationChanged")
}

This helper uses a few notifications, and here’s what they’ll do:

  1. presentGame notifies the menu scene to present an online game.
  2. authenticationChanged notifies the app of any authentication state changes.

To use the authentication notification, add the following to the top of the authenticateHandler block:

NotificationCenter.default
  .post(name: .authenticationChanged, object: GKLocalPlayer.local.isAuthenticated)

To use this notification, open Scenes/MenuScene.swift and add the following below the declaration of the localButton:

private var onlineButton: ButtonNode!

Additionally, setup the onlineButton at the end of the setUpScene(in:) method:

onlineButton = ButtonNode("Online Game", size: buttonSize) {
  print("Pressed the online button.")
}
onlineButton.isEnabled = false
runningYOffset -= sceneMargin + buttonSize.height
onlineButton.position = CGPoint(x: sceneMargin, y: runningYOffset)
addChild(onlineButton)

This button node’s configuration is similar to the local button, but it’s initially disabled until the player is authenticated.

Build and run.

The Game’s menu with a disabled Online Game button.

To update this button’s state when then player authenticates to Game Center, you’ll need to listen for the notification that the helper class fires.

To do so, add the following to the bottom of the MenuSceneclass:

// MARK: - Notifications

@objc private func authenticationChanged(_ notification: Notification) {
  onlineButton.isEnabled = notification.object as? Bool ?? false
}

Then, add the observer in didMove(to:):

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

Build and run.

The Online Game button becomes enabled when you sign into Game Center.

Great, now the online button shows the correct state.

There’s an issue with the button if you leave the menu scene. Starting a local game and then returning to the menu causes the button to become disabled again.

To fix this, open GameCenterHelper.swift and add the following computed property near the top of GameCenterHelper:

static var isAuthenticated: Bool {
  return GKLocalPlayer.local.isAuthenticated
}

Now, back in MenuScene.swift and, at the end of setUpScene(in:), instead of initially disabling the online button, check the current state like so:

onlineButton.isEnabled = GameCenterHelper.isAuthenticated

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!

Where to Go From Here?

You now have a game that can authenticate to Game Center, manage matches and send turns back and forth between players. Once you understand the pieces of a turn-based game with Game Center, you can apply these concepts to other iOS or macOS games.

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

If you’re interested in adding more features to this game, you could add a way to indicate in the app that there’s a new turn. You’ll receive notifications letting you know there’s a new turn, but in the app Game Center notifications don’t show.

Check out this tutorial on Artificial Intelligence with GameplayKit if you’re interested in adding AI to the game to play solo against the computer.

You can also check out the official Apple Documentation about GameKit to see what else it has to offer.

I hope you enjoyed this tutorial! If you have any questions or comments, please join the discussion below!

Add a rating for this content

Contributors

Comments