Adding turn-based multiplayer to your iOS 5 game (Part 1: overview)
I recently released a new version of my digital board game New World Colony to include support for the turn-based online matchmaking feature in iOS 5. I learned a lot, and am really excited about the potential for this feature because it allows lesser-known titles to have robust multiplayer. I’d like to share my experiences from this process in order to help other developers implement this feature in their own games.
First of all, why implement this feature? The biggest reason is that it greatly expands the use that players can get out of your game. Although multiplayer has been available in iOS for a while, until now it was only possible to create real-time matches (without creating your own game server, that is). This meant that players needed to be looking for matches at exactly the same time, and for games with small audience, it’s almost impossible to reach a critical mass where enough players are online to form an active community. Turn-based matches allow players to start a match whenever they want, and they will be joined by an opponent at some future point in time. Play can pass back and forth without the need for both players to be playing at the exact same time.
The other great reason is that players are probably hesitant to start an online match if they don’t have a huge amount of free time - they don’t want to have to quit in the middle of a game. However, with turn-based matches, this allows players to pick up a game, play a turn in about a minute, and set it back down. So it greatly lowers the time commitment for any single play session, and this is a huge bonus for mobile games.
Ok, so how do you add this to your game? Here’s the high-level details. I’ll go into each of these steps in a bit more detail in later posts.
The very first step is to authenticate your player with [GKLocalPlayer localPlayer] authenticateWithCompletionHandler:]. As soon as the player is successfully authenticated, you should set the delegate for the [GKTurnBasedEventHandler sharedTurnBasedEventHandler]. Doing so will allow your device to receive updates such as when a match ended or the turn changed from player to player.
Creating a Matchmaker View
At this point you’ll have to make a pretty important decision. You’ll need a way to present users a list of all the games they have in progress. Do you want to use the built-in matchmaker view controller, or build your own? There are some significant pros and cons.
Using the out-of-the-box matchmaker saves several steps up front, and updates automatically as soon as a turn event is received which is very nice. However, it also allows users to quit out of turn and erase matches at will, which create quite a large headache because of all the new edge cases you’ll need to take care of, and it will make tracking stats and achievements much more difficult (more on this later). Building your own games-in-progress view gives you better control over these events, but obviously requires more work because you need to download all the match data and format a view controller yourself.
For New World Colony, I used the out-of-the-box matchmaker view to present games in progress. However, if I were to start again from scratch, I probably would have created my own view because it can eliminate many of those annoying edge cases that can wreak havoc with your game logic.
Setting up a new game
The GKMatchmakerViewController optionally displays games in progress, but it can also let users create new games. For simplicity, I would definitely recommend using the GKMatchmakerViewController to do this rather than creating a custom solution. It saves you lots of steps, and very nicely also includes a place for users to rate the game (which as you may know can be a challenge otherwise). Players can set the number of participants, and invite other players as well. You get a lot of functionality for ‘free’.
Loading match data
Assuming you’re using the GKMatchmakerViewController - once a player selects an in-progress match, or starts a new match, your GKMatchmakerViewController Delegate will receive a ‘didFindMatch’ notification and hand off a GKTurnBasedMatch object. You should never create GKTurnBasedMatch objects yourself - instead, matches will be handed to back to you from the matchmaker. You can also call [GKTurnBasedMatch loadMatchesWithCompletionHandler:] to retrieve an array of matches.
Once you’ve got your match, retain it, and then load its data by calling loadMatchDataWithCompletionHandler on it. This will return the all-important matchData property of your GKTurnBasedMatch. Without this, your GKTurnBasedMatch will not have any data and you can not restore its state properly.
On brand new games, the ‘participants’ property of your GKTurnBasedMatch will contain the current player, and several placeholder participants that have a status of ‘matching’. That means they’ll be filled with real players later on when they join up.
Now that you’ve got your match data you can use it to recreate the game state. If the [matchData length] is equal to 0, then it means you’ve got a new match and you can create a new game state.
Note that when a player creates a new game, they may actually be joined up into an existing match that is waiting for an opponent. So even if the user is creating a new game, you may actually have existing match data that you need to process.
Ending the turn
Ok, the player has taken some action and it’s time to end the turn. It’s fairly easy to pass the turn to the next player. Simply call endTurnWithNextParticipant:matchData on your currently retained GKTurnBasedMatch. You need to tell the match who will be the next participant, and what the game state data will be within matchData. Be warned that the match data has some size restrictions on it, so you need to carefully compress your match data. I’ll offer some tips on that later on as well.
If the player had invited a friend during the initial match creation, this is when that player would receive the invitation - not when the match is first created. Also note that if any player declines an invitation, the entire match is cancelled immediately.
Notifying users of their turn
It’s important for other users to know when their turn is up. Remember the GKTurnBasedEventHandlerDelegate that you set up earlier? Now’s when that comes into play. All players (except the one who just ended the turn) will receive a notification through this delegate via the method handleTurnEventForMatch:(GKTurnBasedMatch *)match.
It’s up to you to determine if it’s actually the local player’s turn on each device. Fortunately, it’s easy to do this by looking at the currentParticipant property of the match you were just handed. If the currentParticipants’s player ID matches the player ID of the locally authenticated user, then you can notify the local player that it’s their turn - preferably using the handy GKNotificationBanner class.
Ending the game
On the surface, this is fairly simple. When a player meets a certain victory condition, you can call endMatchInTurnWithMatchData: on the currently retained GKTurnBasedMatch. All you need to do is update all the match participants’ matchOutcome properties (so we know who won or lost), and send the new match data. It’ll notify the other users that the game is over, and that’s that.
But this has the potential to get extremely complicated. For instance, what if a player surrendered? Or the game finished due to hitting a turn limit and you want to ensure the winning player gets notified? When and how do you adjust stats and achievements? These complexities will be discussed in a further post as well.
So that’s a high-level overview of how your game state needs to flow. It’s fairly easy to get a basic game going, but as I hinted at, there are some subtle complexities that can create a lot more work for you.
I’ll follow-up in the next few days about some specific implementation guidelines for what I’ve covered above, and will provide some source code for you to hack at yourself.
If anything is unclear, please let me know in the comments and I’ll either update the post or address it in a future post.