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

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

Gratuitous sound effects

Let's spice this up with some sound!

The last part's resources also included a Sound folder that contains several .caf files. Add that folder to the project, and make sure they are added to your Snap target.

You'll be using the AVAudioPlayer class to play the sound effects, so import the headers for the AVFoundation.framework in Snap-Prefix.pch (the framework itself should already have been added to the project):

	#import <AVFoundation/AVFoundation.h>

Now add a new instance variable to GameViewController.m:

@implementation GameViewController
{
	. . .
	AVAudioPlayer *_dealingCardsSound;
}

Then add a new method named loadSounds to create this AVAudioPlayer object:

- (void)loadSounds
{
	AVAudioSession *audioSession = [AVAudioSession sharedInstance];
	audioSession.delegate = nil;
	[audioSession setCategory:AVAudioSessionCategoryAmbient error:NULL];
	[audioSession setActive:YES error:NULL];

	NSURL *url = [[NSBundle mainBundle] URLForResource:@"Dealing" withExtension:@"caf"];
	_dealingCardsSound = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
	_dealingCardsSound.numberOfLoops = -1;
	[_dealingCardsSound prepareToPlay];
}

This first sets up the audio session, just to play nice with any other sounds that may be playing on your device (such as iPod audio), and then loads the Dealing.caf sound.

Next add a line to call this new method from viewDidLoad:

- (void)viewDidLoad
{
	. . .
	[self loadSounds];
}

Then add a few lines to play this sound in gameShouldDealCards:startingWithPlayer:

- (void)gameShouldDealCards:(Game *)game startingWithPlayer:(Player *)startingPlayer
{
	. . .

	NSTimeInterval delay = 1.0f;

	_dealingCardsSound.currentTime = 0.0f;
	[_dealingCardsSound prepareToPlay];
	[_dealingCardsSound performSelector:@selector(play) withObject:nil afterDelay:delay];

	for (int t = 0; t < 26; ++t)
	{
		. . .
	}	

	[self performSelector:@selector(afterDealing) withObject:nil afterDelay:delay];	
}

This plays the dealing sound after 1 second. Because you set the AVAudioPlayer's numberOfLoops property to -1, the sound keeps looping indefinitely, so you have to stop it after the dealing animation completes. That's what the call to performSelector:withObject:afterDelay: is for.

Add this new method to the class:

- (void)afterDealing
{
	[_dealingCardsSound stop];
	self.snapButton.hidden = NO;
}

Just to make sure you clean up nicely, add the following lines to the dealloc method:

- (void)dealloc
{
	. . .

	[_dealingCardsSound stop];
	[[AVAudioSession sharedInstance] setActive:NO error:NULL];
}

And in the UIAlertViewDelegate method:

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
	if (buttonIndex != alertView.cancelButtonIndex)
	{
		[NSObject cancelPreviousPerformRequestsWithTarget:self];

		[self.game quitGameWithReason:QuitReasonUserQuit];
	}
}

Because you've used performSelector:withObject:afterDelay:, it is conceivable that the user taps the exit button while the cards are still being dealt. You don't want the afterDealing method to be called anymore in that case, so you use cancelPreviousPerformRequestsWithTarget: to stop any pending messages.

Try it out. The card dealing animation should now be accompanied by a cool sound effect!

Note: performSelector:withObject:afterDelay: is not the only way that you can schedule operations to run in the future. You can also use GCD with blocks, for example. Because you're using UIKit our game does not have a "game loop" that runs 30 or 60 times per second, so you have to do our timing using other mechanisms. If you really wanted to you could create your own a NSTimer, but performSelector:withObject:afterDelay: is just as easy, as long as you remember to cancel the requests when you no longer need them to be performed.

Dealing the cards at the clients

So far the dealing animation only happens on the server. The server also needs to tell the clients that the cards have been dealt. Because dealing cards includes a randomness factor, you cannot have each client do its own dealing. Instead, the server needs to send a message to each client that says which player has received which cards.

Add a new Objective-C class to the project, named PacketDealCards, subclass of Packet. Replace PacketDealCards.h with:

#import "Packet.h"

@class Player;

@interface PacketDealCards : Packet

@property (nonatomic, strong) NSDictionary *cards;
@property (nonatomic, copy) NSString *startingPeerID;

+ (id)packetWithCards:(NSDictionary *)cards startingWithPlayerPeerID:(NSString *)startingPeerID;

@end

The "cards" property is a dictionary of Card objects. The dictionary keys are peer IDs. The startingPeerID property contains the peerID of the player who gets the first turn. Replace PacketDealCards.m with:

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

@implementation PacketDealCards

@synthesize cards = _cards;
@synthesize startingPeerID = _startingPeerID;

+ (id)packetWithCards:(NSDictionary *)cards startingWithPlayerPeerID:(NSString *)startingPeerID
{
	return [[[self class] alloc] initWithCards:cards startingWithPlayerPeerID:startingPeerID];
}

- (id)initWithCards:(NSDictionary *)cards startingWithPlayerPeerID:(NSString *)startingPeerID
{
	if ((self = [super initWithType:PacketTypeDealCards]))
	{
		self.cards = cards;
		self.startingPeerID = startingPeerID;
	}
	return self;
}

+ (id)packetWithData:(NSData *)data
{
	size_t offset = PACKET_HEADER_SIZE;
	size_t count;

	NSString *startingPeerID = [data rw_stringAtOffset:offset bytesRead:&count];
	offset += count;

	NSDictionary *cards = [[self class] cardsFromData:data atOffset:offset];

	return [[self class] packetWithCards:cards startingWithPlayerPeerID:startingPeerID];
}

- (void)addPayloadToData:(NSMutableData *)data
{
	[data rw_appendString:self.startingPeerID];
	[self addCards:self.cards toPayload:data];
}

@end

This uses two methods you haven't seen before, addCards:toPayload: to write a dictionary of Card objects into an NSMutableData object, and cardsFromData:atOffset: to do the reverse, read a dictionary of Card objects from NSData.

Because you're going to be using these methods in a few other Packets as well, I decided to move them into the base class. Add their method signatures to Packet.h:

+ (NSDictionary *)cardsFromData:(NSData *)data atOffset:(size_t) offset;
- (void)addCards:(NSDictionary *)cards toPayload:(NSMutableData *)data;

And the implementations into Packet.m:

- (void)addCards:(NSMutableDictionary *)cards toPayload:(NSMutableData *)data
{
	[cards enumerateKeysAndObjectsUsingBlock:^(id key, NSArray *array, BOOL *stop)
	{
		[data rw_appendString:key];
		[data rw_appendInt8:[array count]];

		for (int t = 0; t < [array count]; ++t)
		{
			Card *card = [array objectAtIndex:t];
			[data rw_appendInt8:card.suit];
			[data rw_appendInt8:card.value];
		}
	}];
}

Writing the card data is pretty straightforward, you first write the peer ID of the player (which is in "key"), then the number of cards for this player. For each card, you write two bytes: the suit and the value. This does require an import for Card.h at the top of the file:

#import "Card.h"

Reading the Card objects back in is pretty simple too:

+ (NSMutableDictionary *)cardsFromData:(NSData *)data atOffset:(size_t) offset
{
	size_t count;

	NSMutableDictionary *cards = [NSMutableDictionary dictionaryWithCapacity:4];

	while (offset < [data length])
	{
		NSString *peerID = [data rw_stringAtOffset:offset bytesRead:&count];
		offset += count;

		int numberOfCards = [data rw_int8AtOffset:offset];
		offset += 1;

		NSMutableArray *array = [NSMutableArray arrayWithCapacity:numberOfCards];

		for (int t = 0; t < numberOfCards; ++t)
		{
			int suit = [data rw_int8AtOffset:offset];
			offset += 1;

			int value = [data rw_int8AtOffset:offset];
			offset += 1;
			
			Card *card = [[Card alloc] initWithSuit:suit value:value];
			[array addObject:card];
		}

		[cards setObject:array forKey:peerID];
	}

	return cards;
}

While you're editing Packet.m, you should add an import for the new PacketDealCards class at the top of the file:

#import "PacketDealCards.h"

And add a case-statement to packetWithData:

		case PacketTypeDealCards:
			packet = [PacketDealCards packetWithData:data];
			break;

Now let's do something useful with this new packet. Switch to Game.m and import the new file:

#import "PacketDealCards.h"

Then change dealCards to the following:

- (void)dealCards
{
	. . .

	Player *startingPlayer = [self activePlayer];

	NSMutableDictionary *playerCards = [NSMutableDictionary dictionaryWithCapacity:4];
	[_players enumerateKeysAndObjectsUsingBlock:^(id key, Player *obj, BOOL *stop)
	{
		NSArray *array = [obj.closedCards array];
		[playerCards setObject:array forKey:obj.peerID];
	}];

	PacketDealCards *packet = [PacketDealCards packetWithCards:playerCards startingWithPlayerPeerID:startingPlayer.peerID];
	[self sendPacketToAllClients:packet];

	[self.delegate gameShouldDealCards:self startingWithPlayer:startingPlayer];
}

Here you make a new NSMutableDictionary named playerCards that puts the Card objects into the dictionary under the player's peerID as the key. Then you create a PacketDealCards packet and send it to all the clients.

That's the sending part. For the receiving part add the following case-statement in clientReceivedPacket:

		case PacketTypeDealCards:
			if (_state == GameStateDealing)
			{
				[self handleDealCardsPacket:(PacketDealCards *)packet];
			}
			break;

In the interest of keeping the code readable, I've moved the logic into a new method, handleDealCardsPacket, so add that next:

- (void)handleDealCardsPacket:(PacketDealCards *)packet
{
	[packet.cards enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
	{
		Player *player = [self playerWithPeerID:key];
		[player.closedCards addCardsFromArray:obj];
	}];

	Player *startingPlayer = [self playerWithPeerID:packet.startingPeerID];
	_activePlayerPosition = startingPlayer.position;

	Packet *responsePacket = [Packet packetWithType:PacketTypeClientDealtCards];
	[self sendPacketToServer:responsePacket];

	_state = GameStatePlaying;

	[self.delegate gameShouldDealCards:self startingWithPlayer:startingPlayer];
}

This takes the Card objects for each Player, and places them on its closedCards stack. It also figures out what the value for _activePlayerPosition needs to be.

Note: Recall that player positions are relative for each client, so the server couldn't simply send his position values. Instead, you always send peerIDs and then let the client figure out which position corresponds to that peer ID.

The client then sends a PacketTypeClientDealtCards message back to the server to let it know the client received the cards. Finally, it starts the exact same dealing animation that you saw on the server by calling the gameShouldDealCards:startingWithPlayer: delegate method.

This requires a few additional steps to make everything work. First of all, the Stack class needs a new method, addCardsFromArray:. Add the method to Stack.h and Stack.m:

- (void)addCardsFromArray:(NSArray *)array
{
	_cards = [array mutableCopy];
}

You could run the app at this point and see the dealing animation on both the server and the clients, but let's finish up first. The client sends the PacketTypeClientDealtCards packet back to the server. When the server has received this message from all the clients, it can change its state to "playing". Add the following case-statement to serverReceivedPacket:fromPlayer:

		case PacketTypeClientDealtCards:
			if (_state == GameStateDealing && [self receivedResponsesFromAllPlayers])
			{
				_state = GameStatePlaying;
			}
			break;

Of course, you should add a case-statement to Packet.m's packetWithData: as well, or this new packet type won't be recognized:

	switch (packetType)
	{
		case PacketTypeSignInRequest:
		case PacketTypeClientReady:
		case PacketTypeClientDealtCards:
		case PacketTypeServerQuit:
		case PacketTypeClientQuit:
			packet = [Packet packetWithType:packetType];
			break;

	. . .

And that's it as far as the dealing is concerned. Compile and run, and the game should deal cards on both devices, and you're ready to play!

Contributors

Over 300 content creators. Join our team.