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 2 of 5 of this article. Click here to view the first page.

Animating the Intro

Now you’ll liven up the main screen a little. How about when the app starts, the logo cards fly into the screen?

To create this effect, add the following methods to MainViewController.m:

- (void)prepareForIntroAnimation
{
	self.sImageView.hidden = YES;
	self.nImageView.hidden = YES;
	self.aImageView.hidden = YES;
	self.pImageView.hidden = YES;
	self.jokerImageView.hidden = YES;
}

- (void)performIntroAnimation
{
	self.sImageView.hidden = NO;
	self.nImageView.hidden = NO;
	self.aImageView.hidden = NO;
	self.pImageView.hidden = NO;
	self.jokerImageView.hidden = NO;

	CGPoint point = CGPointMake(self.view.bounds.size.width / 2.0f, self.view.bounds.size.height * 2.0f);

	self.sImageView.center = point;
	self.nImageView.center = point;
	self.aImageView.center = point;
	self.pImageView.center = point;
	self.jokerImageView.center = point;

	[UIView animateWithDuration:0.65f
		delay:0.5f
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.sImageView.center = CGPointMake(80.0f, 108.0f);
			self.sImageView.transform = CGAffineTransformMakeRotation(-0.22f);
			
			self.nImageView.center = CGPointMake(160.0f, 93.0f);
			self.nImageView.transform = CGAffineTransformMakeRotation(-0.1f);

			self.aImageView.center = CGPointMake(240.0f, 88.0f);

			self.pImageView.center = CGPointMake(320.0f, 93.0f);
			self.pImageView.transform = CGAffineTransformMakeRotation(0.1f);

			self.jokerImageView.center = CGPointMake(400.0f, 108.0f);
			self.jokerImageView.transform = CGAffineTransformMakeRotation(0.22f);
		}
		completion:nil];
}

The first method, prepareForIntroAnimation, simply hides the five UIImageViews that hold the logo cards. The actual animation happens in performIntroAnimation. First, you place the cards off-screen, horizontally centered but vertically below the bottom of the screen. Then you start a UIView animation block that places the UIImageViews at their final positions, to look like they’re fanned out from the center.

You’ll call these methods from viewWillAppear: and viewDidAppear:

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

	[self prepareForIntroAnimation];
}

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

	[self performIntroAnimation];
}

Now when you run the app, the cards fly up into the screen and fan out. Pretty cool.

Logo with fanned out cards

The animation is not perfect yet, though. It would be nicer if the buttons subtly faded into view while the cards were flying to their final positions. Add the following lines to the bottom of prepareForIntroAnimation:

- (void)prepareForIntroAnimation
{
	. . .
	
	self.hostGameButton.alpha = 0.0f;
	self.joinGameButton.alpha = 0.0f;
	self.singlePlayerGameButton.alpha = 0.0f;

	_buttonsEnabled = NO;
}

That makes the buttons fully transparent. Also add a second UIView-animation block to the end of performIntroAnimation:

- (void)performIntroAnimation
{
	. . .

	[UIView animateWithDuration:0.5f
		delay:1.0f
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.hostGameButton.alpha = 1.0f;
			self.joinGameButton.alpha = 1.0f;
			self.singlePlayerGameButton.alpha = 1.0f;
		}
		completion:^(BOOL finished)
		{
			_buttonsEnabled = YES;
		}];
}

Here you simply animate the buttons back to fully opaque. But what is that _buttonsEnabled variable? You don’t want the user to tap the buttons while they’re still fading in. Only after the animations have completed should the buttons become available for tapping.

You’ll use the _buttonsEnabled instance variable to ignore taps on the buttons while the animation is still taking place. For now, just add this new instance variable to the @implementation section:

@implementation MainViewController
{
	BOOL _buttonsEnabled;
}

Run the app and check out the animation. Pretty smooth!

Game Kit and Multiplayer Games

Game Kit is a standard framework that comes with the iOS SDK. Its main features are for use in Game Center (which you won’t use in this tutorial) and voice chat, but it also has a peer-to-peer connectivity feature that connects devices over a Bluetooth connection. If all devices are on a local Wi-Fi network, Game Kit can also use that instead of Bluetooth. (There is also a provision for peer-to-peer matchmaking over the Internet, but you’ll have to write most of the code for that yourself – and these days it’s probably easier to use Game Center.)

Game Kit’s peer-to-peer feature is great for multiplayer games where the players are in the same room together and all using their own devices. Reportedly, the players cannot be more than about 10 meters (or 30 feet) away from each other when using Bluetooth.

So what does peer-to-peer connectivity mean? Each device that participates in a Game Kit networking session is named a “peer.” A device can act as a “server” that broadcasts a particular service, as a “client” that looks for servers that provide a particular service, or as both client and server at the same time. To do this, Game Kit uses Bonjour technology behind the scenes, but you don’t need to work directly with Bonjour in order to use Game Kit.

When using Bluetooth, devices don’t need to be paired, the way you’d need to pair a Bluetooth mouse or keyboard with your device. Game Kit simply lets clients discover servers, and once this connection is made, the devices can send messages to each other over the local network.

You don’t have the option to choose between Bluetooth or Wi-Fi; GameKit makes this decision for you. Bluetooth is also not supported in the Simulator, but Wi-Fi is.

While developing and testing for this tutorial, I found it easiest to use the Simulator and one or two physical devices, and play over the local Wi-Fi network. If you want to play over Bluetooth, you’ll need to have at least two physical devices that both have Bluetooth enabled.

Note: It’s possible to do network communications with Bonjour and Bluetooth without Game Kit, but if you’re building a multiplayer game, using Game Kit is a lot easier. It hides all the nasty networking stuff from you and gives you a single class to use, GKSession. That will be the only Game Kit class you’re going to be working with in this tutorial (and its delegate, GKSessionDelegate).

If you’re making a two-player multiplayer game that uses Bluetooth or Wi-Fi, then you can use Game Kit’s GKPeerPickerController to establish the connection between the two devices. It looks like this:

The GKPeerPickerController user interface

The GKPeerPickerController is pretty easy to use, but it’s limited to establishing a connection between two devices. Snap! can have up to four players, so this tutorial takes you through writing your own matchmaking code.

The “Host Game” Screen

In this section, you’ll add the Host Game screen to the app. This screen lets a player host a gaming session that other players can join. When you’re done, it will look like this:

The Host Game screen

The table view lists the players who have connected to this host, and the Start button begins the game. There is also a text field that allows you to name your player (by default it will use the name of your device).

Add a new UIViewController subclass to the project, named HostViewController. Disable the “With XIB for user interface” option. The starter code already comes with a fully-prepared nib file for the Host Game screen. You can find it in the “Snap/en.lproj” folder. Drag the HostViewController.xib file into the project.

The nib file looks like this:

The Host Game nib

Most of the UI elements are hooked up to properties and action methods, so you should add these to your HostViewController class. Otherwise, the app will crash when you try to load this nib.

In HostViewController.m, add the following lines to the class extension (at the top of the file):

@interface HostViewController ()
@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, weak) IBOutlet UIButton *startButton;
@end

Notice that you’re putting the IBOutlet properties inside the .m file, not in the .h file. This is one of the new features of the LLVM compiler that ships with the latest versions of Xcode (4.2 and up). It helps to keep your .h files really clean so that you only expose the properties and methods that other objects need to see.

Of course, you need to synthesize these properties, so add the following lines below @implementation:

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

Tip: It’s rumored that in the next version of Xcode (which you may already be using by the time you read this tutorial), you can simply leave out the @synthesize lines. But if you’re on Xcode 4.3, it’s still necessary to put them in.

Replace shouldAutorotateToInterfaceOrientation: so that it will only support the landscape orientation:

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

Also, add the following placeholder methods to the bottom of the file:

- (IBAction)startAction:(id)sender
{
}

- (IBAction)exitAction:(id)sender
{
}

#pragma mark - UITableViewDataSource

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

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

Finally, replace the @interface line in HostViewController.h with:

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

That’s enough to get a basic version of the Host Game screen working, but you still have to display it when the user taps the corresponding button on the main screen. Add an import statement to MainViewController.h:

#import "HostViewController.h"

And replace the hostGameAction: method in MainViewController.m with the following:

- (IBAction)hostGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];

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

If you run the app now, you’ll see that tapping the Host Game button instantly brings up the Host Game screen. It works, although it doesn’t look particularly pretty. You’re presenting this new view controller modally, but because you passed NO to the animated: parameter, there is no standard “slide up” animation for this new screen.

Such an animation wouldn’t look particularly good here – you’d see the felt background of the Host Game screen slide up over the felt from the main screen – so instead, create a new animation by adding the following method to MainViewController.m:

- (void)performExitAnimationWithCompletionBlock:(void (^)(BOOL))block
{
	_buttonsEnabled = NO;

	[UIView animateWithDuration:0.3f
		delay:0.0f
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.sImageView.center = self.aImageView.center;
			self.sImageView.transform = self.aImageView.transform;

			self.nImageView.center = self.aImageView.center;
			self.nImageView.transform = self.aImageView.transform;

			self.pImageView.center = self.aImageView.center;
			self.pImageView.transform = self.aImageView.transform;

			self.jokerImageView.center = self.aImageView.center;
			self.jokerImageView.transform = self.aImageView.transform;
		}
		completion:^(BOOL finished)
		{
			CGPoint point = CGPointMake(self.aImageView.center.x, self.view.frame.size.height * -2.0f);

			[UIView animateWithDuration:1.0f
				delay:0.0f
				options:UIViewAnimationOptionCurveEaseOut
				animations:^
				{
					self.sImageView.center = point;
					self.nImageView.center = point;
					self.aImageView.center = point;
					self.pImageView.center = point;
					self.jokerImageView.center = point;
				}
				completion:block];

			[UIView animateWithDuration:0.3f
				delay:0.3f
				options:UIViewAnimationOptionCurveEaseOut
				animations:^
				{
					self.hostGameButton.alpha = 0.0f;
					self.joinGameButton.alpha = 0.0f;
					self.singlePlayerGameButton.alpha = 0.0f;
				}
				completion:nil];
		}];
}

Tip: It doesn’t really matter where you add this method (as long as it’s between the @implementation and @end directives). Previously you had to declare the method in the .h file, and put its signature in a class extension at the top of the .m file, or make sure that any method you call is higher up in the source file. That’s no longer necessary thanks to the LLVM compiler that ships with Xcode 4.3. The compiler is now smart enough to find the method, no matter where you put it in the source file, and even if you didn’t forward-declare it previously.

The animation in performExitAnimationWithCompletionBlock: slides the logo cards off the screen and at the same time fades out the buttons. When the animation is done, it executes the code from the block that you pass in as a parameter.

Now change hostGameAction: to:

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

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

It’s almost the same as before, but now the logic that creates and presents the Host Game screen is wrapped in a block that gets performed when the exit animation completes. Run the app and see for yourself. You put the animation code in a separate method so that you can also use it when the user taps the other buttons.

As you can see in the nib, the Host Game screen also uses the default Helvetica font, and its Start button doesn’t have a border. This is easily fixed. Add these two imports to HostViewController.m:

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

And replace viewDidLoad with the following:

- (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.nameTextField.font = [UIFont rw_snapFontWithSize:20.0f];

	[self.startButton rw_applySnapStyle];
}

Because you created these convenient categories to add your font and style your buttons, it’s a snap (ha ha) to make the screen look good. Run the app to see for yourself.

There are a few more tweaks to make. The screen has a UITextField that allows the player to type in his name. When you tap in this text field, the on-screen keyboard slides up. The keyboard takes up about half of the screen space, and there is currently no way to dismiss it.

The on-screen keyboard

The first way to dismiss the keyboard is with the big blue Done button. Currently this does nothing when tapped, but adding the following method to HostViewController.m will solve that:

#pragma mark - UITextFieldDelegate

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

The second way to dismiss the keyboard is to add the following code to the end of viewDidLoad:

- (void)viewDidLoad
{
	. . .

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

You’re creating a gesture recognizer that responds to simple taps and adding it to the view controller’s main view. Now when the user taps outside of the text field, the gesture recognizer sends the “resignFirstResponder” message to the text field, which will make the keyboard disappear.

Note that you need to set the cancelsTouchesInView property to NO, otherwise it will no longer be possible to tap on anything else in the screen, such as the table view and the buttons.

Contributors

Over 300 content creators. Join our team.