GameKit Real Time Multiplayer Tutorial

Geovana Contine
Academy@EldoradoCPS
8 min readJul 3, 2020

The GameKit framework provides a number of features for game development, such as Leaderboards, Achievements and Multiplayer. The latter, although extremely practical for small teams, is not as famous as the others, a fact that reflects the advanced age of the tutorials available.

With the changes that WWDC20 brought to the Game Center and the consequent expectation that the use of the platform will increase, this tutorial aims to present a basic implementation of a real time multiplayer game.

The following steps will be explored in this article:

  • How to add Game Center to your project
  • How to authenticate a player in Game Center
  • How to use the matchmaking tool
  • How a multiplayer match works
  • Creating the Game Model
  • How to send / receive data between players

At the end of these steps we will result in a game of two players, each controlling a character, where they can perform an action that is reflected on the other’s screen. Additionally, there is a time counter synchronized between the two.

The final code is available at: https://github.com/pedrocontine/GameKitMultiplayerTutorial

Important: Download the material! During the article I will comment on some important functions, but I will not go into all the details of the implementation. And of course, two devices will be needed to test the project, be they simulators or real ones.

How to add Game Center

Click on the project target -> Signing & Capabilities -> Game Center.

Then go to the App Store Connect and add a new app. In the Features tab, you can already see that the Game Center has been added. Theoretically this alone would be enough, but sometimes it is necessary to create any Leaderboard for your app to be recognized. Just in case, create a temporary one to avoid problems.

If you have any questions in this step, this other more detailed tutorial can help you.

Authentication

To create an online match, players must be connected to the Game Center. Within the GameCenterHelper class we have the authenticatePlayer method, which will check if the player is connected and, if not, return a standard login controller to display. We don’t need to do anything else, the login step is entirely internal to the Game Center. It is recommended to perform this authentication as soon as possible within your application, preferably on the AppDelegate. Once authenticated, we can access all available information about the player through GKLocalPlayer.local.

Login View Controller

Matchmaker

As well as authentication, in our GameCenterHelper we also have the presentMatchmaker method, which allows us to pass on information on how many players we will have in the match, as well as a message that will be displayed when an invitation is sent to a friend. This also returns us with a standard Game Center View Controller, which allows the user to invite friends and find random opponents automatically, all without having to do anything at all.

extension GameCenterHelper: GKMatchmakerViewControllerDelegate {
func matchmakerViewController(_ viewController: GKMatchmakerViewController, didFind match: GKMatch) {
viewController.dismiss(animated: true)
delegate?.presentGame(match: match)
}
}

Finally, through GKMatchmakerViewControllerDelegate, we receive a GKMatch when a match is created.

Matchmaker View Controller

How a multiplayer match works

Until now all of our code was concentrated in the GameCenterHelper, implemented by MenuViewController, precisely because they are mandatory steps and our only job is to display the screens that Game Center already makes available to us. However, now that we have generated a match, we will pass it on to our GameViewController, which will be responsible for managing our game.

First of all, we need to understand the logic of a multiplayer game. We have an object that will store all the essential information for the game, such as players’ lives, game time, etc. Each player will have an object like this and their screen will load according to that information. For everyone to see the same things, all objects must be the same.

Each action the player takes, such as attacking or moving, will only change the information of his object and, consequently, his own game. Therefore, the player sends this object with the changes to everyone else, and they replace the ones they already had with the one they received. Since every change in that object will update the screen, everyone will see the same things. So, basically the essence of our game will be to send and receive this object.

Game Model

Our object to be shared is this struct that is called GameModel. It contains the remaining time and a list of players, who have a name, a state of animation and the remaining life. As stated earlier, you can put what you need in this model, the only requirement is that it can be converted to the Data type and therefore it must be Codable.

import Foundation
import UIKit
import GameKit

struct GameModel: Codable {
var players: [Player] = []
var time: Int = 60
}

extension GameModel {
func encode() -> Data? {
return try? JSONEncoder().encode(self)
}

static func decode(data: Data) -> GameModel? {
return try? JSONDecoder().decode(GameModel.self, from: data)
}
}
struct Player: Codable {
var displayName: String
var status: PlayerStatus = .idle
var life: Float = 100
}

Note that all visual elements on our screen that are dynamic are updated according to the values contained in the GameModel. With didSet we guarantee that any changes in our model will call the updateUI function, responsible for updating all game values.

private var gameModel: GameModel! {
didSet {
updateUI()
}
}

Send / Receive data

With our GameModel defined and our entire interface updated from it, it is time to send it to the other players when there is a change. We use the sendData method available in GKMatch, passing the GameModel converted to Data.

private func sendData() {
guard let match = match else { return }

do {
guard let data = gameModel.encode() else { return }
try match.sendData(toAllPlayers: data, with: .reliable)
} catch {
print("Send data failed")
}
}

When this information is received, the didReceive data method of the GKMatchDelegate is triggered and the other players will receive the GameModel. After receiving the new model, just replace the current one with the new one. This is just what is necessary to carry out the exchange of information that we mentioned earlier.

extension GameViewController: GKMatchDelegate {
func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) {
guard let model = GameModel.decode(data: data) else { return }
gameModel = model
}
}

Identify players

We already have users authenticated, within a match and being able to communicate changes to the GameModel. In our example game we have two fighters, each player responsible for controlling only one. To do this, we need to define which connected user will be player1 and which will be player2.

GKMatch has the list of connected players, but unfortunately it contains all players except the local user. The solution I found was to create my own array of players, save it in GameModel and use the index they occupy in the array to define which player they will be. This process is in the savePlayers method.

private func savePlayers() {
guard let player2Name = match?.players.first?.displayName else { return }

let player1 = Player(displayName: GKLocalPlayer.local.displayName)
let player2 = Player(displayName: player2Name)

gameModel.players = [player1, player2]

gameModel.players.sort { (player1, player2) -> Bool in
player1.displayName < player2.displayName
}

sendData()
}

This creates some difficulties such as ensuring that all users generate an identical list, among other complications that you will encounter if you follow the same reasoning. I imagine there is a better way to do this, if you find it leave it in the comments :)

Although it does not seem the ideal solution, with that we are already able to define who will be the blue fighting dinosaur and who will be red. The getLocalPlayerType method allows you to identify which of these two the local player is and consequently change the color of the attack button.

private func getLocalPlayerType() -> PlayerType {
if gameModel.players.first?.displayName == GKLocalPlayer.local.displayName {
return .one
} else {
return .two
}
}

Attack Example

By clicking to attack, we identify who the attacker is and naturally who will be attacked. We change the animation status of the players, we update the life of the attacked player and everything else that is necessary for our combat. Finally, just call sendData to communicate the changes to the other player.

@IBAction func buttonAttackPressed() {
let localPlayer = getLocalPlayerType()

gameModel.players[localPlayer.index()].status = .attack
gameModel.players[localPlayer.enemyIndex()].status = .hit
gameModel.players[localPlayer.enemyIndex()].life -= 10
sendData()

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.gameModel.players[localPlayer.index()].status = .idle
self.gameModel.players[localPlayer.enemyIndex()].status = .idle
self.sendData()
}
}

The attack is an example of a method that both players can call. Realize that the difficulty is to write the code so that it works regardless of who the player is.

Timer Example

Unlike the attack, the timer may not be a good idea to let the two players update. In this example we define that only player1 will start the timer, and whenever 1 second elapses, the new remaining time is sent to the other player. This is an example of an update performed independently, without the need for user action.

private func initTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
let player = self.getLocalPlayerType()
if player == .one, self.gameModel.time >= 1 {
self.gameModel.time -= 1
self.sendData()
}
})
}

What about the lag?

So far everything is beautiful. If you are testing with two simulators, the information update is almost instantaneous, everything works perfectly. But we know that this will hardly always be the case. In a real situation users will experience differences in the quality of the internet connection and these transitions in our GameModel will have a slight delay. This is inevitable and all online games suffer from it, it is the famous lag. However, there are techniques to mitigate the damage from this situation. This is a very interesting article if you want to go deeper.

Game Over

I hope the code provided has helped to understand a little how the dynamics of a multiplayer game works. Explore the GKMatch and matchmaker delegates a little and you will see that there are a lot of extra features that I didn’t comment on, such as voice channel, error handling, etc.

As stated, this is a field where few tutorials are available, so any contribution you want to make here is totally welcome :)

--

--