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

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. Card games are quite popular on the App Store – over 2,500 apps and counting – so it’s about time that raywenderlich.com shows you how to make one! In addition, […] By Matthijs Hollemans.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

Exiting the Host Game Screen

Speaking of the buttons, they don’t do anything yet. Leave the Start button alone for now, but it would be nice if you could return to the main screen by tapping the X in the bottom-left corner.

This button is hooked up to exitAction:, which is currently empty. It should close the screen, and you’ll implement this using a delegate protocol. There will be several view controllers in this app, and they will communicate with one another through delegates to keep the dependencies minimal and clean.

Add the following to HostViewController.h, above the @interface line:

@class HostViewController;

@protocol HostViewControllerDelegate <NSObject>

- (void)hostViewControllerDidCancel:(HostViewController *)controller;

@end

Inside the @interface section, add a new property:

@property (nonatomic, weak) id <HostViewControllerDelegate> delegate;

Properties need to be synthesized, so in HostViewController.m, do:

@synthesize delegate = _delegate;

Finally, replace exitAction: with:

- (IBAction)exitAction:(id)sender
{
	[self.delegate hostViewControllerDidCancel:self];
}

The idea should be clear: you’ve declared a delegate protocol for the HostViewController. When the exit button is tapped, the HostViewController tells its delegate that the Host Game screen has been cancelled. The delegate is then responsible for closing the screen.

In this case, the role of the delegate is played by the MainViewController, of course. Change the following line in MainViewController.h:

@interface MainViewController : UIViewController <HostViewControllerDelegate>

In MainViewController.m, the hostGameAction: method becomes:

- (IBAction)hostGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		[self performExitAnimationWithCompletionBlock:^(BOOL finished)
		{	
			HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];
			controller.delegate = self;

			[self presentViewController:controller animated:NO completion:nil];
		}];
	}
}

Now you’re making the MainViewController the delegate of the HostViewController. Lastly, add the implementation of the delegate method to MainViewController.m:

#pragma mark - HostViewControllerDelegate

- (void)hostViewControllerDidCancel:(HostViewController *)controller
{
	[self dismissViewControllerAnimated:NO completion:nil];
}

This simply closes the HostViewController screen without an animation. Because MainViewController’s viewWillAppear will be called at this point, the flying cards animation will be performed again. Run the app and try it out.

Note: For debugging purposes, I like to make sure my view controllers (and any other objects) really do get deallocated when the screen gets dismissed, so I always add a dealloc method to my view controllers that logs a message to the Debug Output pane:

Even though this project uses ARC, it’s still possible that your apps leak memory. ARC greatly simplifies memory management, but it cannot magically make so-called “retain cycles” (or “ownership cycles”) disappear. If you have two objects that have strong pointers at each other, then they will stay in memory forever. That’s why I like to log when my objects get deallocated, just to keep an eye on things.

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

The Host Game screen is done for now. Before you can start the Game Kit session and broadcast your service, you have to build-in the Join Game screen. Otherwise, there’s no way for other devices to find that new Game Kit session!

The “Join Game” Screen

This screen looks very similar to the Host Game screen, but there are enough differences below the hood to warrant making this a totally separate class (rather than reusing or subclassing the HostViewController). But because it’s quite similar to what you did before, you can get through this quite quickly.

Add a new UIViewController subclass to the project and name it JoinViewController. Disable the XIB option, as I have already provided a nib for you in the starter code. Drag that nib file (from “Snap/en.lproj/”) into the project file.

Replace the contents of JoinViewController.h with:

@class JoinViewController;

@protocol JoinViewControllerDelegate <NSObject>

- (void)joinViewControllerDidCancel:(JoinViewController *)controller;

@end

@interface JoinViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>

@property (nonatomic, weak) id <JoinViewControllerDelegate> delegate;

@end

This is very similar to what you did with the Host Game screen. Replace JoinViewController.m with:

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

@interface JoinViewController ()
@property (nonatomic, weak) IBOutlet UILabel *headingLabel;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UILabel *statusLabel;
@property (nonatomic, weak) IBOutlet UITableView *tableView;

@property (nonatomic, strong) IBOutlet UIView *waitView;
@property (nonatomic, weak) IBOutlet UILabel *waitLabel;
@end

@implementation JoinViewController

@synthesize delegate = _delegate;

@synthesize headingLabel = _headingLabel;
@synthesize nameLabel = _nameLabel;
@synthesize nameTextField = _nameTextField;
@synthesize statusLabel = _statusLabel;
@synthesize tableView = _tableView;

@synthesize waitView = _waitView;
@synthesize waitLabel = _waitLabel;

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

- (void)viewDidLoad
{
	[super viewDidLoad];

	self.headingLabel.font = [UIFont rw_snapFontWithSize:24.0f];
	self.nameLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.statusLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.waitLabel.font = [UIFont rw_snapFontWithSize:18.0f];
	self.nameTextField.font = [UIFont rw_snapFontWithSize:20.0f];

	UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.nameTextField action:@selector(resignFirstResponder)];
	gestureRecognizer.cancelsTouchesInView = NO;
	[self.view addGestureRecognizer:gestureRecognizer];
}

- (void)viewDidUnload
{
	[super viewDidUnload];
	self.waitView = nil;
}

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

- (IBAction)exitAction:(id)sender
{
	[self.delegate joinViewControllerDidCancel:self];
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	return nil;
}

#pragma mark - UITextFieldDelegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
	[textField resignFirstResponder];
	return NO;
}

@end

There’s not much new here, except for the waitView outlet. Notice that this property is declared “strong,” instead of “weak” like the other properties. That’s done because it’s actually a top-level view in the nib:

The Join Game nib

It’s important to mark this as strong so that something has a reference to it, to prevent it from becoming deallocated. You don’t have to do that for the first view because it is retained from the built-in self.view property on the view controller.

After the user has tapped on the name of a host in the table view, you’ll place that second view (the one that says “Connecting…”) on top of the main view. You could have used a new view controller for that, but this is just as easy.

Augment MainViewController.h to read:

#import "HostViewController.h"
#import "JoinViewController.h"

@interface MainViewController : UIViewController <HostViewControllerDelegate, JoinViewControllerDelegate>

@end

In MainViewController.m, replace joinGameAction: with:

- (IBAction)joinGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		[self performExitAnimationWithCompletionBlock:^(BOOL finished)
		{
			JoinViewController *controller = [[JoinViewController alloc] initWithNibName:@"JoinViewController" bundle:nil];
			controller.delegate = self;

			[self presentViewController:controller animated:NO completion:nil];
		}];
	}
}

And add an implementation for the delegate method:

#pragma mark - JoinViewControllerDelegate

- (void)joinViewControllerDidCancel:(JoinViewController *)controller
{
	[self dismissViewControllerAnimated:NO completion:nil];
}

Now you have a working Join Game screen. Time to finally add the matchmaking logic!

Note: When writing multiplayer games (or any networked software, really) you basically have a choice of two architectures: client-server and peer-to-peer. Even though you’re using the “peer-to-peer” API from Game Kit, this game actually uses the client-server model. The person who hosts the match is the server, and all the other players are the clients.

Client-server vs peer-to-peer

In a client-server setup, the server is in charge of everything and determines what is the “truth.” The clients send their updates to the server and the server updates all the clients, but clients don’t communicate between themselves. In a true peer-to-peer game, however, all participants are equal. All the peers do the same amount of work, but you need to take care to make sure each peer sees the same things, since there is no central authority.

Again, in this tutorial, we will be using a client-server model, where the peer who hosts the game will be the server.

Contributors

Over 300 content creators. Join our team.