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

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

Waiting For the "Client Ready" Response

You're not done yet. Because the server wants to be sure that all clients have received the ServerReady message and have created their dictionary of Player objects, it expects each client to send back a "client ready" message. As before, the server waits for each client to send this message before it will continue.

The new PacketTypeClientReady message does not require any additional data, so you don't need to make a Packet subclass for it. Add the following lines to the case-statement in clientReceivedPacket:

		case PacketTypeServerReady:
			if (_state == GameStateWaitingForReady)
			{
				_players = ((PacketServerReady *)packet).players;
				[self changeRelativePositionsOfPlayers];

				Packet *packet = [Packet packetWithType:PacketTypeClientReady];
				[self sendPacketToServer:packet];

				[self beginGame];
			}
			break;

There are two new methods here, changeRelativePositionsOfPlayers, and beginGame. Add these to Game.m as well:

- (void)beginGame
{
	_state = GameStateDealing;
	NSLog(@"the game should begin");
}

- (void)changeRelativePositionsOfPlayers
{
	NSAssert(!self.isServer, @"Must be client");

	Player *myPlayer = [self playerWithPeerID:_session.peerID];
	int diff = myPlayer.position;
	myPlayer.position = PlayerPositionBottom;

	[_players enumerateKeysAndObjectsUsingBlock:^(id key, Player *obj, BOOL *stop)
	{
		if (obj != myPlayer)
		{
			obj.position = (obj.position - diff) % 4;
		}
	}];
}

As you can see, beginGame doesn't do much yet, but you'll change that shortly. For now, the interesting method is changeRelativePositionsOfPlayers. Do you remember how I mentioned that a user always sees herself sitting at the bottom of the screen? That means every player sees something different. Here's the illustration again:

How the different players see each other

The local user's own Player object always sits at the bottom of the screen, i.e. in PlayerPositionBottom. But when the server created the Player objects and sent them to the clients, it was the server's own Player object that sat at PlayerPositionBottom.

changeRelativePositionsOfPlayers rotates the players around the table so that the local user is always at the bottom. This means each client has different values for the "position" properties of its Player objects. This is no problem if you always use the peer ID, and not the position, to identify players.

After receiving the ServerReady message, the client goes from the GameStateWaitingForReady state to the GameStateDealing state, and sends a PacketTypeClientReady packet back to the server. You still need to handle these packets on the server.

In Packet.m, change the switch-statement in packetWithData: to read:

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

		. . .
	}

That is, you'll simply return a plain Packet object for both PacketTypeSignInRequest and the new PacketTypeClientReady message.

In Game.m, add a case-statement to serverReceivedPacket:fromPlayer to handle the "client ready" packet:

		case PacketTypeClientReady:
			if (_state == GameStateWaitingForReady && [self receivedResponsesFromAllPlayers])
			{
				[self beginGame];
			}
			break;

Pretty simple! If you've received responses from all the clients, you call beginGame to start the game on the server as well. Notice two things:

  1. Both the client and server use the beginGame method. For Snap! it makes sense that the client and server share most of the code, which is why the Game class does both. For some other games, you may want to create separate "ClientGame" and "ServerGame" classes.
  2. You only accept the ClientReady packet if the game is in the GameStateWaitingForReady state. That's one more example of defensive programming. For example, you don't want to call beginGame if you're in the middle of a game and a misbehaving client sends the ClientReady packet again. Never trust what's on the other side of the line!

Build and run the app again for all your devices. Both the client and server should now say "the game should begin." Of course, you can't see any of this on the screen yet. You're about to make it look a bit more interesting.

Showing the Players

The beginGame method is where it happens. Change this method to:

- (void)beginGame
{
	_state = GameStateDealing;
	[self.delegate gameDidBegin:self];
}

This calls a new delegate method, gameDidBegin:, so add that to the protocol in Game.h:

@protocol GameDelegate <NSObject>
. . .
- (void)gameDidBegin:(Game *)game;
. . .
@end

Since GameViewController is the delegate of Game, you have to add the implementation of this method to that class. But before you can do that, there are a few things to add to the GameViewController nib. Quite a few things, in fact. You need to add labels for the player names, a Snap! button, and much more.

Rather than boring you with several pages of Interface Builder instructions, download the resources for this part and replace the old GameViewController.xib with the new one. Be sure to copy it over the existing xib file in the en.lproj folder!

Also add the new images from the download to the project. When you open the new nib in Interface Builder, it should look like this:

The full GameViewController nib

It's a bit messy, but obviously you won't be showing all of these elements on the screen at the same time. Feel free to click around in the nib to see what's in there. Here's a quick summary:

  • Labels for the players. There is a label for the player's name and for how many games the player has won. There is also a red indicator (a UIImageView) for each player that shows which player is currently active.
  • Snap! buttons. There is a Snap! button for the local user, and a Snap! speech bubble for all the players. You'll show the bubble whenever any player taps the Snap! button.
  • A yellow smiley and a red X. These are shown when the player who first yells "Snap!" is correct – there really is a matching pair of cards on the table – or wrong, respectively.
  • The "next round" button. This is in the bottom-right corner, shown after a player wins the current game. You've already seen the exit button in the bottom-left corner and the label in the center, which is used to let the player know what's going on.

Notice that the labels are all in their own container view, named "Labels Container" in the sidebar. The same thing is done for the buttons, and there's also a separate container view for the cards (although there are no card views in the nib). Giving the different UI elements their own container views makes it easier to layer them. For example, the card views will be on top of the labels, but below the buttons.

Many of the views from the nib are connected to outlets and actions on File's Owner. However, you haven't added the corresponding properties and methods to GameViewController, so let's do that now.

Add the following to the class extension at the top of GameViewController.m:

@property (nonatomic, weak) IBOutlet UIImageView *backgroundImageView;
@property (nonatomic, weak) IBOutlet UIView *cardContainerView;
@property (nonatomic, weak) IBOutlet UIButton *turnOverButton;
@property (nonatomic, weak) IBOutlet UIButton *snapButton;
@property (nonatomic, weak) IBOutlet UIButton *nextRoundButton;
@property (nonatomic, weak) IBOutlet UIImageView *wrongSnapImageView;
@property (nonatomic, weak) IBOutlet UIImageView *correctSnapImageView;

@property (nonatomic, weak) IBOutlet UILabel *playerNameBottomLabel;
@property (nonatomic, weak) IBOutlet UILabel *playerNameLeftLabel;
@property (nonatomic, weak) IBOutlet UILabel *playerNameTopLabel;
@property (nonatomic, weak) IBOutlet UILabel *playerNameRightLabel;

@property (nonatomic, weak) IBOutlet UILabel *playerWinsBottomLabel;
@property (nonatomic, weak) IBOutlet UILabel *playerWinsLeftLabel;
@property (nonatomic, weak) IBOutlet UILabel *playerWinsTopLabel;
@property (nonatomic, weak) IBOutlet UILabel *playerWinsRightLabel;

@property (nonatomic, weak) IBOutlet UIImageView *playerActiveBottomImageView;
@property (nonatomic, weak) IBOutlet UIImageView *playerActiveLeftImageView;
@property (nonatomic, weak) IBOutlet UIImageView *playerActiveTopImageView;
@property (nonatomic, weak) IBOutlet UIImageView *playerActiveRightImageView;

@property (nonatomic, weak) IBOutlet UIImageView *snapIndicatorBottomImageView;
@property (nonatomic, weak) IBOutlet UIImageView *snapIndicatorLeftImageView;
@property (nonatomic, weak) IBOutlet UIImageView *snapIndicatorTopImageView;
@property (nonatomic, weak) IBOutlet UIImageView *snapIndicatorRightImageView;

And synthesize these properties:

@synthesize backgroundImageView = _backgroundImageView;
@synthesize cardContainerView = _cardContainerView;
@synthesize turnOverButton = _turnOverButton;
@synthesize snapButton = _snapButton;
@synthesize nextRoundButton = _nextRoundButton;
@synthesize wrongSnapImageView = _wrongSnapImageView;
@synthesize correctSnapImageView = _correctSnapImageView;

@synthesize playerNameBottomLabel = _playerNameBottomLabel;
@synthesize playerNameLeftLabel = _playerNameLeftLabel;
@synthesize playerNameTopLabel = _playerNameTopLabel;
@synthesize playerNameRightLabel = _playerNameRightLabel;

@synthesize playerWinsBottomLabel = _playerWinsBottomLabel;
@synthesize playerWinsLeftLabel = _playerWinsLeftLabel;
@synthesize playerWinsTopLabel = _playerWinsTopLabel;
@synthesize playerWinsRightLabel = _playerWinsRightLabel;

@synthesize playerActiveBottomImageView = _playerActiveBottomImageView;
@synthesize playerActiveLeftImageView = _playerActiveLeftImageView;
@synthesize playerActiveTopImageView = _playerActiveTopImageView;
@synthesize playerActiveRightImageView = _playerActiveRightImageView;

@synthesize snapIndicatorBottomImageView = _snapIndicatorBottomImageView;
@synthesize snapIndicatorLeftImageView = _snapIndicatorLeftImageView;
@synthesize snapIndicatorTopImageView = _snapIndicatorTopImageView;
@synthesize snapIndicatorRightImageView = _snapIndicatorRightImageView;

Also add the following action method placeholders:

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

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

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

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

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

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

At this point, it's a good idea to test whether you made all these changes correctly. To get the app to compile again, add an empty version of the gameDidBegin: delegate method to GameViewController.m:

- (void)gameDidBegin:(Game *)game
{
}

And build and run. After pressing Start on the server, the screen should look something like this:

What starting a game looks like with the new nib

That's a bit too colorful for my taste. ;-) Don't worry, you'll clean this up in the following section!

Note: If you still see the old screen, then you may have to do a Clean first and/or remove the app from your device or the simulator. Sometimes Xcode keeps the old nib around, and you don't want that to happen.

Contributors

Over 300 content creators. Join our team.