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

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 part of the series, you created the main menu and the basics for the host and join game screens.

In second part of the series, you implemented the join/host game logic, complete with elegant disconnection handling.

In this third part of the series, we’ll implement the ability for the client and server to send messages to each other. In addition, we’ll start creating our Game’s model class, the game setup code, and more. Let’s deal back in!

Getting Started (With the Game!)

The game properly starts when the player who hosts the session taps the Start button on the Host Game screen. The server will then send messages to the clients – these are packets of data that will fly over the Bluetooth or Wi-Fi network – that instruct the clients to get ready.

These network packets are received by what is known as a “data receive handler.” You have to give GKSession an object that will handle any incoming packets. It’s a bit like a delegate, but it doesn’t have its own @protocol.

Here’s the trick: the server needs to send the clients a bunch of messages before the game can begin, but you don’t really want to make JoinViewController the data-receive-handler for GKSession. The main game logic will be handled by a data model class named Game, and you want this Game class to be the one to handle the packets from GKSession. Therefore, on the client side, the game can start as soon as the client connects to the server. (If that doesn’t make any sense to you, it will soon.)

Add the following method signature to the MatchmakingClient’s delegate protocol:

- (void)matchmakingClient:(MatchmakingClient *)client didConnectToServer:(NSString *)peerID;

You’ll call this method as soon as the client connects to the server. Add the code to do this in session:peer:didChangeState: in MatchmakingClient.m:

		. . .

		// We're now connected to the server.
		case GKPeerStateConnected:
			if (_clientState == ClientStateConnecting)
			{
				_clientState = ClientStateConnected;
				[self.delegate matchmakingClient:self didConnectToServer:peerID];
			}
			break;

		. . .

That delegate method should be implemented by JoinViewController, so add it there:

- (void)matchmakingClient:(MatchmakingClient *)client didConnectToServer:(NSString *)peerID
{
	NSString *name = [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
	if ([name length] == 0)
		name = _matchmakingClient.session.displayName;

	[self.delegate joinViewController:self startGameWithSession:_matchmakingClient.session playerName:name server:peerID];
}

Here you first get the name of the player from the text field (stripped from any whitespace). Then you call a new delegate method to let MainViewController know it has to start the game for this client.

Add the declaration of this new delegate method to JoinViewControllerDelegate:

- (void)joinViewController:(JoinViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID;

Notice that you pass three important pieces of data to the MainViewController: the GKSession object, which you’ll need to use in the new Game class to communicate with the server; the name of the player; and the peerID of the server. (You could get the server’s peer ID from the GKSession object, but this is just as easy.)

The body of this method goes into MainViewController.m:

- (void)joinViewController:(JoinViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID
{
	_performAnimations = NO;

	[self dismissViewControllerAnimated:NO completion:^
	{
		_performAnimations = YES;

		[self startGameWithBlock:^(Game *game)
		{
			[game startClientGameWithSession:session playerName:name server:peerID];
		}];
	}];
}

What is all that? Let’s begin with the _performAnimations variable. This is a new ivar that needs to be added at the top of the source file:

@implementation MainViewController
{
	. . .
	BOOL _performAnimations;
}

Remember the cool animation from the main screen, with the logo cards flying into the screen and the buttons fading in? That animation happens any time the main screen becomes visible, including when a modally-presented view controller closes.

When starting a new game, though, you don’t want the main screen to do any animations. You want to immediately switch from the Join Game screen to the actual game screen where all the action happens. The _performAnimations variable simply controls whether the flying-cards animation is supposed to happen or not.

To set to default value of _performAnimations to YES, override initWithNibName:

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
	if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]))
	{
		_performAnimations = YES;
	}
	return self;
}

Change viewWillAppear: and viewDidAppear: to the following:

- (void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear:animated];

	if (_performAnimations)
		[self prepareForIntroAnimation];
}

- (void)viewDidAppear:(BOOL)animated
{
	[super viewDidAppear:animated];

	if (_performAnimations)
		[self performIntroAnimation];
}

So back to explaining the code you added earlier. When the startClientGameWithSession… method is called, you disable the animations by setting _performAnimations to NO. Then you dismiss the Join Game screen, set _performAnimations back to YES, and do this:

		[self startGameWithBlock:^(Game *game)
		{
			[game startClientGameWithSession:session playerName:name server:peerID];
		}];

This probably requires more explanation. You’re going to make a new Game class that serves as the data model for the game, and a GameViewController class that manages the game screen.

There are three ways to start a new game: as the server, as a client, or in single-player mode. The main setup for these three game types is the same, except for how the Game object will be initialized. startGameWithBlock: takes care of all the shared details, and the block you give it does the things that are specific to the type of the game.

In this case, because you’re the client, you call startClientGameWithSession:playerName:server: on the Game object to get it started. But before you can do all that, you must first write some code for that new Game object and the GameViewController.

Begin by adding startGameWithBlock:.

- (void)startGameWithBlock:(void (^)(Game *))block
{
	GameViewController *gameViewController = [[GameViewController alloc] initWithNibName:@"GameViewController" bundle:nil];
	gameViewController.delegate = self;

	[self presentViewController:gameViewController animated:NO completion:^
	{
		Game *game = [[Game alloc] init];
		gameViewController.game = game;
		game.delegate = gameViewController;
		block(game);
	}];
}

None of this compiles yet, but you can see what it’s supposed to do: allocate the GameViewController, present it, and then allocate the Game object. Finally, it calls your block to do the game-type specific initializations.

Even though these files don’t exist yet, go ahead and add imports for them to MainViewController.h:

#import "GameViewController.h"

And add to MainViewController.m:

#import "Game.h"

While you’re at it, you might as well add the still-illusive GameViewControllerDelegate to MainViewController’s @interface:

@interface MainViewController : UIViewController <HostViewControllerDelegate, JoinViewControllerDelegate, GameViewControllerDelegate>

Now add a new Objective-C class to the project, subclass of UIViewController, named GameViewController. No XIB is necessary – it’s already been provided in the starter code download from part one. Drag GameViewController.xib (from the “Snap/en.lproj/” folder) into the project. Replace the contents of GameViewController.h with the following:

#import "Game.h"

@class GameViewController;

@protocol GameViewControllerDelegate <NSObject>

- (void)gameViewController:(GameViewController *)controller didQuitWithReason:(QuitReason)reason;

@end

@interface GameViewController : UIViewController <UIAlertViewDelegate, GameDelegate>

@property (nonatomic, weak) id <GameViewControllerDelegate> delegate;
@property (nonatomic, strong) Game *game;

@end

Pretty simple. You’ve declared a new delegate protocol, GameViewControllerDelegate, with a single method that is used to let the MainViewController know the game should end. The GameViewController itself is the delegate for the Game object. Lots of delegates everywhere.

The first version of GameViewController.xib that you’ll use is very simple. It only has an exit button and a single label in the center of the screen.

The simple version of the GameViewController nib

Replace the contents of GameViewController.m with the following:

#import "GameViewController.h"
#import "UIFont+SnapAdditions.h"

@interface GameViewController ()

@property (nonatomic, weak) IBOutlet UILabel *centerLabel;

@end

@implementation GameViewController

@synthesize delegate = _delegate;
@synthesize game = _game;

@synthesize centerLabel = _centerLabel;

- (void)dealloc
{
	#ifdef DEBUG
	NSLog(@"dealloc %@", self);
	#endif
}

- (void)viewDidLoad
{
	[super viewDidLoad];

	self.centerLabel.font = [UIFont rw_snapFontWithSize:18.0f];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
	return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}

#pragma mark - Actions

- (IBAction)exitAction:(id)sender
{
	[self.game quitGameWithReason:QuitReasonUserQuit];
}

#pragma mark - GameDelegate

- (void)game:(Game *)game didQuitWithReason:(QuitReason)reason
{
	[self.delegate gameViewController:self didQuitWithReason:reason];
}

@end

Nothing too exciting going on here. exitAction: tells the Game object to quit, and the Game object responds by calling game:didQuitWithReason:. MainViewController is the delegate for GameViewController, so that’s where you should implement gameViewController:didQuitGameWithReason::

#pragma mark - GameViewControllerDelegate

- (void)gameViewController:(GameViewController *)controller didQuitWithReason:(QuitReason)reason
{
	[self dismissViewControllerAnimated:NO completion:^
	{
		if (reason == QuitReasonConnectionDropped)
		{
			[self showDisconnectedAlert];
		}
	}];
}

That also looks very familiar. You close the game screen and show an error alert view if necessary.

This leaves just the Game object to be implemented.

Contributors

Over 300 content creators. Join our team.