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
You are currently viewing page 5 of 5 of this article. Click here to view the first page.

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.