iOS开发-------MVC架构思想-植物大战僵尸

        十月长假也就这么过去了,利用假期想磨练一下自己的MVC架构的思想,于是找了一个相对比较麻烦的Demo来锻炼下,在之前很喜欢植物大战僵尸这个游戏,也算为游戏致敬了,但是这个植物大战僵尸肯定不是之前玩过的那个游戏,只是致敬,类似打地鼠,但是代码感觉非常多,所以也算楼主的目前博客里面最有料的了,不得不说,想逻辑的时候直接的虐心啊,先看一下效果吧





然后看一下如果家中的僵尸达到60个,那么就会跳出一个失败的动画,并伴随着僵尸的奸笑声,这里就不给动态图了




要用MVC架构,逻辑最重要,在写代码的时候明显的感觉的到,没有逻辑步步为艰,并且还会出现写不下去的情况,首先看看以我个人见解建构的MVC目录




Model类:

Zombie(僵尸类): 考虑到以后会更加僵尸种类,所以先创建了一个BaseZombie(僵尸的基类),拥有各种僵尸都有的属性,因为这个小游戏中有四种类型的僵尸,所以上面的图可以看到,有4种类型的僵尸。这里是先理理思路,代码后面会有的。

Manager(管理器):单例

ZomibieManager(僵尸管理器):顾名思义,里面存的是僵尸的对象

MusicManager(背景音乐管理器):能够控制音乐的播放


View类:

BackgroundView(背景视图):继承于UIView,是负责显示背景,也就是绿草坪,标签上的僵尸数以及得分,会根据得分以及僵尸数,分别向VC汇报是否结束游戏,什么时候提高僵尸移动加速

ZombieView(单个僵尸视图):继承于UIImageView,是负责每个僵尸的显示,接收点击事件回调以及动画的形成

Zombies_View(所有僵尸视图):继承于UIView,负责僵尸被点击后的操作、将是进入家中的回调以及负责加快僵尸移动速度

GameOverView(游戏结束视图):继承于UIView,负责展示游戏结束的视图,并能够将重新开始按钮点击实现回调给VC


Controller类

全局的一个boss,负责指挥所有的子控件,并实现最终的回调


大体的逻辑就如上了,只挂图和讲理论不适合我,还是上代码,会更直接简明一些,那就按照MVC的顺序,注:本demon中所有的汇报回调都是Block回调


全局用的宏

       因为在程序中必然要减少数字的使用,面的这种情况,最简单的方法就是定义一个宏,楼主将所有的宏打包成一个头文件,叫做DEFINE.h,下面就是宏文件
#ifndef ____DEFINE_h
#define ____DEFINE_h

#define GAMEOVERCOUNT 60        //家里的僵尸达到60,游戏结束
#define ADD_SPEED_STAND 20       //加速标准,达到20即加速

#define ZOMBIE_MOVE_SPEED 0.5   //僵尸的移动速度
#define TIMER_SPEED       0.05  //计时器回调的间隔

#define ZOMBIE_COUNT 15         //每轮僵尸的个数
#define ZOMBIE_TYPE_COUNT 4     //僵尸的种类

#define ZOMBIE_ADD_SPEED 0.1    //每次加速时增量

#endif



Model(模型)类


Zombie(僵尸类)

         这里面最简单的也就是僵尸(模型)类了,里面只需记住相对应的僵尸类型的 图片数量(zombiesFrameCount)  僵尸图片的宽度(zombiesFrameWidth) 以及僵尸图片的高度(zombiesFrameHeight),全部定义在BaseZombie类中,如下
//
//  BaseZombie.h
//  打僵尸
//
//  Created by YueWen on 15/10/5.
//  Copyright (c) 2015年 YueWen. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface BaseZombie : NSObject

@property(nonatomic,assign)NSInteger zombiesFrameCount;//僵尸图片的张数
@property(nonatomic,assign)NSInteger zombiesFrameWidth;//僵尸图片的宽度
@property(nonatomic,assign)NSInteger zombiesFrameHeight;//僵尸图片的高度

@end

       不需要实现任何的方法,其他四种类型的model类中,也只需重写init方法即可,这里只给出zombieType1(僵尸类型1)的init方法,其他的一样,只需记住图片的数量,以及高和宽即可。
- (instancetype)init
{
    self = [super init];
    if (self) {
        
        //为框架赋值
        self.zombiesFrameHeight = 72;
        self.zombiesFrameWidth = 83;
        
        //图片的数量
        self.zombiesFrameCount = 31;
        
    }
    return self;
}

ZombiesManager(僵尸对象管理器)

        zombiesManager(僵尸对象管理器)是一个存储僵尸对象的类,它不管视图的东西,只负责管理僵尸的类对象,它能够告知VC他的僵尸对象是否已经加载完毕,如果加载完毕,将数组返回交给Zombies_View来继续包装成视图;楼主的习惯,只要是管理者,一般情况下,都是会定义成单例的,这次必然也不例外

首先会声明一个单例方法
/**
 *  获取单例对象
 *
 *  @return 单例对象
 */
+(instancetype)shareZombieManager;

因为要存储僵尸对象,但是又不希望外界能够轻易的改动,那么设置一个开放的数组属性,只能读,即readOnly
/**
 *  存放僵尸对象的数组
 */
@property(nonatomic,strong,readonly)NSArray * zombies;


接着需要一个加载僵尸的方法,来加载 宏 规定的僵尸数
/**
 *  加载僵尸
 */
-(void)loadZombies;

这个Manager需要一个回调,是告知VC,他的僵尸数组已经加载完毕,并且返回加载完毕的数组,并用该数组完成下一步的操作
typedef void(^ZombieManagerLoadFinishBlock)(NSArray * zombies);

有回调比有回调属性,有回调属性必然会有设置回调属性的方法
/**
 *  为回调的代码块赋值
 *
 *  @param zombieMangerLoadFinishBlock 回调的代码块
 */
-(void)setZombieManagerLoadFinishHandleBlock:(ZombieManagerLoadFinishBlock)zombieMangerLoadFinishBlock;

声明文件完毕,接下来就是实现文件了,延展中的代码块属性不赘余,但是延展中需要一个存放对象的可变数组
@property(nonatomic,strong)NSMutableArray * zombies_m;

首先实现init方法以及单例方法
-(instancetype)init
{
    if (self = [super init])
    {
        //初始化数组
        self.zombies_m = [NSMutableArray array];
    }
    return self;
}

+(instancetype)shareZombieManager
{
    static ZombieManager * zombieManager = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        zombieManager = [[ZombieManager alloc]init];
    });
    
    return zombieManager;
}

加载数组的属性,这里唯一一个小心的地方就是每次加载前需要清空数组,不然会因为单例属性而出现对象数量的累加
/**
 *  加载僵尸数组
 */
-(void)loadZombies
{
    //开始之前清除数组
    [self.zombies_m removeAllObjects];
    
    //开始循环加载僵尸
    for (NSInteger i = 0; i < ZOMBIE_COUNT; i++)
    {
        [self addZombie];
    }
}

添加僵尸,貌似只是简单的一句话,但是添加僵尸是随机添加的,不能规定那种僵尸是几个,所以有一个随机返回僵尸对象的方法
/**
 *  随机返回僵尸对象
 *
 *  @return 增加的僵尸
 */
-(BaseZombie *)arcdToModeZombie
{
    NSInteger temp = arc4random() % ZOMBIE_TYPE_COUNT + 1;//随机数 1~4,并根据数组返回僵尸类型
    
    switch (temp) {
        case 1:
            return [[ZombieType1 alloc]init];
            break;
        case 2:
            return [[ZombieType2 alloc]init];
            break;
        case 3:
            return [[ZombieType3 alloc]init];
            break;
        case 4:
            return [[ZombieType4 alloc]init];
            break;
        default:
            break;
    }
    return nil;
}

那么添加僵尸的方法就很简单了,只需给数组添加一个对象即可
//增加一个僵尸
-(void)addZombie
{
    [self.zombies_m addObject:[self arcdToModeZombie]];
}


这里说一下设置代码块的方法,因为他的回调是通过这个设置方法启动的,设置之后立马进行加载,因为是同步的,所以加载完毕后就会通过代码块的行回调给VC
/**
 *  设置回调代码块,并触发加载僵尸数据
 *
 *  @param zombieMangerLoadFinishBlock 回调的代码块
 */
-(void)setZombieManagerLoadFinishHandleBlock:(ZombieManagerLoadFinishBlock)zombieMangerLoadFinishBlock
{
    self.b = zombieMangerLoadFinishBlock;
    
    [self loadZombies];//加载数据
    
    self.b(self.zombies_m);//返回处理好的数组
}

僵尸管理器完成。

MusicManager(音乐管理器)

它的职责很简单,虽然是一个单例,但是只管音乐的播放,VC告诉他放什么音乐,它放音乐即可,单例方法的生命不再赘余,下面是声明各种音乐播放的方法即可
/**
 *  播放开始时的背景音乐
 */
-(void)playStartMusic;


/**
 *  播放结束的音乐
 */
-(void)playEndMusic;


/**
 *  播放失败的音乐
 */
-(void)playLoseMusic;


/**
 *  播放僵尸被敲打的声音
 */
-(void)playBeatMusic;

实现方法,首先延展中需要声明四个音乐播放器属性
@property(nonatomic,strong)AVAudioPlayer * backgrountPlayer;
@property(nonatomic,strong)AVAudioPlayer * endMusicPlayer;
@property(nonatomic,strong)AVAudioPlayer * loseMusicPlayer;
@property(nonatomic,strong)AVAudioPlayer * beatMusicPlayer;

楼主比较懒,因此依旧初始化方法都是通过一个方法返回的,自定义的播放器初始方法如下
/**
 *  加载播放器
 *
 *  @param musicName 音乐名称
 *  @param musicType 音乐类型
 *
 *  @return 播放器地址
 */
-(AVAudioPlayer *)loadPlayWithMusicName:(NSString *)musicName Type:(NSString *)musicType WithNumbersOfLoop:(NSInteger)loop
{
    NSString * path = [[NSBundle mainBundle]pathForResource:musicName ofType:musicType];
    
    //转成url
    NSURL * url = [NSURL fileURLWithPath:path];
    
    //加载播放器
    AVAudioPlayer * player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil];
    
    //设置播放次数
    player.numberOfLoops = loop;
    
    //准备播放
    [player prepareToPlay];
    
    //返回加载好的播放器
    return player;
}

因此自定义的初始化方法就简单多了
/**
 *  加载所有的音乐器
 */
-(void)loadAllMusic
{
    self.backgrountPlayer = [self loadPlayWithMusicName:@"bg" Type:@"mp3" WithNumbersOfLoop:-1];
    self.endMusicPlayer = [self loadPlayWithMusicName:@"end" Type:@"mp3" WithNumbersOfLoop:0];
    self.loseMusicPlayer = [self loadPlayWithMusicName:@"lose" Type:@"mp3" WithNumbersOfLoop:0];
    self.beatMusicPlayer = [self loadPlayWithMusicName:@"shieldhit2" Type:@"caf" WithNumbersOfLoop:0];
}

在init方法中调用即可,单例方法不再说明,和上一个Manager一样
- (instancetype)init
{
    self = [super init];
    if (self) {
        //初始化音乐
        [self loadAllMusic];
    }
    return self;
}

播放音乐的方法,就是控制播放器开始播放,停止播放即可
/******************** 播放音乐的方法 ************************/
-(void)playStartMusic
{
    //背景音乐开启
    [self.backgrountPlayer play];
    
}

-(void)playEndMusic
{
    //背景音乐关闭
    [self.backgrountPlayer stop];
    //开启结束音乐
    [self.endMusicPlayer play];
}

-(void)playLoseMusic
{
    //背景音乐关
    [self.backgrountPlayer stop];
    //开启失败音乐
    [self.loseMusicPlayer play];
}

-(void)playBeatMusic
{
    //开启敲打音乐
    [self.beatMusicPlayer play];
}

音乐管理器完成。


Views(视图类)


BackgroundView(背景视图)

      在之前的逻辑简述中也说过了,他负责展示背景、控制标签上的数量,以及 汇报是否游戏结束 与 僵尸什么时候该加速,为了好做,所以结合了xib,xib如下,不要忘记绑定即可。



接着需要在延展中拖入两个输出口
@property (strong, nonatomic) IBOutlet UILabel *lblNumberOfZombiesInHome;

@property (strong, nonatomic) IBOutlet UILabel *lblScore;


     它负责回调游戏结束以及为僵尸加速的回调,所以定义两个Block,当然这里也可以用其他的方法,但是楼主最近一直在练习Block,所以这次依旧用的Block
typedef void(^GameOverBlock)(void);//游戏结束的回调
typedef void(^AddZombiesMoveSpeed)(void);//需要增加速度的回调,这两个作用一样,只不过为了好区分,所以定义了两个代码块

然后需要随着打到僵尸以及僵尸过线需要改变label上的值,所以声明文件上声明两个方法
/**
 *  增加得分
 */
-(void)addScore;

/**
 *  增加在家的僵尸
 */
-(void)addNumberOfZombieInHome;

然后自然有设置回调的方法
/**
 *  设置游戏结束时候的回调
 *
 *  @param grameOverBlock 游戏结束的回调
 */
-(void)backgroundGameOverBlockHandle:(GameOverBlock)gameOverBlock;


/**
 *  设置加速的回调
 *
 *  @param addZombiesMoveSpeed 加速回调
 */
-(void)backgroundAddZombiesMoveSpeed:(AddZombiesMoveSpeed)addZombiesMoveSpeed;

会不会只有这么点的方法呢,不一定,因为延展中会有更多的方法处理,接下来看实现文件

首先需要记录得分以及进入家中的僵尸的个数
@property (nonatomic,assign)NSInteger score;//得分
@property (nonatomic,assign)NSInteger numberOfZombiesInHome;//在家中的僵尸数量

如何判定僵尸该不该加速呢,所以必然存在一个标志位
@property (nonatomic,assign)NSInteger bgTag;//得分速度的标志位

因为需要回调,所以楼主的习惯必然会存在两个属性
@property (nonatomic,strong)GameOverBlock  gameOverBlock;
@property (nonatomic,strong)AddZombiesMoveSpeed addZombiesMoveSpeed;

延展中属性如此,接下来看方法,既然是继承于UIView,那么必然会有两个初始化方法,谁也不能肯定一定会用哪种方法,所以必先重写两个方法,以后的视图类不再做过多的赘余,记得写即可
//代码创建是运行的方法
-(instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        [self backgroundViewInit];
    }
    return self;
}

//用xib或者stroyboard加载运行的方法
-(void)awakeFromNib
{
    [self backgroundViewInit];
}

接下来就是自定义的创建方法了
//自定义的背景视图加载方法
-(void)backgroundViewInit
{
    self.bgTag = 0;//标志位为0
    self.score = 0;//初始化分数为0
    self.numberOfZombiesInHome = 0;//初始化进入房屋的僵尸个数为0
    
    /*********初始化标签的title*********/
    self.lblNumberOfZombiesInHome.text = [NSString stringWithFormat:@"家中的僵尸数量:   %ld",self.numberOfZombiesInHome];
    self.lblScore.text = [NSString stringWithFormat:@"得分:   %ld",self.score];

}

然后就是为block的属性赋值的方法,以后的block属性也必然会有赋值,只不过如果不特殊,也就不再赘余了
//为游戏结束代码块赋值
-(void)backgroundGameOverBlockHandle:(GameOverBlock)gameOverBlock
{
    self.gameOverBlock = gameOverBlock;
}

//为增加僵尸速度代码块赋值
-(void)backgroundAddZombiesMoveSpeed:(AddZombiesMoveSpeed)addZombiesMoveSpeed
{
    self.addZombiesMoveSpeed = addZombiesMoveSpeed;
}

加分的方法,当分数达到某个数值(即加速标准)的时候会告知VC,需要加速了,这个判断会通过标志位bgTag来判断
/**
 *  加分的方法
 */
-(void)addScore
{
    self.score ++;
    self.lblScore.text = [NSString stringWithFormat:@"得分是:   %ld",self.score];
    
    //如果满 宏 定义的数量,那么取余必然会与标志位不符合
    if (self.score % ADD_SPEED_STAND != self.bgTag)
    {
        //重置标志位
        self.bgTag = self.score % ADD_SPEED_STAND;
        
        //回调加速
        if (self.addZombiesMoveSpeed)
        {
            self.addZombiesMoveSpeed();
        }
    }
    
}

增加家里的僵尸的数量,一样,当到达某个数值(即允许进入僵尸的最大数量) 的时候,即告知VC,游戏需要结束了
-(void)addNumberOfZombieInHome
{
    self.numberOfZombiesInHome++;
    self.lblNumberOfZombiesInHome.text = [NSString stringWithFormat:@"家中的僵尸数量:   %ld",self.numberOfZombiesInHome];
    
    //在家的僵尸等于游戏结束的数值的时候
    if (self.numberOfZombiesInHome == GAMEOVERCOUNT)
    {
        //执行回调
        if (self.gameOverBlock)
        {
            self.gameOverBlock();
        }
    }
}

背景视图完成。


ZombieView(单个僵尸视图)

它是继承于UIImageView,就是演示僵尸行走动画以及被点击的时候进行回调,告知多个僵尸视图,被点击,然后视图告知接下来需要干什么

首先是定义在声明文件中的代码块
typedef void(^zombiePressedBlock)(void);

因为在后面的地方需要用到它是什么类型的僵尸,所以需要一个属性来存储僵尸类型,但是不能修改,所以用readOnly
/**
 *  存储展示的僵尸类型
 */
@property(nonatomic,strong,readonly)BaseZombie * zombie;

在Zombie的时候里面存取着相应的frame的高和宽,以及图片的数量,所以选择整体传入,并多一个参数,用来描述中心点位置的point
/**
 *  根据僵尸图片的大小进行创建
 *
 *  @param zombie 僵尸对象
 *  @param origin 中心店
 */
-(instancetype)initWithZombie:(BaseZombie *)zombie WithOrigin:(CGPoint)origin;


接着就是实现文件,延展中只需要一个代码块属性即可
@property(nonatomic,strong)zombiePressedBlock b;


因为这个类只能通过代码创建,所以直接写了便利构造方法
/**
 *  自定义的便利方法
 *
 *  @param zombie 僵尸对象,里面存着大小
 *  @param origin 中心点
 *
 */
-(instancetype)initWithZombie:(BaseZombie *)zombie WithOrigin:(CGPoint)origin
{
    if (self = [super init])
    {
        //初始化僵尸属性
        _zombie = zombie;
        
        //框架初始化
        [self frameSetWithZombie:zombie WithOrigin:origin];
        
        //设置图片
        [self setImageToImageView:self withType:[self tagForZomieButton:zombie] withZombie:zombie];
        
        //初始化相关属性
        [self attributeSet];
        
    }
    
    return self;
}

因为上面的方法看起来比较简单,是因为分别将三个功能的方法封装起来了,首先是框架的初始化方法,根据zombie中存的数据frame,来创建该视图
/**
 *  初始化位置框架的属性
 *
 *  @param zombie 僵尸类型
 *  @param origin 中心点
 */
-(void)frameSetWithZombie:(BaseZombie *)zombie WithOrigin:(CGPoint)origin
{
    //初始化frame
    self.frame = CGRectMake(0, 0, zombie.zombiesFrameWidth, zombie.zombiesFrameHeight);
    
    //初始化中心点
    self.center = origin;
}

接着是相关属性的设置方法
/**
 *  初始化相关属性
 */
-(void)attributeSet
{
    //可以人机交互
    self.userInteractionEnabled = YES;
    
    //设置动画属性
    self.animationDuration = 1;
    
    //循环播放
    self.animationRepeatCount = -1;
    
    //开启动画
    [self startAnimating];
}

稍微麻烦点的就是设置图片,设置图片需要根据僵尸的类型以及图片的类型来设置,但是这个type是通过方法获得的,先来看设置图片的方法
/**
 *  为imageView设置图片
 *
 *  @param ImageView 设置图片的imageView
 *  @param type   图片的类型
 */
-(void)setImageToImageView:(UIImageView *)imageView withType:(NSInteger)type withZombie:(BaseZombie *)zombie
{
    //表示返回的类型合法
    if (type != 0)
    {
        //接收数组
        NSArray * pic = [self zombiesPictureWithType:type withZombie:zombie];
        
        //加到imageView上
        self.animationImages = pic;
    }
}

里面的type是一个NSInteger类型,会根据僵尸的类型来判断返回哪一个数字,毕竟我们僵尸的名字也是有规律的
/**
 *  根据僵尸类的名字获得僵尸类型的后缀数字
 *
 *  @param baseZomebie 僵尸的父类
 *
 *  @return 僵尸的数字类型的后缀数字,如果不存在,返回0
 */
-(NSInteger)tagForZomieButton:(BaseZombie *)baseZomebie
{
    //获得僵尸的类名字符串
    NSString * zomebieType = NSStringFromClass([baseZomebie class]);

    //循环匹配
    for (NSInteger i = 1; i <= 4; i++)
    {
        if ([zomebieType isEqualToString:[NSString stringWithFormat:@"ZombieType%ld",i]])
        {
            return i;
        }
    }
    
    return 0;
}

继承于UIImageView的原因是因为他有自带的图帧动画效果,所以必然会有图片数组的加载,即方法中的接收数组
/**
 *  根据僵尸的类型和图片数加载图片数组
 *
 *  @param type   僵尸的类型
 *  @param zombie 僵尸的对象
 *
 *  @return 图片数组
 */
-(NSArray *)zombiesPictureWithType:(NSInteger)type withZombie:(BaseZombie *)zombie
{
    //存储图片的数组
    NSMutableArray * pics = [NSMutableArray array];
    
    //开始循环添加图片
    for (NSInteger i = 1; i <= zombie.zombiesFrameCount ; i++)
    {
        //获取UIImage
        UIImage * image = [UIImage imageNamed:[NSString stringWithFormat:@"%ld_%ld.tiff",type,i]];
        
        //添加到可变数组中
        [pics addObject:image];
    }
    
    //返回
    return [NSArray arrayWithArray:pics];
}

毕竟不是button,UIImageView没有被点击的功能,但是并不能代表不能添加,既然是UIView,必然会存在响应触摸事件的方法,如下
//touch事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //捕获touch事件
    UITouch * touch = [touches anyObject];
    
    //获取touch的点
    CGPoint point = [touch locationInView:self];
    
    //判断在该区域内
    if (CGRectContainsPoint(self.bounds, point))
    {
       //点击之后的操作,由上一个视图决定,回调,即多个僵尸视图
        if (self.b)
        {
            self.b();
        }
    }
}


单个僵尸视图完毕。

Zombies_View(多个僵尸的视图)

这个视图算是比较麻烦的一个视图了,因为他需要移动每个僵尸视图,并且还需要汇报僵尸视图被点击,僵尸进入了家,所以这个视图需要一点点的计算,说道回调,必然会存在两个回调
typedef void(^ZombiesBeatBlock)(void);//僵尸被点击的回调
typedef void(^ZombiesIntoHomeBlock)(void);//僵尸进入家的回调

因为这个视图上会出现多个单个僵尸视图,所以需要通过ZombiesManager返回的数组进行加载视图
/**
 *  加载僵尸视图
 *
 *  @param zombies 存储僵尸对象的数组
 */
-(void)startLoadZombiesView:(NSArray *)zombies;

接着有两个为回调代码块赋值的方法
/**
 *  设置僵尸被敲击的回调
 *
 *  @param zombieBeatBlock 被敲击时执行的方法
 */
-(void)zombiesBeatBlockHandle:(ZombiesBeatBlock)zombieBeatBlock;


/**
 *  设置僵尸进入家中的回调
 *
 *  @param zombieIntoHomeBlock 进入家中时执行的回调
 */
-(void)zombiesIntoHomeBlockHandle:(ZombiesIntoHomeBlock)zombieIntoHomeBlock;

他之所以能够让僵尸按时间段移动,必然存在一个计时器,但当游戏结束后,不需要动了,那么还需要要一个关闭定时器的方法
/**
 *  计时器停止
 */
-(void)zombiesStopMove;

游戏结束,僵尸不能动,还希望不能点,还需要一个失去响应的方法
/**
 *  结束所有的僵尸视图的响应
 */
-(void)endAllZombiesEditing;

如果backgroundView(背景视图)告知VC需要加速,那么VC就会让这个视图加速,所以需要一个加速方法
/**
 *  加快僵尸移动的速度
 *
 *  @param speed 加快的速度 */
-(void)addZombiesMoveSpeed:(Speed)speed;

Speed只是自定义的一个类型,实际是CGFloat
typedef CGFloat Speed;

接下来就是更麻烦的实现方法了

首先在延展中定义如下属性
@property(nonatomic,strong)NSMutableArray * zombieViews;//里面存的是僵尸view对象
@property(nonatomic,strong)NSTimer * timer;//计时器
@property(nonatomic,assign)Speed speed;//控制僵尸移动的速度

@property(nonatomic,strong)ZombiesBeatBlock zombiesBeatBlock;
@property(nonatomic,strong)ZombiesIntoHomeBlock zombiesIntoHomeBlock;

重写init方法
#pragma mark - 相关创建的方法
-(instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        //初始化数组
        self.zombieViews = [NSMutableArray array];
        
        //初始化僵尸的速度
        self.speed = ZOMBIE_MOVE_SPEED;

    }
    return self;
}

接着实现加载僵尸视图的方法,因为每种僵尸的视图大小是不固定的,所以需要通过知道僵尸视图的frame来控制视图,避免出现在屏幕的边缘
/**
 *  开始加载僵尸视图
 *
 *  @param zombies 存僵尸对象的数组
 */
-(void)startLoadZombiesView:(NSArray *)zombies
{
    
    //加载僵尸视图对象
    [self loadZombiesViewsWithArray:zombies];
    
    //启动定时器
    self.timer = [NSTimer scheduledTimerWithTimeInterval:TIMER_SPEED target:self selector:@selector(moveAllZombie) userInfo:nil repeats:YES];
}

传进来的参数数组里面存的是Zombie对象,所以需要一个模型转视图的过程,因此有一个loadZomiesViewsWithArray方法
/**
 *  根据存放僵尸对象数组 处理成 存储僵尸视图的 数组
 *
 *  @param zombies 存放僵尸对象的数组
 */
-(void)loadZombiesViewsWithArray:(NSArray *)zombies
{
    for (NSInteger i = 0; i < zombies.count; i++)
    {
        //初始化一个僵尸视图
        ZombieView * zombieView = [[ZombieView alloc]initWithZombie:zombies[i] WithOrigin:[self randomPointWithZombieView:zombies[i]]];
        
        //报告VC已经被敲击
        [zombieView doneWithZombiePressedHandleBlock:^{
       
            //实现回调,敲击声音
            if (self.zombiesBeatBlock)
            {
                self.zombiesBeatBlock();
            }
                
            //被点击之后的动画效果
            [self zombiesDidBeat:zombieView];
            
        }];
         
        //添加视图数组
        [self.zombieViews addObject:zombieView];
        
        //添加视图
        [self addSubview:zombieView];

    }
}

上面的方法用到一个随机中心点的方法,需要传入一个Zombie对象,根据frame来创建,即randomPointWithZombieView:(BaseZombie *)zombie方法,如下
/**
 *  根据僵尸类型创建随机点
 *
 *  @param zombie 僵尸对象
 *
 *  @return 随机创建的点
 */
-(CGPoint)randomPointWithZombieView:(BaseZombie *)zombie
{
    
    //屏幕的宽
    NSInteger width = self.bounds.size.width;
    //屏幕的高
    NSInteger height = self.bounds.size.height;
    
    //获得随机的center
    NSInteger x = arc4random()%(width - zombie.zombiesFrameWidth) + width;//让其移动到右侧屏幕外
    NSInteger y = arc4random()%(height - zombie.zombiesFrameHeight) + zombie.zombiesFrameHeight/2;
    
    return CGPointMake(x, y);
}

这就就是需要僵尸移动了,这个示例里,一共是30个僵尸,所以不会一下子移动30个,必然会有一个移动一只僵尸的方法,30个僵尸的移动便由一个循环即可搞定,首先是一个僵尸移动
/**
 *  单个僵尸移动
 *
 *  @param zombieView 移动的僵尸视图对象
 */
-(void)moveZombie:(ZombieView *)zombieView
{
    //首先获得视图的中心点
    CGPoint point = zombieView.center;
    
    //说明已经进入了家,重新分配位置
    if (point.x <= -1 * zombieView.bounds.size.width / 2)
    {
        //重新分配位置
        zombieView.center = [self randomPointWithZombieView:zombieView.zombie];
        
        //并且执行进入家的回调方法
        if (self.zombiesIntoHomeBlock)
        {
            self.zombiesIntoHomeBlock();
        }
    }
    //前进
    else
    {
        //point坐标减
        point.x -= self.speed;
        
        zombieView.center = point;
    }
}

多个僵尸移动就简单化了,只需要一个循环即可
/**
 *  所有的僵尸视图移动
 */
-(void)moveAllZombie
{
    for (ZombieView * zombieView in self.zombieViews)
    {
        [self moveZombie:zombieView];
    }
}

僵尸被点击后放大,并且一段时候又变小,接着会被移动到最右侧,方法如下
/**
 *  僵尸被点击的时候实现的动画效果
 *
 *  @param zombieView 被点击的僵尸
 */
-(void)zombiesDidBeat:(ZombieView *)zombieView
{
    //实现缩放2倍大小.0.25秒之内完成
    [UIView animateWithDuration:0.25 animations:^{
        
        //获取仿射对象
        CGAffineTransform transForm = CGAffineTransformMakeScale(2, 2);
        
        //赋值
        zombieView.transform = transForm;
        
    } completion:^(BOOL finished) {

        //实现缩放0.5,0.25秒完成
        [UIView animateWithDuration:0.25 animations:^{
            
            //获取仿射对象
            CGAffineTransform transform = CGAffineTransformMakeScale(0.5, 0.5);
            
            //赋值
            zombieView.transform = transform;
            
        } completion:^(BOOL finished) {
            
            //重新分配位置
            zombieView.center = [self randomPointWithZombieView:zombieView.zombie];
            
            /*恢复原来大小*/
            zombieView.transform = CGAffineTransformIdentity;

        }];
    }];
}

剩下的三个方法就显得很简单了,让僵尸不能再移动,关闭定时器即可
/**
 *  僵尸停止移动
 */
-(void)zombiesStopMove
{
    //销毁计时器
    [self.timer invalidate];
}

游戏结束后,背面的僵尸不能进行点击,实际就是不能进行人机交互,将属性设置为NO即可
/**
 *  gameOver之后让僵尸不可点
 */
-(void)endAllZombiesEditing
{
    for (ZombieView * zombieView in self.zombieViews)
    {
        //不能人机交互
        zombieView.userInteractionEnabled = NO;
    }
}

加速呢,只需要改变速度属性即可
/**
 *  加快僵尸移动的速度
 *
 *  @param speed 加快的速度 */
-(void)addZombiesMoveSpeed:(Speed)speed
{
    if (speed >= 0)
    {
        self.speed += speed;
    }
}

多个僵尸视图完成


GameOverView(游戏结束视图)

最后一个视图了,前边的代码估计已经疯了,坚持一下吧,楼主依旧如此,这个视图负责游戏结束后,弹出,并且在弹出之后,会根据 重新开始 按钮进行相应的回调

在实现文件只,定义一个Block回调,负责告知玩家需要重新开始游戏
typedef void(^gameOverRestartBlock)(void);

接着需要声明两个方法,这个是根据父视图,可以确定在父视图的哪个位置
/**
 *  开始出现动画
 *
 *  @param superView 父视图
 */
-(void)startAnimateWithView:(UIView *)superView;

接着就是最后设置回调的方法了
/**
 *  设置重新开始游戏的回调
 *
 *  @param gameOverRestartBlock 回调方法
 */
-(void)gameOverRestartBlockHandle:(gameOverRestartBlock)gameOverRestartBlock;

延展中需要一个UIImageView来显示最后失败的那个图片
//显示失败头像的imageView
@property(nonatomic,strong)UIImageView * imageView;

不要忘记两个初始化方法
//代码实现创建的方法
-(instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        [self gameOverViewInit];
    }
    
    return self;
}

//xib和storyboard创建的方法
-(void)awakeFromNib
{
    [self gameOverViewInit];
}

自定义的初始化方法
/**
 *  自定义的创建方法
 */
-(void)gameOverViewInit
{
    //重置大小
    self.frame = CGRectMake(0, 0, 141, 180);
    
    //初始化imageView
    self.imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 141, 117)];

    //设置图片
    [self.imageView setImage:[UIImage imageNamed:@"ZombiesWon.png"]];
    
    //添加图片
    [self addSubview:self.imageView];

}

失败的时候动画弹出的视图,实现方法如下
//最后失败跳出动画
-(void)startAnimateWithView:(UIView *)superView;
{
    //设置自己的中心点
    self.center = CGPointMake(superView.bounds.size.width / 2, superView.bounds.size.height / 2);
    
    //将自己添加到父视图上
    [superView addSubview:self];
    
    
    //实现动画
    [UIView animateWithDuration:1.0 animations:^{
        
        //创建一个仿射对象
        CGAffineTransform transform = CGAffineTransformMakeScale(2, 2);
        
        //赋值
        self.transform = transform;
        
    } completion:^(BOOL finished) {
        
        //创建一个重新来过的按钮
        UIButton * button = [self loadRestartButton];
        
        //添加button
        [self addSubview:button];
        
    }];
}

因为那么button的属性比较多,所以楼主打包成一个方法了,如下
/**
 *  创建一个重新来过的按钮
 *
 *  @return 创建备好的按钮
 */
-(UIButton *)loadRestartButton
{
    //创建一个重新来过的button
    UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
    
    //设置frame
    button.frame = CGRectMake(0, 0, 126, 26);
    
    //设置图片
    [button setBackgroundImage:[UIImage imageNamed:@"a.png"] forState:UIControlStateNormal];
    [button setBackgroundImage:[UIImage imageNamed:@"aHighlight.png"] forState:UIControlStateHighlighted];
    
    //设置中心
    button.center = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2 + 50);
    
    //设置目标动作回调
    [button addTarget:self action:@selector(restartGame) forControlEvents:UIControlEventTouchUpInside];
    
    //设置title
    [button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    button.titleLabel.font = [UIFont systemFontOfSize:10];
    [button setTitle:@"大侠请重新来过0.0" forState:UIControlStateNormal];
    
    return button;
    
}

button的目标动作回调其实就是该视图的回调方法
//重新开始游戏
-(void)restartGame
{
    //回调方法
    if (self.b) {
        self.b();
    }
}

整个视图类完成。

ViewController(控制层)

经过一系列的铺垫与吐血,那么控制层的逻辑就很简单了,下面是属性
@property(nonatomic,strong)BackgroundView * backgroundView;//背景view,负责报告加速以及结束游戏
@property(nonatomic,strong)MusicManager * musicManager;//负责控制播放音乐
@property(nonatomic,strong)ZombieManager * zombieManager;//负责控制僵尸对象
@property(nonatomic,strong)Zombies_View * zombies_view;//负责显示显示僵尸对象以及敲击事件
@property(nonatomic,strong)GameOverView * gameOverView;//负责显示结束游戏界面以及重新游戏的回调

viewDidLoad中初始化即可
- (void)viewDidLoad {
    [super viewDidLoad];

    //加载音乐管理器
    self.musicManager = [MusicManager shareMusicManager];
    
    //加载僵尸管理器
    self.zombieManager = [ZombieManager shareZombieManager];
    
    //加载除了管理器的其他组件
    [self loadExceptManager];

}

在其他的组件加载中,初始化其他的组件loadExceptManager
    //加载背景视图
    self.backgroundView = [[[NSBundle mainBundle] loadNibNamed:@"BackgroundView" owner:nil options:nil] lastObject];
    self.backgroundView.frame = self.view.bounds;
    
    //加载僵尸管理控制器
    self.zombies_view = [[Zombies_View alloc]initWithFrame:self.view.bounds];
    
    //加载结束视图
    self.gameOverView = [[GameOverView alloc]initWithFrame:CGRectZero];

最后就是最爽的回调过程了,不要忘记强引用循环
    //避免强引用循环
    __block __weak ViewController * copy_self = self;


首先是Zombies_View(多个僵尸视图)的回调设置
//僵尸视图被点击的时候回调
    [self.zombies_view zombiesBeatBlockHandle:^{
        
        //音乐管理器播放被敲击的声音
        [copy_self.musicManager playBeatMusic];
        
        //背景分数增加
        [copy_self.backgroundView addScore];
        
    }];
    
    //僵尸进入家进行的回调
    [self.zombies_view zombiesIntoHomeBlockHandle:^{
        
        //背景进入家中的僵尸数量增加
        [copy_self.backgroundView addNumberOfZombieInHome];
        
    }];

然后是ZombieManager(僵尸管理器)的回调方法
    //加载完毕僵尸后的回调
    [self.zombieManager setZombieManagerLoadFinishHandleBlock:^(NSArray *zombies) {
        
        //开始转换成视图僵尸
        [copy_self.zombies_view startLoadZombiesView:zombies];
    }];

接着是BackgroundView(背景视图)的回调方法
    //游戏加速的回调
    [self.backgroundView backgroundAddZombiesMoveSpeed:^{
       
        //僵尸视图加速
        [copy_self.zombies_view addZombiesMoveSpeed:ZOMBIE_ADD_SPEED];
        
    }];
    
    
    //游戏结束时候的回调
    [self.backgroundView backgroundGameOverBlockHandle:^{
        
        //结束游戏音乐开启
        [copy_self.musicManager playLoseMusic];
        
        //僵尸原地不动
        [copy_self.zombies_view zombiesStopMove];
        
        //结束僵尸视图的响应
        [copy_self.zombies_view endAllZombiesEditing];
        
        //结束视图播放
        [copy_self.gameOverView startAnimateWithView:copy_self.view];
    }];

最后是GameOverView(结束视图)的回调方法
    //结束后点击重新开始的方法
    [self.gameOverView gameOverRestartBlockHandle:^{
        
        [self loadExceptManager];
        
    }];

不要忘记音乐播放器播放音乐以及组件的添加
    [self.musicManager playStartMusic];
    
    //添加控件
    [self.view addSubview:self.backgroundView];
    [self.backgroundView addSubview:self.zombies_view];

注:如果不想最上面的信号以及电量栏挡住,一下方法即可解决
/**
 *  隐藏偏好状态栏
 *
 *  @return YES表示隐藏,默认为NO
 */
-(BOOL)prefersStatusBarHidden
{
    return YES;
}


如果有事情,想暂停怎么办呢,就按home键吧,就会暂停了,这就需要在appDelegate中进行设置了
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    UIWindow * windows = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];//创建一个window对象
    
    UIStoryboard * storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];//获得storyboard
    
    ViewController * viewController = [storyboard instantiateViewControllerWithIdentifier:@"ViewController"];
    
    windows.rootViewController = viewController;//设置根视图
    
    self.window = windows;
    
    [self.window makeKeyAndVisible];//被适用对象的主窗口显示到屏幕的最前端
    
    return YES;
}

那么当被挂起的时候会执行如下方法
- (void)applicationDidEnterBackground:(UIApplication *)application {
    
    ViewController * viewController = (ViewController *)self.window.rootViewController;
    
    [viewController.zombies_view zombiesStopMove];//停止移动
    [viewController.musicManager closeBackPlayerMusic];//关闭背景音乐
    
}

那么再次进入游戏后开始就需要写下面的方法
- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    ViewController * viewController = (ViewController *)self.window.rootViewController;
    
    [viewController.zombies_view zombiesStartMove];//开始移动
    [viewController.musicManager playStartMusic];//播放开始音乐
}

这样的话,想暂停就直接home键吧,后台就自动暂停了哦,祝大家国庆快乐0.0





  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值