Now that you’ve got the methods in place to handle passing the game data back and forth between players, you need to update the rest of the project code to use those methods. You also need to add a way for players to launch a multiplayer match.
To speed up the process, use the Find Navigator to search for the TODO marks, like so:
Now that you know which files need your attention, you’re ready to begin.
Working from the bottom up, select the second TODO listed in the SKScene+SceneManager.swift file. This will open the SKScene+SceneManager.swift file in the Source Editor, with the corresponding TODO highlighted, as shown here:
As you add the necessary code to this file, you’ll receive some warnings and errors. These errors and warnings are expected, so you can ignore them for now. If the errors become too distracting, or you wish to rely on Xcode’s autocomplete functionality, you can scroll up to the top of this file now and import the GameKit framework; otherwise, you’ll add this import later.
The first step is to add a new method to the scene manager that handles loading multiplayer games. Replace the line that reads // TODO: Add code to load Game Center game with the following code:
| func loadGameCenterGame(match: GKTurnBasedMatch) { |
| print("Attempting to load the game scene using Game Center match data.") |
| |
| } |
This is the start of your new method. You’ll call this method when the player launches a multiplayer Game Center game.
Below the print statement, add the following code:
| match.loadMatchData(completionHandler: {(data, error) in |
| |
| }) |
This code calls the loadMatchData(completionHandler:) method on the GKTurnBasedMatch object. This method is responsible for fetching the match data for the match.
Next, inside the completion handler, add the following code:
| // Set current match data |
| GameKitHelper.shared.currentMatch = match |
| |
| // Set up the Game Center data model |
| var gcDataModel: GameCenterData |
|
| // If available, use the match data to set up the model |
| if let data = data { |
| do { |
| gcDataModel = try JSONDecoder().decode(GameCenterData.self, |
| from: data) |
| } catch { |
| gcDataModel = GameCenterData() |
| } |
| } else { |
| gcDataModel = GameCenterData() |
| } |
Here, you’re setting the GameKitHelper.shared.currentMatch property using the match object, and you’re also creating the GameCenterData object either by decoding the existing data or creating new data.
Below the code you just added, and still within the completion handler, add the following code:
| // Set up the players and mark the local player |
| for participant in match.participants { |
| |
| } |
This for-in loop is where you’ll loop through the participants to find and set the local player.
Next, inside the for-in loop, add this code:
| if let player = participant.player { |
| |
| // Create the gc player object |
| let gcPlayer = GameCenterPlayer(playerId: player.gamePlayerID, |
| playerName: player.displayName) |
| |
| // Check if this is the local player |
| if player == GKLocalPlayer.local { |
| gcPlayer.isLocalPlayer = true |
| } |
| |
| // Check for a winner |
| if participant.matchOutcome == .won { |
| gcPlayer.isWinner = true |
| } |
| |
| // Add gc player to the gc model |
| gcDataModel.addPlayer(gcPlayer) |
| } |
Here, you’re setting up the players of the game and marking some key properties, like isLocalPlayer and isWinner.
Finally, outside the for-in loop, but still inside the completion handler, add the following code:
| // Load the game scene |
| self.loadGameScene(gameType: .remoteMatch, |
| matchData: gcDataModel, matchID: match.matchID) |
At this point, you’ll get another error about “extra arguments at positions #2, #3 in call.” You’ll take care of that error in a moment. In the meantime—and for reference—the entire method you just added looks like this:
| func loadGameCenterGame(match: GKTurnBasedMatch) { |
| print("Attempting to load the game scene using Game Center match data.") |
| |
| match.loadMatchData(completionHandler: {(data, error) in |
| // Set current match data |
| GameKitHelper.shared.currentMatch = match |
| |
| // Set up the Game Center data model |
| var gcDataModel: GameCenterData |
| |
| // If available, use the match data to set up the model |
| if let data = data { |
| do { |
| gcDataModel = try JSONDecoder().decode(GameCenterData.self, |
| from: data) |
| } catch { |
| gcDataModel = GameCenterData() |
| } |
| } else { |
| gcDataModel = GameCenterData() |
| } |
| |
| // Set up the players and mark the local player |
| for participant in match.participants { |
| if let player = participant.player { |
| |
| // Create the gc player object |
| let gcPlayer = GameCenterPlayer(playerId: player.gamePlayerID, |
| playerName: player.displayName) |
| |
| // Check if this is the local player |
| if player == GKLocalPlayer.local { |
| gcPlayer.isLocalPlayer = true |
| } |
| |
| // Check for a winner |
| if participant.matchOutcome == .won { |
| gcPlayer.isWinner = true |
| } |
|
| // Add gc player to the gc model |
| gcDataModel.addPlayer(gcPlayer) |
| } |
| } |
| |
| // Load the game scene |
| self.loadGameScene(gameType: .remoteMatch, |
| matchData: gcDataModel, matchID: match.matchID) |
| }) |
| } |
All right, time to fix the error. Do you see that little red X in the error? It looks something like this:
Click that little red X and you’re taken to the top of the file where you’ll see something like this:
What’s happening here is that you’re passing in arguments that don’t yet exist. To fix this problem, you need to change the method signature for func loadGameScene(gameType: GameType) { to:
| func loadGameScene(gameType: GameType, |
| matchData: GameCenterData? = nil, |
| matchID: String? = nil) { |
While you’re here, if you didn’t already add the import statement for the GameKit framework, you can add it now just below the line that reads import GameplayKit, like so:
| import GameKit |
Now, scroll down to the next TODO that reads // TODO: Add code to populate Game Center match data and replace it with the following two lines of code:
| sceneNode.gameCenterData = matchData |
| sceneNode.gameCenterMatchID = matchID |
You’ll get another two errors about the GameScene class missing these two “members,” but you’ll take care of fixing that problem momentarily. For now, save the file and move on to the next batch of TODOs in the Find Navigator panel.
If you’d like, you can refresh the search results so that you’re looking only at the remaining TODOs.
For now, skip the GameScene.swift file TODOs and focus on the TODOs in the LobbyScene.swift file; you’ll find them in the didMove(to:) method.
Once again, you can either disregard the errors you’ll get as you add the new code, or you can scroll up to the top of the LobbyScene.swift file and import the GameKit framework. Either way, you’ll be reminded to import the framework later in this section.
In the didMove(to:) method, you’ll see both of the TODOs in this file. Replace those TODOs with the following code:
| // Reset current match data |
| GameKitHelper.shared.currentMatch = nil |
| |
| // Set up GC Remote Game notification |
| NotificationCenter.default.addObserver( |
| self, |
| selector: #selector(self.processGameCenterRequest), |
| name: .receivedTurnEvent, object: nil) |
You’ll get another error about a missing member, which will look something like this:
To fix this “missing member” error, add a new method below the didMove(to:) method:
| @objc func processGameCenterRequest(_ notification: Notification) { |
| guard let match = notification.object as? GKTurnBasedMatch else { |
| return |
| } |
| |
| loadGameCenterGame(match: match) |
| } |
And, of course, to fix the error related to the missing GKTurnBasedMatch type, import the GameKit framework, if you haven’t done so already.
Save the file and get ready to resolve the final list of TODOs in the GameScene.swift file.
There are six TODOs in the GameScene.swift file. This time, before you get started, import the GameKit framework:
| import GameKit |
Taking each TODO one at a time, replace the line that reads // TODO: Add Game Center-related properties with the following code:
| // Multiplayer game properties |
| var gameCenterMatchID: String? |
| var gameCenterData: GameCenterData? |
You’ll use these properties to store the Game Center match data. Adding these properties also resolve the error your were getting in the SKScene+SceneManager.swift file.
Next, replace the line that reads // TODO: Add Game Center Observers, with the following code:
| // Set up GC Remote Game notification |
| NotificationCenter.default.addObserver( |
| self, |
| selector: #selector(self.processGameCenterRequest), |
| name: .receivedTurnEvent, object: nil) |
You’ll receive the following error:
To fix that error, replace the line that reads // TODO: Add Game Center Notification Handlers with the following code:
| @objc func processGameCenterRequest(_ notification: Notification) { |
| guard let match = notification.object as? GKTurnBasedMatch else { |
| return |
| } |
| |
| if gameCenterMatchID == match.matchID { |
| loadGameCenterGame(match: match) |
| } else { |
| print("Player is playing a different game.") |
| } |
| } |
All right, on to the next TODO.
In the setupRemoteGame() method, replace the line that reads // TODO: Add code to set up the game with the following code:
| // Check if it's the local player's turn |
| if GameKitHelper.shared.canTakeTurn() == false { |
| print("It's the remote player's turn.") |
| |
| // Move play to the next player in the game model |
| gameModel.nextPlayer() |
| |
| // Visually disable pass and roll buttons |
| rollButton.texture = rollButtonTextureDisabled |
| passButton.texture = passButtonTextureDisabled |
| } |
This code first verifies that it’s not the local player’s turn, and if that’s the case, it moves play to the next (remote) player; otherwise, no action is needed.
Next, you need to update the players’ scores. Starting with the local player, add the following code below the code you just added:
| // Update local player scoreboard |
| if let localPlayer = gameCenterData?.getLocalPlayer() { |
| gameModel.players[0].totalPoints = localPlayer.totalPoints |
| if localPlayer.isWinner == true { |
| gameModel.currentPlayerIndex = 0 |
| endGame() |
| } |
| } |
Now, the remote player:
| // Update remote player scoreboard |
| if let remotePlayer = gameCenterData?.getRemotePlayer() { |
| gameModel.players[1].totalPoints = remotePlayer.totalPoints |
| if remotePlayer.isWinner == true { |
| gameModel.currentPlayerIndex = 1 |
| endGame() |
| } |
| } |
Moving right along to the next TODO in the processEndTurnForRemoteGame() method, find the line that reads // TODO: Add code to end the turn, and replace it with this:
| // Update the local player's stats |
| if let localPlayer = gameCenterData?.getLocalPlayer() { |
| if let index = gameCenterData?.getPlayerIndex(for: localPlayer) { |
| gameCenterData?.players[index].totalPoints = |
| gameModel.players[0].totalPoints |
| } |
| } |
| |
| // Update the remote player's stats |
| if let remotePlayer = gameCenterData?.getRemotePlayer() { |
| if let index = gameCenterData?.getPlayerIndex(for: remotePlayer) { |
| gameCenterData?.players[index].totalPoints = |
| gameModel.players[1].totalPoints |
| } |
| } |
| |
| // End the turn and send the data |
| GameKitHelper.shared.endTurn(gameCenterData!) |
| |
| // Switch back to the remote player? Yes. |
| if GameKitHelper.shared.canTakeTurn() == false { |
| // Visually disable pass and roll buttons |
| rollButton.texture = rollButtonTextureDisabled |
| passButton.texture = passButtonTextureDisabled |
| } |
Finally, you need to add the code to the endGame() method to clear up the last TODO. Find the line that reads // TODO: Add code to end Game Center game, and replace it with the following code:
| if gameType == .remoteMatch && isWinner == true { |
| GameKitHelper.shared.winGame(gameCenterData!) |
| } else if gameType == .remoteMatch && isWinner == false { |
| GameKitHelper.shared.lostGame(gameCenterData!) |
| } |
Fantastic, you have everything in place, and you’re ready to test the game.