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

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!

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, second, and third parts of the series, you created a basic shell of the game, got the clients and server connected, and added the ability to send packets back and forth.

In this fourth part of the series, you will implement a game handshake, allowing the clients and servers to exchange information like the names of the connected players. In addition, you’ll start creating the main gameplay screen and start laying out the labels.

Synchronizing the Clients

In the previous part of the series you made the server and the client send packets to each other over a Bluetooth or Wi-Fi network. The server sent a “sign-in request” to each client, and the clients replied with a “sign-in response” packet that contained the player’s name.

But the server can’t start the game until all the clients have signed in. You need a way to keep track of this.

One way is to keep a counter. You’d initialize it with the number of connected clients, and every time a client sends the “sign-in response” message, you’d decrement the counter. Once the counter reaches 0, all clients have signed in.

Simple enough, but there is one problem: what if a single client sends a message twice? That shouldn’t happen during normal usage, but as a general rule, with networked programming, you can’t trust what happens at the other end.

It would be better to keep track of which peers have sent the server a response. You already have an object that represents each client – Player – so you’ll simply add a BOOL to the Player class for this sort of bookkeeping. Add the following property to Player.h:

@property (nonatomic, assign) BOOL receivedResponse;

And synthesize it in Player.m:

@synthesize receivedResponse = _receivedResponse;

You’ll set this variable to NO every time the server sends a message to the client, and set it to YES whenever the server receives a response from the corresponding client. When the flags for all the Players are YES, the server knows it is ready to continue.

In Game.m, add the following lines to the top of sendPacketToAllClients:

- (void)sendPacketToAllClients:(Packet *)packet
{
	[_players enumerateKeysAndObjectsUsingBlock:^(id key, Player *obj, BOOL *stop)
	{
		obj.receivedResponse = [_session.peerID isEqualToString:obj.peerID];
	}];

	. . .
}

This loops through all Players from the _players dictionary and sets their receivedResponse property to NO, except for the player that belongs to the server (identified by _session.peerID), because you need no confirmation from that one.

At the other end of things, you set the property to YES after having received a message from that Player:

- (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 (player != nil)
	{
		player.receivedResponse = YES;  // this is the new bit
	}

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

In serverReceivedPacket:fromPlayer:, you check whether you’ve received “sign-in responses” from all clients, and if so, continue to the next game state:

		case PacketTypeSignInResponse:
			if (_state == GameStateWaitingForSignIn)
			{
				player.name = ((PacketSignInResponse *)packet).playerName;

				if ([self receivedResponsesFromAllPlayers])
				{
					_state = GameStateWaitingForReady;

					NSLog(@"all clients have signed in");
				}
			}
			break;

receivedResponsesFromAllPlayers simply checks whether the receivedResponse boolean on all Player objects is YES. Add this method to the Game class:

- (BOOL)receivedResponsesFromAllPlayers
{
	for (NSString *peerID in _players)
	{
		Player *player = [self playerWithPeerID:peerID];
		if (!player.receivedResponse)
			return NO;
	}
	return YES;
}

Now when you run the app, the server should say “all clients have signed in” after it has received the responses from the clients.

The Game State Machine

You’ve seen in a previous installment of this tutorial that the MatchmakingServer and MatchmakingClient both use a state machine to keep track of what they’re doing (accepting new connections, discovering servers, etc). These state machines are very basic, with only a few states.

The Game class also has a state machine and it’s a bit more complicated, although still nothing to lose sleep over. The game states also vary slightly between server and client. This is the state diagram for the server:

State diagram for the server game

This diagram roughly describes all the things you’ll be building in the rest of this tutorial, including the packets that the server will send to the clients. The state machine for the clients looks like this:

State diagram for the client game

At the moment you’ve only implemented the WaitingForSignIn state. As you can see from the above diagrams, after signing in, the app goes into the “waiting for ready” state, which is what you’ll implement in the next section. You may want to keep referring to these diagrams as you move through the rest of the tutorial.

The Waiting-for-Ready State

So far, the server has sent a message to each of the clients asking them to sign in and send a player name back to the server. Now that the server has received the answers from all the clients, the game can start in earnest. The server now knows who everyone is, but the clients themselves don’t know anything about the other players yet.

That’s what the “waiting for ready” state is for: the server will send a message to each client to tell them about the other players. Each client will use this message to make a dictionary of Player objects, so that it has the same information as the server. (Remember, until now only the server has had a dictionary of Player objects.)

The handshake before the game starts

This process is often called a handshake, where the different parties involved need to go through a couple of steps to agree on what is happening. You’ve done the first part – sending and receiving the “sign-in” packets – now you’ll do the second part, where the server sends the “I’m ready, here are all the players” packet to the clients.

The new packet type is PacketTypeServerReady, and this gets its own Packet subclass. Add a new Objective-C class to the project, named PacketServerReady, subclass of Packet. Replace the new .h file with:

#import "Packet.h"

@interface PacketServerReady : Packet

@property (nonatomic, strong) NSMutableDictionary *players;

+ (id)packetWithPlayers:(NSMutableDictionary *)players;

@end

And replace the .m file with:

#import "PacketServerReady.h"
#import "NSData+SnapAdditions.h"
#import "Player.h"

@implementation PacketServerReady

@synthesize players = _players;

+ (id)packetWithPlayers:(NSMutableDictionary *)players
{
	return [[[self class] alloc] initWithPlayers:players];
}

- (id)initWithPlayers:(NSMutableDictionary *)players
{
	if ((self = [super initWithType:PacketTypeServerReady]))
	{
		self.players = players;
	}
	return self;
}

- (void)addPayloadToData:(NSMutableData *)data
{
	[data rw_appendInt8:[self.players count]];

	[self.players enumerateKeysAndObjectsUsingBlock:^(id key, Player *player, BOOL *stop)
	{
		[data rw_appendString:player.peerID];
		[data rw_appendString:player.name];
		[data rw_appendInt8:player.position];
	}];
}

@end

When you create the PacketServerReady object, you pass it the dictionary of Player objects. addPayloadToData: loops through this dictionary and for each Player, it appends to the mutable data the peer ID (a string), the player’s name (also a string), and the player’s position (an 8-bit integer, which is big enough to fit all the possible values from the PlayerPosition enum).

In Game.m, add an import for this new class:

#import "PacketServerReady.h"

Then in serverReceivedPacket:fromPlayer:, change the case-statement to:

		case PacketTypeSignInResponse:
			if (_state == GameStateWaitingForSignIn)
			{
				player.name = ((PacketSignInResponse *)packet).playerName;

				if ([self receivedResponsesFromAllPlayers])
				{
					_state = GameStateWaitingForReady;

					Packet *packet = [PacketServerReady packetWithPlayers:_players];
					[self sendPacketToAllClients:packet];
				}
			}
			break;

This is mostly the same as before, except now you send that new PacketServerReady message to all clients, after they’ve all signed in. Run the app and check the debug output on the clients. Testing with two devices and the simulator, both of my clients received the following packet:

Game: receive data from peer: 1371535819, data: <534e4150 00000000 00660331 32393632 38353131 37004d61 74746869 6a732069 50616420 32000231 33373135 33353831 3900636f 6d2e686f 6c6c616e 63652e53 6e617033 35363234 32393639 2e313034 32363100 00313339 32313930 36393000 4d617474 68696a73 2069506f 640001>, length: 111

(The clients also complain about this being an invalid packet, but that’s because you haven’t yet built in the support for this packet type.)

In Hex Fiend, this looks like:

ServerReady packet in Hex Fiend

The first 10 bytes are as usual (SNAP, the so-far-unused packet number field, and the packet type, which is now 0x66). The next byte, highlighted in the screenshot, is the number of players that have signed on to the game. The information for each of the players follows.

For the first player in this list, the information is comprised of “1296285117” (its peer ID), followed by a NUL-byte to terminate the string, then “Matthijs iPad 2,” also followed by a NUL-byte, and then an 8-bit number with the value 0x02, which is this player’s position, PlayerPositionRight. You should be able to decode the bytes for the other two players yourself.

It’s time to build-in the client side of things to support this new Packet. First of all, you have to add a case-statement to packetWithData:. Open Packet.m and add an import to the top:

#import "PacketServerReady.h"

Then, inside packetWithData:, add a new case-statement:

		case PacketTypeServerReady:
			packet = [PacketServerReady packetWithData:data];
			break;

Of course, you still need to override the packetWithData: method in the subclass, so open PacketServerReady.m and add the following:

+ (id)packetWithData:(NSData *)data
{
	NSMutableDictionary *players = [NSMutableDictionary dictionaryWithCapacity:4];

	size_t offset = PACKET_HEADER_SIZE;
	size_t count;

	int numberOfPlayers = [data rw_int8AtOffset:offset];
	offset += 1;

	for (int t = 0; t < numberOfPlayers; ++t)
	{
		NSString *peerID = [data rw_stringAtOffset:offset bytesRead:&count];
		offset += count;

		NSString *name = [data rw_stringAtOffset:offset bytesRead:&count];
		offset += count;

		PlayerPosition position = [data rw_int8AtOffset:offset];
		offset += 1;

		Player *player = [[Player alloc] init];
		player.peerID = peerID;
		player.name = name;
		player.position = position;
		[players setObject:player forKey:player.peerID];
	}

	return [[self class] packetWithPlayers:players];
}

Remember how on the server, the dictionary of Player objects was created in the startServerGameWithSession:playerName:clients: method? Well, on the client, the Player objects are created right here, using the information the server sent us.

For each player, you read the peer ID, player name, and position from the NSData object, use these to create a new Player object, and add the new Player to a dictionary using the peer ID as the key. The client will now have the same Player objects as the server.

In Game.m, add the following case-statement to clientReceivedPacket:

		case PacketTypeServerReady:
			if (_state == GameStateWaitingForReady)
			{
				_players = ((PacketServerReady *)packet).players;
				
				NSLog(@"the players are: %@", _players);
			}
			break;

Now run the game again. The client should print out its new _players dictionary:

Snap[327:707] the players are: {
    1966531510 = "<Player: 0x19ce80> peerID = 1966531510, name = Matthijs iPod, position = 1";
    2094524878 = "<Player: 0x194fc0> peerID = 2094524878, name = com.hollance.Snap356243785.503477, position = 0";
    313323322 = "<Player: 0x1982c0> peerID = 313323322, name = Matthijs iPad 2, position = 2";
}

Contributors

Over 300 content creators. Join our team.