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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make a Simple Playing Card Game with Multiplayer and Bluetooth, Part 3
55 mins
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:
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.
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:
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.