How To Make A Simple Multiplayer Game with Game Center Tutorial:
Part 2/2
If you're new here, you may want to subscribe to my RSS feed
or follow me on Twitter
. Thanks for visiting!
This is the second part of a two part tutorial series on creating a
simple networked game with Game Center matchmaking.
In the first
part of the tutorial series
, you learned how to enable Game Center
for your app and find matches with other players using the built-in
GKMatchmakerViewController.
In this second and final part of the tutorial series, you’ll learn
how to look up player aliases, how to send and receive data between
games, and how to support invites.
In the end, you’ll have a fully functional (but simple) networked
game that uses Cocos2D and Game Center that you can play with your
friends!
If you haven’t got it already, start by downloading the sample
project
where we left it off in the last tutorial.
Network Code Strategy: Picking Players
Before we pick things back up, let’s talk about the strategy we’re
going to take with our network code, starting with how to pick players.
We have two players in this game: player 1 (the dog) and player 2
(the kid). The problem is, how do we decide who should be the dog and
which the kid?
The strategy we’re going to take is each side will generate a random
number on startup, and send it to the other side. Whichever side has
the larger random number will be player 1, and the other side will be
player 2.
In the rare event each side generates the same random number, we’ll
just try again.
Whichever player is player 1 will get some special “privileges.”
First, that side signals the game should start by sending a message to
the other side. That side also is the one who is responsible for
checking when the game ends, and sending a message to the other side
when the game is over (and who won).
In other words, “player 1″ takes the role of the “server”. He’s the
final authority on what goes on for this game.
Network Code Stragegy: Player Aliases
Since it’s random which player is the dog and which is the kid, we
need to have some way of showing the player which one he is.
For this game, we’ll just print the player’s alias right on top of
the character they are, so they can tell who they are.
If you don’t know what a player alias is, it’s the nickname that you
set up for your account in Game Center. When a match is made, you don’t
get these automatically – you have to call a method so Game Center will
send them to you.
Network Code Strategy: Game States
One of the challenges with making networked code is that things can
happen in different orders than you might expect.
For example, one side may finish initializing the match and send the
random number to the other side before the other side even finishes
initializing the match!
So if we’re not careful, we can get into strange timing issues in our
code that causes problems. One good way to keep things clean is to
keep careful track of what state each side of the game is currently in.
The following diagram illustrates the states Cat Race will need:
Let’s go through each of these:
- Waiting for Match
: The game is waiting for a match to be
connected, and for the player aliases to be looked up. When both of
these are complete, it checks to see if it’s received the random number
from the other side yet, and advances to the next state appropriately. - Waiting for Random #
: The game has a match and has the
player aliases, but is still waiting to receive a random number from the
other side. - Waiting for Start
: The game is waiting for the other side
to start the game (this occurs when the game is player 2). - Active
: The game is currently playing – no winner has been
found yet. Every time a player moves, they will tell the other side
that they’ve moved by sending a message. - Done
: The game is over (player 1 has sent a message to
player 2 to tell him that the game is over). No more messages are sent
and the playre can then restart the game.
OK now that you have a plan in mind, let’s move onto the first step,
which is retrieving the player aliases!
Looking up Player Aliases
Switch to GCHelper.h and make the following changes:
// Add inside @interface |
This defines an instance variable and a property for a dictionary
that will allow easy lookup of GKPlayer data (which includes the
player’s alias) based on a player’s unique ID.
Then switch to GCHelper.m and make the following changes:
// Add to top of file |
The main logic occurs inside lookupPlayers. This is called when the
match is ready, and it looks up the info for all of the players
currently in the match (except for the local player – we already have
that info via the GKLocalPlayer singleton!)
Game Center will return a GKPlayer object for each player in the
match as a result. To make things easier to access later, this code
puts each GKPlayer object in a dictionary, keyed by player ID.
Finally, it marks the match as started and calls the game’s delegate
so it can start the game.
Before we move onto that though, test it out! Compile and run your
code on two devices, and this time when you examine your console output
you should see the players looked up and the game ready to go:
CatRace[16918:207] Authentication changed: player authenticated.
CatRace[16918:207] Player connected!
CatRace[16918:207] Ready to start match!
CatRace[16918:207] Looking up 1 players...
CatRace[16918:207] Found player: Vickipsq
CatRace[16918:207] Match started
Adding Network Code
You have the match set up and the player names available, so now
you’re on to the real meat of the project – adding the network code!
The first thing you need to do is define a few new game states based
on our diagram from earlier. Open up HelloWorldLayer.h and modify your
GameState structure to be the following:
typedef enum { |
You also need to add a new reason the game can end – disconnection.
So modify the EndReason enum as follows:
typedef enum { |
Next you need to define some structures for the messages you’ll be
sending back and forth. So add the following to the top of
HelloWorldLayer.h:
typedef enum { |
Note that each message starts with a message type – this is so that
you have a known number you can look at for each message to identify
what type of message it is.
Finally add a few more instance variables to the HelloWorldLayer
class:
uint32_t ourRandom; |
These will keep track of the random number for this device, whether
we’re received the random number from the other side, and the other
player’s player ID.
OK, now let’s start implementing the networking code. Switch to
HelloWorldLayer.m and modify the matchStarted method as follows:
- ( void ) matchStarted { |
So when the match starts up, we check to see whether we’ve already
received the random number (which can happen due to timing), and set the
state appropriately. Then it calls methods (which you’re about to
write) to send the random number and maybe start the game.
Let’s start with sendRandomNumber. Make the following changes to
HelloWorldLayer.m:
// Add at bottom of init, anbd comment out previous call to setGameState |
sendRandomNumber creates a new MessageRandomNumber structure, and
sets the random number to a random number that was generated in init.
It then converts the structure into an NSData to send to the other side.
sendData calls the sendDataToAllPlayers method in the GCHelper’s
match object to send the packet to the other side.
Next implement the tryStartGame method you referred to earlier. Make
the following changes to HelloWorldLayer.m:
// Add right after sendRandomNumber |
This is quite simiple – if it’s player 1 (who has the special
privilege of acting like the “server”) and the game is ready to go, it
sets the game to active, and tells the other side to do the same by
sending a MessageGameBegin to the other side.
OK – now for the code that handles receiving messages from the other
side. Modify your match:didReceiveData:fromPlayer method to be the
following:
- ( void ) match: ( GKMatch * ) match didReceiveData: ( NSData * ) data fromPlayer: ( NSString * ) playerID { |
This method casts the incoming data as a Message structure (which
always works, because we’ve set up each structure to begin with a
Message structure). It can then look at the message type to see which
type of message it actually is.
- For the MessageRandomNumber
case, it checks to see if it’s
player 1 or player 2 based on the random number, and advances to the
next state if it was waiting for the random number (and also possibly
starts the game up if it’s player 1). - For the MessageGameBegin
case, it just switches the game to
active, since this means that player 1 has just sent a begin message to
player 2. - For the MessageMove
case, it moves the other player forward
a bit. - For the MessageGameOver
case, it ends the scene based on
the appropriate reason.
You’re almost done! You have most of the game logic in place, you
just have a few finishing tidbits to add here and there. Make the
following changes to HelloWorldLayer.m next:
// Modify setGameState as follows |
The above code is pretty simple (and commented to tell you what it
does) so we won’t dwell on it further here.
Phew! That was a lot of code, but the good news is it’s DONE!
Compile and run your code on two devices, and you should be able to have
a complete race!
Displaying Player Names
At this point you have a functional game, but it’s kinda hard to tell
which player you are, especially since it’s random.
So let’s fix this by displaying the player names for each player
above their character. Make the following changes to HelloWorldLayer.h:
CCLabelBMFont * player1Label; |
Then switch to HelloWorldLayer.m and add a new method to create the
labels right below tryStartGame:
- ( void ) setupStringsWithOtherPlayerId: ( NSString * ) playerID { |
This creates two CCLabelBMFonts, one for each character. To get the
alias for the local player, it can get it by using the GKLocalPlayer
singleton. For the other player, it has to look up the GKPlayer object
from the GCHelper’s dictionary first.
Next, make some final changes to HelloWorldLayer.m:
// Inside if statement for tryStartGame |
Basically when the game is transtioning to the Active state, we know
for sure we’ve received the player names and the game is starting, so
that’s a good time to set up the labels.
In the update method, each frame it updates the position of the label
to the corresponding player’s position.
You might think that’s a bit weird – why not simply add the labels as
a child of the sprite instead? Well we can’t do this because the
sprites are children of a batch node (hence they can only have other
sprites from the same sprite sheet as children). So this is an
alternative.
Compile and run your code on two devices, and now you should see the
label of each player’s name on top of their character!
Supporting Invites
We have a fully functional game, but let’s put in one more feature
for fun and coolness – supporting invites.
You may have noticed when the matchmaker view controller appears, it
gives you an option to invite a friend. Right now this wouldn’t work
because we haven’t added the code for it yet, but it’s pretty simple so
let’s put it in there.
Open up GCHelper.h and make the following changes:
// Add inside GCHelperDelegate |
This creates instance variables and properties to keep track of some
invite information you’ll see in a bit. It also adds a new method to
the GCHelperDelegate to tell it when an invite is received.
Next switch to GCHelper.m and make the following changes:
// At top of file |
The way invites work in Game Center is that you have to register a
callback block to be called when an invite is received. You should
register this block as soon as possible after your game starts up – the
recommended time is right after a user is authenticated.
For this game, the callback squirrels away the invite info, then
notifies the delegate. The delegate will then restart the Cocos2D scene
and try to find a match by calling the findMatchWithMinPlayers method –
we’ll modify this to take into account any pending invite info.
So modify the findMatchWithMinPlayers method as follows:
- ( void ) findMatchWithMinPlayers: ( int ) minPlayers maxPlayers: ( int ) maxPlayers viewController: ( UIViewController * ) viewController delegate: ( id<GCHelperDelegate>) theDelegate { |
It’s quite similar to last time, except it uses the pendingInvite and
pendingPlayersToInvite values if they exist, when creating the
GKMatchmakerViewController.
Finally, switch to HelloWorldLayer.m and implement inviteReceived as
the following:
- ( void ) inviteReceived { |
And that’s it! Compile and run your code on two devices, and when
one of them starts up, use the GKMatchmakerViewController to send an
invite to the other. You should receive a popup like this:
Tap Accept”, then “Play Now” on the original device, and you should
be able to play the game as usual – but this time with a friend of your
choice! :]
Where To Go From Here?
Here is a sample
project
with the completed Cocos2D and Game Center multiplayer
networked game we created in this tutorial series.
If you’re implementing Game Center in your games, you might want to
add Leaderboards and Achievements too. If you’re interested in learning
about this, check out the chapter on Game Center in Rod’s and my
upcoming Cocos2D
book
!
If there’s something else about Game Center you’re interested in
learning about (such as voice chat, or server-hosted games so you can
implement MMOs and the like), please feel free to suggest
a tutorial
! Also, don’t forget to vote for what tutorial you’d
like to see next in the sidebar.
If you have any questions, comments, or tips for using Game Center
that you’ve learned while making your apps, please join in the forum
discussion below!