您的位置:首页 > 运维架构 > 网站架构

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

2015-10-07 16:12 543 查看
十月长假也就这么过去了,利用假期想磨练一下自己的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

GitHub:https://github.com/YRunIntoLove/BeatZombies
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: