手把手教你如何实现iOS消消乐小游戏Demo

文章也同时在个人博客 http://kimihe.com/更新

引言

做消消乐Demo属于一个意外,本想借助学习iOS游戏开发把CoreAnimation学好,并完成第一个游戏Demo:俄罗斯方块。却在这过程中发现了一些实现消消乐的小技巧,于是兴起完成了这个小Demo,供大家参考。

当然,这个Demo不是平白无故产生的,笔者也是参考了一些资料,其中就包括斯坦福大学的iOS公开课,这里放上百度云的链接(含字幕):Dynamic Animation。视频是用swift讲的,笔者从视频中获取了帮助和灵感,大家英语好的话也可以尝试学习一下。

本文将会讲解如何实现这个消消乐小游戏,相信你一定会有所收获。

项目地址

Cubee Game
欢迎一切fork,issue,pull request来帮助该项目做得更好。

效果演示

如下图,和大多数消消乐一样,Demo根据颜色,进行垂直,水平以及两个斜向的三消。用户可以上下左右自由交换两个方块的位置。

消消乐Demo效果

基本思路

先讲解一下基本思路。主要分如下几个部分:

  • 首先,大家可以看到这个消消乐需要一些动画,以及一些诸如碰撞和重力下落等物理特性的支持。
  • 其次,我们需要能够正确计算出三消,并以美观的动画样式将其消除。
  • 接着,我们需要响应用户的移动方块的操作,实现方块位置的调换。
  • 最后,我们添加一些美化效果。

物理特性及其对应的动画

很显然,物理特性实现的好坏,直接关系到消消乐游戏的体验。在Demo中笔者使用了UIDynamicAnimatorUIDynamicBehavior这两个基于UIKit的类来进行管理。

通过UIDynamicAnimator来实现各种物理特性发生时的动画,如下落加速动画和碰撞反弹动画。而其中涉及的物理特性则使用UIDynamicBehavior

KMAnimatorManager

动画管理器:KMAnimatorManager继承自UIDynamicAnimator,用来管理各种物理特性对应的动画效果。它会关联到一个UIView,这个UIView是我们动画展现的场所,之后所有的物理特性和动画显示都在这个view上进行。如下:

_animator = [[KMAnimatorManager alloc] initWithReferenceView:self];
_animator.delegate = self;

Demo中,我们所有的游戏场景都在KMGameView的实例_gameView中,上述代码的self就是_gameView。而封装好的_gameView就可以直接添加到任意ViewController了。如下:

_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];

KMCubeBehavior

通过自定义UIDynamicBehavior的子类KMCubeBehavior,笔者向其中封装了诸如重力,碰撞检测,弹性系数,是否围绕质心旋转等特性。这可能需要你有一些相关物理学方面的基础。但幸好Apple已经做好了封装,我们大可以放心地使用它提供的接口。如下:

- (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];
}

我们向KMCubeBehavior类中加入了所需的各种物理特性,使得之后基于此生成的每一个小方块都有这些效果。如下:

_cubeBehavior = [[KMCubeBehavior alloc] init];
[_animator addBehavior:_cubeBehavior];

三消计算及消除动画

消除的时机

在Demo中,我们以随机下落不同颜色方块的形式来累积砖块,供用户调换位置来消除。因此,需要在两个情况下进行三消判断。一个是在方块下落动画结束后,一个是在用户执行完调换操作。

对于前者我们可以利用<UIDynamicAnimatorDelegate>中的接口- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator。每当物理动画执行完毕,我们都可以进入该方法,在其中执行我们的三消计算。

消除的计算

整个的计算,我们会多次调用- (NSArray *)checkCrossAt:(KMDropView *)centerView方法。该方法类似于一个扫描,传入一个方块视图,然后执行四个方向的扫描,发现可以三消的方块后,将它们进行标记,最后以数组统一返回。供外部程序进行消除。

这其中涉及到如何识别方块是否属于同一个类型。虽然Demo是通过颜色区分,但在更多实际场景中,我们可以加载各种图片,比如各种颜色的糖果点心等。因此对于视图中所有小方块,笔者让它们继承于自定义的KMDropView类,其中封装了方块所需的各种属性,详细内容我们放到下一小节讲。

这里,你需要知道,我们通过任一方块的type属性来进行识别,type是一个字符串,其内容会在方块创建时进行赋值,不同类型的方块有不同的type。用户看到的仅仅是视图样式,背后真正的匹配可以和视图样式完全独立。例如,我们把匹配三消的方块加入消除数组中:

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];
}

因此,整个的三消计算思路是遍历所有的方块,利用- (NSArray *)checkCrossAt:(KMDropView *)centerView检测可消除的方块,不断地进行消除。该算法思路比较简单,可以后续进一步优化。

消除动画

有了需要消除的方块,我们就可以执行消除动画,将它们从视图中移除。在- (void)kickAwayDrops:(NSArray *)drops方法中进行响应的实现。我们将这些视图移动至视图的视野外侧,然后从父视图上移除。最后我们的动画管理器KMAnimatorManager的实例_cubeBehavior会移除这些方块。方块就会以美观的动画形式消除。如下:

[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];
}

用户调换操作

KMPanGestureRecognizer

消消乐需要响应用户对于方块调换的操作,笔者在这里首先想到了使用Gesture。为了能够更好地响应用户操作,并简化View的代码,我自己封装了一个手势KMPanGestureRecognizer,并将其添加到游戏主视图_gameView中。

查看其头文件,可以看到一些外部需要的属性和接口。其中比较重要的就是对于手势的判断。用户移动方块属于一种Pan操作,而不是简单的Swipe。这表明,用户除了常规的轻扫屏幕,也可以先按住一个方块,然后再慢慢悠悠地往一个方向滑动。因此,系统原生的UISwipeGestureRecognizer可能就不能很好满足需求了。特自定义一个。

在自定义的手势中,对于手指滑动的方向,我们需要设定阈值,某些范围内的滑动我们需要将其标记为无效滑动,即该操作不匹配我们的手势。通过枚举KMPanGestureRecognizerDirection,笔者定义了一系列方向类型,并通过direction这个@property供外部读取。

此外,笔者来提供了一些接口,供特定情况下的使用,如可以在手指按住方块时进行回调接口,通知外部代码让改方块高亮,以达到更好的显示效果。

KMDropView

有了调换手势,我们就可以在手势提供的帮助下,正确得知移动两个方块的时机。上文提到过,我们的方块的视图和背后的type是分离的。type确定了,方块的类型就确定了,用户看到的显示效果可以额外设置,与type独立。所以调换两个方块,最根本的是调换它们的type,而显示的视图效果是可以通过动画来“伪装”的。

因此,在自定义的KMDropView中。笔者提供了一系列@property来正确设置方块的属性。对于方块使用的思路,笔者经过思考,认为如下是比较合理的:对于方块属性的设置并不直接体现在方块的样式上,方块通过state字段的设置才最终完成样式的绘制。而这个state也是通过枚举,举出了方块所有可能的状态。因此一个方块最终显示的效果,其实是取决于它当前所处的状态的。例如普通状态或者高亮状态。

调换动画

有了调换的时机和调换所需改变的东西,我们就可以实现最终的调换动画了。这里笔者使用了一些“伪装”。笔者并没有真的移动两个方块的位置,而是在底层的模型中简单地调换type,而上层的用户视图中,临时生成两个方块,覆盖在两个原方块上方。然后将这两个临时方块进行位移操作,在动画完成后消除,从而产生方块调换的假象。为此,我特地在KMDropView中加入了一个深拷贝(KMDropView *)duplicateFrom:(KMDropView *)originView;类方法,使得临时生成的方块能够和原来的看起来一模一样。

美化操作

有了上述三步最关键的操作,剩下的就是一些美化和代码整理。例如高亮选中的方块,把游戏主视图封装起来,独立于ViewConroller等等。

总结

这个消消乐小Demo的编写,还是涉及到了不少新内容。并且含有很多可以值得优化算法的地方。越往后学习真的越感觉到基础的重要性,甚至出现了跨学科的需求。希望大家对于编程,能够静下心打好基础,避免急于求成。

希望我的这篇文章能够给大家带来帮助,也非常欢迎大家提出宝贵意见,帮助改进这个Demo。感谢您的阅读,欢迎分享~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,612评论 4 59
  • 第一次注意到这部电影是因为它的导演杨德昌先生,他凭借该片获得戛纳国际电影节最佳导演奖。这部电影拍摄于2000年,...
    裂锦公子阅读 513评论 0 1
  • 无回应之地即是绝境。如果缺乏情感回应,人就等于出于绝境,甚至是死亡之地。回应,就是光。 人是社会性动物,是群居类的...
    刘霞阅读 235评论 0 0
  • 模块是构建应用程序的基础,也使得函数和变量私有化,不直接对外暴露出来,接下来我们就要介绍Node的模块化系统和它最...
    一个胖子的我阅读 507评论 0 1