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.
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 4
50 mins
Handling the Exit Button
Clients (as well as the server) can get disconnected in two ways:
- There is a networking problem and Game Kit drops the connection.
- The player leaves the game on purpose by tapping the exit button.
You've already handled the first situation. It's time to improve how you deal with the second.
Right now, pressing the exit button works. If you do it as the server, the GKSession object gets destroyed and all clients are automatically disconnected. Exiting as the client destroys its GKSession , and the server sees the disconnect and informs the other clients.
The only problem is, there's no way to distinguish between an intentional disconnect (pressing the exit button) and an accidental one (network error). It sometimes it takes a while for the disconnect to be recognized.
To solve this, you will send a "quit" packet to the other players to inform them that this user is leaving the game on purpose. Change GameViewController's exitAction: to the following:
- (IBAction)exitAction:(id)sender
{
if (self.game.isServer)
{
_alertView = [[UIAlertView alloc]
initWithTitle:NSLocalizedString(@"End Game?", @"Alert title (user is host)")
message:NSLocalizedString(@"This will terminate the game for all other players.", @"Alert message (user is host)")
delegate:self
cancelButtonTitle:NSLocalizedString(@"No", @"Button: No")
otherButtonTitles:NSLocalizedString(@"Yes", @"Button: Yes"),
nil];
[_alertView show];
}
else
{
_alertView = [[UIAlertView alloc]
initWithTitle: NSLocalizedString(@"Leave Game?", @"Alert title (user is not host)")
message:nil
delegate:self
cancelButtonTitle:NSLocalizedString(@"No", @"Button: No")
otherButtonTitles:NSLocalizedString(@"Yes", @"Button: Yes"),
nil];
[_alertView show];
}
}
Before you allow the user to quit, the game will display an alert popup asking for confirmation. Notice that _alertView is a new instance variable, so add that to the declaration section at the top of the file:
@implementation GameViewController
{
UIAlertView *_alertView;
}
I will explain why you do this shortly. The GameViewController must become the delegate for the UIAlertView, but the protocol declaration is already in the @interface line, so that saves some time. You do have to add the implementation of the method to the view controller:
#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (buttonIndex != alertView.cancelButtonIndex)
{
[self.game quitGameWithReason:QuitReasonUserQuit];
}
}
This simply exits the game when the user taps the OK button. In Game.m, change the quitGameWithReason: method to:
- (void)quitGameWithReason:(QuitReason)reason
{
_state = GameStateQuitting;
if (reason == QuitReasonUserQuit)
{
if (self.isServer)
{
Packet *packet = [Packet packetWithType:PacketTypeServerQuit];
[self sendPacketToAllClients:packet];
}
else
{
Packet *packet = [Packet packetWithType:PacketTypeClientQuit];
[self sendPacketToServer:packet];
}
}
[_session disconnectFromAllPeers];
_session.delegate = nil;
_session = nil;
[self.delegate game:self didQuitWithReason:reason];
}
Just before you end the session, you send a PacketTypeServerQuit or a PacketTypeClientQuit message, depending on whether you're the server or a client.
Add the following case-statement in clientReceivedPacket:
case PacketTypeServerQuit:
[self quitGameWithReason:QuitReasonServerQuit];
break;
And add the following in serverReceivedPacket:fromPlayer:
case PacketTypeClientQuit:
[self clientDidDisconnect:player.peerID];
break;
In Packet.m, add these two new packet types to the switch-statement in packetWithData:
switch (packetType)
{
case PacketTypeSignInRequest:
case PacketTypeClientReady:
case PacketTypeServerQuit:
case PacketTypeClientQuit:
packet = [Packet packetWithType:packetType];
break;
. . .
That should do it. Try it out. Now when the server quits, clients should not get the "you're disconnected" alert (because the quit reason is QuitReasonServerQuit and not QuitReasonConnectionDropped). Also try exiting a client.
There's one problematic edge case that I found when testing. What happens if a client taps the exit button and the alert view is showing, and at the same time that client gets disconnected?
You can try it for yourself. Tap the exit button on the client but leave the alert view on the screen, then kill the app on the server. After a few seconds, the client returns to the main screen and displays a new alert popup to inform you that you've been disconnected.
At this point, or after you've tapped OK, the app may crash. I'm not sure why, but it seems the two alert views are interfering with each other. The workaround is simple: in GameViewController's viewWillDisappear:, you''ll dismiss the alert view if it's still showing:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[_alertView dismissWithClickedButtonIndex:_alertView.cancelButtonIndex animated:NO];
}
Tip: Try to think through all weird situations your app might be in during a network disconnect, because it can happen at any time!
Tip: Try to think through all weird situations your app might be in during a network disconnect, because it can happen at any time!
Just to cover all the bases, the GKSessionDelegate has another method that you should implement, session:didFailWithError:. Replace this method in Game.m with:
- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
#ifdef DEBUG
NSLog(@"Game: session failed %@", error);
#endif
if ([[error domain] isEqualToString:GKSessionErrorDomain])
{
if (_state != GameStateQuitting)
{
[self quitGameWithReason:QuitReasonConnectionDropped];
}
}
}
If a fatal Game Kit error occurs, then you exit the game. This will still try to send a Quit packet, although that probably won't succeed.
Note: If the app goes into the background and stays there for too long (more than a few seconds), the Game Kit connection is typically dropped. When you return to the game, it recognizes this (in the GKSession delegate methods), and the game will exit.
You can also handle this more intelligently. If your game allows peers to reconnect after the connection has been dropped, then you'll need to create a new GKSession object and set up the connections again. Unfortunately, GKSession doesn't do this automatically.
Note that the server will never get a "you're disconnected" message, only the clients; the client session gets destroyed when it is disconnected, but the server keeps the session alive until you exit the screen.
Note: If the app goes into the background and stays there for too long (more than a few seconds), the Game Kit connection is typically dropped. When you return to the game, it recognizes this (in the GKSession delegate methods), and the game will exit.
You can also handle this more intelligently. If your game allows peers to reconnect after the connection has been dropped, then you'll need to create a new GKSession object and set up the connections again. Unfortunately, GKSession doesn't do this automatically.
Note that the server will never get a "you're disconnected" message, only the clients; the client session gets destroyed when it is disconnected, but the server keeps the session alive until you exit the screen.