iOS小项目之第二弹- 别踩白块

iOS-OC项目 别踩白块(Don't Tap The White Tile)


写在前面的话

①别踩白块游戏简介:

《别踩白块儿 Don't Tap The White Tile》,这就是这个游戏唯一的一个规则,我们只需要不断踩着黑色方块前进即可,很简单吧?谁都可以会玩,但并不是谁都能玩得很好噢,你呢?快来挑战看看吧!经典模式,以最快的速度到达终点。街机模式,你有能力得多少分就得多少分,没有任何限制,这也是最具挑战性的一个模式。限时模式,在30秒内看你能走几步。极速模式,没有最高速限制的街机模式, 挑战你的极限接力模式,规定时间内完成50块儿,然后会有更多时间去完成另外的50块儿。【复制于百度百科】

如果你没玩过,没关系 ,上图看看


别踩白块儿.gif

意思就是:每行有四个按钮,黑色的按钮是正确的需要你点击的按钮,白色块点击后游戏失败,所以这个游戏就叫"别踩白块儿"😂,别问我我为什么这么机智。

②写这个小游戏的目的

最重要 : 纯属好玩

我觉得做一件事情最大的推动力就是 兴趣 兴趣 兴趣
看到好多游戏都是拿Unity3D、Cocos2D开发的,我就想 我用系统自带的一些东海也可以实现一些简单的平面2D动画呀,所以我试了试,所以就有了这篇简书。

电商之类的app不美观

前面仿写过半糖app,公司也是电商的app,所以对于电商app无爱了,,,Ծ‸Ծ,,,都是 首页、分类、购物车、个人中心的套路,你懂的。相反我倒是更喜欢一些电商入口的APP,就比如半糖、美丽说、小红书之类的app,UI很美观,这是吸引我的地方。


项目整体的概括

①开发周期:

工作闲暇之余累计有10天左右,最近一个半月在忙公司的项目,只好抽时间写啦

②开发工具和语言:

开发工具为Xcode7.3 语言为Objective-C
因为前面仿写半糖app的时候,好多朋友给我说不太懂swift,那我就写个OC的小项目

③开发要点:

游戏成功、失败的逻辑代码基于 ReactiveCocoa ,代码有详细的解释。
音频管理、整体的UI搭建不难,难的是整体游戏逻辑的设计。


项目详细实现过程

①首页,页面较为简单,主要介绍一下 UIButton与 ReactiveCocoa的结合使用

tmp58f5a0f6.png

--主要代码如下

//开始游戏按钮
    _startButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH/2, SCREEN_HEIGHT)];
    _startButton.backgroundColor = [UIColor blackColor];
    [_startButton setTitle:@"开始游戏" forState:UIControlStateNormal];
    [_startButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    WeakSelf;
    [[_startButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        [weakSelf buttonHideAnimation];
        [weakSelf  presentViewController:[[GameSceneViewController alloc] init] animated:false completion:^{
            //弹出游戏窗体后 暂停BGM
            [SoundPlayer pauseBGM];
        }];
        NSLog(@" start game :)");
    }];
    [self.view addSubview:_startButton];

解释:[_startButton rac_signalForControlEvents:UIControlEventTouchUpInside]返回一个 RACSignal,可以理解为 一个信号,后面 subscribeNext:^(id x) {} 意思就是 接受到这个信号所做的操作。从字面意思上可以理解为,前者当你点击按钮,触发UIControlEventTouchUpInside事件的时候 ,发出一个信号,后者是接受到这个信号,你要干什么,由你决定。

②游戏页面view,

倒计时动画介绍

上图:

倒计时动画.gif

----构成:view背景为黑色,中间添加了一个label,显示当前的数字

/**
*  倒计时 anim ......3 2 1 动画
*
*  @param anim          anim 变化的数字初始值
*  @param completeBlock 动画结束后的操作
*/
- (void)showWithAnimNum:(NSInteger)anim CompleteBlock:(CompleteBlock)completeBlock;

----实现思路: 要做的从给定的参数 anim 递减到 0 之后结束动画,然后执行completeBlock
,我是这样写一个类似于递归的动画:
①整体动画可以分为很多个,单个数字,做缩放和透明度改变的动画组

//返回一个 动画group
- (CAAnimationGroup *)animationGroup {
    //缩放
    CABasicAnimation *animation1 = [CABasicAnimation animation];
    [animation1 setKeyPath:@"transform.scale"];
    [animation1 setFromValue:@1.0];
    [animation1 setToValue:@4.0];
    [animation1 setDuration:1.0];
    //改变透明度
    CABasicAnimation *animation2 = [CABasicAnimation animation];
    [animation2 setKeyPath:@"alpha"];
    [animation2 setFromValue:@1.0];
    [animation2 setToValue:@0.3];
    [animation2 setDuration:1.0];
    
    
    CAAnimationGroup *animGroup = [[CAAnimationGroup alloc] init];
    animGroup.animations = [NSArray arrayWithObjects:animation1,animation2, nil];
    [animGroup setDuration:1.0];
    [animGroup setDelegate:self];
    return animGroup;
}

@用一个参数控制整体的动画的继续和停止,就是_animIndex
每一个动画group执行完毕后,在 - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag 中进行_animIndex--,并且添加下一组动画,看代码:

// - MARK: Animation Delegate
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if (flag && _animIndex > 0) {
        _animIndex--;
        _animLabel.text = [NSString stringWithFormat:@"%ld",(long)_animIndex];
        [_animLabel.layer addAnimation:[self animationGroup] forKey:nil];
    }
    //动画执行完毕后 执行的操作
    if (_animIndex == 0) {
        if (_completeBlock != nil) {
            //延迟0.4s之后 再开始游戏 防止游戏开始太快 user接受不了
            self.hidden = true;
            WeakSelf;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                _completeBlock();
                [weakSelf.animLabel.layer removeAllAnimations];
                weakSelf.hidden = false;
                [weakSelf removeFromSuperview];
            });
        }
    }
}
游戏界面如何搭建

①首页整体界面搭建

B7A2A5EB-B936-4ABD-BF2A-F6135389EC0C.png

界面中每一个被我框起来的都是我封装的 GameSceneView,当然我们不能在每一个GameSceneView那如何实现''重用'呢'
实现思路:
①计算你的屏幕中需要多少个gameScene

// - 1: 先计算出 屏幕中需要多少个 scene
    _sceneCount = ceil(SCREEN_HEIGHT / WhiteBlockHeight) + 1;

②创建与之等同数量的scene
_frameArray存储的是 scene创建时的初始frame,便于点击错误时,直接恢复到初始的frame
_operateArray将创建好的scene集中到一起,便于管理

[_frameArray addObject:[NSValue valueWithCGRect:scene.frame]];
[_operateArray addObject:scene];

屏幕上所有的 scene都是向下滚动的,每次滚动判断是否超出屏幕,当用户点击正确、scene漂移到屏幕外的时候,再更新sceneframe,这个scene将被安排到最后一个scene的后面,上代码:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    //用 CADisplayLink 进行刷新 频率更快,动画效果不会卡顿
    disPlayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(startScroll)];
    
    //先执行 3 2 1 这样的动画
    [[StartAnimView shareInstance] showWithAnimNum:3 CompleteBlock:^{
        [disPlayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    }];
}

disPlayLink一直在调用 - (void)startScroll这个方法,刷新界面

//开始滚动
- (void)startScroll {
    WeakSelf;
    
    [_operateArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        //
        dispatch_async(dispatch_get_main_queue(), ^{
            GameSceneView *scene = (GameSceneView *)obj;
            CGRect frame = scene.frame;
            frame.origin.y += _gameSpeed * 2.0;
            if (!*stop) {
                scene.frame = frame;
            }

            //  用户点击超时没有点击
            if ( scene.frame.origin.y >= SCREEN_HEIGHT && scene.completeType == CompleteTypeNotClick)
            {
                //停止刷新
                [disPlayLink invalidate];
                *stop = false;
                
                //弹出失败页面
                [weakSelf resetGame];
        
            }else if( scene.frame.origin.y > SCREEN_HEIGHT && scene.completeType == CompleteTypeVictory){
                // 用户点击成功  计算新的frame
                scene.frame = [weakSelf calculateNewFrameWithTag:scene.tag];
                [scene reSet];
            }
        });
    }];
    
}

点击正确之后 计算该 scene新的frame

//点击正确之后 将超出屏幕的scene 改变frame ,让玩家感觉一直有新scene的出来
- (CGRect)calculateNewFrameWithTag: (NSInteger )tag {
    tag = tag == 0 ? _sceneCount - 1 : tag - 1;
    
    GameSceneView *scene = (GameSceneView *)[_operateArray objectAtIndex:tag];
    CGRect frame = scene.frame;
    frame.origin.y -= WhiteBlockHeight;
    return frame;
}

这一部分用图很好解释:

tmp13f239f2.png

红色框为屏幕范围, 第0个scene超出屏幕后,新的位置即在 第4个scene的后面

GameSceneView 的构建

这一部分有一个小遗憾,应该将每行的button数量写成 参数,不应该写成4😀
每个button通过 button.layer的borderColor 和 borderWidth属性设置边框,所以显的有点粗糙。
主要代码:

// - MARK: private method
//快速构建button
- (UIButton *)quickCreateButtonWithFrame:(CGRect)frame{
    UIButton *btn = [[UIButton alloc] initWithFrame:frame];
    
    btn.selected = false;
    btn.layer.borderColor = [UIColor blackColor].CGColor;
    btn.layer.borderWidth = 0.5;
    btn.backgroundColor = [UIColor whiteColor];

    WeakSelf;
    [[[btn rac_signalForControlEvents:UIControlEventTouchUpInside]
      map:^id(id value) {
          if(!weakSelf.isHasOtherSelected && weakSelf.goalIndex == btn.tag){
              // 没人点击 and 点击按钮为目标按钮 -> click正确
              btn.selected = false;
              weakSelf.isHasOtherSelected = true;
              weakSelf.completeType = CompleteTypeVictory;
//              [SoundPlayer playWithMusicName:_effect];
              weakSelf.clickBlock(CompleteTypeVictory);
              return [UIColor whiteColor];
          }else if(!weakSelf.isHasOtherSelected && weakSelf.goalIndex != btn.tag) {
              //点击btn 不是 目标 -> click错误
              weakSelf.completeType = CompleteTypeFailure;
              weakSelf.isHasOtherSelected = true;
//              [SoundPlayer playWithMusicName:ErrorEffect];
              //点击错误之后 将正确的btn 执行动画
              [weakSelf clickButtonFailureWithButton:btn];
              return [UIColor orangeColor];
          }
          return [UIColor whiteColor];
          
    } ]subscribeNext:^(UIColor *color) {
        btn.backgroundColor = color;
    }];
    
    return btn;
}

解释一下:
对于其中ReactiveCocoa的解释
[btn rac_signalForControlEvents:UIControlEventTouchUpInside]就是当你点击button的时候 会发出一个信号,map:^id(id value) { ........}是接受到该信号之后,你做的一些操作,返回一个value对象 ,subscribeNext:^(id value) {.......} 就是 对于接受到的value做的一些操作

用一个比喻来说,[btn rac_signalForControlEvents:UIControlEventTouchUpInside] 就是水源,map:^id(id value) { ........}就是农夫山泉加工厂,subscribeNext:^(id value) {.......}就是水送到你手里了,你要干什么,洗脸洗脚喝掉随便你。。

对于游戏逻辑的解释:
isHasOtherSelected这个bool类型的变量 :
true代表已经有一个button被点击 false代表没有button被点击
②根据点击成功与否,返回完成状态completeType
点击成功:weakSelf.clickBlock(CompleteTypeVictory);
点击失败:

//点击错误之后  将失败的btn 闪烁下
              [weakSelf clickButtonFailureWithButton:btn];

button闪烁动画如下

//点击错误 让点错的button 闪烁
- (void)clickButtonFailureWithButton:(UIButton *)sender{
    
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];//必须写opacity才行。
    animation.fromValue = [NSNumber numberWithFloat:1.0f];
    animation.toValue = [NSNumber numberWithFloat:0.0f];//这是透明度。
    animation.autoreverses = YES;
    animation.duration = 0.2;
    animation.repeatCount = 4;
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;
    animation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];///渐进动画
    [animation setDelegate:self];
    [sender.layer addAnimation:animation forKey:nil];
}

//闪烁完毕后 返回失败状态
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if (flag) {
        self.clickBlock(CompleteTypeFailure);
    }
}
音频处理、

SoundPlayer这个类负责播放音频,调用AVAudioPlayer来播放音频
有三个方法:

//按钮点击时播放的声音
+ (void)playWithMusicName: (NSString *)fileName;

//播放背景音乐
+ (void)playBackgroundMusicWithName: (NSString *)fileName;

//暂停背景音乐
+ (void)pauseBGM;

关于点击音效的处理
我有两个方案,都在代码里面
①点击按钮时,按点击次数来播放对应的声音:
具体实现:

if (type == CompleteTypeVictory) {
        //点击成功  播放相应的 music
        if (_clickIndex > 213) {
            _clickIndex = (NSInteger)_clickIndex % 213;
        }
        NSString *fileName = [NSString stringWithFormat:@"C-%ld",_clickIndex];
        if (_clickIndex <= 9) {
            fileName = [NSString stringWithFormat:@"C-0%ld",_clickIndex];
        }

        [SoundPlayer playWithMusicName:fileName];
        // 更新当前分数
        _currentScore += 1.0;
        // 根据当前分数 计算白块移动速度
        [self updateGameSpeed];
    }

关于音频的来源:我将 马克西姆克罗地亚狂想曲分成了每个部分为1s的片段,大概200多个片段
这一部分有个疑问:声音文件一般不是分为左右声道吗,我将其中一个声道抽离出来当做背景音乐,另一个声道文件分成若干分,当按钮点击时播放是不是更好?懂电子音频处理的朋友出来回答下呗。

项目感想

①其实复杂动画也是 "缩放" "旋转" "改变透明度" "移动"等一些基础动画的组合,下次遇到动画的时候可以试着将动画进行分解,这样就会减小实现的难度,
②尽量将代码写的清晰易懂,不忙的时候多回去review代码,就可以发现当初代码的一些不合理的逻辑,这样才会进步,这些天回去将我的高仿半糖app review下,古人老话,温故而知新 可以为师矣
③学习之路,任重而道远,路漫漫其修远兮,吾将上下而求索

Github项目下载地址

温馨提示:app启动后 会有背景音乐响起,so,你懂的,点击按钮也会播放 克罗地亚狂想曲的片段,运行时请请调低音量

请运行.xcworkspace

点击去我的Github下载

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,387评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • IELTS的写作分为两个部分 第一部分:曲线图,表格,饼图..etc 这种曲线图、表格、饼图….等等的图型是最能掌...
    Riche阅读 293评论 0 0
  • 马斯洛需求为产品经理分析、发掘一个产品的需求,提供了很好的思路; 1、什么是马斯洛需求? 马斯洛需求全名是马斯洛需...
    王毓琼阅读 7,847评论 3 5
  • 对她我从未开口,甚至没有说过一句话,但总感觉跟她谈过一场恋爱似得,我疯狂的爱着她。可以说我是一个大理想主义...
    觅Find阅读 455评论 0 1