1 翻牌游戏
1.1 问题
根据苹果MVC设计模式的思想原则实现一个简单的翻牌游戏,功能如下:
1)界面上随机摆放12张背面朝上的纸牌,界面效果如图-1所示:
图- 1
2)点击纸牌可以使纸牌翻页,翻牌后进行数字和花色的匹配,如果数字一样得4分,花色一样得1分;
3)在界面的左下角有一个记录得分的标签,界面如图-2所示:
图- 2
1.2 方案
首先使用Xcode创建一个带有xib的项目,在xib界面拖放12个UIButton对象和一个计分的UILabel对象,因为纸牌可以点击,所以使用UIButton控件作为纸牌对象,并且在检查器中设置好纸牌的背景图片。
其次根据苹果IOS开发的MVC原则,将整个案例的类分为三个群组,即Model层——用来保存游戏的纸牌数据和分数计算,View层——保存游戏的界面(xib文件)以及Controller层——控制程序的流程,协调View层和Model层。
然后创建TRCard类(纸牌类)用来管理纸牌的数据,TRDeck类(牌桌类)管理发牌的规则,TRCardGame类(游戏类)管理整个游戏的逻辑,这三个类都属于Model层,分别给这三个类增加属性和方法,实现各自管理的数据和逻辑。
最后在TRCardGameController类里面实现Model层和View层的通信逻辑。
1.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:搭建游戏界面
首先在xib界面拖放12个UIButton对象和一个计分的UILabel对象,因为纸牌可以点击这是使用UIButton对象作为纸牌。
其次给纸牌设置背景图片,选中纸牌的背景图片拖放到Xcode导航栏的View群组里面,会弹出如图-3所示对话框,在Copy items if needed选项前的复选框打钩,表明将图片拷贝到项目中:
图-3
点击Finish按钮之后,可以在导航栏的View群组里面看见添加进来的两张图片,名字分别为cardback.png和cardfront.png,分别是纸牌正面图片和背面图片,如图-4所示:
图-4
然后在右边栏的第四个检查器里面设置纸牌的背景图,初始界面将按钮的背景图设置为纸牌的背面图片cardback.png,如图-5所示:
图-5
步骤二:创建TRCard类、TRDeck类和TRCardGame类
创建Model层的三个类,TRCard,TRDeck和TRGame,这三个类全都继承至NSObject,如图-6所示:
图-6
其次在TRCard类里面声明NSString类型的用来表示纸牌内容的属性content,而每张牌由级别和花色组成,因此再声明两个属性rank和suit,分别用来表示级别和花色,在这里级别使用NSUInteger类型,花色使用特殊的字符表示,因此是NSString类型,代码如下所示:
- //这张牌的内容, 如:"♣A"
- @property (nonatomic, strong, readonly)NSString *content;
- //纸牌花色♠♥♣♦
- @property (nonatomic, strong)NSString *suit;
- //纸牌级别
- @property (nonatomic) NSUInteger rank;
然后再声明两个BOOL类型的属性用来表示纸牌的两个状态,chosen表示是否被选中状态,matched表示是否被匹配状态,代码如下所示:
- @property (nonatomic, getter=isChosen) BOOL chosen;
- @property (nonatomic, getter=isMatched) BOOL matched;
在这里定义属性使用了getter关键字,这是一种开发习惯,将BOOL类型属性的getter方法进行重新命名,表达更清楚明确,在这里自动生成的两个属性的getter方法名为isChosen和isMatched。
实际的开发中为了提高代码的效率,根据程序的需要通常会重写属性的setter和getter方法,属性suit只能接受♠♥♣♦这四种花色,赋值其他字符是非法的,为了保护suit属性,可以在suit的setter方法里面增加以下限制代码,如下所示:
- + (NSArray *)validSuits
- {
- return @[@"♠", @"♥", @"♣", @"♦"];
- }
- - (void)setSuit:(NSString *)suit
- {
- //判断传入的参数是否合法
- if([[TRCardvalidSuits] containsObject:suit]){
- _suit = suit;
- }
- }
以上代码中validSuits方法可以在TRCard.h文件中公开方便外部使用,否则外部可能不知道哪些是合法字符。同样的为了保护suit属性在getter方法里面也可以增加一些限制的代码,如下所示:
- -(NSString *)suit
- {
- //判断_suit实例变量是否为空,如果为空则返回?,保证_suit不会为空
- return _suit ? _suit : @"?";
- }
此时需要注意,重写完setter和getter方法之后系统不会再自动生成实例变量_suit,因此需要使用@synthesize关键字来生成实例变量_suit,代码如下所示:
- @synthesize suit = _suit;
同样为了保护rank属性,也可以将rank的setter方法和getter方法重写,代码如下所示:
- - (void)setRank:(NSUInteger)rank
- {
- //纸牌一共有13个级别
- if(rank <= 13){
- _rank = rank;
- }
- }
纸牌的内容是由花色和级别组成,因此重写content的getter方法即可,每次调用getter方法时即可获取到纸牌的显示内容,代码如下所示:
- + (NSArray *)randStrins
- {
- //纸牌13个级别对应的字符串,由小到大放入数组
- return @[@"?", @"A", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"10", @"J", @"Q", @"K"];
- }
- - (NSString *)content
- {
- NSString *rankString = [TRCardrandStrins][self.rank];
- return [self.suitstringByAppendingString:rankString];
- }
最后在TRCard类里面增加一个公开的方法,用于获取纸牌的最大级别,方便其他对象使用,代码如下所示:
- //返回级别的最大值
- + (NSUInteger)maxRank
- {
- return [[TRCardrandStrins] count] - 1;
- }
步骤三:TRDeck类添加属性和方法
TRDeck类主要是管理发牌的规则,既然是发牌,就需要拥有一个纸牌数组的属性,因此声明一个NSMutableArray类型的私有属性cards,使用懒汉模式(延迟加载)给实例变量_cards进行初始化,代码如下所示:
- //声明属性
- @property (nonatomic, strong) NSMutableArray *cards;
- //重写setter方法初始化_card实例变量
- - (NSMutableArray *)cards
- {
- if(!_cards)_cards = [[NSMutableArrayalloc]init];
- return _cards;
- }
然后在TRDeck的初始化方法里面按照纸牌的规则给_card数组添加纸牌对象,一共52个纸牌对象,代码如下所示:
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- for (NSString *suit in [TRCardvalidSuits]) {
- for(NSUInteger rank = 1; rank<=[TRCardmaxRank]; rank++){
- //创建纸牌对象
- TRCard *card = [[TRCardalloc]init];
- card.suit = suit;
- card.rank = rank;
- //将纸牌对象添加的纸牌数组中
- [self.cardsaddObject:card];
- }
- }
- }
- return self;
- }
最后实现随机发牌的方法randomCard,此方法需要在TRDeck.h文件中公开,代码如下所示:
- - (TRCard *)randomCard
- {
- //使用随机函数,随机出一个数组下标,
- unsignedint index = arc4random() % self.cards.count;
- //根据随机出的数组下标从数组中获取到纸牌对象
- TRCard *card = self.cards[index];
- //从纸牌数值将刚才获取的纸牌对象移除
- [self.cardsremoveObjectAtIndex:index];
- return card;
- }
步骤四:TRCardGame类添加属性和方法
TRCardGame类是用来管理整个游戏的逻辑,分析本案例主要需要实现纸牌匹配和计算分数的逻辑,因此首先声明一个NSInteger的属性score,此时需要注意分数只能由TRCardGame类进行计算和修改,外部对该属性只能读取不能修改,因此在TRCardGame.h文件中声明一个只读的score属性,在TRCardGame.m文件中声明一个可读写的score属性,代码如下所示:
- //TRCardGame.h文件中
- @property (nonatomic, readonly) NSInteger score;
- //TRCardGame.m文件中
- @property (nonatomic, readwrite) NSInteger score;
其次TRCardGame需要随机产生12张纸牌,所以需要声明一个NSMutableArray类型的属性cards,同样在setter方法中进行实例变量_cards初始化,代码如下所示:
- //声明属性
- @property (nonatomic, strong) NSMutableArray *cards;
- //重写setter方法初始化_card实例变量
- - (NSMutableArray *)cards
- {
- if(!_cards)_cards = [[NSMutableArrayalloc]init];
- return _cards;
- }
然后自定义一个TRCardGame类的初始化方法initWithCardCount:usingDeck:,此方法用来初始化TRCardGame对象,此方法中使用一个TRDeck对象随机出12张纸牌,此时需要注意初始化方法通常都是需要公开在.h文件中进行声明,代码如下所示:
- - (instancetype)initWithCardCount:(NSUInteger)count usingDeck:(TRDeck *)deck
- {
- self = [super init];
- if (self) {
- for(inti=0; i<count; i++){
- TRCard *card = [deck randomCard];
- [self.cardsaddObject:card];
- }
- }
- return self;
- }
最后实现纸牌的匹配逻辑,定义一个动态方法chooseCardAtIndex:,当用户选中某张牌是调用此方法,其中TRCard对象的匹配判断应有TRCard类提供,同样此方法需要在.h文件中公开,代码如下所示:
- TRCardGame类中的代码:
- //根据下标返回指定扑克牌
- - (TRCard *)cardAtIndex:(NSUInteger)index
- {
- return index<self.cards.count ? self.cards[index] : nil;
- }
- //用户选中了一张牌
- - (void)chooseCardAtIndex:(NSUInteger)index
- {
- //进行匹配
- TRCard *card = [self cardAtIndex:index];
- if(![card isMatched]){
- if([card isChosen]){
- card.chosen = NO;//再翻回去
- }else{//没有匹配,也没有在正面
- //匹配其他翻过来的牌
- for (TRCard *otherCard in self.cards) {
- if([otherCardisChosen] && ![otherCardisMatched]){
- int score = [card match:otherCard];
- if(score){//匹配成功
- self.score += score;
- card.matched = YES;
- otherCard.matched = YES;
- }else{//匹配失败
- otherCard.chosen = NO;
- }
- }
- }
- //把牌翻过来
- card.chosen = YES;
- }
- }
- }
- TRCard类中的代码:
- - (int)match:(TRCard *)otherCard
- {
- int score = 0;
- if(self.rank == otherCard.rank) score = 4;
- else if(self.suit == otherCard.suit) score = 1;
- return score;
- }
步骤五:通过TRCardGameController实现View和Model层的通信
第一步已经搭建好了界面,此时需要将xib中的对象关联到TRCardGameController中,首先关联计分UILabel对象,以拉线的方式关联成TRCardGameController的私有属性scoreLabel,代码如下所示:
- @property (weak, nonatomic) IBOutletUILabel *scoreLabel;
其次关联12个纸牌对象,选中一张纸牌按住control键,往TRCardGameController.m文件的类扩展中拖,在弹出的对话框Connection选项中选择Outlet Collection,如图-7所示:
图-7
释放鼠标会自动生成一个NSArray类型的属性cardButtons,该数组里面的元素指向xib中的一个对象,选中代码前的是新圆圈,依次关联xib上的其他11张纸牌,这样_cardButtons数组里面的元素指向12个xib创建的纸牌对象,如图-8所示:
图-8
然后将12个纸牌对象以拉线的方式关联同一个IBAction方法touchCardButton:,每当选择纸牌时需要进行纸牌匹配的逻辑判断计算得分,这件事情需要TRCardGame对象去实现,因此TRCardGameController类需要拥有一个TRCardGame类的私有属性以及一个TRDeck类的私有属性,声明属性并且进行初始化,代码如下所示:
- @property (nonatomic, strong) TRDeck *deck;
- @property (nonatomic, strong) TRCardGame *game;
- - (TRDeck *)deck
- {
- if(!_deck)_deck = [[TRDeckalloc]init];
- return _deck;
- }
- - (TRCardGame *)game
- {
- if(!_game)_game = [[TRCardGamealloc]initWithCardCount:self.cardButtons.countusingDeck:self.deck];
- return _game;
- }
然后在touchCardButton:方法里通过TRCardGame对进行纸牌的匹配逻辑计算,代码如下所示:
- //点击按钮计算纸牌的匹配逻辑
- - (IBAction)touchCardButton:(UIButton *)sender
- {
- NSUIntegerchooseCardIndex = [self.cardButtonsindexOfObject:sender];
- [self.gamechooseCardAtIndex:chooseCardIndex];
- }
最后通过TRCardGame对象得出的匹配的结果更新界面,代码如下所示:
- //更新整个界面
- - (void)updateUI
- {
- for (UIButton *cardButton in self.cardButtons) {
- NSUInteger index = [self.cardButtonsindexOfObject:cardButton];
- TRCard *card = [self.gamecardAtIndex:index];
- [cardButtonsetTitle:[self titleForCard:card] forState:UIControlStateNormal];
- [cardButtonsetBackgroundImage:[self imageForCard:card] forState:UIControlStateNormal];
- cardButton.enabled = ![card isMatched];
- self.scoreLabel.text = [NSStringstringWithFormat:@"Score:%d", self.game.score];
- }
- }
- //根据纸牌选中的状态更改纸牌背景图片
- - (UIImage *)imageForCard:(TRCard *)card
- {
- if([card isChosen]){
- return [UIImageimageNamed:@"cardfront.png"];
- }else{
- return [UIImageimageNamed:@"cardback.png"];
- }
- }
- //根据纸牌选中的状态更改纸牌的title
- - (NSString *)titleForCard:(TRCard *)card
- {
- return [card isChosen] ? card.content : @"";
- }
- //点击按钮更新界面
- - (IBAction)touchCardButton:(UIButton *)sender
- {
- NSUIntegerchooseCardIndex = [self.cardButtonsindexOfObject:sender];
- [self.gamechooseCardAtIndex:chooseCardIndex];
- [selfupdateUI];
- }
1.4 完整代码
本案例中,TRCardGameViewController.m文件中的完整代码如下所示:
- #import "TRCardGameViewController.h"
- #import "TRCardGame.h"
- @interfaceTRCardGameViewController ()
- @property (nonatomic, strong) TRDeck *deck;
- @property (nonatomic, strong) TRCardGame *game;
- @property (strong, nonatomic) IBOutletCollection(UIButton) NSArray *cardButtons;
- @property (weak, nonatomic) IBOutletUILabel *scoreLabel;
- @end
- @implementationTRCardGameViewController
- - (TRDeck *)deck
- {
- if(!_deck)_deck = [[TRDeckalloc]init];
- return _deck;
- }
- - (TRCardGame *)game
- {
- if(!_game)_game = [[TRCardGamealloc]initWithCardCount:self.cardButtons.countusingDeck:self.deck];
- return _game;
- }
- - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil
- {
- self = [super initWithNibName:nibNameOrNilbundle:nibBundleOrNil];
- if (self) {
- // Custom initialization
- }
- return self;
- }
- - (IBAction)touchCardButton:(UIButton *)sender
- {
- NSUIntegerchooseCardIndex = [self.cardButtonsindexOfObject:sender];
- [self.gamechooseCardAtIndex:chooseCardIndex];
- [selfupdateUI];
- }
- - (void)updateUI
- {
- for (UIButton *cardButton in self.cardButtons) {
- NSUInteger index = [self.cardButtonsindexOfObject:cardButton];
- TRCard *card = [self.gamecardAtIndex:index];
- [cardButtonsetTitle:[self titleForCard:card] forState:UIControlStateNormal];
- [cardButtonsetBackgroundImage:[self imageForCard:card] forState:UIControlStateNormal];
- cardButton.enabled = ![card isMatched];
- self.scoreLabel.text = [NSStringstringWithFormat:@"Score:%d", self.game.score];
- }
- }
- - (UIImage *)imageForCard:(TRCard *)card
- {
- if([card isChosen]){
- return [UIImageimageNamed:@"cardfront.png"];
- }else{
- return [UIImageimageNamed:@"cardback.png"];
- }
- }
- - (NSString *)titleForCard:(TRCard *)card
- {
- return [card isChosen] ? card.content : @"";
- }
- @end
本案例中,TRCard.h文件中的完整代码如下所示:
- #import<Foundation/Foundation.h>
- //扑克牌类
- @interfaceTRCard : NSObject
- @property (nonatomic, getter=isChosen) BOOL chosen;//被选中
- @property (nonatomic, getter=isMatched) BOOL matched;//被匹配
- @property (nonatomic, strong, readonly)NSString *content;//这张牌的内容, 如:"♣️A"
- @property (nonatomic, strong)NSString *suit;//花色♠️♥️♣️♦️
- @property (nonatomic) NSUInteger rank;//级别
- //和另外的一个扑克牌匹配,返回得分
- - (int)match:(TRCard *)otherCard;
- //返回合法的花色
- + (NSArray *)validSuits;
- //返回级别的最大值
- + (NSUInteger)maxRank;
- @end
本案例中,TRCard.m文件中的完整代码如下所示:
- #import "TRCard.h"
- @implementationTRCard
- @synthesize suit = _suit;
- - (int)match:(TRCard *)otherCard
- {
- int score = 0;
- if(self.rank == otherCard.rank) score = 4;
- else if(self.suit == otherCard.suit) score = 1;
- return score;
- }
- + (NSArray *)validSuits
- {
- return @[@"♠️", @"♥️", @"♣️", @"♦️"];
- }
- - (void)setSuit:(NSString *)suit
- {
- if([[TRCardvalidSuits] containsObject:suit]){
- _suit = suit;
- }
- }
- -(NSString *)suit
- {
- return _suit ? _suit : @"?";
- }
- - (void)setRank:(NSUInteger)rank
- {
- if(rank <= 13){
- _rank = rank;
- }
- }
- + (NSArray *)randStrins
- {
- return @[@"?", @"A", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"10", @"J", @"Q", @"K"];
- }
- - (NSString *)content
- {
- NSString *rankString = [TRCardrandStrins][self.rank];
- return [self.suitstringByAppendingString:rankString];
- }
- //返回级别的最大值
- + (NSUInteger)maxRank
- {
- return [[TRCardrandStrins] count] - 1;
- }
- @end
本案例中,TRDeck.h文件中的完整代码如下所示:
- #import<Foundation/Foundation.h>
- #import "TRCard.h"
- //牌桌类
- @interfaceTRDeck : NSObject
- - (TRCard *)randomCard;
- @end
本案例中,TRDeck.m文件中的完整代码如下所示:
- #import "TRDeck.h"
- @interfaceTRDeck ()
- @property (nonatomic, strong) NSMutableArray *cards;
- @end
- @implementationTRDeck
- - (NSMutableArray *)cards
- {
- if(!_cards)_cards = [[NSMutableArrayalloc]init];
- return _cards;
- }
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- for (NSString *suit in [TRCardvalidSuits]) {
- for(NSUInteger rank = 1; rank<=[TRCardmaxRank]; rank++){
- //创建纸牌对象
- TRCard *card = [[TRCardalloc]init];
- card.suit = suit;
- card.rank = rank;
- //将纸牌对象添加的纸牌数组中
- [self.cardsaddObject:card];
- }
- }
- }
- return self;
- }
- //随机发牌
- - (TRCard *)randomCard
- {
- unsignedint index = arc4random() % self.cards.count;
- TRCard *card = self.cards[index];
- [self.cardsremoveObjectAtIndex:index];
- return card;
- }
- @end
本案例中,TRCardGame.h文件中的完整代码如下所示:
- #import<Foundation/Foundation.h>
- #import "TRCard.h"
- #import "TRDeck.h"
- //游戏类
- @interfaceTRCardGame : NSObject
- - (instancetype)initWithCardCount:(NSUInteger)count usingDeck:(TRDeck *)deck;
- //用户选中了一张牌
- - (void)chooseCardAtIndex:(NSUInteger)index;
- //根据下标返回指定扑克牌
- - (TRCard *)cardAtIndex:(NSUInteger)index;
- @property (nonatomic, readonly) NSInteger score;//分数
- @end
本案例中,TRCardGame.m文件中的完整代码如下所示:
- #import "TRCardGame.h"
- @interfaceTRCardGame ()
- @property (nonatomic, strong)NSMutableArray *cards;
- @property (nonatomic, readwrite) NSInteger score;//分数
- @end
- @implementationTRCardGame
- - (NSMutableArray *)cards
- {
- if (!_cards) {
- _cards = [[NSMutableArrayalloc]init];
- }
- return _cards;
- }
- - (instancetype)initWithCardCount:(NSUInteger)count usingDeck:(TRDeck *)deck
- {
- self = [super init];
- if (self) {
- for(inti=0; i<count; i++){
- TRCard *card = [deck randomCard];
- [self.cardsaddObject:card];
- }
- }
- return self;
- }
- //用户选中了一张牌
- - (void)chooseCardAtIndex:(NSUInteger)index
- {
- //进行匹配
- TRCard *card = [self cardAtIndex:index];
- if(![card isMatched]){
- if([card isChosen]){
- card.chosen = NO;//再翻回去
- }else{//没有匹配,也没有在正面
- //匹配其他翻过来的牌
- for (TRCard *otherCard in self.cards) {
- if([otherCardisChosen] && ![otherCardisMatched]){
- int score = [card match:otherCard];
- if(score){//匹配成功
- self.score += score;
- card.matched = YES;
- otherCard.matched = YES;
- }else{//匹配失败
- otherCard.chosen = NO;
- }
- }
- }
- //把牌翻过来
- card.chosen = YES;
- }
- }
- }
- //根据下标返回指定扑克牌
- - (TRCard *)cardAtIndex:(NSUInteger)index
- {
- return index<self.cards.count ? self.cards[index] : nil;
- }
- @end