斯坦福大学IOS7开发课程3
课程3主要内容是继续之前的纸牌游戏。上节课实现的纸牌游戏App只是实现了简单的纸牌翻转,这节课使其变得真正可玩。
主要分为三个部分:
- 先回顾上节课编写的App
- 对上节课的App改进
- 使游戏变得可玩
上节课实现的纸牌翻转App
将背景颜色设置为绿色
打开
Main.storyboard
,选中View后,在属性栏里将Background
的颜色设置为绿色。
添加纸牌正反面的图片
在网上获取纸牌正反面图片,比如:
选中Assets.xcassets
,将两张图片拖至其中。
- 添加按钮,并将按钮的背景图片改为纸牌正面图片(CardFront), 文字改为“♠️A”
添加一个
Label
,显示纸牌翻转的次数为按钮添加“Touch Up Inside”事件,命名为
touchCardButton
为
Label
在ViewController.m
的@interface ViewController() @end
之间添加“Referencing Outlets“,命名为flipsLabel
。
另外,在
ViewController.m
的@interface ViewController() @end
之间添加int属性flipCount
在
ViewController.m
中实现touchCardButton事件函数1
2
3
4
5
6
7
8
9
10
11
12
13- (IBAction)touchCardButton:(UIButton *)sender {
if ([sender.currentTitle length]) {
[sender setBackgroundImage:[UIImage imageNamed:@"CardBack"] forState:UIControlStateNormal];
[sender setTitle:@"" forState:UIControlStateNormal];
} else {
Card *card = [self.deck drawRandomCard];
if(card) {
[sender setBackgroundImage:[UIImage imageNamed:@"CardFront"] forState:UIControlStateNormal];
[sender setTitle:@"A♣️" forState:UIControlStateNormal];
}
}
self.flipCount++;
}重构属性
flipCount
的setter
函数。1
2
3
4
5- (void)setFlipCount:(int)flipCount
{
_flipCount = flipCount;
self.flipsLabel.text = [NSString stringWithFormat:@"Flips: %d",self.flipCount];
}
此时App完成,代码如下:
1 | // ViewController.h |
1 | // ViewController.m |
最终效果如下:
对该App改进
上节课完成的App只能实现纸牌的翻转,每次翻转显示的纸牌都是同一张。那么如何让每次翻转,随机显示牌堆中的一张纸牌呢?其实也很简单,方法如下:
在
ViewController.m
中引入Deck.h
在
ViewController.m
的@interface ViewController() @end
之间添加Deck*
属性deck
在
ViewController.m
中引入PlayingCardDeck.h
修改属性
deck
的getter
,当_deck
为空时,通过[[PlayingCardDeck alloc]init]
创建牌堆,然后再返回Deck。此时
ViewController.m
的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49// ViewController.m
#import "ViewController.h"
#import "Deck.h"
#import "PlayingCardDeck.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UILabel *flipsLabel;
@property (nonatomic) int flipCount;
@property (strong, nonatomic) Deck *deck;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (Deck *)deck
{
if(!_deck)
_deck = [self createDeck];
return _deck;
}
- (Deck *)createDeck
{
return [[PlayingCardDeck alloc]init];
}
- (IBAction)touchCardButton:(UIButton *)sender {
if ([sender.currentTitle length]) {
[sender setBackgroundImage:[UIImage imageNamed:@"CardBack"] forState:UIControlStateNormal];
[sender setTitle:@"" forState:UIControlStateNormal];
} else {
[sender setBackgroundImage:[UIImage imageNamed:@"CardFront"] forState:UIControlStateNormal];
[sender setTitle:@"A♣️" forState:UIControlStateNormal];
}
self.flipCount++;
}
- (void)setFlipCount:(int)flipCount
{
_flipCount = flipCount;
self.flipsLabel.text = [NSString stringWithFormat:@"Flips: %d",self.flipCount];
}
@end在
touchCardButton
函数中,利用deck
的drawRandomCard
函数随机抽取一张纸牌。将随机抽取出的Card的内容显示出来。需要注意的是,牌堆里的牌抽完以后要停止抽牌。
完成后ViewController.m
的代码如下:
1 | // ViewController.m |
效果如下:
使该游戏变得真正可玩
接下来我们让游戏变得真正可玩儿。在MVC开发模型中,游戏的逻辑属于Model,Model和UI是完全独立的,所以在编写过程中不需要考虑UI。
Model开发
首先添加一个新的类CardMatchingGame
,这个类就是MVC模型中的Model,在编码CardMathingGame
的过程中,需要思考需要哪些公开的API。
- 首先是构造函数,因为需要传进一些参数,比如牌的张数,所以新建构造函数
initWithCardCount
,指定构造函数(designated initializer) - 公开的属性 score,但同时我们不希望别人能够随意修改socre属性,因此需要在公开API中将其设置为readonly,同时在.m文件中的私密API中声明为readwrite。其实readwrite用的不多,因为默认情况下就是readwrite,只有在公开API只读的时候才会用到。
- 允许用户通过index选中纸牌
chooseCardAtIndex
- 通过index从牌堆中获取Card,
CardAtIndex
1 | // CardMatchingGame.h |
接下来开始在CardMatchingGame.m
中开始实现:
利用关键字
readwrite
使得属性score在CardMatchingGame.m
中可写。1
2
3@interface CardMatchingGame()
@property (nonatomic, readwrite) NSInteger score;
@end新建属
cards
,用于存储游戏中的纸牌。1
2
3
4@interface CardMatchingGame()
@property (nonatomic, readwrite) NSInteger score;
@property (nonatomic, strong) NSMutableArray *cards; // of Card
@end修改属性
cards
的getter
函数属性的默认初始值为nil,当属性
cards
的指针为nil时,创建一个NSMutableArray
1
2
3
4
5
6
7-(NSMutableArray *)cards
{
if(!_cards) {
_cards = [[NSMutableArray alloc] init];
}
return _cards;
}实现构造函数
initWithCardCount
这个构造函数是designated initializer,也就是说使用这个类的用户必须调用这个构造函数,否则类无法被正确的初始化。使用构造函数init会返回nil。这个信息要传递给用户,所以以注释的形式写在公开API中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16-(instancetype) initWithCardCount:(NSUInteger)count usingDeck:(Deck *)deck
{
self = [super init];
if(self) {
for(int i = 0;i < count;i++) {
Card *card = [deck drawRandomCard];
if(card) {
[self.cards addObject:card];
} else {
self = nil;
break;
}
}
}
return self;
}实现函数
cardAtIndex
。1
2
3
4- (Card *)cardAtIndex:(NSUInteger)index
{
return index < [self.cards count] ? self.cards[index] : nil;
}实现函数
chooseCardAtIndex
这个函数是游戏的关键。表示某个card被选中后的处理方式,包括匹配过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34static const int PENALTY_SCORE = 1;
static const int COST_OF_CHOOSE = 1;
-(void)chooseCardAtIndex:(NSUInteger)index
{
Card *card = [self cardAtIndex:index];
if(!card.isMatched) {
// if choosen card is not matched
if(card.isChosen) { // if the card is already chosen
card.chosen = NO; // filp the card back
} else { // match it against another card
// here, only match two cards, but may make it match multiple cards in later
for(Card *otherCard in self.cards) {
if(otherCard.isChosen && !otherCard.isMatched) { // Bingo, find the card we want to match
int matchScore = [card match:@[otherCard]]; // try to match it
if(matchScore == 0) { // not match
self.score -= PENALTY_SCORE; // penalty of un match
otherCard.chosen = NO; // flip the other card back
} else { // match
self.score += matchScore;
card.matched = YES;
otherCard.matched = YES;
}
break;
}
}
card.chosen = YES;
self.score -= COST_OF_CHOOSE;
}
} else {
// if choosen card is already matched
// do nothing
}
}
UI开发
到目前,完成了App中Model的实现,接下来是实现App的UI。
在UI中创建多张纸牌,可以通过复制粘贴的方式完成
创建OutletCollections,添加所有纸牌。
创建方法,右键点击某个button,按住“New Referencing Outlet Collections“,拖到
ViewController.m
中。添加其他button的方法,按住Ctrl键,点击button并拖到
ViewController.m
中
在
ViewController.m
中引入CardMatchingGame.h
新增
CardMatchingGame
的属性game
@property (strong, nonatomic) CardMatchingGame *game;
重写属性
game
的getter
1
2
3
4
5
6- (CardMatchingGame *)game{
if(!_game) {
_game = [[CardMatchingGame alloc] initWithCardCount:[self.cardButtons count] usingDeck:[self createDeck]];
}
return _game;
}在
ViewController.m
中创建函数titleForCard
1
2
3
4- (NSString *)titleForCard:(Card *)card
{
return card.isChosen ? card.contents : @"";
}在
ViewController.m
中创建函数backgroundImageForCard
1
2
3
4- (UIImage *)backgroundImageForCard:(Card *)card
{
return [UIImage imageNamed:card.isChosen ? @"CardFront" : @"CardBack"]; // CardFront & CardBack are name of card image
}在
ViewController.m
中创建函数updateUI
用于同步Model和UI。1
2
3
4
5
6
7
8
9
10- (void)updateUI
{
for (UIButton *cardButton in self.cardButtons) {
int cardIndex = [self.cardButtons indexOfObject:cardButton];
Card *card = [self.game cardAtIndex:cardIndex];
[cardButton setTitle:[self titleForCard:card] forState:UIControlStateNormal];
[cardButton setBackgroundImage:[self backgroundImageForCard:card] forState:UIControlStateNormal];
cardButton.enabled = !card.isMatched;
}
}创建Touch Up Inside事件,命名为touchCardButton,并将所有button加入其中,方法类似于步骤2.
.m
文件中实现touchCardButton
1
2
3
4
5- (IBAction)touchCardButton:(UIButton *)sender {
int cardIndex = [self.cardButtons indexOfObject:sender];
[self.game chooseCardAtIndex:cardIndex];
[self updateUI];
}
到这个时候App已经可以运行了,运行效果如下:
可以正常运行,但是两张黑桃却没有正常匹配,问题出在哪里?
问题在我们在课程2中实现的Card
类中的匹配函数:
1 | // Card.m |
这显然不是一个完整的匹配逻辑。
所以需要在PlayingCard
类的.m
文件中重构match
函数。
1 | - (int)match:(NSArray *)otherCards |
现在完成了匹配规则,最后一个任务是添加一个Label
用于展示分数。
在UI中增加一个Label
为Label增加一个Outlet
在
updateUI
中更新score Label。1
2
3
4
5
6
7
8
9
10
11- (void)updateUI
{
for (UIButton *cardButton in self.cardButtons) {
int cardIndex = [self.cardButtons indexOfObject:cardButton];
Card *card = [self.game cardAtIndex:cardIndex];
[cardButton setTitle:[self titleForCard:card] forState:UIControlStateNormal];
[cardButton setBackgroundImage:[self backgroundImageForCard:card] forState:UIControlStateNormal];
cardButton.enabled = !card.isMatched;
}
self.scoreLabel.text = [NSString stringWithFormat:@"Score: %d", self.game.score];
}
至此,一个可玩的纸牌匹配游戏App已经完成,大家可以试着让App可以同时匹配三张及以上的纸牌。