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

Handling Disconnects

Before you can build the fun part of the game, there is something you still need to deal with. Because networking is unpredictable, clients may get disconnected from the server at any time. A player wanders out of range, the network has too high a packet loss, a freak solar flare passes through the galaxy, or any other number of things can make this happen.

You should deal with such sudden disconnects gracefully, so that they don't interrupt the game too much. That's what you'll do in this section.

Recalling MatchmakingServer, then may remember that GKSession lets its delegate know about new connections and disconnects in the session:peer:didChangeState: method. This method is already part of the Game class, but it doesn't do anything yet. Replace that method in Game.m with:

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
	#ifdef DEBUG
	NSLog(@"Game: peer %@ changed state %d", peerID, state);
	#endif
	
	if (state == GKPeerStateDisconnected)
	{
		if (self.isServer)
		{
			[self clientDidDisconnect:peerID];
		}
	}
}

Pretty simple. If you are the server and the peer is now in state GKPeerStateDisconnected, then you call the new clientDidDisconnect: method. Add that method as well:

- (void)clientDidDisconnect:(NSString *)peerID
{
	if (_state != GameStateQuitting)
	{
		Player *player = [self playerWithPeerID:peerID];
		if (player != nil)
		{
			[_players removeObjectForKey:peerID];
			[self.delegate game:self playerDidDisconnect:player];
		}
	}
}

This simply looks up the Player object and removes it from the _players dictionary. Note that you don't do anything here if the game is already in the process of quitting (GameStateQuitting), but more about that later. You also have to add a new delegate method to GameDelegate, in Game.h:

- (void)game:(Game *)game playerDidDisconnect:(Player *)disconnectedPlayer;

Add the implementation of this method in GameViewController.m:

- (void)game:(Game *)game playerDidDisconnect:(Player *)disconnectedPlayer
{
	[self hidePlayerLabelsForPlayer:disconnectedPlayer];
	[self hideActiveIndicatorForPlayer:disconnectedPlayer];
	[self hideSnapIndicatorForPlayer:disconnectedPlayer];
}

This requires three new methods, so add them as well:

- (void)hidePlayerLabelsForPlayer:(Player *)player
{
	switch (player.position)
	{
		case PlayerPositionBottom:
			self.playerNameBottomLabel.hidden = YES;
			self.playerWinsBottomLabel.hidden = YES;
			break;
		
		case PlayerPositionLeft:
			self.playerNameLeftLabel.hidden = YES;
			self.playerWinsLeftLabel.hidden = YES;
			break;
		
		case PlayerPositionTop:
			self.playerNameTopLabel.hidden = YES;
			self.playerWinsTopLabel.hidden = YES;
			break;
		
		case PlayerPositionRight:
			self.playerNameRightLabel.hidden = YES;
			self.playerWinsRightLabel.hidden = YES;
			break;
	}
}

- (void)hideActiveIndicatorForPlayer:(Player *)player
{
	switch (player.position)
	{
		case PlayerPositionBottom: self.playerActiveBottomImageView.hidden = YES; break;
		case PlayerPositionLeft:   self.playerActiveLeftImageView.hidden   = YES; break;
		case PlayerPositionTop:    self.playerActiveTopImageView.hidden    = YES; break;
		case PlayerPositionRight:  self.playerActiveRightImageView.hidden  = YES; break;
	}
}

- (void)hideSnapIndicatorForPlayer:(Player *)player
{
	switch (player.position)
	{
		case PlayerPositionBottom: self.snapIndicatorBottomImageView.hidden = YES; break;
		case PlayerPositionLeft:   self.snapIndicatorLeftImageView.hidden   = YES; break;
		case PlayerPositionTop:    self.snapIndicatorTopImageView.hidden    = YES; break;
		case PlayerPositionRight:  self.snapIndicatorRightImageView.hidden  = YES; break;
	}
}

OK, try it out. Run the game, preferably with two or more clients, and wait until they've all signed in. Then exit the app on one of the clients. The name of that player should now disappear from the screen of the server. However, it doesn't yet disappear from the screen of the other client(s).

To make that happen, the server needs to let the remaining clients know that one of the other players has disconnected. For that, you'll introduce a new packet type: PacketTypeOtherClientQuit.

Change clientDidDisconnect: in Game.m to:

- (void)clientDidDisconnect:(NSString *)peerID
{
	if (_state != GameStateQuitting)
	{
		Player *player = [self playerWithPeerID:peerID];
		if (player != nil)
		{
			[_players removeObjectForKey:peerID];
			
			if (_state != GameStateWaitingForSignIn)
			{
				// Tell the other clients that this one is now disconnected.
				if (self.isServer)
				{
					PacketOtherClientQuit *packet = [PacketOtherClientQuit packetWithPeerID:peerID];
					[self sendPacketToAllClients:packet];
				}			

				[self.delegate game:self playerDidDisconnect:player];
			}
		}
	}
}

You now create a PacketOtherClientQuit packet and send it to all clients. Note, however, that you don't do this if you're in the initial "waiting for sign-in" state. At that point, the clients don't know anything about each other, so there's no need to tell them about another player's disconnect.

Add an import for this new Packet subclass:

#import "PacketOtherClientQuit.h"

This class doesn't exist yet, so add a new Objective-C class to the project named PacketOtherClientQuit, subclass of Packet. Replace the contents of the new .h file with:

#import "Packet.h"

@interface PacketOtherClientQuit : Packet

@property (nonatomic, copy) NSString *peerID;

+ (id)packetWithPeerID:(NSString *)peerID;

@end

Replace the .m file with:

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

@implementation PacketOtherClientQuit

@synthesize peerID = _peerID;

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

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

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

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

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

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

@end

This is very similar to what you've seen before. Simply add the contents of the peerID variable to the NSMutableData object. Don't forget that you also need to tell the Packet superclass about this new type. Add an import to Packet.m:

#import "PacketOtherClientQuit.h"

And a new case-statement to packetWithData:

		case PacketTypeOtherClientQuit:
			packet = [PacketOtherClientQuit packetWithData:data];
			break;

Now all that remains is reading this new packet on the client side of things. In Game.m's clientReceivedPacket:, add a new case-statement:

		case PacketTypeOtherClientQuit:
			if (_state != GameStateQuitting)
			{
				PacketOtherClientQuit *quitPacket = ((PacketOtherClientQuit *)packet);
				[self clientDidDisconnect:quitPacket.peerID];
			}	
			break;

Here you call the same clientDidDisconnect: method that was used on the server, and as a result, the player should disappear from the screen. Try it out! (Note: You need to try this with at least two clients, because you need to disconnect one of those clients to see the effect.)

Note: In this tutorial, when a client drops the connection, that client is removed from the game. You could come up with an alternative scheme that allows clients to reconnect, if that makes sense for your game, but it won't be covered here.

If you tried disconnecting the server, then you'll have seen that the client didn't react at all. The GKSessionDelegate method does recognize that the server went away (in the debug output it says, "peer XXX changed state 3," which means that peer disconnected), but the game doesn't quit. That's a fairly easy fix. Replace the Game.m's session:peer:didChangeState: GKSession delegate method with:

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
	#ifdef DEBUG
	NSLog(@"Game: peer %@ changed state %d", peerID, state);
	#endif
	
	if (state == GKPeerStateDisconnected)
	{
		if (self.isServer)
		{
			[self clientDidDisconnect:peerID];
		}
		else if ([peerID isEqualToString:_serverPeerID])
		{
			[self quitGameWithReason:QuitReasonConnectionDropped];
		}
	}
}

The else-if clause checks to see whether it was the server that got disconnected, and if so calls the quitGameWithReason: method that you already have.

Note that it's necessary to check for the server's peer ID, because Game Kit also connects all the clients to all the other clients. You don't do anything with those additional connections in this particular app, but it does mean you get a "disconnected" mention when one of those clients disappears. As you've just seen, those other-client disconnects aren't handled here, but with the PacketOtherClientQuit message.

Now the client should return to the main screen with a disconnected message after the server disappears. Try it out.

Disconnected alert

Contributors

Over 300 content creators. Join our team.