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

Cleaning Up the Game Screen

Now that you have the GameViewController nib and have connected all the outlets and actions, you want to show only the labels for the names of the connected players. Change GameViewController's viewDidLoad to:

- (void)viewDidLoad
{
	[super viewDidLoad];

	self.centerLabel.font = [UIFont rw_snapFontWithSize:18.0f];

	self.snapButton.hidden = YES;
	self.nextRoundButton.hidden = YES;
	self.wrongSnapImageView.hidden = YES;
	self.correctSnapImageView.hidden = YES;

	[self hidePlayerLabels];
	[self hideActivePlayerIndicator];
	[self hideSnapIndicators];
}

In addition to setting the center label's font, you hide all the labels and other indicators. These methods don't exist yet, so add them:

#pragma mark - Game UI

- (void)hidePlayerLabels
{
	self.playerNameBottomLabel.hidden = YES;
	self.playerWinsBottomLabel.hidden = YES;

	self.playerNameLeftLabel.hidden = YES;
	self.playerWinsLeftLabel.hidden = YES;

	self.playerNameTopLabel.hidden = YES;
	self.playerWinsTopLabel.hidden = YES;

	self.playerNameRightLabel.hidden = YES;
	self.playerWinsRightLabel.hidden = YES;
}

- (void)hideActivePlayerIndicator
{
	self.playerActiveBottomImageView.hidden = YES;
	self.playerActiveLeftImageView.hidden   = YES;
	self.playerActiveTopImageView.hidden    = YES;
	self.playerActiveRightImageView.hidden  = YES;
}

- (void)hideSnapIndicators
{
	self.snapIndicatorBottomImageView.hidden = YES;
	self.snapIndicatorLeftImageView.hidden   = YES;
	self.snapIndicatorTopImageView.hidden    = YES;
	self.snapIndicatorRightImageView.hidden  = YES;
}

If you run the game now, you're back where you were and will no longer see all the new buttons and labels. The game screen will only shows the center label and the exit button and it will look like nothing changed.

To show the names of the players who've signed on, replace the gameDidBegin: method in GameViewController.m with:

- (void)gameDidBegin:(Game *)game
{
	[self showPlayerLabels];
	[self calculateLabelFrames];
	[self updateWinsLabels];
}

This requires three new methods, starting with showPlayerLabels:

- (void)showPlayerLabels
{
	Player *player = [self.game playerAtPosition:PlayerPositionBottom];
	if (player != nil)
	{
		self.playerNameBottomLabel.hidden = NO;
		self.playerWinsBottomLabel.hidden = NO;
	}

	player = [self.game playerAtPosition:PlayerPositionLeft];
	if (player != nil)
	{
		self.playerNameLeftLabel.hidden = NO;
		self.playerWinsLeftLabel.hidden = NO;
	}

	player = [self.game playerAtPosition:PlayerPositionTop];
	if (player != nil)
	{
		self.playerNameTopLabel.hidden = NO;
		self.playerWinsTopLabel.hidden = NO;
	}

	player = [self.game playerAtPosition:PlayerPositionRight];
	if (player != nil)
	{
		self.playerNameRightLabel.hidden = NO;
		self.playerWinsRightLabel.hidden = NO;
	}
}

Here you simply look at the four Player objects, but unlike what you did in Game, you're not enumerating the _players dictionary.

When designing the public interface for Game, I decided not to expose the list of players as a dictionary. Having them in a dictionary is useful for the Game class, but its delegate doesn't need to know about that internal structure. Instead, you'll give Game a method that lets other objects look up Player objects based on their position around the table.

Add the method signature to Game.h:

- (Player *)playerAtPosition:(PlayerPosition)position;

And add the implementation in Game.m:

- (Player *)playerAtPosition:(PlayerPosition)position
{
	NSAssert(position >= PlayerPositionBottom && position <= PlayerPositionRight, @"Invalid player position");

	__block Player *player;
	[_players enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
	{
		player = obj;
		if (player.position == position)
			*stop = YES;
		else
			player = nil;
	}];

	return player;
}

Notice the NSAssert that just makes sure any users of the Game object aren't trying to be too clever. I've sprinkled such assertions throughout the code, because they help catch silly programming errors.

Don't forget to add an import for Game.h to GameViewController.m, otherwise it won't see this new method:

#import "Game.h"

There are two methods left to add to GameViewController: updateWinsLabels and calculateLabelFrames. Start with updateWinsLabels. This method will put the number of games that each player has won into the label next to the player's name. Currently, you're not keeping track of this "games won" value, so first you need to add a new property to Player.h:

@property (nonatomic, assign) int gamesWon;

And synthesize it in Player.m:

@synthesize gamesWon = _gamesWon;

You don't need to give this variable an initial value, because the default value for ints is 0. Now add updateWinsLabels to GameViewController.m:

- (void)updateWinsLabels
{
	NSString *format = NSLocalizedString(@"%d Won", @"Number of games won");
	
	Player *player = [self.game playerAtPosition:PlayerPositionBottom];
	if (player != nil)
		self.playerWinsBottomLabel.text = [NSString stringWithFormat:format, player.gamesWon];

	player = [self.game playerAtPosition:PlayerPositionLeft];
	if (player != nil)
		self.playerWinsLeftLabel.text = [NSString stringWithFormat:format, player.gamesWon];

	player = [self.game playerAtPosition:PlayerPositionTop];
	if (player != nil)
		self.playerWinsTopLabel.text = [NSString stringWithFormat:format, player.gamesWon];

	player = [self.game playerAtPosition:PlayerPositionRight];
	if (player != nil)
		self.playerWinsRightLabel.text = [NSString stringWithFormat:format, player.gamesWon];
}

This is pretty self-explanatory. You do have to check each time whether "player" is nil, because if there are fewer than four players, not all positions around the table are filled.

Now for calculateLabelFrames. This is a big one. It even comes with a little helper method, resizeLabelToFit. Add both these methods to GameViewController.m:

- (void)resizeLabelToFit:(UILabel *)label
{
	[label sizeToFit];

	CGRect rect = label.frame;
	rect.size.width = ceilf(rect.size.width/2.0f) * 2.0f;  // make even
	rect.size.height = ceilf(rect.size.height/2.0f) * 2.0f;  // make even
	label.frame = rect;
}

- (void)calculateLabelFrames
{
	UIFont *font = [UIFont rw_snapFontWithSize:14.0f];
	self.playerNameBottomLabel.font = font;
	self.playerNameLeftLabel.font = font;
	self.playerNameTopLabel.font = font;
	self.playerNameRightLabel.font = font;

	font = [UIFont rw_snapFontWithSize:11.0f];
	self.playerWinsBottomLabel.font = font;
	self.playerWinsLeftLabel.font = font;
	self.playerWinsTopLabel.font = font;
	self.playerWinsRightLabel.font = font;

	self.playerWinsBottomLabel.layer.cornerRadius = 4.0f;
	self.playerWinsLeftLabel.layer.cornerRadius = 4.0f;
	self.playerWinsTopLabel.layer.cornerRadius = 4.0f;
	self.playerWinsRightLabel.layer.cornerRadius = 4.0f;

	UIImage *image = [[UIImage imageNamed:@"ActivePlayer"] stretchableImageWithLeftCapWidth:20 topCapHeight:0];
	self.playerActiveBottomImageView.image = image;
	self.playerActiveLeftImageView.image = image;
	self.playerActiveTopImageView.image = image;
	self.playerActiveRightImageView.image = image;

	CGFloat viewWidth = self.view.bounds.size.width;
	CGFloat centerX = viewWidth / 2.0f;

	Player *player = [self.game playerAtPosition:PlayerPositionBottom];
	if (player != nil)
	{
		self.playerNameBottomLabel.text = player.name;

		[self resizeLabelToFit:self.playerNameBottomLabel];
		CGFloat labelWidth = self.playerNameBottomLabel.bounds.size.width;

		CGPoint point = CGPointMake(centerX - 19.0f - 3.0f, 306.0f);
		self.playerNameBottomLabel.center = point;

		CGPoint winsPoint = point;
		winsPoint.x += labelWidth/2.0f + 6.0f + 19.0f;
		winsPoint.y -= 0.5f;
		self.playerWinsBottomLabel.center = winsPoint;

		self.playerActiveBottomImageView.frame = CGRectMake(0, 0, 20.0f + labelWidth + 6.0f + 38.0f + 2.0f, 20.0f);

		point.x = centerX - 9.0f;
		self.playerActiveBottomImageView.center = point;
	}

	player = [self.game playerAtPosition:PlayerPositionLeft];
	if (player != nil)
	{
		self.playerNameLeftLabel.text = player.name;
	
		[self resizeLabelToFit:self.playerNameLeftLabel];
		CGFloat labelWidth = self.playerNameLeftLabel.bounds.size.width;

		CGPoint point = CGPointMake(2.0 + 20.0f + labelWidth/2.0f, 48.0f);
		self.playerNameLeftLabel.center = point;

		CGPoint winsPoint = point;
		winsPoint.x += labelWidth/2.0f + 6.0f + 19.0f;
		winsPoint.y -= 0.5f;
		self.playerWinsLeftLabel.center = winsPoint;

		self.playerActiveLeftImageView.frame = CGRectMake(2.0f, 38.0f, 20.0f + labelWidth + 6.0f + 38.0f + 2.0f, 20.0f);
	}

	player = [self.game playerAtPosition:PlayerPositionTop];
	if (player != nil)
	{
		self.playerNameTopLabel.text = player.name;

		[self resizeLabelToFit:self.playerNameTopLabel];
		CGFloat labelWidth = self.playerNameTopLabel.bounds.size.width;

		CGPoint point = CGPointMake(centerX - 19.0f - 3.0f, 15.0f);
		self.playerNameTopLabel.center = point;

		CGPoint winsPoint = point;
		winsPoint.x += labelWidth/2.0f + 6.0f + 19.0f;
		winsPoint.y -= 0.5f;
		self.playerWinsTopLabel.center = winsPoint;

		self.playerActiveTopImageView.frame = CGRectMake(0, 0, 20.0f + labelWidth + 6.0f + 38.0f + 2.0f, 20.0f);

		point.x = centerX - 9.0f;
		self.playerActiveTopImageView.center = point;
	}

	player = [self.game playerAtPosition:PlayerPositionRight];
	if (player != nil)
	{
		self.playerNameRightLabel.text = player.name;

		[self resizeLabelToFit:self.playerNameRightLabel];
		CGFloat labelWidth = self.playerNameRightLabel.bounds.size.width;

		CGPoint point = CGPointMake(viewWidth - labelWidth/2.0f - 2.0f - 6.0f - 38.0f - 12.0f, 48.0f);
		self.playerNameRightLabel.center = point;

		CGPoint winsPoint = point;
		winsPoint.x += labelWidth/2.0f + 6.0f + 19.0f;
		winsPoint.y -= 0.5f;
		self.playerWinsRightLabel.center = winsPoint;

		self.playerActiveRightImageView.frame = CGRectMake(self.playerNameRightLabel.frame.origin.x - 20.0f, 38.0f, 20.0f + labelWidth + 6.0f + 38.0f + 2.0f, 20.0f);
	}
}

This might look scary, but it simply sets the font and text on the player name labels, and then moves them around so they are in their ideal positions. Notice there's a bit of arithmetic going on. Rather than adding everything up myself, I let the compiler take care of it (laziness is a programmer's greatest virtue).

Because this code uses the .layer.cornerRadius property to give some of the labels rounded corners, you have to link the app with the QuartzCore framework. Add this framework to the project, and add an import in Snap-Prefix.pch:

	#import <QuartzCore/QuartzCore.h>

Run the app and you should now see something like this, on both the server and the clients:

The player labels on the game screen

Contributors

Over 300 content creators. Join our team.