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

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

The Snap! button

Everything you've done so far has been very important -- letting players take turns, animating card views, handling disconnects -- but the fun part of the Snap! game is of course yelling "Snap!" when you see a pair of matching cards. There are three things that can happen when you press the Snap! button:

  1. There is a match and you're the first person to yell "Snap!". You will now receive the complete stacks of open cards from the players who hold the matching cards. These cards will go on your closed stack.
  2. There isn't a match. You will now have to pay all other players one card from your closed stack.
  3. You're not the first person to yell "Snap!". Nothing happens, although the game will still show a speech bubble to point out that you're too slow. ;-)

As always you'll start simple and slowly build in all the required features. Simple in this case means that tapping the Snap! button always counts as "no match" and whoever tapped the button will have to pay cards to the other players.

In GameViewController.m, change the snapAction: method to:

- (IBAction)snapAction:(id)sender
{
	[self.game playerCalledSnap:[self.game playerAtPosition:PlayerPositionBottom]];
}

Add this new method to Game.h and Game.m:

- (void)playerCalledSnap:(Player *)player
{
	[self.delegate game:self playerCalledSnapWithNoMatch:player];
}

Soon you'll be doing a lot more in playerCalledSnap:, but for now anytime someone yells "Snap!", you mark it down as a wrong move. Add this new delegate method to the GameDelegate protocol:

- (void)game:(Game *)game playerCalledSnapWithNoMatch:(Player *)player;

And implement it in GameViewController.m:

- (void)game:(Game *)game playerCalledSnapWithNoMatch:(Player *)player
{
	[_wrongMatchSound play];

	[self showSplashView:self.wrongSnapImageView forPlayer:player];
	[self showSnapIndicatorForPlayer:player];
	[self performSelector:@selector(hideSnapIndicatorForPlayer:) withObject:player afterDelay:1.0f];

	self.turnOverButton.enabled = NO;
	self.centerLabel.text = NSLocalizedString(@"No Match!", @"Status text: player called snap with no match");
}

There's some new stuff here, a sound effect and two new methods. The sound effect is easy, just add a new instance variable:

@implementation GameViewController
{
	. . .
	AVAudioPlayer *_wrongMatchSound;
	AVAudioPlayer *_correctMatchSound;
}

In the interest of saving some time, you might as well add the sound effect for a "correct match" here. These two new AVAudioPlayer objects are created in loadSounds:

- (void)loadSounds
{
	. . .

	url = [[NSBundle mainBundle] URLForResource:@"WrongMatch" withExtension:@"caf"];
	_wrongMatchSound = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
	[_wrongMatchSound prepareToPlay];

	url = [[NSBundle mainBundle] URLForResource:@"CorrectMatch" withExtension:@"caf"];
	_correctMatchSound = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
	[_correctMatchSound prepareToPlay];
}

Next up is the showSplashView:forPlayer: method. The "splash view" is the smiley face that shows up when the "Snap!" was correct (self.correctSnapImageView) or the big red X that shows up on a wrong Snap! (self.wrongSnapImageView).

These two image views are part of the GameViewController nib but so far they have been hidden. What showSplashView:forPlayer: does, is move the image view over to the player's position, show it with a "splash!"-type animation (hence the name), and then remove it again.

Add this method next:

- (void)showSplashView:(UIImageView *)splashView forPlayer:(Player *)player
{
	splashView.center = [self splashViewPositionForPlayer:player];
	splashView.hidden = NO;
	splashView.alpha = 1.0f;
	splashView.transform = CGAffineTransformMakeScale(2.0f, 2.0f);

	[UIView animateWithDuration:0.1f
		delay:0.0f
		options:UIViewAnimationOptionCurveEaseIn
		animations:^
		{
			splashView.transform = CGAffineTransformIdentity;
		}
		completion:^(BOOL finished)
		{
			[UIView animateWithDuration:0.1f 
				delay:1.0f
				options:UIViewAnimationOptionCurveEaseIn
				animations:^
				{
					splashView.alpha = 0.0f;
					splashView.transform = CGAffineTransformMakeScale(0.5f, 0.5f);
				}
				completion:^(BOOL finished)
				{
					splashView.hidden = YES;
				}];
		}];
}

- (CGPoint)splashViewPositionForPlayer:(Player *)player
{
	CGRect rect = self.view.bounds;
	CGFloat midX = CGRectGetMidX(rect);
	CGFloat midY = CGRectGetMidY(rect);
	CGFloat maxX = CGRectGetMaxX(rect);
	CGFloat maxY = CGRectGetMaxY(rect);

	if (player.position == PlayerPositionBottom)
		return CGPointMake(midX, maxY - CardHeight/2.0f - 30.0f);
	else if (player.position == PlayerPositionLeft)
		return CGPointMake(31.0f + CardWidth / 2.0f, midY - 22.0f);
	else if (player.position == PlayerPositionTop)
		return CGPointMake(midX, 29.0f + CardHeight/2.0f);
	else
		return CGPointMake(maxX - CardWidth + 1.0f, midY - 22.0f);
}

The other new method to add is showSnapIndicatorForPlayer:. It simply unhides an image view. This is the purple speech bubble graphic that shows which player yelled "Snap!":

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

That should be enough to try it out. Tapping either on the server or a client should work, although they don't communicate with each other yet.

The big red X that's shown when there is no match

Notice that you temporarily disable the UIButton that is used for turning over the cards after you've tapped the Snap! button, just so the user cannot mess with our animations. Currently there is no code yet to re-enable that button, but you'll fix that in a moment.

Sending "Snap!" to the server

When a client taps the Snap! button, it needs to tell the server, so the server can figure out whether or not it's a good Snap! and tell everyone else. Change playerCalledSnap: in Game.m to:

- (void)playerCalledSnap:(Player *)player
{
	if (self.isServer)
	{
		[self.delegate game:self playerCalledSnapWithNoMatch:player];
	}
	else
	{
		Packet *packet = [PacketPlayerShouldSnap packetWithPeerID:_session.peerID];
		[self sendPacketToServer:packet];
	}
}

If the player is on a client, then you send the new PacketPlayerShouldSnap packet. Create this new class (as always a subclass of Packet) and add it to the project. Replace PacketPlayerShouldSnap.h with:

#import "Packet.h"

@interface PacketPlayerShouldSnap : Packet

@property (nonatomic, copy) NSString *peerID;

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

@end

And replace PacketPlayerShouldSnap.m with:

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

@implementation PacketPlayerShouldSnap

@synthesize peerID = _peerID;

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

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

+ (id)packetWithData:(NSData *)data
{
	size_t count;
	NSString *peerID = [data rw_stringAtOffset:PACKET_HEADER_SIZE bytesRead:&count];
	return [[self class] packetWithPeerID:peerID];
}

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

@end

It's just a packet that sends the peer ID of the player who called snap. You already have at least one Packet subclass that does exactly the same thing, but in a short while you'll add something extra to this class that makes it different. (As I write this I realize it is technically not necessary to send the peer ID because you always already know the peer ID of the sender of a message, but it won't hurt either way.)

In Packet.m, add an import for this new subclass:

#import "PacketPlayerShouldSnap.h"

And a case-statement in packetWithData:

		case PacketTypePlayerShouldSnap:
			packet = [PacketPlayerShouldSnap packetWithData:data];
			break;

Also add an import in Game.m:

#import "PacketPlayerShouldSnap.h"

And then add the following case-statement to serverReceivedPacket:fromPlayer:

		case PacketTypePlayerShouldSnap:
			if (_state == GameStatePlaying)
			{
				NSString *peerID = ((PacketPlayerShouldSnap *)packet).peerID;
				Player *player = [self playerWithPeerID:peerID];
				if (player != nil)
					[self playerCalledSnap:player];
			}
			break;

This figures out which player you're talking about and then calls the playerCalledSnap: method again.

Try it out! Tap the Snap! button on the client and watch a speech bubble (and for now, the big red X) appear on the server.

Contributors

Over 300 content creators. Join our team.