第三课:
本节课主要是游戏实现的demo,因此我将把课程中简单的几个编程技巧提取出来,重点介绍如何自己实现作业中的要求。
①游戏的进行式模型的一部分(理解什么是模型: Model = What your application is (but not how it is displayed) )UI无关。
②编程技巧:创建一个新类时首先考虑公共部分(API),即别人如何使用这个类,再考虑其细节实现,以此来驱动整个设计。
③关于属性中使用NSInteger,NSUInteger,int,unsigned int只存在风格问题,只需要保持风格统一。
关于 NSInteger 与 NSUInteger 的官方定义:
1 #if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64 2 typedef long NSInteger; 3 typedef unsigned long NSUInteger; 4 #else 5 typedef int NSInteger; 6 typedef unsigned int NSUInteger; 7 #endif
④关于属性只读问题:公有API中可以设置属性为只读,实现代码中可以再次声明属性为可读写。属性声明时除非指定,否则默认为读写属性。
例如:
1 @interface example : NSObject 2 3 @property (nonatomic,readonly) Objectype number; 4 5 @end
1 @interface example() 2 3 @property (nonatomic,readwrite) Objectype number; 4 5 @end 6 7 8 @implementation NUNetWork 9 10 @end
⑤IBOutletConllection:多输出口,必须为strong,因为NSArray储存输出口,UI由视图指针指向,而NSArray必须指定为strong才能保持在堆中。
如 : @property (strong, nonatomic) IBOutletCollection(UIButton) NSArray *cardButtons;
IBOutletCollection(UIButton)类似于IBOutlet或IBAction,编译器会忽略。
NSArray中的UI顺序未知(不可视为拖入的顺序)
⑥作业
a.在模拟器上运行课程当前最后版本的Matchismo
b.添加一个button实现重新开始游戏功能(包括UI重置,牌堆重置,分数重置等)
c.使用UISwitch或者UISegmentedControl 实现两个card匹配与三个card匹配游戏模式之间的转换。(注意三个card匹配成功或失败的得分要比两个card更显著,在三card匹配中两个纸牌不会产生匹配结果)(可以考虑n张牌匹配)
d.游戏开始后,c中控制游戏的功能失效直到游戏结束或重新开始。
e.加入lable提示游戏进行状态(如:“Matched J♥ J♠ for 4 points.” 或“6♦ J♣ don’t match! 2 point penalty!” 或“8♦”表示只选择一张牌还没有进行匹配等)(不要违反MVC规则)
f.将纸牌数量由12扩展到30(40*60)
g.不要改变之前的代码,可以增加公有或私有API
作业解答:
最终实现效果如下图所示
(纸牌按钮字体大小设置为system 15,否则原有字体不能完整显示)
实现过程:
1、加入如下输出口:
1 @property (weak, nonatomic) IBOutlet UIButton *restartButton; 2 3 @property (weak, nonatomic) IBOutlet UISegmentedControl *gameModelSelectSegmented; 4 5 @property (weak, nonatomic) IBOutlet UILabel *gameModelLable; 6 7 @property (weak, nonatomic) IBOutlet UITextField *matchModelTextFiled; 8 9 @property (weak, nonatomic) IBOutlet UILabel *gameStateLable;
2、重新开始游戏功能实现
考虑到重新启动游戏后还未确定游戏模式,因此必须重写不包含game属性的初始化(game的setter会自动初始化game)
1 - (IBAction)touchRestartButton 2 3 { 4 //恢复默认值 5 self.gameModelSelectSegmented.enabled = YES; 6 self.matchModelTextFiled.enabled = YES; 7 self.matchModelTextFiled.enabled = YES; 8 self.gameModelSelectSegmented.selectedSegmentIndex = 0; 9 self.selfDefiningModel = 2; 10 self.gameModelLable.text = [NSString stringWithFormat:@"game model:%d",_selfDefiningModel]; 11 self.matchModelTextFiled.text = nil; 12 13 self.game = nil; 14 [self updateUIWithNotCreateGame]; 15 } 16 17 - (void) updateUIWithNotCreateGame 18 { 19 for (UIButton *cardButton in self.cardButtons) 20 { 21 [cardButton setTitle:@"" forState:UIControlStateNormal]; 22 [cardButton setBackgroundImage:[UIImage imageNamed:@"cardBack"] forState:UIControlStateNormal]; 23 cardButton.enabled = YES; 24 } 25 26 self.gameStateLable.text = @"State"; 27 self.scoreLable.text = @"Score:0"; 28 }
3、多种游戏模式及游戏状态输出的实现
//Viewcontroller加入用户自定义游戏模式
@property (nonatomic) NSUInteger selfDefiningModel;
选择器初始化 (此处放在了viewDidLoad方法,有点超范围,其中选择器的target-action即为MVC中的view同controller通信的一种方法,selfDefiningModel为用户可自行设定的游戏模式,初始值为2)
还可以使用UISwitch,本文为了实现多纸牌匹配因此采用了UISegmentedControll,感兴趣的朋友可以试试UISwitch实现。
1 [self.gameModelSelectSegmented addTarget:self action:@selector(segmentAction:) forControlEvents:UIControlEventValueChanged];//target-action 2 3 _selfDefiningModel = 2;//default model
选择器响应方法 (action)
1 - (void) segmentAction:(UISegmentedControl *)Seg 2 { 3 if (self.gameModelSelectSegmented.selectedSegmentIndex == 2)//此时为自定义输入 4 { 5 [self assertSelfDefiningModel:self.matchModelTextFiled.text];//检测用户输入 6 } 7 else 8 { 9 self.selfDefiningModel = self.gameModelSelectSegmented.selectedSegmentIndex + 2;//SegmengtdControll选择器选项栏从0开始计数 10 } 11 12 self.gameModelLable.text = [NSString stringWithFormat:@"game model:%d",self.selfDefiningModel]; 13 }
自定义输入检测,设置为数字键盘,只检测输入范围 (由于用户输入的任意性,需要考虑到各种可能的输入情况,本文为了简便,采用了数字键盘,因此只需检测数字输入的合理性即可)
1 - (void) assertSelfDefiningModel:(NSString *)text 2 { 3 if ([self.matchModelTextFiled.text integerValue] < 2) 4 { 5 [[[UIAlertView alloc] initWithTitle:@"Wrong" message:@"game model at least 2" delegate:nil cancelButtonTitle:@"certain" otherButtonTitles:nil, nil] show];//匹配模式至少为2 6 self.matchModelTextFiled.text = @""; 7 self.gameModelSelectSegmented.selectedSegmentIndex = 0; 8 } 9 else if ([self.matchModelTextFiled.text integerValue] > 30) 10 { 11 [[[UIAlertView alloc] initWithTitle:@"Wrong" message:@"beyond card max number" delegate:nil cancelButtonTitle:@"certain" otherButtonTitles:nil, nil] show];//匹配纸牌模式不能超过界面最大纸牌数量 12 self.matchModelTextFiled.text = @""; 13 self.gameModelSelectSegmented.selectedSegmentIndex = 0; 14 } 15 else 16 { 17 self.selfDefiningModel = [self.matchModelTextFiled.text integerValue];//自定义输入正确时保存输入结果 18 } 19 }
UITextFiled委托 (即MVC中View与Controller的另一种通讯方式,略超范围,不理解没有关系,暂不做要求),目的实现点击键盘的return按钮收起键盘
1 - (BOOL) textFieldShouldReturn:(UITextField *)textField 2 { 3 [textField resignFirstResponder]; 4 return YES; 5 }
4、Model部分的实现
PlayingCard重写match:方法
重写前 (只实现了两张牌的匹配):
1 - (int)match:(NSArray *)otherCards 2 { 3 int score = 0; 4 5 // two card match 6 if ([otherCards count] == 1) 7 { 8 PlayingCard *otherCard = [otherCards firstObject]; 9 if ([self.suit isEqualToString:otherCard.suit]) 10 { 11 score = 1; 12 } 13 else if (self.rank == otherCard.rank) 14 { 15 score = 4; 16 } 17 } 18 return score; 19 }
重写后(适用于多纸牌匹配): 此处我简单的实现了如下规则:任意两张牌含有相同的属性即认为此牌与数组中的牌匹配,读者可以尝试更复杂的规则,如所有的牌数字相等或者花色相等才能视为匹配等)
1 - (int)match:(NSArray *)otherCards 2 { 3 int score = 0; 4 5 // n card match ( at least two card have same element,we think they are matched ) 6 for (PlayingCard *otherCard in otherCards) 7 { 8 if ([self.suit isEqualToString:otherCard.suit]) 9 { 10 score += 1; 11 } 12 else if (self.rank == otherCard.rank) 13 { 14 score += 4; 15 } 16 } 17 18 return score; 19 }
game公有API加入游戏模式及状态属性
@property (nonatomic) NSUInteger gameModel;// >=2
@property (nonatomic,readonly) NSString *gameState;
实现文件更改权限
@property (nonatomic,readwrite) NSString *gameState;
重写选择纸牌方法逻辑 :
此次作业最难的部分,整个游戏的逻辑核心。首先保持两张纸牌匹配的框架不变,引入新的数组变量otherCards保存需要匹配的纸牌,先用for循环遍历检测纸牌状态,若符合要求则加入otherCards,遍历完毕再检测数组大小是否达到游戏模式要求(注意n张牌匹配数组大小应为n-1,想想为什么?:)),若不符合则返回,若符合则开始进行匹配。数组大小小于游戏模式要求时,此处应注意纸牌状态的保存位置与两张牌匹配时不同,下方代码注释有详细说明。匹配时的计分逻辑:在原有基础上乘以游戏模式数量,当匹配规则越难时匹配成功得分越高。并在匹配过程中保存游戏状态到gameState。
1 - (void) chooseCardAtIndex:(NSUInteger)index 2 { 3 Card *card = [self cardAtIndex:index]; 4 if (!card.isMacthed) 5 { 6 if (card.isChosen) 7 { 8 card.chosen = NO; 9 } 10 else 11 { 12 // match against other chosen cards 13 14 NSMutableArray *otherCards = [NSMutableArray arrayWithCapacity:self.gameModel]; 15 for (Card *otherCard in self.cards) 16 { 17 if (otherCard.isChosen && !otherCard.isMacthed) 18 { 19 [otherCards addObject:otherCard]; 20 } 21 } 22 23 //不能放于for循环之前,否则会将本次被选择的牌加入cards,不能放于下面的if之后,否则当if成立时返回,没有将本次翻牌的cost记录,且不能翻牌 24 self.score -= COST_TO_CHOOSE * self.gameModel; 25 card.chosen = YES; 26 27 if ([otherCards count] < self.gameModel - 1) 28 { 29 self.gameState = [NSString stringWithFormat:@"State:%@",card.contents]; 30 return; 31 } 32 else 33 { 34 int matchScore = [card match:otherCards]; 35 36 if (matchScore) 37 { 38 NSMutableString *state = [NSMutableString stringWithFormat:@"State:%@ matched",card.contents]; 39 self.score += matchScore * MATCH_BOUNDS * self.gameModel; 40 for (Card *otherCard in otherCards) 41 { 42 [state appendFormat:@" %@",otherCard.contents]; 43 otherCard.matched = YES; 44 } 45 [state appendFormat:@". Get %d score!",matchScore * MATCH_BOUNDS * self.gameModel]; 46 card.matched = YES; 47 48 self.gameState = state; 49 } 50 else 51 { 52 NSMutableString *state = [NSMutableString stringWithFormat:@"State:%@ with",card.contents]; 53 self.score -= MISMATCH_PENALTY * self.gameModel; 54 for (Card *otherCard in otherCards) 55 { 56 [state appendFormat:@" %@",otherCard.contents]; 57 otherCard.chosen = NO; 58 } 59 [state appendFormat:@" not matched! %d point penalty!",MISMATCH_PENALTY * self.gameModel]; 60 61 self.gameState = state; 62 } 63 } 64 } 65 } 66 }
作业总结:虽然完成了作业的要求,但还有许多地方需要改进。如输入情况检测的30阈值为特定的数字,可以考虑改为[cardButtons count],游戏的计分逻辑不够完善,在游戏匹配模式较大的情况下(貌似到5张牌匹配就出现了:p)容易出现极端的得分情况,大家可以下载源代码玩玩试试。游戏状态的Lable字数过多时的显示问题。restart按钮的逻辑有没有更好的实现方法,本文为了防止restart中的updateUI方法在game置为nil后马上重新初始化nil(此时selfDefiningModel为初始值2)特意重写了一个更新UI的方法,有没有更轻便的方法(如更符合MVC一点)?这些问题都是抛砖引玉,我并没有细想,希望大家积极思考交流,如有不对欢迎指正:)
作业源码地址: https://github.com/NSLogMeng/Stanford_iOS7_Study_Machismo/commit/39819086ac1c0b7a38679b75307b1cf0a687a190 (今后同一个项目每次作业我会把当次完成的commit链接发给大家)
课程视频地址:网易公开课: http://open.163.com/movie/2014/1/H/U/M9H7S9F1H_M9H7VNFHU.html
或者iTunes U搜索standford课程