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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make a Simple Playing Card Game with Multiplayer and Bluetooth, Part 5
55 mins
More data model: the Stack class
Now let's assign the Card objects to the Players. For this you need to make a new class, Stack, that simply keeps a pile of cards.
Add a new Objective-C class to the project, named Stack, subclass of NSObject. This is also a data model class. Replace Stack.h with:
@class Card;
@interface Stack : NSObject
- (void)addCardToTop:(Card *)card;
- (NSUInteger)cardCount;
- (NSArray *)array;
@end
And Stack.m with:
#import "Stack.h"
#import "Card.h"
@implementation Stack
{
NSMutableArray *_cards;
}
- (id)init
{
if ((self = [super init]))
{
_cards = [NSMutableArray arrayWithCapacity:26];
}
return self;
}
- (void)addCardToTop:(Card *)card
{
NSAssert(card != nil, @"Card cannot be nil");
NSAssert([_cards indexOfObject:card] == NSNotFound, @"Already have this Card");
[_cards addObject:card];
}
- (NSUInteger)cardCount
{
return [_cards count];
}
- (NSArray *)array
{
return [_cards copy];
}
@end
Instead of making this Stack class, you could have just given Player two NSMutableArrays to keep its piles of cards in, but having a separate Stack class makes the code clearer.
You'll give the Player two Stack properties, so add a forward declaration in Player.h:
@class Card;
@class Stack;
And add the properties:
@property (nonatomic, strong, readonly) Stack *closedCards;
@property (nonatomic, strong, readonly) Stack *openCards;
You declare these properties readonly because outside objects cannot replace the Stack object with another. In Player.m, import the classes you just made:
#import "Card.h"
#import "Stack.h"
Then synthesize the properties:
@synthesize closedCards = _closedCards;
@synthesize openCards = _openCards;
And create the objects in the init method:
- (id)init
{
if ((self = [super init]))
{
_closedCards = [[Stack alloc] init];
_openCards = [[Stack alloc] init];
}
return self;
}
Now switch back to Game.m and finish the dealCards method:
- (void)dealCards
{
NSAssert(self.isServer, @"Must be server");
NSAssert(_state == GameStateDealing, @"Wrong state");
Deck *deck = [[Deck alloc] init];
[deck shuffle];
while ([deck cardsRemaining] > 0)
{
for (PlayerPosition p = _startingPlayerPosition; p < _startingPlayerPosition + 4; ++p)
{
Player *player = [self playerAtPosition:(p % 4)];
if (player != nil && [deck cardsRemaining] > 0)
{
Card *card = [deck draw];
[player.closedCards addCardToTop:card];
}
}
}
Player *startingPlayer = [self activePlayer];
[self.delegate gameShouldDealCards:self startingWithPlayer:startingPlayer];
}
This requires some new imports:
#import "Player.h"
#import "Stack.h"
You're also calling a new GameDelegate method, gameShouldDealCards:startingWithPlayer:. The Player object that you give it comes from the [self activePlayer] method which you haven't added yet, so go ahead and add this simple implementation:
- (Player *)activePlayer
{
return [self playerAtPosition:_activePlayerPosition];
}
Add the new delegate method to the protocol in Game.h:
- (void)gameShouldDealCards:(Game *)game startingWithPlayer:(Player *)startingPlayer;
And implement it in GameViewController.m:
- (void)gameShouldDealCards:(Game *)game startingWithPlayer:(Player *)startingPlayer
{
self.centerLabel.text = NSLocalizedString(@"Dealing...", @"Status text: dealing");
}
There's more to come in that method, but this is a good time to build & run the app to see if it still works. The text on the screen should now change to "Dealing...".
The dealing cards animation
So far you have created only data model classes and used few of the standard views (UILabels, UIImageView). In this section you will make a CardView class that represents a Card on the screen. In a Cocos2D game you might make Card extend from CCSprite, in which case it serves both as the data model and the view, but in Snap! you'll strictly separate our classes along the lines of the Model-View-Controller pattern.
Add a new Objective-C class to the project, named CardView, subclass of UIView. Place it in the Views group. Replace the contents of CardView.h with:
const CGFloat CardWidth;
const CGFloat CardHeight;
@class Card;
@class Player;
@interface CardView : UIView
@property (nonatomic, strong) Card *card;
- (void)animateDealingToPlayer:(Player *)player withDelay:(NSTimeInterval)delay;
@end
The CardView object has a reference to the Card object that it represents, and a method that performs the dealing animation. Over the course of this tutorial you'll be adding more animation methods to this class.
Replace CardView.m with:
#import "CardView.h"
#import "Card.h"
#import "Player.h"
const CGFloat CardWidth = 67.0f; // this includes drop shadows
const CGFloat CardHeight = 99.0f;
@implementation CardView
{
UIImageView *_backImageView;
UIImageView *_frontImageView;
CGFloat _angle;
}
@synthesize card = _card;
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
{
self.backgroundColor = [UIColor clearColor];
[self loadBack];
}
return self;
}
- (void)loadBack
{
if (_backImageView == nil)
{
_backImageView = [[UIImageView alloc] initWithFrame:self.bounds];
_backImageView.image = [UIImage imageNamed:@"Back"];
_backImageView.contentMode = UIViewContentModeScaleToFill;
[self addSubview:_backImageView];
}
}
@end
CardView is a UIView (with dimensions of CardWidth by CardHeight points). It employs a UIImageView as a subview that contains the image of the card.
In the Images folder for last part's resources (which you already should have added to the project earlier), there are 52 images for the front of the cards, and one for the back.
I made these images by scanning in an ancient deck of cards -- a family heirloom -- and fixing them up in Photoshop. Here are some of these images:
Initially the card is face down, so you only load the back image. The animation is performed in the following method. Add it to CardView.m:
- (void)animateDealingToPlayer:(Player *)player withDelay:(NSTimeInterval)delay
{
self.frame = CGRectMake(-100.0f, -100.0f, CardWidth, CardHeight);
self.transform = CGAffineTransformMakeRotation(M_PI);
CGPoint point = [self centerForPlayer:player];
_angle = [self angleForPlayer:player];
[UIView animateWithDuration:0.2f
delay:delay
options:UIViewAnimationOptionCurveEaseOut
animations:^
{
self.center = point;
self.transform = CGAffineTransformMakeRotation(_angle);
}
completion:nil];
}
The card view starts out off-screen (a little beyond the top-left corner), rotated upside-down from the point of view of the bottom player (which is the user of the device). Then you calculate the final position and angle for the card, and set these in a UIView animation block. Pretty simple, but you do have to add these two helper methods:
- (CGPoint)centerForPlayer:(Player *)player
{
CGRect rect = self.superview.bounds;
CGFloat midX = CGRectGetMidX(rect);
CGFloat midY = CGRectGetMidY(rect);
CGFloat maxX = CGRectGetMaxX(rect);
CGFloat maxY = CGRectGetMaxY(rect);
CGFloat x = -3.0f + RANDOM_INT(6) + CardWidth/2.0f;
CGFloat y = -3.0f + RANDOM_INT(6) + CardHeight/2.0f;
if (player.position == PlayerPositionBottom)
{
x += midX - CardWidth - 7.0f;
y += maxY - CardHeight - 30.0f;
}
else if (player.position == PlayerPositionLeft)
{
x += 31.0f;
y += midY - CardWidth - 45.0f;
}
else if (player.position == PlayerPositionTop)
{
x += midX + 7.0f;
y += 29.0f;
}
else
{
x += maxX - CardHeight + 1.0f;
y += midY - 30.0f;
}
return CGPointMake(x, y);
}
There are four possible player positions and you want to make it look like the players are gathered around a table. So depending on the player's position you have to calculate the position of the cards, but also the rotation angle of these cards. You add a little random fudge factor to the final position, to give a more realistic feel to how the cards end up on a pile.
Also add the angleForPlayer: method:
- (CGFloat)angleForPlayer:(Player *)player
{
float theAngle = (-0.5f + RANDOM_FLOAT()) / 4.0f;
if (player.position == PlayerPositionLeft)
theAngle += M_PI / 2.0f;
else if (player.position == PlayerPositionTop)
theAngle += M_PI;
else if (player.position == PlayerPositionRight)
theAngle -= M_PI / 2.0f;
return theAngle;
}
Here you also fudge the angle a little bit, for added realism. The RANDOM_INT() and RANDOM_FLOAT() are macros that need to be added to Snap-Prefix.pch:
// Returns a random number between 0.0 and 1.0 (inclusive).
#define RANDOM_FLOAT() ((float)arc4random()/0xFFFFFFFFu)
// Returns a random number between 0 and n (inclusive).
#define RANDOM_INT(n) (arc4random() % (n + 1))
This gives us a CardView class that can be animated, so let's put it to good use.
In GameViewController.m, you have to create these CardView objects, add them to the main view (actually to the "Card Container" subview), and tell them to start animating. This happens in the gameShouldDealCards:startingWithPlayer: method. Augment this method to do the following:
- (void)gameShouldDealCards:(Game *)game startingWithPlayer:(Player *)startingPlayer
{
self.centerLabel.text = NSLocalizedString(@"Dealing...", @"Status text: dealing");
self.snapButton.hidden = YES;
self.nextRoundButton.hidden = YES;
NSTimeInterval delay = 1.0f;
for (int t = 0; t < 26; ++t)
{
for (PlayerPosition p = startingPlayer.position; p < startingPlayer.position + 4; ++p)
{
Player *player = [self.game playerAtPosition:p % 4];
if (player != nil && t < [player.closedCards cardCount])
{
CardView *cardView = [[CardView alloc] initWithFrame:CGRectMake(0, 0, CardWidth, CardHeight)];
cardView.card = [player.closedCards cardAtIndex:t];
[self.cardContainerView addSubview:cardView];
[cardView animateDealingToPlayer:player withDelay:delay];
delay += 0.1f;
}
}
}
}
You loop through the players, clockwise from the starting player's position, and for each Card that this Player has you add a CardView and tell it to animate. The delay parameter prevents the cards from flying out all at once. Notice you loop from 0 to 26 because no player will ever have more than 26 cards at this point.
This requires a bunch of imports to compile:
#import "Card.h"
#import "CardView.h"
#import "Player.h"
#import "Stack.h"
You also need to add the cardAtIndex: method to Stack.h and Stack.m:
- (Card *)cardAtIndex:(NSUInteger)index
{
return [_cards objectAtIndex:index];
}
Now you can run the app and have a cool dealing animation. A screenshot really doesn't do it justice, but this is what it looks like: