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

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, second, third, fourth, fifth, and sixth parts of the series, you created most of the game, including the networking infrastructure, card animations, and gameplay.

In this seventh and final part of the series, you will finally wrap up the game! We will finish up the “Snap!” logic, fix some edge cases, add win/lose detection, and even implement a single player mode with a computer player!

Keep reading to finally finish this epic series!

Reliable vs unreliable

So far all the network transmissions you’ve sent were in reliable mode, which guarantees messages will be delivered with their contents 100% intact.

The problem with reliable mode is that on a bad network connection that drops a lot of packets, message sending can be very slow. If a message couldn’t be delivered, the networking stack will try again and again and again, until it either gives up completely and disconnects or the message is successfully delivered.

Also, even though the packets are guaranteed to arrive, the problem is the order in which they arrive is not guaranteed. You’ve seen the problems this can cause and how to work around them in the previous part of the series.

How to deal with these issues? Well, there is another method of sending data over the network and that is in unreliable mode.

With unreliable mode the only guarantee is that if the message arrives it will be 100% complete, but it’s sent only once and if something goes wrong along the way, the entire message is dropped. The recipient will never know about it.

When you’re writing a multiplayer network game that needs to be real-time, you’d usually send unreliable messages, because you don’t need the retry mechanism. By the time the retried message arrives at its destination it is likely no longer relevant. Therefore it’s better to send everything unreliably and hope that it arrives, and have some mechanism in place to deal with messages that get lost.

Tip: In a real-time game that sends position updates using unreliable packets, you’d use a technique called “dead reckoning” to estimate where the game objects will be next, and then adjust when you receive the next network message. Every second or so a larger packet with a full status update is sent, just so that clients are able to bring themselves up-to-date and get the complete picture again.

In Snap! you want the PlayerShouldSnap packets to arrive at the server as quickly as possible. Therefore you will now send them unreliably in order to lose as little time as possible.

This means that sometimes when a player taps the Snap! button their packets will get lost along the way. C’est la vie. Most of the time the packets will arrive properly anyway.

You’ll add a new property to Packet that indicates whether this packet should be sent reliably or unreliably. In Packet.h:

@property (nonatomic, assign) BOOL sendReliably;

Synthesize this property in Packet.m:

@synthesize sendReliably = _sendReliably;

Set its value to YES in the init method because by default you want all packets to be sent reliably:

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

The only packet you will send unreliably in our app is PacketPlayerShouldSnap. Change its init method to:

- (id)initWithPeerID:(NSString *)peerID
{
	if ((self = [super initWithType:PacketTypePlayerShouldSnap]))
	{
		self.peerID = peerID;
		self.sendReliably = NO;
	}
	return self;
}

The actual sending of the packets happens in two places in Game.m, in the sendPacketToAllClients: and sendPacketToServer: methods. In both methods change the line that initializes the dataMode variable to:

	GKSendDataMode dataMode = packet.sendReliably ? GKSendDataReliable : GKSendDataUnreliable;

And that’s it. You should test the app to see that tapping the Snap! button on the client still shows the red X on the server, but you won’t notice any differences in how the app behaves unless you’re on a really crappy network.

“Too late”

The first player to yell “Snap!” wins (or has to pay up if there are no matching cards). For any other players who yell “Snap!” too late, you simply show the speech bubble. To keep track of this you use a new boolean instance variable:

@implementation Game
{
	. . .
	BOOL _haveSnap;
}

You reset this value to NO at the top of beginRound:

- (void)beginRound
{
	_haveSnap = NO;
	. . .
}

And in turnCardForPlayer:

- (void)turnCardForPlayer:(Player *)player
{
	_haveSnap = NO;
	. . .
}

That means after the active player has turned over his card, everyone can yell “Snap!” again.

Finally, playerCalledSnap: becomes:

- (void)playerCalledSnap:(Player *)player
{
	if (self.isServer)
	{
		if (_haveSnap)
		{
			[self.delegate game:self playerCalledSnapTooLate:player];
		}
		else
		{
			_haveSnap = YES;

			[self.delegate game:self playerCalledSnapWithNoMatch:player];
		}
	}
	else
	{
		Packet *packet = [PacketPlayerShouldSnap packetWithPeerID:_session.peerID];
		[self sendPacketToServer:packet];
	}
}

There is now a new delegate method, so add it to the protocol in Game.h:

- (void)game:(Game *)game playerCalledSnapTooLate:(Player *)player;

Add the implementation to GameViewController.m:

- (void)game:(Game *)game playerCalledSnapTooLate:(Player *)player
{
	[self showSnapIndicatorForPlayer:player];
	[self performSelector:@selector(hideSnapIndicatorForPlayer:) withObject:player afterDelay:1.0f];
}

Here you just show the speech bubble and then hide it again after one second. Try it out, for the second player who taps the Snap! button you only show the speech bubble but no red X. Again, for now the speech bubbles will only appear on the server but you’ll remedy that next.

Showing the speech bubbles on the clients

Right now when a client yells snap, the server doesn’t notify the other clients that this has occurred – hence they can’t update their displays.

You’ll fix this now by making the server send a packet to inform all clients when someone yells “Snap!” — even the client that did so.

This packet will also tell the clients whether it was a good snap (there were matching cards), a bad snap (no matches), or whether it was too late (another player beat you to it). The way you implement this game, it is the task of the server to make that judgement and tell it to the clients.

Add a new Packet subclass to the project, named PacketPlayerCalledSnap. Replace PacketPlayerCalledSnap.h with the following:

#import "Packet.h"

typedef enum
{
	SnapTypeWrong = 1,
	SnapTypeTooLate,
	SnapTypeCorrect,
}
SnapType;

@interface PacketPlayerCalledSnap : Packet

@property (nonatomic, copy) NSString *peerID;
@property (nonatomic, assign) SnapType snapType;
@property (nonatomic, strong) NSSet *matchingPeerIDs;

+ (id)packetWithPeerID:(NSString *)peerID snapType:(SnapType)snapType matchingPeerIDs:(NSSet *)matchingPeerIDs;

@end

The snapType property determines whether the call was good, bad or too late. The matchingPeerIDs property contains a list of the players with matching cards; you’ll ignore this property for now.

Replace PacketPlayerCalledSnap.m with:

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

@implementation PacketPlayerCalledSnap

@synthesize peerID = _peerID;
@synthesize snapType = _snapType;
@synthesize matchingPeerIDs = _matchingPeerIDs;

+ (id)packetWithPeerID:(NSString *)peerID snapType:(SnapType)snapType matchingPeerIDs:(NSSet *)matchingPeerIDs
{
	return [[[self class] alloc] initWithPeerID:peerID snapType:snapType matchingPeerIDs:matchingPeerIDs];
}

- (id)initWithPeerID:(NSString *)peerID snapType:(SnapType)snapType matchingPeerIDs:(NSSet *)matchingPeerIDs
{
	if ((self = [super initWithType:PacketTypePlayerCalledSnap]))
	{
		self.peerID = peerID;
		self.snapType = snapType;
		self.matchingPeerIDs = matchingPeerIDs;
	}
	return self;
}

+ (id)packetWithData:(NSData *)data
{
	size_t offset = PACKET_HEADER_SIZE;
	size_t count;

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

	SnapType snapType = [data rw_int8AtOffset:offset];

	NSMutableSet *matchingPeerIDs = nil;

	return [[self class] packetWithPeerID:peerID snapType:snapType matchingPeerIDs:matchingPeerIDs];
}

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

@end

In Packet.m, import the new file:

#import "PacketPlayerCalledSnap.h"

And add a case-statement to packetWithData:

		case PacketTypePlayerCalledSnap:
			packet = [PacketPlayerCalledSnap packetWithData:data];
			break;

Also add an import to Game.m:

#import "PacketPlayerCalledSnap.h"

Then change the top of playerCalledSnap: to:

- (void)playerCalledSnap:(Player *)player
{
	if (self.isServer)
	{
		if (_haveSnap)
		{
			Packet *packet = [PacketPlayerCalledSnap packetWithPeerID:player.peerID snapType:SnapTypeTooLate matchingPeerIDs:nil];
			[self sendPacketToAllClients:packet];

			[self.delegate game:self playerCalledSnapTooLate:player];
		}
		else
		{
			_haveSnap = YES;

			Packet *packet = [PacketPlayerCalledSnap packetWithPeerID:player.peerID snapType:SnapTypeWrong matchingPeerIDs:nil];
			[self sendPacketToAllClients:packet];

			[self.delegate game:self playerCalledSnapWithNoMatch:player];
		}
	}
	else
	{
		Packet *packet = [PacketPlayerShouldSnap packetWithPeerID:_session.peerID];
		[self sendPacketToServer:packet];
	}
}

You send the PacketPlayerCalledSnap packet with type “TooLate” to all clients. To handle this packet on the client side, add a new case-statement to clientReceivedPacket:

		case PacketTypePlayerCalledSnap:
			if (_state == GameStatePlaying)
			{
				[self handlePlayerCalledSnapPacket:(PacketPlayerCalledSnap *)packet];
			}
			break;

This uses a new method, handlePlayerCalledSnapPacket:, so add that as well:

- (void)handlePlayerCalledSnapPacket:(PacketPlayerCalledSnap *)packet
{
	NSString *peerID = packet.peerID;
	SnapType snapType = packet.snapType;

	Player *player = [self playerWithPeerID:peerID];
	if (player != nil)
	{
		if (snapType == SnapTypeTooLate)
		{
			[self.delegate game:self playerCalledSnapTooLate:player];
		}
		else if (snapType == SnapTypeWrong)
		{
			[self.delegate game:self playerCalledSnapWithNoMatch:player];
		}
	}
}

This simply calls the same delegate methods as the server, but now that happens only after receiving a packet from the server, not immediately when the Snap! button is tapped. Try it out. Tapping the Snap! button, either on the client or on a server, should show the speech bubbles (and the big red X) on the client as well.

Contributors

Over 300 content creators. Join our team.