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

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, second, third, fourth, and fifth parts of the series, you created the networking infrastructure and the start of the game, including dealing cards, choosing the start player, and allowing the player to flip cards over.

In this sixth part of the series, you’ll implement the logic to allow players to take turns, deal with networking edge cases such as out-of-order packets, and start implementing the Snap feature!

Going round the table

Right now, the app will only let a single player turn over cards. That’s a bit sad for the other players, so in this section you’ll give the other players a turn!

That is typical for card games: the players take turns, so you’ll have to come up with a mechanism that makes players active turn-by-turn and then send the results of whatever this player did to everyone else.

There are two situations you need to handle: when the player who is also the server turns over a card, and when a player on a client turns over a card. In the latter case, the client needs to tell the server that the card has been turned over (in the first case, the server obviously already knows).

Let’s handle the server situation first, because that’s simplest. Add a new method to Game.m:

- (void)turnCardForActivePlayer
{
	[self turnCardForPlayer:[self activePlayer]];

	if (self.isServer)
		[self performSelector:@selector(activateNextPlayer) withObject:nil afterDelay:0.5f];
}

This turns the card for the active player and then schedules the activateNextPlayer method to be called after a small delay. I chose to use a delay here because that looks better in combination with the card turning animation. You only want to activate the next player when the CardView has been fully turned over.

Change the turnCardForPlayerAtBottom method to:

- (void)turnCardForPlayerAtBottom
{
	if (_state == GameStatePlaying 
		&& _activePlayerPosition == PlayerPositionBottom
		&& [[self activePlayer].closedCards cardCount] > 0)
	{
		[self turnCardForActivePlayer];
	}
}

Here you moved the logic for turning the card for the active player into its own method because you will need to call that method from two other places:

  1. When the client player turns the card.
  2. In single-player mode (more about that near the end of the tutorial).

Next add this new method to Game.m:

- (void)activateNextPlayer
{
	NSAssert(self.isServer, @"Must be server");

	while (true)
	{
		_activePlayerPosition++;
		if (_activePlayerPosition > PlayerPositionRight)
			_activePlayerPosition = PlayerPositionBottom;

		Player *nextPlayer = [self activePlayer];
		if (nextPlayer != nil)
		{
			[self activatePlayerAtPosition:_activePlayerPosition];
			return;
		}
	}
}

This loops clockwise through the player positions until it finds a valid Player object, and then activates that Player. Because you call the activatePlayerAtPosition: method, this will also send a PacketActivatePlayer packet to all clients. You can only call activateNextPlayer on the server.

That should do it for the server. If the server’s player is the starting player (and for testing purposes you can fake this by always setting _activePlayerPosition to PlayerPositionBottom in pickRandomStartingPlayer), then tapping to turn over the card will activate the next player, also on the clients. Try it out!

Turning cards on the client

Of course, turning over a card on a client does not activate the next player yet, because the client does not tell the server yet about this event. For this you’ll introduce a new packet type, PacketTypeClientTurnedCard.

This is a packet that needs no additional data, so the only thing you have to do to make this work is to add a case-statement to Packet.m‘s packetWithData: method:

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

		. . .

In Game.m, change turnCardForPlayerAtBottom to:

- (void)turnCardForPlayerAtBottom
{
	if (_state == GameStatePlaying 
		&& _activePlayerPosition == PlayerPositionBottom
		&& [[self activePlayer].closedCards cardCount] > 0)
	{
		[self turnCardForActivePlayer];

		if (!self.isServer)
		{
			Packet *packet = [Packet packetWithType:PacketTypeClientTurnedCard];
			[self sendPacketToServer:packet];
		}
	}
}

If this method gets called on a device that acts as a client, it will send the new “client turned card” packet to the server. To handle this packet type on the server, add the following case-statement to serverReceivedPacket:fromPlayer:

		case PacketTypeClientTurnedCard:
			if (_state == GameStatePlaying && player == [self activePlayer])
			{
				[self turnCardForActivePlayer];
			}
			break;

If the packet came from the currently active player (defensive programming!), then you should turn over the card for it. Notice that this will only turn the card on the server but the client will never show the cards that the server turned over. With two or more clients this also runs into problems, because client B will never be told that client A turned over its card.

Here’s the trick: according to the game rules, after a player turns over its card you will always activate the next player, and for that all clients already receive an ActivatePlayer packet. On receipt of that message the client can simply turn over the card from the previously active player. A good place to make that happen is handleActivatePlayerPacket:, so change that method to:

- (void)handleActivatePlayerPacket:(PacketActivatePlayer *)packet
{
	NSString *peerID = packet.peerID;

	Player* newPlayer = [self playerWithPeerID:peerID];
	if (newPlayer == nil)
		return;

	[self performSelector:@selector(activatePlayerWithPeerID:) withObject:peerID afterDelay:0.5f];
}

You call turnCardForActivePlayer to turn over the card for the previous player, but not if this client itself was the previous player (i.e. the one in the bottom position) because that card has already been turned manually by the local user. After that, you call the new activatePlayerWithPeerID: method to activate the next player with a delay, which again looks better in combination with the card turn animation.

Add the activatePlayerWithPeerID: method:

- (void)activatePlayerWithPeerID:(NSString *)peerID
{
	NSAssert(!self.isServer, @"Must be client");

	Player *player = [self playerWithPeerID:peerID];
	_activePlayerPosition = player.position;
	[self activatePlayerAtPosition:_activePlayerPosition];
}

Try it out now. Whoops, if the starting player is the server’s, the client will now flip over a card even before the server player has tapped on anything!

The reason for this is that after dealing, the client immediately receives an ActivatePlayer packet and in response it will flip over that player’s top-most card. Obviously, it shouldn’t do that the very first time it receives the ActivatePlayer packet, so let’s handle this with a new boolean instance variable:

@implementation Game
{
	. . .
	BOOL _firstTime;
}

You set this boolean to YES in beginGame (it is best to do this near the top of the method, before any packets are sent out to the clients):

- (void)beginGame
{
	_firstTime = YES;
	. . .
}

Then you check for this variable at the very top of handleActivatePlayerPacket:

- (void)handleActivatePlayerPacket:(PacketActivatePlayer *)packet
{
	if (_firstTime)
	{
		_firstTime = NO;
		return;
	}

	. . .
}

When a new round starts, the starting player has already been activated. In that case, you don’t need to do anything here and you can simply ignore the ActivatePlayer packet.

Try it again, first with one client, then with two (or more). Each player in turn can now flip over a card, and all players should show the cards of the others being turned over. Verify that all devices actually show the same cards.

Testing on devices

Contributors

Over 300 content creators. Join our team.