How To Make a Simple Playing Card Game with Multiplayer and Bluetooth, Part 3

This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on Google+ and Twitter. Welcome back to our monster 7-part tutorial series on creating a multiplayer card game over Bluetooth or Wi-Fi using UIKit! If you are new to this series, check out the […] By Matthijs Hollemans.

Leave a rating/review
Save for later
Share
Create a multiplayer networked card game!

Create a multiplayer networked card game!

This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on and Twitter.

Welcome back to our monster 7-part tutorial series on creating a multiplayer card game over Bluetooth or Wi-Fi using UIKit!

If you are new to this series, check out the introduction first. There you can see a video of the game, and we’ll invite you to our special Reader’s Challenge!

In the first part of the series, you created the main menu and the basics for the host and join game screens.

In second part of the series, you implemented the join/host game logic, complete with elegant disconnection handling.

In this third part of the series, we’ll implement the ability for the client and server to send messages to each other. In addition, we’ll start creating our Game’s model class, the game setup code, and more. Let’s deal back in!

Getting Started (With the Game!)

The game properly starts when the player who hosts the session taps the Start button on the Host Game screen. The server will then send messages to the clients – these are packets of data that will fly over the Bluetooth or Wi-Fi network – that instruct the clients to get ready.

These network packets are received by what is known as a “data receive handler.” You have to give GKSession an object that will handle any incoming packets. It’s a bit like a delegate, but it doesn’t have its own @protocol.

Here’s the trick: the server needs to send the clients a bunch of messages before the game can begin, but you don’t really want to make JoinViewController the data-receive-handler for GKSession. The main game logic will be handled by a data model class named Game, and you want this Game class to be the one to handle the packets from GKSession. Therefore, on the client side, the game can start as soon as the client connects to the server. (If that doesn’t make any sense to you, it will soon.)

Add the following method signature to the MatchmakingClient’s delegate protocol:

- (void)matchmakingClient:(MatchmakingClient *)client didConnectToServer:(NSString *)peerID;

You’ll call this method as soon as the client connects to the server. Add the code to do this in session:peer:didChangeState: in MatchmakingClient.m:

		. . .

		// We're now connected to the server.
		case GKPeerStateConnected:
			if (_clientState == ClientStateConnecting)
			{
				_clientState = ClientStateConnected;
				[self.delegate matchmakingClient:self didConnectToServer:peerID];
			}
			break;

		. . .

That delegate method should be implemented by JoinViewController, so add it there:

- (void)matchmakingClient:(MatchmakingClient *)client didConnectToServer:(NSString *)peerID
{
	NSString *name = [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
	if ([name length] == 0)
		name = _matchmakingClient.session.displayName;

	[self.delegate joinViewController:self startGameWithSession:_matchmakingClient.session playerName:name server:peerID];
}

Here you first get the name of the player from the text field (stripped from any whitespace). Then you call a new delegate method to let MainViewController know it has to start the game for this client.

Add the declaration of this new delegate method to JoinViewControllerDelegate:

- (void)joinViewController:(JoinViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID;

Notice that you pass three important pieces of data to the MainViewController: the GKSession object, which you’ll need to use in the new Game class to communicate with the server; the name of the player; and the peerID of the server. (You could get the server’s peer ID from the GKSession object, but this is just as easy.)

The body of this method goes into MainViewController.m:

- (void)joinViewController:(JoinViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID
{
	_performAnimations = NO;

	[self dismissViewControllerAnimated:NO completion:^
	{
		_performAnimations = YES;

		[self startGameWithBlock:^(Game *game)
		{
			[game startClientGameWithSession:session playerName:name server:peerID];
		}];
	}];
}

What is all that? Let’s begin with the _performAnimations variable. This is a new ivar that needs to be added at the top of the source file:

@implementation MainViewController
{
	. . .
	BOOL _performAnimations;
}

Remember the cool animation from the main screen, with the logo cards flying into the screen and the buttons fading in? That animation happens any time the main screen becomes visible, including when a modally-presented view controller closes.

When starting a new game, though, you don’t want the main screen to do any animations. You want to immediately switch from the Join Game screen to the actual game screen where all the action happens. The _performAnimations variable simply controls whether the flying-cards animation is supposed to happen or not.

To set to default value of _performAnimations to YES, override initWithNibName:

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
	if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]))
	{
		_performAnimations = YES;
	}
	return self;
}

Change viewWillAppear: and viewDidAppear: to the following:

- (void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear:animated];

	if (_performAnimations)
		[self prepareForIntroAnimation];
}

- (void)viewDidAppear:(BOOL)animated
{
	[super viewDidAppear:animated];

	if (_performAnimations)
		[self performIntroAnimation];
}

So back to explaining the code you added earlier. When the startClientGameWithSession… method is called, you disable the animations by setting _performAnimations to NO. Then you dismiss the Join Game screen, set _performAnimations back to YES, and do this:

		[self startGameWithBlock:^(Game *game)
		{
			[game startClientGameWithSession:session playerName:name server:peerID];
		}];

This probably requires more explanation. You’re going to make a new Game class that serves as the data model for the game, and a GameViewController class that manages the game screen.

There are three ways to start a new game: as the server, as a client, or in single-player mode. The main setup for these three game types is the same, except for how the Game object will be initialized. startGameWithBlock: takes care of all the shared details, and the block you give it does the things that are specific to the type of the game.

In this case, because you’re the client, you call startClientGameWithSession:playerName:server: on the Game object to get it started. But before you can do all that, you must first write some code for that new Game object and the GameViewController.

Begin by adding startGameWithBlock:.

- (void)startGameWithBlock:(void (^)(Game *))block
{
	GameViewController *gameViewController = [[GameViewController alloc] initWithNibName:@"GameViewController" bundle:nil];
	gameViewController.delegate = self;

	[self presentViewController:gameViewController animated:NO completion:^
	{
		Game *game = [[Game alloc] init];
		gameViewController.game = game;
		game.delegate = gameViewController;
		block(game);
	}];
}

None of this compiles yet, but you can see what it’s supposed to do: allocate the GameViewController, present it, and then allocate the Game object. Finally, it calls your block to do the game-type specific initializations.

Even though these files don’t exist yet, go ahead and add imports for them to MainViewController.h:

#import "GameViewController.h"

And add to MainViewController.m:

#import "Game.h"

While you’re at it, you might as well add the still-illusive GameViewControllerDelegate to MainViewController’s @interface:

@interface MainViewController : UIViewController <HostViewControllerDelegate, JoinViewControllerDelegate, GameViewControllerDelegate>

Now add a new Objective-C class to the project, subclass of UIViewController, named GameViewController. No XIB is necessary – it’s already been provided in the starter code download from part one. Drag GameViewController.xib (from the “Snap/en.lproj/” folder) into the project. Replace the contents of GameViewController.h with the following:

#import "Game.h"

@class GameViewController;

@protocol GameViewControllerDelegate <NSObject>

- (void)gameViewController:(GameViewController *)controller didQuitWithReason:(QuitReason)reason;

@end

@interface GameViewController : UIViewController <UIAlertViewDelegate, GameDelegate>

@property (nonatomic, weak) id <GameViewControllerDelegate> delegate;
@property (nonatomic, strong) Game *game;

@end

Pretty simple. You’ve declared a new delegate protocol, GameViewControllerDelegate, with a single method that is used to let the MainViewController know the game should end. The GameViewController itself is the delegate for the Game object. Lots of delegates everywhere.

The first version of GameViewController.xib that you’ll use is very simple. It only has an exit button and a single label in the center of the screen.

The simple version of the GameViewController nib

Replace the contents of GameViewController.m with the following:

#import "GameViewController.h"
#import "UIFont+SnapAdditions.h"

@interface GameViewController ()

@property (nonatomic, weak) IBOutlet UILabel *centerLabel;

@end

@implementation GameViewController

@synthesize delegate = _delegate;
@synthesize game = _game;

@synthesize centerLabel = _centerLabel;

- (void)dealloc
{
	#ifdef DEBUG
	NSLog(@"dealloc %@", self);
	#endif
}

- (void)viewDidLoad
{
	[super viewDidLoad];

	self.centerLabel.font = [UIFont rw_snapFontWithSize:18.0f];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
	return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}

#pragma mark - Actions

- (IBAction)exitAction:(id)sender
{
	[self.game quitGameWithReason:QuitReasonUserQuit];
}

#pragma mark - GameDelegate

- (void)game:(Game *)game didQuitWithReason:(QuitReason)reason
{
	[self.delegate gameViewController:self didQuitWithReason:reason];
}

@end

Nothing too exciting going on here. exitAction: tells the Game object to quit, and the Game object responds by calling game:didQuitWithReason:. MainViewController is the delegate for GameViewController, so that’s where you should implement gameViewController:didQuitGameWithReason::

#pragma mark - GameViewControllerDelegate

- (void)gameViewController:(GameViewController *)controller didQuitWithReason:(QuitReason)reason
{
	[self dismissViewControllerAnimated:NO completion:^
	{
		if (reason == QuitReasonConnectionDropped)
		{
			[self showDisconnectedAlert];
		}
	}];
}

That also looks very familiar. You close the game screen and show an error alert view if necessary.

This leaves just the Game object to be implemented.

The Game Class

As I mentioned before, the Game class is the main data model object for the game. It also handles incoming network packets from GKSession. You’re going to build a very basic version of this class to get the game started. Throughout the rest of this tutorial, you’ll expand the Game and GameViewController classes until Snap! is fully complete.

Add a new Objective-C class to the project, subclass of NSObject, named Game. I suggest you place the Game.h and Game.h source files in a new group called “Data Model.” Replace the contents of Game.h with:

@class Game;

@protocol GameDelegate <NSObject>

- (void)game:(Game *)game didQuitWithReason:(QuitReason)reason;

@end

@interface Game : NSObject <GKSessionDelegate>

@property (nonatomic, weak) id <GameDelegate> delegate;
@property (nonatomic, assign) BOOL isServer;

- (void)startClientGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID;
- (void)quitGameWithReason:(QuitReason)reason;

@end

Here you declare the GameDelegate protocol, which you’ve already seen a bit about (GameViewController plays that role), and the Game class. So far you’ve only added the startClientGameWithSession… method to the class, and a delegate and isServer property.

Replace the contents of Game.m with:

#import "Game.h"

typedef enum
{
	GameStateWaitingForSignIn,
	GameStateWaitingForReady,
	GameStateDealing,
	GameStatePlaying,
	GameStateGameOver,
	GameStateQuitting,
}
GameState;

@implementation Game
{
	GameState _state;

	GKSession *_session;
	NSString *_serverPeerID;
	NSString *_localPlayerName;
}

@synthesize delegate = _delegate;
@synthesize isServer = _isServer;

- (void)dealloc
{
	#ifdef DEBUG
	NSLog(@"dealloc %@", self);
	#endif
}

#pragma mark - Game Logic

- (void)startClientGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID
{
}

- (void)quitGameWithReason:(QuitReason)reason
{
}

#pragma mark - GKSessionDelegate

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
	#ifdef DEBUG
	NSLog(@"Game: peer %@ changed state %d", peerID, state);
	#endif
}

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
	#ifdef DEBUG
	NSLog(@"Game: connection request from peer %@", peerID);
	#endif

	[session denyConnectionFromPeer:peerID];
}

- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error
{
	#ifdef DEBUG
	NSLog(@"Game: connection with peer %@ failed %@", peerID, error);
	#endif

	// Not used.
}

- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
	#ifdef DEBUG
	NSLog(@"Game: session failed %@", error);
	#endif
}

#pragma mark - GKSession Data Receive Handler

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context
{
	#ifdef DEBUG
	NSLog(@"Game: receive data from peer: %@, data: %@, length: %d", peerID, data, [data length]);
	#endif
}

@end

This is the bare minimum you need to do to get the app compiling again. None of the methods from the Game class do anything useful yet. Also notice you’ve declared a new enum, GameState, that contains the different states that Game can occupy. More about the Game’s state machine later.

Run the app again. When you connect the client to the server, you’ll briefly see the “Connecting…” message (for as long as it takes to set up the connection to the server, which is usually only a fraction of a second), and then the app switches to the game screen. You won’t notice much difference because the layout stays mostly the same (on purpose), except that the main label now says “Center Label,” and is white instead of green.

Also notice that the client disappears from the server’s table view. This happens because on the client side, the JoinViewController closes, which deallocates the MatchmakingClient object. That object is the only one holding on to the GKSession object, so that gets deallocated as well, and the connection is immediately broken as soon as you leave the Join Game screen. (You can verify this in the server’s debug output; the state of the client peer becomes 3, which is GKPeerStateDisconnected.)

You’ll fix this right now.

Replace Game.m’s startClientGameWithSession… method with:

- (void)startClientGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID
{
	self.isServer = NO;

	_session = session;
	_session.available = NO;
	_session.delegate = self;
	[_session setDataReceiveHandler:self withContext:nil];

	_serverPeerID = peerID;
	_localPlayerName = name;

	_state = GameStateWaitingForSignIn;

	[self.delegate gameWaitingForServerReady:self];
}

This method takes control of the GKSession object and makes “self,” i.e. the Game object, become the new GKSessionDelegate as well as the data-receive-handler. (You’ve already added those delegate methods, but they are currently empty.)

You copy the server’s peer ID and the player’s name into your own instance variables, and then set the game state to “waiting for sign-in.” This means the client will now wait for a specific message from the server. Finally, you tell the GameDelegate that you’re ready for the game to start. That employs a new delegate method, so add that to Game.h in the GameDelegate @protocol:

- (void)gameWaitingForServerReady:(Game *)game;

The delegate for Game is the GameViewController, so you should implement this method there:

- (void)gameWaitingForServerReady:(Game *)game
{
	self.centerLabel.text = NSLocalizedString(@"Waiting for game to start...", @"Status text: waiting for server");
}

That’s all it does. It replaces the text of the center label with “Waiting for game to start…”. Run the app again. After connecting the client, you should see this:

Waiting for the game to start

Because you’re now keeping the GKSession object alive after the Join Game screen closes, the table view of the server should still show the client’s device name.

There’s not much to do yet for the client at this point, but you can at least make the exit button work. Replace the following method in Game.m:

- (void)quitGameWithReason:(QuitReason)reason
{
	_state = GameStateQuitting;

	[_session disconnectFromAllPeers];
	_session.delegate = nil;
	_session = nil;

	[self.delegate game:self didQuitWithReason:reason];
}

Even though the game hasn’t really started yet on the server, you can still exit. To the server, this looks just like a disconnect, and it will remove the client from its table view.

You already implemented game:didQuitWithReason: in GameViewController, which causes the app to return to the main screen. Because you put some NSLogging into the dealloc methods, you can see in Xcode’s debug output pane that everything gets deallocated properly. Test it out!

Starting the Game On the Server

Starting the game on the server is not too different from what you just did. The host taps the Start button from the Host Game screen. In response, you should create a Game object and fire up the GameViewController.

HostViewController has a startAction: method that’s wired up to the Start button. Right now this method does nothing. Replace it with:

- (IBAction)startAction:(id)sender
{
	if (_matchmakingServer != nil && [_matchmakingServer connectedClientCount] > 0)
	{
		NSString *name = [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
		if ([name length] == 0)
			name = _matchmakingServer.session.displayName;

		[_matchmakingServer stopAcceptingConnections];

		[self.delegate hostViewController:self startGameWithSession:_matchmakingServer.session playerName:name clients:_matchmakingServer.connectedClients];
	}
}

The Start button will only work if there is a valid MatchmakingServer object (which is usually the case except when there is no Wi-Fi or Bluetooth available), and there is at least one connected client – it’s no fun playing against yourself! When these conditions are met, you get the player’s name from the text field, tell the MatchmakingServer not to accept any new clients, and tell the delegate (MainViewController) that it should start a server game.

stopAcceptingConnections is new, so add it to MatchmakingServer.h:

- (void)stopAcceptingConnections;

And to MatchmakingServer.m:

- (void)stopAcceptingConnections
{
	NSAssert(_serverState == ServerStateAcceptingConnections, @"Wrong state");

	_serverState = ServerStateIgnoringNewConnections;
	_session.available = NO;
}

Unlike endSession, this doesn’t tear down the GKSession object. It just moves the MatchmakingServer into the “ignoring new connections” state, so that it no longer accepts new connections when the GKPeerStateConnected or GKPeerStateDisconnected callbacks happen. Setting the GKSession’s available property to NO also means that the existence of the service is no longer broadcast.

startAction: also called a new delegate method, so add its signature to HostViewController.h:

- (void)hostViewController:(HostViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients;

And add the implementation for this method in MainViewController.m:

- (void)hostViewController:(HostViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
	_performAnimations = NO;

	[self dismissViewControllerAnimated:NO completion:^
	{
		_performAnimations = YES;

		[self startGameWithBlock:^(Game *game)
		{
			[game startServerGameWithSession:session playerName:name clients:clients];
		}];
	}];
}

This is very similar to what you did for clients, except you’re calling a new method on the Game class. Add this method to Game.h:

- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients;

As well as to Game.m:

- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
	self.isServer = YES;

	_session = session;
	_session.available = NO;
	_session.delegate = self;
	[_session setDataReceiveHandler:self withContext:nil];

	_state = GameStateWaitingForSignIn;

	[self.delegate gameWaitingForClientsReady:self];
}

This is very much the same thing you do for the client game, except that you set the isServer property to YES, and you call another GameDelegate method. Add this method to the @protocol in Game.h:

- (void)gameWaitingForClientsReady:(Game *)game;

And implement it in GameViewController.m:

- (void)gameWaitingForClientsReady:(Game *)game
{
	self.centerLabel.text = NSLocalizedString(@"Waiting for other players...", @"Status text: waiting for clients");
}

That’s it! Now when you press the Start button on the host, the HostViewController gets discreetly dismissed and the GameViewController appears:

Waiting for the other players

Your app can now connect a server (host) to multiple clients. But the devices just sit there with the clients waiting for the host to start the game. There’s nothing much for a player to do except tap the exit button. Because you already implemented exitAction:, and both the client and server share most of the code in Game and GameViewController, tapping the exit button on the server should end the game.

Note: When you exit the server, the client may take a few seconds to recognize that the server is disconnected. It will also remain stuck on the “Waiting for game to start…” screen, because you haven’t yet implemented any disconnection logic in the Game class.

Disconnection logic used to be handled by the MatchmakingServer and MatchmakingClient classes, but now that you’ve started the game, these objects have served their purpose and are no longer being used. The Game object has taken over the duties of GKSessionDelegate.

The Data Model

This is a good time to talk a bit about the data model for this game. Because you’re using UIKit (as opposed to Cocos2D or OpenGL), it makes sense to structure the game using the Model-View-Controller (MVC) pattern.

A common way to make Cocos2D games is to subclass CCSprite, and put your game object logic into that class. Here you’ll do things a bit differently: you’ll make a strict separation between model, view, and view controller classes.

Note: It may not make sense to use MVC for all games, but it does for card and board games. You can capture the game rules in the model classes, separate from any presentation logic. This has the advantage of allowing you to easily unit-test these gameplay rules to ensure they’re always correct, although you’ll skip that in this tutorial.

The Game object is part of the data model. It handles the game play rules, as well as the networking traffic between the clients and the server (it is both the delegate and data-receive-handler for GKSession). But Game is not the only data model object; there are several others:

The model, view, and controller objects

You’ve seen the Game and GameViewController classes, but the others are new and you’ll be adding them to the project in the course of the tutorial. The players participating in the game are represented by Player objects. Each player has two Stacks of Cards, which are drawn from a Deck. These are all model objects.

Cards are drawn on the screen by CardView objects. All the other views are regular UILabels, UIButtons and UIImageViews. For network communication, Game uses GKSession to send and receive Packet objects, which represent a message that is sent over the network between the different devices.

Begin by creating the Player object. Add a new Objective-C class to the project, subclass of NSObject, named Player. Since this is a data model class, add it to the Data Model group. Replace the contents of Player.h with:

typedef enum
{
	PlayerPositionBottom,  // the user
	PlayerPositionLeft,
	PlayerPositionTop,
	PlayerPositionRight
}
PlayerPosition;

@interface Player : NSObject

@property (nonatomic, assign) PlayerPosition position;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *peerID;

@end

And in Player.m:

#import "Player.h"

@implementation Player

@synthesize position = _position;
@synthesize name = _name;
@synthesize peerID = _peerID;

- (void)dealloc
{
	#ifdef DEBUG
	NSLog(@"dealloc %@", self);
	#endif
}

- (NSString *)description
{
	return [NSString stringWithFormat:@"%@ peerID = %@, name = %@, position = %d", [super description], self.peerID, self.name, self.position];
}

@end

You’re keeping it simple right now. The three properties of Player represent the different ways that each player can be identified:

  1. By their name. This is what you show to the user, but it’s not guaranteed to be unique. The name is what the player typed in on the Host Game or Join Game screens (if they didn’t type in anything, you’ll use the device name here).
  2. By the peer ID. This is what GKSession uses internally, and it’s how you will identify the players when you need to send them messages over the network.
  3. By their “position” on the screen. This one is interesting. Each player will see himself sitting at the bottom of the screen, so player positions are relative. As you can see from the typedef, you start at the bottom position and then go clockwise (left, up, right). You’ll use the position when you need to do things in a guaranteed order, such as dealing the cards.

This is how different players see themselves and the other players sitting around the table:

How players see themselves and the other players

Signing In

The Game object has now entered its initial state, GameStateWaitingForSignIn, on both the clients and the server. In the “waiting for sign-in” state, the server will send a message to all clients asking them to respond with their local player name.

So far the server only knows which clients are connected and what their peer IDs and device names are, but it does not know anything about the name that the user typed into the “Your Name” field. Once the server knows everyone’s name, it can tell all the clients about the other players.

Add the import for Player to Game.h:

#import "Player.h"

Add a new instance variable to Game, in Game.m:

@implementation Game
{
	. . .
	NSMutableDictionary *_players;
}

You’ll put the Player objects into a dictionary. To make it easy to look up players by their peer IDs, you’ll make the peer ID the key. The dictionary needs to be allocated right away, so add an init method to the Game class:

- (id)init
{
	if ((self = [super init]))
	{
		_players = [NSMutableDictionary dictionaryWithCapacity:4];
	}
	return self;
}

The method that starts the game on the server is startServerGameWithSession:playerName:clients:, and you already do some stuff in there to set up the game. Add the following code to the bottom of that method:

- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
	. . .

	// Create the Player object for the server.
	Player *player = [[Player alloc] init];
	player.name = name;
	player.peerID = _session.peerID;
	player.position = PlayerPositionBottom;
	[_players setObject:player forKey:player.peerID];

	// Add a Player object for each client.
	int index = 0;
	for (NSString *peerID in clients)
	{
		Player *player = [[Player alloc] init];
		player.peerID = peerID;
		[_players setObject:player forKey:player.peerID];

		if (index == 0)
			player.position = ([clients count] == 1) ? PlayerPositionTop : PlayerPositionLeft;
		else if (index == 1)
			player.position = PlayerPositionTop;
		else
			player.position = PlayerPositionRight;

		index++;
	}
}

First you create the Player object for the server and place it in the “bottom” position on the screen. Then you loop through the array of peer IDs for all connected clients and make Player objects for them. You assign the positions for the client players, in clockwise order, depending on how many players there are in total.

Notice that you don’t set the “name” property of these Player objects yet, because at this point you do not yet know the names of the clients.

Sending Messages Between Devices

Now that you have a Player object for each client, you can send the “sign-in” requests to the clients. Each client will respond asynchronously with their name. Upon receipt of such a response, you’ll look up the Player object for that client and set its “name” property with the name that the client sent back to you.

GKSession has a method called sendDataToAllPeers:withDataMode:error: that will send the contents of an NSData object to all connected peers. You can use this method to send a single message from the server to all the clients. The message in this case is an NSData object, and what is inside this NSData object is completely up to you. In Snap!, all messages have the following format:

The data contents of each packet

A packet is at least 10 bytes. These 10 bytes are called the “header,” and any (optional) bytes that may follow are the “payload.” Different types of packets have different payloads, but they all have the same header structure:

  • The first four bytes from the header form the word SNAP. This is a so-called magic number (0x534E4150 in hexadecimal) that you use to verify that the packets really are yours.
  • The magic number is followed by four bytes (really a 32-bit integer) that’s used to recognize when packets arrive out-of-order (more about this later).
  • The last two bytes from the header represent the packet type. You’ll have many types of messages that you send back-and-forth between the clients and the server, and this 16-bit integer tells you what sort of packet it is.

For some packet types, there may be more data following the header (the payload). The “sign-in response” packet that the client sends back to the server, for example, also contains a UTF-8 string with the name of the player.

This is all well and good, but you want to abstract this low-level stuff behind a nicer interface. You’re going to make a Packet class that takes care of the bits-and-bytes behind the scenes. Add a new Objective-C class to the project, subclass of NSObject, named Packet. To keep things tidy, place this in the “Networking” group.

Replace the contents of Packet.h with:

typedef enum
{
	PacketTypeSignInRequest = 0x64,    // server to client
	PacketTypeSignInResponse,          // client to server

	PacketTypeServerReady,             // server to client
	PacketTypeClientReady,             // client to server

	PacketTypeDealCards,               // server to client
	PacketTypeClientDealtCards,        // client to server

	PacketTypeActivatePlayer,          // server to client
	PacketTypeClientTurnedCard,        // client to server
	
	PacketTypePlayerShouldSnap,        // client to server
	PacketTypePlayerCalledSnap,        // server to client

	PacketTypeOtherClientQuit,         // server to client
	PacketTypeServerQuit,              // server to client
	PacketTypeClientQuit,              // client to server
}
PacketType;

@interface Packet : NSObject

@property (nonatomic, assign) PacketType packetType;

+ (id)packetWithType:(PacketType)packetType;
- (id)initWithType:(PacketType)packetType;

- (NSData *)data;

@end

The enum at the top contains a list of all the different types of messages you will send and receive. The Packet class itself it pretty simple at this point: it has a convenience constructor and init method for setting the packet type. The “data” method returns a new NSData object with the contents of this particular message. That NSData object is what you’ll send through GKSession to the other devices.

Replace the contents of Packet.m with:

#import "Packet.h"
#import "NSData+SnapAdditions.h"

@implementation Packet

@synthesize packetType = _packetType;

+ (id)packetWithType:(PacketType)packetType
{
	return [[[self class] alloc] initWithType:packetType];
}

- (id)initWithType:(PacketType)packetType
{
	if ((self = [super init]))
	{
		self.packetType = packetType;
	}
	return self;
}

- (NSData *)data
{
	NSMutableData *data = [[NSMutableData alloc] initWithCapacity:100];

	[data rw_appendInt32:'SNAP'];   // 0x534E4150
	[data rw_appendInt32:0];
	[data rw_appendInt16:self.packetType];

	return data;
}

- (NSString *)description
{
	return [NSString stringWithFormat:@"%@, type=%d", [super description], self.packetType];
}

@end

The interesting bit here is the data method. It allocates an NSMutableData object and then places two 32-bit integers and one 16-bit integer into it. This is the 10-byte header I mentioned earlier. The first part is the word “SNAP,” the second is the packet number – which for the time being you’ll keep at 0 – and the third part is the type of the packet.

From the names of these “rw_appendIntXX” methods, you can already infer that they come from a category. Add a new Objective-C category file to the project. Name the category “SnapAdditions” and make it on NSData (not NSMutableData!).

You’re cheating a bit here, as the category will actually be on NSMutableData. Because you need a similar category on NSData later, you’re putting both of them into the same source file. Replace the contents of NSData+SnapAdditions.h with:

@interface NSData (SnapAdditions)

@end

@interface NSMutableData (SnapAdditions)

- (void)rw_appendInt32:(int)value;
- (void)rw_appendInt16:(short)value;
- (void)rw_appendInt8:(char)value;
- (void)rw_appendString:(NSString *)string;

@end

You’re leaving the category on NSData empty for now, and adding a second category on NSMutableData. As you can see, there are methods for adding integers of different sizes, as well as a method to add an NSString. Replace the contents of NSData+SnapAdditions.m with:

#import "NSData+SnapAdditions.h"

@implementation NSData (SnapAdditions)

@end

@implementation NSMutableData (SnapAdditions)

- (void)rw_appendInt32:(int)value
{
	value = htonl(value);
	[self appendBytes:&value length:4];
}

- (void)rw_appendInt16:(short)value
{
	value = htons(value);
	[self appendBytes:&value length:2];
}

- (void)rw_appendInt8:(char)value
{
	[self appendBytes:&value length:1];
}

- (void)rw_appendString:(NSString *)string
{
	const char *cString = [string UTF8String];
	[self appendBytes:cString length:strlen(cString) + 1];
}

@end

These methods are all pretty similar, but take a closer look at rw_appendInt32::

- (void)rw_appendInt32:(int)value
{
	value = htonl(value);
	[self appendBytes:&value length:4];
}

In the last line, you call [self appendBytes:length:] in order to add the memory contents of the “value” variable, which is four bytes long, to the NSMutableData object. But before that, you call the htonl() function on “value.” This is done to ensure that the integer value is always transmitted in “network byte order,” which happens to be big endian. However, the processors on which you’ll be running this app, the x86 and ARM CPUs, use little endian.

Big endian vs little endian

You could send the memory contents of the “value” variable as-is, but who knows, a new model iPhone may in the future use a different byte ordering and then what one device sends and what another receives could have an incompatible structure.

For this reason, it’s always a good idea to decide on one specific byte ordering when dealing with data transfer, and for network programming that should be big endian. If you simply take care to call htonl() for 32-bit integers and htons() for 16-bit integers before sending, then you should always be fine.

Another thing of note is rw_appendString:, which first converts the NSString to UTF-8 and then adds it to the NSMutableData object, including a NUL byte at the end to mark the end of the string.

Back to Game and the startServerGameWithSession… method. Add the following lines to the bottom of that method:

- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
	. . .

	Packet *packet = [Packet packetWithType:PacketTypeSignInRequest];
	[self sendPacketToAllClients:packet];
}

Of course, this won’t compile yet. First add the required import:

#import "Packet.h"

And then add the sendPacketToAllClients: method:

#pragma mark - Networking

- (void)sendPacketToAllClients:(Packet *)packet
{
	GKSendDataMode dataMode = GKSendDataReliable;
	NSData *data = [packet data];
	NSError *error;
	if (![_session sendDataToAllPeers:data withDataMode:dataMode error:&error])
	{
		NSLog(@"Error sending data to clients: %@", error);
	}
}

Great, now you can run the app again and send some messages between the server and the clients. Host a new game, join with one or more clients, and keep your eye on the debug output pane. On the client it should now say something like this:

Game: receive data from peer: 1995171355, data: <534e4150 00000000 0064>, length: 10

This output comes from the GKSession data-receive-handler method receiveData:fromPeer:inSession:context:. You’re not doing anything in this method yet, but at least it logs that it’s received a message from the server. The actual data, from the NSData object that you gave GKSession to send on the server, is this:

534e4150 00000000 0064

This is 10 bytes long, but in hexadecimal notation. If your hex is a little rusty, then download Hex Fiend or a similar hex editor, and simply copy-paste the above into it:

Sign-in packet in Hex Fiend

This shows you that the packet indeed started with the word SNAP (in big endian byte order, or 0x534E4150), followed by four 0 bytes (for the packet number, which you’re not using yet), followed by the 16-bit packet type. For readability reasons, I gave the first packet type, PacketTypeSignInRequest, the value 0x64 (you can see this in Packet.h), so it would be easy to spot in the hexadecimal data.

Cool, so you managed to send a message to the client. Now the client has to respond to it.

Sending Responses Back to the Server

I’ve mentioned the GKSession data-receive-handler method a few times now. One of the parameters to that method is a new NSData object, with the binary contents of the message as it was received from the sender. This method is where you’ll turn that NSData object back into a Packet, look at the packet type, and decide what to do with it.

In Game.m, replace the following method with:

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context
{
	#ifdef DEBUG
	NSLog(@"Game: receive data from peer: %@, data: %@, length: %d", peerID, data, [data length]);
	#endif

	Packet *packet = [Packet packetWithData:data];
	if (packet == nil)
	{
		NSLog(@"Invalid packet: %@", data);
		return;
	}

	[self clientReceivedPacket:packet];
}

You still log the incoming data, but then call a new convenience constructor on Packet – packetWithData: – to turn that NSData into a new Packet object. That may not always work. If, for example, the incoming data doesn’t start with the ‘SNAP’ header, then you log an error. But if you do have a valid Packet object, you pass it along to another new method, clientReceivedPacket:.

First add that convenience constructor to the Packet class. Add the method signature to Packet.h:

+ (id)packetWithData:(NSData *)data;

To Packet.m, first add the following line above the @implementation:

const size_t PACKET_HEADER_SIZE = 10;

Then add the implementation of packetWithData: in Packet.m:

+ (id)packetWithData:(NSData *)data
{
	if ([data length] < PACKET_HEADER_SIZE)
	{
		NSLog(@"Error: Packet too small");
		return nil;
	}

	if ([data rw_int32AtOffset:0] != 'SNAP')
	{
		NSLog(@"Error: Packet has invalid header");
		return nil;
	}

	int packetNumber = [data rw_int32AtOffset:4];
	PacketType packetType = [data rw_int16AtOffset:8];

	return [Packet packetWithType:packetType];
}

First you verify that the data is at least 10 bytes. If it's any smaller, something is wrong. Most of the time you'll send packets in "reliable" mode, which means they are guaranteed to arrive with exactly the same content as you sent them, so you don't have to worry about any bits "falling over" during transmission.

But it's good to do some sanity checks anyway, as a form of defensive programming. After all, who says some "rogue" client won't be sending different messages in order to fool us? For the same reason, you check that the first 32-bit integer truly represents the word SNAP.

Note: The word 'SNAP' may look like a string, but it isn't. It's also not a single character, but something known as a four-character-code, or "fourcc" for short. Many networking protocols and file formats use such 32-bit codes to indicate their format. For fun, open some random files in Hex Fiend. You'll see they often start with a fourcc.

Xcode doesn't know yet about these new rw_intXXAtOffset: methods, so add them to the NSData category. First to NSData+SnapAdditions.h:

@interface NSData (SnapAdditions)

- (int)rw_int32AtOffset:(size_t)offset;
- (short)rw_int16AtOffset:(size_t)offset;
- (char)rw_int8AtOffset:(size_t)offset;
- (NSString *)rw_stringAtOffset:(size_t)offset bytesRead:(size_t *)amount;

@end

This time you're adding the methods to NSData, not NSMutableData. After all, the GKSession data-receive-handler only receives an immutable NSData object. Put the methods themselves in NSData+SnapAdditions.m:

@implementation NSData (SnapAdditions)

- (int)rw_int32AtOffset:(size_t)offset
{
	const int *intBytes = (const int *)[self bytes];
	return ntohl(intBytes[offset / 4]);
}

- (short)rw_int16AtOffset:(size_t)offset
{
	const short *shortBytes = (const short *)[self bytes];
	return ntohs(shortBytes[offset / 2]);
}

- (char)rw_int8AtOffset:(size_t)offset
{
	const char *charBytes = (const char *)[self bytes];
	return charBytes[offset];
}

- (NSString *)rw_stringAtOffset:(size_t)offset bytesRead:(size_t *)amount
{
	const char *charBytes = (const char *)[self bytes];
	NSString *string = [NSString stringWithUTF8String:charBytes + offset];
	*amount = strlen(charBytes + offset) + 1;
	return string;
}

@end

Unlike the "append" methods from NSMutableData, which update a write pointer every time you call them, these reading methods do not automatically update a read pointer. That would have been the most convenient solution, but categories cannot add new data members to a class.

Instead, you're required to pass in a byte offset from where you want to read. Note that these methods assume the data is in network byte-order (big endian), and therefore use ntohl() and ntohs() to convert them back to host byte-order.

rw_stringAtOffset:bytesRead: deserves special mention, as it returns the number of bytes read in a by-reference parameter. With the integer methods, you already know how many bytes you will read, but with a string that number can be anything. (The bytesRead parameter contains the number of bytes read, including the NUL-byte that terminates the string.)

All that's left now is implementing clientReceivedPacket: in Game.m. This is it:

- (void)clientReceivedPacket:(Packet *)packet
{
	switch (packet.packetType)
	{
		case PacketTypeSignInRequest:
			if (_state == GameStateWaitingForSignIn)
			{
				_state = GameStateWaitingForReady;

				Packet *packet = [PacketSignInResponse packetWithPlayerName:_localPlayerName];
				[self sendPacketToServer:packet];
			}
			break;

		default:
			NSLog(@"Client received unexpected packet: %@", packet);
			break;
	}
}

You simply look at the packet type, and then decide how to handle it. For PacketTypeSignInRequest – the only type we currently have – you change the game state to "waiting for ready," then send a "sign-in response" packet back to the server.

This uses a new class, PacketSignInResponse, rather than just Packet. The sign-in response will contain additional data beyond the standard 10-byte header, and for this project, you'll make such packets subclasses of Packet.

But before you get to that, first add the sendPacketToServer: method (below sendPacketToAllClients: is a good place):

- (void)sendPacketToServer:(Packet *)packet
{
	GKSendDataMode dataMode = GKSendDataReliable;
	NSData *data = [packet data];
	NSError *error;
	if (![_session sendData:data toPeers:[NSArray arrayWithObject:_serverPeerID] withDataMode:dataMode error:&error])
	{
		NSLog(@"Error sending data to server: %@", error);
	}
}

It looks very similar to sendPacketToAllClients:, except you don't send the packet to all connected peers, but just to the one identified by _serverPeerID.

The reason for this is that behind the scenes, Game Kit appears to connect every peer to every other peer, so if there are two clients and one server, then the clients are not only connected to the server but also to each other. For this game, you don't want clients to send messages to each other, only to the server. (You can see this in the "peer XXX changed state 2" output from GKSession's session:peer:didChangeState: callback.)

Now you need to make the new PacketSignInResponse class. Add a new Objective-C class to the project, subclass of Packet, named PacketSignInResponse. Replace the new .h file with:

#import "Packet.h"

@interface PacketSignInResponse : Packet

@property (nonatomic, copy) NSString *playerName;

+ (id)packetWithPlayerName:(NSString *)playerName;

@end

In addition to the regular data of the Packet superclass, this particular packet also contains a player name. Replace the .m file with:

#import "PacketSignInResponse.h"
#import "NSData+SnapAdditions.h"

@implementation PacketSignInResponse

@synthesize playerName = _playerName;

+ (id)packetWithPlayerName:(NSString *)playerName
{
	return [[[self class] alloc] initWithPlayerName:playerName];
}

- (id)initWithPlayerName:(NSString *)playerName
{
	if ((self = [super initWithType:PacketTypeSignInResponse]))
	{
		self.playerName = playerName;
	}
	return self;
}

- (void)addPayloadToData:(NSMutableData *)data
{
	[data rw_appendString:self.playerName];
}

@end

This should be fairly straightforward, except maybe for addPayloadToData:. This is a new method that Packet subclasses can override to place their own data into the NSMutableData object. Here you simply append the string contents of the playerName property to the data object. To make this work, you have to call this method from the superclass.

Add an empty version of addPayloadToData: to Packet.m:

- (void)addPayloadToData:(NSMutableData *)data
{
	// base class does nothing
}

And call it from the "data" method:

- (NSData *)data
{
	NSMutableData *data = [[NSMutableData alloc] initWithCapacity:100];

	[data rw_appendInt32:'SNAP'];   // 0x534E4150
	[data rw_appendInt32:0];
	[data rw_appendInt16:self.packetType];

	[self addPayloadToData:data];
	return data;
}

By default, addPayloadToData: doesn't do anything, but subclasses can use it to put their own additional data into the message.

One more thing is needed to make everything compile again, and that is an import for the new Packet subclass in Game.m:

#import "PacketSignInResponse.h"

The compiler may still warn that the value stored in the local packetNumber variable is never read (in Packet's packetWithData:). That's OK for now. You'll do something with that variable soon enough.

Now build and run the app again, both on the client and the server, and start a new game. As before, the client should receive the "sign-in" packet (type code 0x64), but in response it should now send the server a packet that contains its player name. The debug output for the server should say something like this:

Game: receive data from peer: 1100677320, data: <534e4150 00000000 00656372 617a7920 6a6f6500>, length: 20

Paste that data (everything between < and >) into Hex Fiend to figure out what was sent back to the server:

Sign-in response in Hex Fiend

The packet type is now 0x65 instead of 0x64 (PacketTypeSignInResponse instead of PacketTypeSignInRequest), and the highlighted bit shows the name of this particular player ("crazy joe"), including the NUL-byte that terminates the string.

Note: It's a good idea to keep your Game Kit transmissions limited in size. Apple recommends 1,000 bytes or less, although the upper limit seems to be 87 kilobytes or so.

If you send less than 1,000 bytes, all the data can fit into a single TCP/IP packet, which will guarantee speedy delivery. Larger messages will have to be split up and recombined by the receiver. Game Kit takes care of that for you, and it's still pretty fast, but for best performance, stay below 1,000 bytes.

Handling Responses On the Server

You may have noticed that the server's debug output not only showed that data was received, it also said this:

Client received unexpected packet: <Packet: 0x9294ba0>

That's seems weird, considering that you're on the server, not the client. But it is not so strange at all, for the server and client share a lot of the code, and the GKSession data-receive-handler is the same for both. So you have to make a distinction here between incoming messages that are intended for the server, and messages that are only intended for clients.

Change the data-receive-handler method (in Game.m) to:

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context
{
	#ifdef DEBUG
	NSLog(@"Game: receive data from peer: %@, data: %@, length: %d", peerID, data, [data length]);
	#endif
	
	Packet *packet = [Packet packetWithData:data];
	if (packet == nil)
	{
		NSLog(@"Invalid packet: %@", data);
		return;
	}

	Player *player = [self playerWithPeerID:peerID];

	if (self.isServer)
		[self serverReceivedPacket:packet fromPlayer:player];
	else
		[self clientReceivedPacket:packet];
}

You now make a distinction based on the isServer property. For the server, it's important to know which player the Packet came from, so you look up the Player object based on the sender's peer ID with the new playerWithPeerID: method. Add this method to Game.m:

- (Player *)playerWithPeerID:(NSString *)peerID
{
	return [_players objectForKey:peerID];
}

Pretty simple, but worth putting into a method of its own, because you'll be using it in a few more places. serverReceivedPacket:fromPlayer: is also new:

- (void)serverReceivedPacket:(Packet *)packet fromPlayer:(Player *)player
{
	switch (packet.packetType)
	{
		case PacketTypeSignInResponse:
			if (_state == GameStateWaitingForSignIn)
			{
				player.name = ((PacketSignInResponse *)packet).playerName;

				NSLog(@"server received sign in from client '%@'", player.name);
			}
			break;
	
		default:
			NSLog(@"Server received unexpected packet: %@", packet);
			break;
	}
}

Run the app on the server with as many clients as you can afford. When the game starts, the server should now output:

server received sign in from client 'Crazy Joe'
server received sign in from client 'Weird Al'
...and so on...

Except that it doesn't and crashes with an "unrecognized selector sent to instance" error! :P

In the code above, you cast the Packet to a PacketSignInResponse object because that's what the client sent you, right? Well, not really. The client only sent a bunch of bytes that GKSession puts into an NSData object, and you put it into a Packet object using packetWithData:.

You have to make Packet a bit smarter to create and return a PacketSignInResponse object when the packet type is PacketTypeSignInResponse (i.e. hex 0x65). In Packet.m, change the packetWithData: method to:

+ (id)packetWithData:(NSData *)data
{
	if ([data length] < PACKET_HEADER_SIZE)
	{
		NSLog(@"Error: Packet too small");
		return nil;
	}

	if ([data rw_int32AtOffset:0] != 'SNAP')
	{
		NSLog(@"Error: Packet has invalid header");
		return nil;
	}

	int packetNumber = [data rw_int32AtOffset:4];
	PacketType packetType = [data rw_int16AtOffset:8];

	Packet *packet;

	switch (packetType)
	{
		case PacketTypeSignInRequest:
			packet = [Packet packetWithType:packetType];
			break;

		case PacketTypeSignInResponse:
			packet = [PacketSignInResponse packetWithData:data];
			break;

		default:
			NSLog(@"Error: Packet has invalid type");
			return nil;
	}

	return packet;
}

Every time you add support for a new packet type, you also have to add a case-statement to this method. Don't forget to import the class in Packet.m as well:

#import "PacketSignInResponse.h"

Note that for a "sign-in response" packet, you call packetWithData: on PacketSignInResponse instead of the regular one from Packet itself. The idea is to override packetWithData in the subclass to read the data that is specific for this packet type – in this case, the player's name. Add the following method to PacketSignInResponse.m:

+ (id)packetWithData:(NSData *)data
{
	size_t count;
	NSString *playerName = [data rw_stringAtOffset:PACKET_HEADER_SIZE bytesRead:&count];
	return [[self class] packetWithPlayerName:playerName];
}

Here you simply read the player name, which begins at an offset of 10 bytes in the NSData object (as indicated by the PACKET_HEADER_SIZE constant). Then you call the regular convenience constructor to allocate and initialize the object.

The only problem here is that PACKET_HEADER_SIZE is an unknown symbol. It's declared in Packet.m, but isn't visible to any other objects, so add a forward declaration to Packet.h:

const size_t PACKET_HEADER_SIZE;

And now everything should compile – and run! – again. Try it out. The server properly writes out the names of all connected clients. You have achieved two-way communication between the server and the clients!

Where To Go From Here?

Here is a sample project with all of the code from the tutorial series so far.

I hope you aren't sick of all this networking stuff yet, because there's more to come in Part 4! Click through when you are ready to implement the "game handshake" between clients and servers, and to create the main gameplay screen!

In the meantime, if you have any questions or comments about this part of the series, please join the forum discussion below!


This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on and Twitter.

Contributors

Over 300 content creators. Join our team.