iOS游戏开发没有你想的那么难--Hardest

jhcw3950 7年前
   <p>项目说明:考虑到许多不会使用Cocos2D-X和Swift的朋友,此次项目采用Objective-C并且基于UIKit框架实现的.意思就是你会使用UIView,就可以尝试开发游戏了,嘿嘿!</p>    <p>原生项目是采用Cocos2D-X开发的,所以在对图片的动画处理时,有些地方会没有原生显得那么流畅(如切割图片,对图片的变形处理,图片快速替换等),并且在性能上来说,UIKit也不如Cocos2D-X流畅,毕竟术业有专攻.如果是要开发游戏来上架的话,最好采用专门的游戏引擎来搭建项目(Cocos-2D,Unity3D,Sprite Kit等).</p>    <ul>     <li> <p>开发语言:Objective-C</p> </li>     <li> <p>开发工具:Xcode7.1</p> </li>     <li> <p>编译环境:大于Xcode7.0</p> </li>     <li> <p>辅助工具:Photoshop CS6</p> </li>    </ul>    <p>项目讲解: 把整个项目用文字带着大家过一遍有点不现实.这里我将项目的大体结构和一些主要逻辑,以及主要对象提供的接口功能下面列举出来.建议同学们先看代码,配合代码再来看这篇文章,顺着代码和文字搞懂项目主体逻辑.当需要学习具体功能如何实现时,在看.m文件下的实现代码学习如何实现功能,如果有哪些地方不清楚,在简书下面留言或者微博留言.</p>    <p>学习建议:最好使用真机来进行运行调试,有些关卡需要使用加速计与陀螺仪等功能,模拟器是没有的.当遇到实在无法过去的关卡时,点击首页的有些手柄按钮,点击解锁下一关或者在代码启动时,手动写入关卡得分信息即可.</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d694ae75fa31bf279f7a0a7889091490.jpg"></p>    <p style="text-align:center">Hardest</p>    <p><strong>主体架构</strong></p>    <p><strong>音效和背景音乐</strong></p>    <p>音效和背景音乐采用了AVFoundation框架封装了一个WNXSoundToolManager的单利对象,背景音乐采用AVAudioPlayer,背景音效采用AudioServicesPlaySystemSound.</p>    <p>提供以下方法和属性供全局调用或修改,通过修改bgMusicType和soundType可以控制背景音乐和音效声音的大小,通过playSoundWithSoundName:方法根据音效名称设置播放不同的音效.</p>    <pre>  <code class="language-objectivec">// 音效或背景音乐播放声音打大小枚举  typedef NS_ENUM(NSInteger, SoundPlayType) {      SoundPlayTypeHight = 0,      SoundPlayTypeMiddle,      SoundPlayTypeLow,      SoundPlayTypeMute  };    @interface WNXSoundToolManager : NSObject    // 背景音乐声音大小Type  @property (nonatomic, assign) SoundPlayType bgMusicType;  // 音效声音大小Type  @property (nonatomic, assign) SoundPlayType soundType;    // 暂停背景音乐  - (void)pauseBgMusic;  // 停止播放背景音乐  - (void)stopBgMusic;  // 重新播放背景音乐  - (void)playBgMusicWihtPlayAgain:(BOOL)playAgain;  // 播放音效:音效名称  - (void)playSoundWithSoundName:(NSString *)soundName;  // 设置背景音乐音量:音量大小0~1  - (void)setBackgroundMusicVolume:(float)volume;    // 获取SoundManager单利对象  + (instancetype)sharedSoundToolManager;    @end</code></pre>    <p><strong>保存和读取玩家关卡记录(WNXStageInfoManager)</strong></p>    <p>如何持久化存储玩家过关信息和每关的得分记录.本项目采用归档和解档的方案.</p>    <p>拿到WNXStageInfoManager的单例对象,通过调用Save和Read方法保存或读取关卡信息,当游戏关卡进入结算得分控制器后,判断新记录是否需要保存,如果需要调用保存接口.具体实现代码请参照WNXStageInfoManager.m文件</p>    <pre>  <code class="language-objectivec">// 单例方法  + (instancetype)sharedStageInfoManager;    // 保存关卡信息  - (BOOL)saveStageInfo:(WNXStageInfo *)stageInfo;  // 读取指定关卡编号的关卡信息  - (WNXStageInfo *)stageInfoWithNumber:(int)number;    // 这个接口是当游戏无法过关时,在RootViewController点击手柄按钮,解锁下一关卡使用(**秘籍~慎用**)  - (BOOL)unlockNextStage;</code></pre>    <p><strong>启动页动画</strong></p>    <p>启动页动画是目前App比较常见的功能(顺丰优选,顺手付,顺丰海淘等都有).其实这里有一种假象,在AppDelegate的didFinishLaunchingWithOptions()方法中,添加一个与启动图片完全一样的AnimVC,将AnimVC设置为keyWindow的rootViewController,在AnimVC的viewDidApper()方法中执行动画,当动画完成后通过Block切换keyWindow的rootViewController为首页VC就OK了.</p>    <pre>  <code class="language-objectivec">- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {        [[UIApplication sharedApplication] setStatusBarHidden:YES];        [NSThread sleepForTimeInterval:1.0];        [self setKeyWindow];        return YES;  }    - (void)setKeyWindow {      __weak typeof(self) weakSelf = self;        WNXLaunchAnimationViewController *launchAnimationVC = [[WNXLaunchAnimationViewController alloc] init];      launchAnimationVC.animationFinish = ^{          UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];          WNXBaseNavigationController *rootNav = (WNXBaseNavigationController *)[sb instantiateViewControllerWithIdentifier:@"RootNavigationController"];          weakSelf.window.rootViewController = rootNav;      };        self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];      self.window.rootViewController = launchAnimationVC;      [self.window makeKeyAndVisible];  }</code></pre>    <p>关于动画这里我就不讲什么了,有兴趣的朋友可以自己参考工程代码研究下.</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/45ed6b5955fa711ac36c8502c4f48cfa.gif"></p>    <p>启动页动画</p>    <p><strong>首页(WNXRootViewController)</strong></p>    <p>首页其实就是一张图片,通过判断当前设备屏幕尺寸,读取当前设备尺寸对应按钮的Plist文件,拿到首页6个按钮位置的Frame,在touchesBegan()方法中,通过CGRectContainsPoint方法判断当前点击位置时候在指定的Frame内,符合条件时做出对应 的操作,具体代码</p>    <pre>  <code class="language-objectivec">// 加载当前设备对应首页按钮Frame  - (void)loadHomeButtonFrame {      NSString *framePath = [[NSBundle mainBundle] pathForResource:@"home.plist" ofType:nil];      NSDictionary *frameDic = [NSDictionary dictionaryWithContentsOfFile:framePath];        NSDictionary *dict;        if (iPhone5) {          dict = frameDic[@"iphone5"];      } else {          dict = frameDic[@"iphone4"];      }        _settingFrame = CGRectFromString(dict[@"btn_setting_frame"]);      _languageFrame = CGRectFromString(dict[@"btn_language_frame"]);      _moreFrame = CGRectFromString(dict[@"btn_more_frame"]);      _rankFrame = CGRectFromString(dict[@"btn_rank_frame"]);      _playFrame = CGRectFromString(dict[@"btn_play_frame"]);      _getFrame = CGRectFromString(dict[@"btn_get_frame"]);  }    // 判断点击点是否在对应的Frame内  - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {      UITouch *touch = [touches anyObject];        CGPoint touchPoint = [touch locationInView:touch.view];        [[WNXSoundToolManager sharedSoundToolManager] playSoundWithSoundName: kSoundCliclName];        if (CGRectContainsPoint(_settingFrame, touchPoint)) {            [self performSegueWithIdentifier:@"Setting" sender:nil];        } else if (CGRectContainsPoint(_languageFrame, touchPoint)) {            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:kBlogURL]];        } else if (CGRectContainsPoint(_moreFrame, touchPoint)) {          [self performSegueWithIdentifier:@"Rare" sender:nil];        } else if (CGRectContainsPoint(_rankFrame, touchPoint)) {            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:kWeiBoURL]];        } else if (CGRectContainsPoint(_playFrame, touchPoint)) {            [self performSegueWithIdentifier:@"PlayGame" sender:nil];        } else if (CGRectContainsPoint(_getFrame, touchPoint)) {            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:kGithubUrl]];        }  }</code></pre>    <p>关卡选择控制器(WNXSelectStageViewController)</p>    <p>关卡选择控制器采用UIScrollView实现,在scrollView放入24个WNXStageListView(当然这里也可以自己创建缓存池复用,个人觉得没必要),每个WNXStageListView都有对应的一个关卡信息模型stageModel,模型属性从工程->Resources->Plist->stages.plist文件中读取,根据model里的成员变量,加载关卡对应的信息,如关卡图片,是否解锁,玩家历史得分以及Rank标记等.</p>    <p>每个WNXStageListView,根据ID设置不同的Tag,并且提供单击手势,在stageView的点击事件中.调用导航控制器,Push到WNXPrepareViewController控制器,并将选择关卡的stageModel作为参数传过去,WNXPrepareViewController做出相应的展示即可.</p>    <p>选择关卡效果如下图所示</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6cfebfea0ae2a21d643e253702b1d836.gif"></p>    <p style="text-align:center">选择关卡效果图</p>    <p><strong>关卡准备开始控制器(WNXPrepareViewController)</strong></p>    <p>每个关卡开始游戏前,都会以动画的形式出现本关游戏名称,过关规则,以及历史得分等一系列功能.都是由这个控制器完成的.通过选择关卡时传入的stageModel,展示model内对应的数据,当用户点击Play按钮时,使用WNXGameControllerViewManager单例对象,根据传入的stageModel,返回对应的关卡ViewController,然后Push到返回的ViewController游戏关卡即可.</p>    <p style="text-align:center"><img alt="iOS游戏开发没有你想的那么难--Hardest" src="https://simg.open-open.com/show/6cfebfea0ae2a21d643e253702b1d836.gif" width="282" height="501"></p>    <p style="text-align:center">准备开始控制器效果图</p>    <p><strong>关卡控制器</strong></p>    <p>24关,每关都有很多重复的功能,这里我们按照不同关卡的属性抽取出几种公共的父类,每个关卡根据自己的需求选择继承相应的控制器,并且在ViewDidLoad函数中初始化每个关卡不同的属性,具体分类效果如下图所示</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c05dc0a91845cc3def5e7ed898b8ab2c.jpg"></p>    <p style="text-align:center">逻辑图</p>    <p><strong>WNXBaseGameViewController --> UIViewController</strong></p>    <p>WNXBaseGameViewController是所有关卡ViewController的基类控制器,提供每个游戏关卡的基本属性设置,并且每个关卡的初始化操作都封装在了这里,每个关卡只需要在自己的ViewDidLoad方法中调用buildStageInfo()函数,添加构建自己的UI即可,重写父类的方法,完成每关不同的操作.</p>    <p>公有属性</p>    <p>1.WNXGameGuideType guideType每关第一次进入关卡,本关游戏手势提示样式</p>    <ul>     <li> <p>WNXGameGuideTypeNone无提示</p> </li>     <li> <p>WNXGameGuideTypeOneFingerClick单个手指头点击</p> </li>     <li> <p>WNXGameGuideTypeReplaceClick左右按钮交替点击</p> </li>     <li> <p>WNXGameGuideTypeMultiPointClick多个手指同时点击</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1e045d70468b70ed67d074ab97c47e97.gif"></p>    <p style="text-align:center">单个手指头点击效果</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9f5c6a90ad9d852a35ab4fa104b59204.gif"></p>    <p style="text-align:center">左右按钮交替点击效果</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ba0f2d6fb6ef98f3761493bf5857c4e2.gif"></p>    <p style="text-align:center">多个手指同时点击效果样式</p>    <p>2.WNXStage *stage每关关卡信息model(model详情)</p>    <p>3.WNXScoreboardType每关计分板样式</p>    <ul>     <li> <p>WNXScoreboardTypeNone无计分板</p> </li>     <li> <p>WNXScoreboardTypeCountPTS [WNXScoreboardTypeCountPTS]()</p> </li>     <li> <p>WNXScoreboardTypeTimeMS [WNXScoreboardTypeTimeMS]()</p> </li>     <li> <p>WNXScoreboardTypeSecondAndMS [WNXScoreboardTypeSecondAndMS]()</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/efe5c6cbe0af608d0d4bd44df79fe447.jpg"></p>    <p>WNXScoreboardTypeCountPTS计分板样式</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e55b4d4edc43faec5e95bc98a3e11405.jpg"></p>    <p>WNXScoreboardTypeTimeMS计分板样式</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/43ff569163cacdd12f9df7e82a073fa0.jpg"></p>    <p>WNXScoreboardTypeSecondAndMS计分板样式</p>    <p>4.UIView *countScoreView计分板(考虑有多种样式,使用了UIView,每个关卡在用的时候根据自己类型进行强制转换)</p>    <p>5.WNXStateView *stateView关卡提示状态View</p>    <p>6.UIButton *playAgainButton 重新开始游戏按钮</p>    <p>7.UIButton *pauseButton暂停按钮</p>    <p><strong>公有方法</strong></p>    <pre>  <code class="language-objectivec">- (void)beginGame; // 开始游戏  - (void)endGame;   // 结束游戏  - (void)beginRedayGoView; // 开始显示RedayGo动画  - (void)readyGoAnimationFinish; // RedayGo动画显示结束  - (void)pauseGame;    // 暂停游戏  - (void)continueGame; // 继续游戏  - (void)playAgainGame; // 重新开始游戏  - (void)showGameFail; //  游戏失败(部分关卡有, 进入失败ViewController)    // 显示关卡游戏结果  - (void)showResultControllerWithNewScroe:(double)scroe // 玩家得分                                      unit:(NSString *)unil  // 本关计分器显示单位                                     stage:(WNXStage *)stage // 关卡信息                                isAddScore:(BOOL)isAddScroe; // 是否是添加分数(这里偷了个懒,只做了添加动画,应该有分数增长加动画或者减少动画)    // 构建关卡信息  - (void)buildStageInfo;    // 将广告,重新开始,暂停按钮放到最上层  - (void)bringPauseAndPlayAgainToFront;    // 构建显示状态View  - (void)buildStageView;</code></pre>    <p><strong>WNXRYBViewController --> WNXBaseGameViewController</strong></p>    <p>WNXRYBViewController,继承至WNXBaseGameViewController,底部拥有三个按钮,并且默认有三条红黄蓝背景条(拥有高亮时图片),底部按钮默认Tag为0,1,2,游戏大部分关卡为这种样式</p>    <p><strong>公有属性</strong></p>    <pre>  <code class="language-objectivec">@property (strong, nonatomic) UIImageView *redImageView;  @property (strong, nonatomic) UIImageView *yellowImageView;  @property (strong, nonatomic) UIImageView *blueImageView;    @property (strong, nonatomic) UIButton    *redButton;  @property (strong, nonatomic) UIButton    *yellowButton;  @property (strong, nonatomic) UIButton    *blueButton;    @property (nonatomic, strong) NSMutableArray *buttons;  @property (nonatomic, strong) NSArray *buttonImageNames;</code></pre>    <p><strong>公有方法</strong></p>    <pre>  <code class="language-objectivec">- (void)setButtonsIsActivate:(BOOL)isActivate; // 设置全部按钮是否可以点击    - (void)setButtonImage:(UIImage *)image // 当底部按钮图片相同时,设置底部按钮图片        contenEdgeInsets:(UIEdgeInsets)insets; // 图片的contenEdgeInsets    - (void)removeAllImageView; // 有写关卡不需要红黄蓝背景图片时,删除三个UIImageView    // 底部按钮Action  - (void)addButtonsActionWithTarget:(id)target                               action:(SEL)action                    forControlEvents:(UIControlEvents)forControlEvents;</code></pre>    <p><strong>WNXTwoButtonViewController --> WNXBaseGameViewController</strong></p>    <p>WNXTwoButtonViewController,底部拥有俩个按钮关卡,并且默认带有背景ImageView.</p>    <p><strong>公有属性</strong></p>    <pre>  <code class="language-objectivec">@property (nonatomic, strong) UIImageView *backgroundIV;    @property (nonatomic, strong) UIButton *leftButton;  @property (nonatomic, strong) UIButton *rightButton;</code></pre>    <p><strong>公有方法</strong></p>    <pre>  <code class="language-objectivec">// 统一设置按钮是否可以被点击,部分关卡按钮点击后,不允许再次点击  - (void)setButtonActivate:(BOOL)isActivate;</code></pre>    <p><strong>WNXBackgroundViewController --> WNXBaseGameViewController</strong></p>    <p>只带有背景图关卡,项目中有些关卡是采用陀螺仪和加速计的关卡.</p>    <p>关于每一关如何实现,我这里就不一一列举了,有点太多了,但是都并不复杂,写个2~3关基本就能掌握套路了,就个别关卡使用了加速计和陀螺仪,具体实现的代码我都在工程中写的很明白了,在Stage文件夹下,大家自行参考即可.</p>    <p>分数结算控制器(WNXResultViewController)</p>    <p>当每个关卡游戏结束后,都会进入分数结算控制器,这里通过在WNXBaseGameViewController中封装了一个方法以保证每个关卡控制器都可以直接调用计算得分,当关卡游戏结束后,调用当前关卡的下面函数即可,这里小熊偷了个懒,只实现了相加的功能,不过相信通过参考相加的功能,大家实现相减的功能也是小csae啦~</p>    <pre>  <code class="language-objectivec">- (void)showResultControllerWithNewScroe:(double)scroe                                      unit:(NSString *)unil                                     stage:(WNXStage *)stage                                isAddScore:(BOOL)isAddScroe;</code></pre>    <p>说明下isAddScore的作用</p>    <ul>     <li> <p>有些关卡是得分越高越好.这总关卡在显示结果的时候分数是从0一点点网上加的,这种情况isAddScore传入YES</p> </li>     <li> <p>有些关卡是得分越少越好,这总卡在显示结果的时候分数是从大网小一点点减少的,这种情况isAddScore传入NO</p> </li>    </ul>    <p>当结算分数完成后,会出现以下几种情况,跟据不同的得分情况执行不同的逻辑即可,具体逻辑如下所示</p>    <p><strong>状态一: 游戏失败(当得分小于等于F,不保存得分),出现下图</strong></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/67fc2e8a48b0edefcf382793e5f68447.gif"></p>    <p style="text-align:center">得分不够,显示失败</p>    <p><strong>状态二: 游戏成功</strong></p>    <ul>     <li> <p>当前关卡无得分记录,并且得分大于F,保存玩家得分,正常显示得分结果,并且解锁下一关.</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b99e129497d2ae1163cbd1081e563845.gif"></p>    <p style="text-align:center">成功状态1</p>    <ul>     <li> <p>当前关卡有记录,但是本次游戏得分没有超越历史记录,正常显示得分结果,不保存本次游戏得分.</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5650ff1a7c3ff28dfd77fc152fff8974.gif"></p>    <p style="text-align:center">成功状态2</p>    <ul>     <li> <p>当前关卡有记录,并且本次游戏得分超越历史记录,显示超越历史得分动画,并且讲本次得分替换掉上一次得分.</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cabf68e582d4579baab72c880b659dbf.gif"></p>    <p style="text-align:center">成功状态3</p>    <p><strong>失败(WNXFailViewController)</strong></p>    <ul>     <li> <p>部分关卡会有在游戏中失败的情况,如下图</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/173508db7db795c6114940cedc8f6b9d.gif"></p>    <p style="text-align:center">游戏失败</p>    <p>这里也是在WNXBaseGameViewController中封装了一个方法,当关卡失败后,直接调用showGameFail()方法,Push到失败控制器即可.</p>    <p>如果需要失败时执行一些操作,如停止计时,停止动画等,在当前关卡重写showGameFail()方法,在调用父类方法前调用需要执行的相应代码即可,如下</p>    <pre>  <code class="language-objectivec">- (void)showGameFail {      // 需要在游戏失败时执行的相应代码      // do something            [super showGameFail];  }</code></pre>    <p><strong>暂停控制器(WNXPauseViewController)</strong></p>    <p>每个游戏关卡都有暂停的功能,所以将暂停的功能封装到WNXBaseGameViewController中,并且提供两个接口供子控制器调用,分别为</p>    <ul>     <li> <p>(void)pauseGame; 暂停游戏</p> </li>     <li> <p>(void)continueGame; 继续游戏</p> </li>    </ul>    <p>在每个游戏关卡重写上面两个方法,当玩家点击暂停按钮时,回调用暂停方法,点击返回时,会调用继续方法,具体实现如下</p>    <pre>  <code class="language-objectivec">// 玩家点击暂停按钮  - (void)pauseGame {      // 关卡暂停,本关需要执行的相应操作,如暂停计时器,动画等.        [super pauseGame];  }    - (void)continueGame {      [super continueGame];        // 继续游戏,继续执行暂停前的操作  }</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c5bc1a9064a0004b92721bf9126b446b.gif"></p>    <p style="text-align:center">暂停控制器效果图</p>    <p><strong>项目总结</strong></p>    <p>项目写的比较匆忙,基本每天晚上抽空写点,写完也没有回头CodeReview,说实话,这是一个非常非常不好的习惯,大家一定要养成定期回头看看自己写过代码的习惯.随着越网后写,发现前面有很多地方可以修改,我吧有点懒,So你懂的...</p>    <p>感觉光靠文字来讲述一个项目实在是太困难.希望大家还是参考工程代码,当遇到无法看懂或者不理解的时候参考下我写的Blog应该会更好一些.这个游戏项目说实话还是比较简单的,相信大家仔细研究下都可以实现的.游戏还有24关,有兴趣的同学可以尝试自己将剩下的24关自己实现下~</p>    <p> </p>    <p> </p>    <p>来自:http://www.cocoachina.com/ios/20161025/17823.html</p>    <p> </p>