手把手教你如何实现iOS消消乐小游戏Demo
Adr27H
8年前
<h2>引言</h2> <p>做消消乐Demo属于一个意外,本想借助学习iOS游戏开发把CoreAnimation学好,并完成第一个游戏Demo:俄罗斯方块。却在这过程中发现了一些实现消消乐的小技巧,于是兴起完成了这个小Demo,供大家参考。</p> <p>当然,这个Demo不是平白无故产生的,笔者也是参考了一些资料,其中就包括斯坦福大学的iOS公开课,这里放上百度云的链接(含字幕): <a href="/misc/goto?guid=4959747412679357040" rel="nofollow,noindex">Dynamic Animation</a> 。视频是用swift讲的,笔者从视频中获取了帮助和灵感,大家英语好的话也可以尝试学习一下。</p> <p>本文将会讲解如何实现这个消消乐小游戏,相信你一定会有所收获。</p> <h2>项目地址</h2> <p>欢迎一切fork,issue,pull request来帮助该项目做得更好。</p> <h2>效果演示</h2> <p>如下图,和大多数消消乐一样,Demo根据颜色,进行垂直,水平以及两个斜向的三消。用户可以上下左右自由交换两个方块的位置。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/90ab2e80fe71388eb14916126d28161e.gif"></p> <p>消消乐Demo效果</p> <h2>基本思路</h2> <p>先讲解一下基本思路。主要分如下几个部分:</p> <ul> <li>首先,大家可以看到这个消消乐需要一些动画,以及一些诸如碰撞和重力下落等物理特性的支持。</li> <li>其次,我们需要能够正确计算出三消,并以美观的动画样式将其消除。</li> <li>接着,我们需要响应用户的移动方块的操作,实现方块位置的调换。</li> <li>最后,我们添加一些美化效果。</li> </ul> <h2>物理特性及其对应的动画</h2> <p>很显然,物理特性实现的好坏,直接关系到消消乐游戏的体验。在Demo中笔者使用了 <strong>UIDynamicAnimator</strong> 和 <strong>UIDynamicBehavior</strong> 这两个基于 <strong>UIKit</strong> 的类来进行管理。</p> <p>通过 <strong>UIDynamicAnimator</strong> 来实现各种物理特性发生时的动画,如下落加速动画和碰撞反弹动画。而其中涉及的物理特性则使用 <strong>UIDynamicBehavior</strong> 。</p> <h3>KMAnimatorManager</h3> <p>动画管理器: <strong>KMAnimatorManager</strong> 继承自 <strong>UIDynamicAnimator</strong> ,用来管理各种物理特性对应的动画效果。它会关联到一个UIView,这个UIView是我们动画展现的场所,之后所有的物理特性和动画显示都在这个view上进行。如下:</p> <pre> <code class="language-objectivec">_animator = [[KMAnimatorManager alloc] initWithReferenceView:self]; _animator.delegate = self;</code></pre> <p>Demo中,我们所有的游戏场景都在 <strong>KMGameView</strong> 的实例 <strong>_gameView</strong> 中,上述代码的 <strong>self</strong> 就是 <strong>_gameView</strong> 。而封装好的 <strong>_gameView</strong> 就可以直接添加到任意ViewController了。如下:</p> <pre> <code class="language-objectivec">_gameView = [[KMGameView alloc] initWithFrame:self.view.frame]; UIImage *background = [UIImage imageNamed:@"background"]; _gameView.contentMode = UIViewContentModeScaleAspectFill; _gameView.layer.contents = (__bridge id _Nullable)(background.CGImage); _gameView.delegate = self; [self.view addSubview:_gameView];</code></pre> <h3>KMCubeBehavior</h3> <p>通过自定义 <strong>UIDynamicBehavior</strong> 的子类 <strong>KMCubeBehavior</strong> ,笔者向其中封装了诸如 <strong>重力,碰撞检测,弹性系数,是否围绕质心旋转等特性</strong> 。这可能需要你有一些相关物理学方面的基础。但幸好Apple已经做好了封装,我们大可以放心地使用它提供的接口。如下:</p> <pre> <code class="language-objectivec">- (instancetype)init { self = [super init]; [self addChildBehavior:self.gravity]; [self addChildBehavior:self.collider]; [self addChildBehavior:self.animationOptions]; return self; } - (void)addItem:(id<UIDynamicItem>)item { [self.gravity addItem:item]; [self.collider addItem:item]; [self.animationOptions addItem:item]; }</code></pre> <p>我们向 <strong>KMCubeBehavior</strong> 类中加入了所需的各种物理特性,使得之后基于此生成的每一个小方块都有这些效果。如下:</p> <pre> <code class="language-objectivec">_cubeBehavior = [[KMCubeBehavior alloc] init]; [_animator addBehavior:_cubeBehavior];</code></pre> <h2>三消计算及消除动画</h2> <h3>消除的时机</h3> <p>在Demo中,我们以随机下落不同颜色方块的形式来累积砖块,供用户调换位置来消除。因此,需要在两个情况下进行三消判断。一个是在方块下落动画结束后,一个是在用户执行完调换操作。</p> <p>对于前者我们可以利用 <UIDynamicAnimatorDelegate> 中的接口 - (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator 。每当物理动画执行完毕,我们都可以进入该方法,在其中执行我们的三消计算。</p> <h3>消除的计算</h3> <p>整个的计算,我们会多次调用 - (NSArray *)checkCrossAt:(KMDropView *)centerView 方法。该方法类似于一个扫描,传入一个方块视图,然后执行四个方向的扫描,发现可以三消的方块后,将它们进行标记,最后以数组统一返回。供外部程序进行消除。</p> <p>这其中涉及到如何识别方块是否属于同一个类型。虽然Demo是通过颜色区分,但在更多实际场景中,我们可以加载各种图片,比如各种颜色的糖果点心等。因此对于视图中所有小方块,笔者让它们继承于自定义的 <strong>KMDropView</strong> 类,其中封装了方块所需的各种属性,详细内容我们放到下一小节讲。</p> <p>这里,你需要知道,我们通过任一方块的 <strong>type</strong> 属性来进行识别, <strong>type</strong> 是一个字符串,其内容会在方块创建时进行赋值,不同类型的方块有不同的 <strong>type</strong> 。用户看到的仅仅是视图样式,背后真正的匹配可以和视图样式完全独立。例如,我们把匹配三消的方块加入消除数组中:</p> <pre> <code class="language-objectivec">NSString *centerColor = centerView.type; NSString *leftColor = (leftView)?leftView.type : @"#$%^&*"; NSString *rightColor = (rightView)?rightView.type : @"#$%^&*"; ... ... NSMutableArray *totalArr = [NSMutableArray new]; if ([centerColor isEqualToString:leftColor] && [centerColor isEqualToString:rightColor]) { NSArray *arr = [NSArray arrayWithObjects:leftView, centerView, rightView, nil]; [totalArr addObjectsFromArray:arr]; }</code></pre> <p>因此,整个的三消计算思路是遍历所有的方块,利用 - (NSArray *)checkCrossAt:(KMDropView *)centerView 检测可消除的方块,不断地进行消除。 <strong>该算法思路比较简单,可以后续进一步优化。</strong></p> <h3>消除动画</h3> <p>有了需要消除的方块,我们就可以执行消除动画,将它们从视图中移除。在 - (void)kickAwayDrops:(NSArray *)drops 方法中进行响应的实现。我们将这些视图移动至视图的视野外侧,然后从父视图上移除。最后我们的动画管理器 <strong>KMAnimatorManager</strong> 的实例 <strong>_cubeBehavior</strong> 会移除这些方块。方块就会以美观的动画形式消除。如下:</p> <pre> <code class="language-objectivec">[UIView animateWithDuration:0.5 animations:^{ for (UIView *drop in drops) { //设定移除后的位置 int x = self.bounds.size.width+DROP_SIZE.width; int y = - DROP_SIZE.height; drop.center = CGPointMake(x, y); } } completion:^(BOOL finished) { [drops makeObjectsPerformSelector:@selector(removeFromSuperview)]; }]; for (UIView *drop in drops) { [_cubeBehavior removeItem:drop]; }</code></pre> <h2>用户调换操作</h2> <h3>KMPanGestureRecognizer</h3> <p>消消乐需要响应用户对于方块调换的操作,笔者在这里首先想到了使用 <strong>Gesture</strong> 。为了能够更好地响应用户操作,并简化View的代码,我自己封装了一个手势 <strong>KMPanGestureRecognizer</strong> ,并将其添加到游戏主视图 <strong>_gameView</strong> 中。</p> <p>查看其头文件,可以看到一些外部需要的属性和接口。其中比较重要的就是对于手势的判断。用户移动方块属于一种 <strong>Pan</strong> 操作,而不是简单的 <strong>Swipe</strong> 。这表明,用户除了常规的轻扫屏幕,也可以先按住一个方块,然后再慢慢悠悠地往一个方向滑动。因此,系统原生的 <strong>UISwipeGestureRecognizer</strong> 可能就不能很好满足需求了。特自定义一个。</p> <p>在自定义的手势中,对于手指滑动的方向,我们需要设定阈值,某些范围内的滑动我们需要将其标记为无效滑动,即该操作不匹配我们的手势。通过枚举 <strong>KMPanGestureRecognizerDirection</strong> ,笔者定义了一系列方向类型,并通过 <strong>direction</strong> 这个 <strong>@property</strong> 供外部读取。</p> <p>此外,笔者来提供了一些接口,供特定情况下的使用,如可以在手指按住方块时进行回调接口,通知外部代码让改方块高亮,以达到更好的显示效果。</p> <h3>KMDropView</h3> <p>有了调换手势,我们就可以在手势提供的帮助下,正确得知移动两个方块的时机。上文提到过,我们的方块的视图和背后的 <strong>type</strong> 是分离的。 <strong>type</strong> 确定了,方块的类型就确定了,用户看到的显示效果可以额外设置,与 <strong>type</strong> 独立。所以调换两个方块,最根本的是调换它们的 <strong>type</strong> ,而显示的视图效果是可以通过动画来“伪装”的。</p> <p>因此,在自定义的 <strong>KMDropView</strong> 中。笔者提供了一系列 <strong>@property</strong> 来正确设置方块的属性。对于方块使用的思路,笔者经过思考,认为如下是比较合理的:对于方块属性的设置并不直接体现在方块的样式上,方块通过 <strong>state</strong> 字段的设置才最终完成样式的绘制。而这个 <strong>state</strong> 也是通过枚举,举出了方块所有可能的状态。因此一个方块最终显示的效果,其实是取决于它当前所处的状态的。例如普通状态或者高亮状态。</p> <h3>调换动画</h3> <p>有了调换的时机和调换所需改变的东西,我们就可以实现最终的调换动画了。这里笔者使用了一些“伪装”。笔者并没有真的移动两个方块的位置,而是在底层的模型中简单地调换 <strong>type</strong> ,而上层的用户视图中,临时生成两个方块,覆盖在两个原方块上方。然后将这两个临时方块进行位移操作,在动画完成后消除,从而产生方块调换的假象。为此,我特地在 <strong>KMDropView</strong> 中加入了一个 <strong>深拷贝</strong> (KMDropView *)duplicateFrom:(KMDropView *)originView; <strong>类方法</strong> ,使得临时生成的方块能够和原来的看起来一模一样。</p> <h2>美化操作</h2> <p>有了上述三步最关键的操作,剩下的就是一些美化和代码整理。例如高亮选中的方块,把游戏主视图封装起来,独立于ViewConroller等等。</p> <h2>总结</h2> <p>这个消消乐小Demo的编写,还是涉及到了不少新内容。并且含有很多可以值得优化算法的地方。越往后学习真的越感觉到基础的重要性,甚至出现了跨学科的需求。希望大家对于编程,能够静下心打好基础,避免急于求成。</p> <p>希望我的这篇文章能够给大家带来帮助,也非常欢迎大家提出宝贵意见,帮助改进这个Demo。感谢您的阅读,欢迎分享~</p> <p> </p> <p>来自:http://www.jianshu.com/p/55cc6908d7dd</p> <p> </p>