ios游戏引擎之Cocos2d(一)
cocos2d是一个免费开源的ios游戏开发引擎,并且完全采用object-c进行编写,这对于已经用惯object-c进行ios应用开发的童鞋来说非常容易上手。这些也是我推荐使用cocos2d进行ios游戏开发的原因,当然从字面上已经可以开出来,这是一款专注于“2d”游戏的开发引擎,您也可以自己编写3d渲染代码或者使用第三方的解决方案,在cocos2d里加载显示3d模型。此外对于3d,也可以选用cocos3d来进行游戏开发。好了,废话不多说,还是先专注在cocos2d。
下面我会主要通过最近正在着手开发的一款小游戏来说一下cocos2d:(关于如何安装cocos2d,网上有大把的资料可查,这里就不详细说了,安装推荐下这篇http://hi.baidu.com/784500515/item/402e6d100d7b8df6dceecaf1,我亲身实践过是靠谱的。此外网上还有很多关于cocos2d的教程也不错的,可以去看看,像Learn iPhone and iPad cocos2d game development和知易cocos2d-iphone开发教程。)
大概介绍下这款游戏:通过iphone的三轴陀螺仪来操纵游戏主角,避开沿路的炸弹,并获取随机散落的金币来得分,我只说整个游戏的骨架。当然你也可以有更多的拓展,比如让沿路的炸弹有多种随机类型,有的炸弹吃到会变让游戏主角变大,有的炸弹会把主角沿前进的方向炸飞使其加速等等。最寻常的炸弹就是仅仅炸到主角使其生命值减少。此外也可以对主角进行选择,比如让不同的主角类型拥有不同的横向移动速度和不同的满格生命值。。。
游戏主界面的截图主要包括:得分(左上角),主角生命值(右上角),游戏速度控制按钮(右上角),炸弹和金币,游戏主角(屏幕底部)以及一直滚动的背景图
—————————————下面是游戏主界面截图—————————————
首先,我们需要先创建一个cocos2d的工程,在xcode的新建工程界面选择cocos2d分类下的cocos2d Application,键入工程名称(我把它命名为DropBomb)后,点击“save”。如果你已经安装成功cocos2d,xcode会帮助创建一个基本的cocos2d Application工程,里面会包含一些基本的文件和代码,包括“Helloworld”。我们需要对这个基本的工程进行一些修改,以使其符合这个游戏的需要。
我们先分别添加两个继承CCSprite的类myBomb和myCoin,这两个类用于控制炸弹和金币,注意添加的时候同时勾选“Also create XXX.h”,这样添加完成后,xcode会自动将该类的.m文件和.h头文件都添加到当前工程下。然后删除xcode帮我们自动生成的Helloworld.m和相应的.h头文件,添加游戏的主场景,新建一个继承CCLayer的类GameScene。此外还需要添加一个结束呈现得分以及重新开始的界面EndScene(继承自CCLayer)以及一个控制跳转和载入过程的界面LoadingScene(继承自CCScene)。另外我对系统提供的CCAnimation类进行了扩展,使其可以更方便地用于CCSprite播放帧动画,这个类我命名为Helper。
好了,这样工程的基本文件结构就完成了,整个工程文件的结构如下(其他一些不是很重要的图片资源文件不是很影响整个工程,所以就没有截全)
—————————————下面是整个工程文件截图—————————————
由于我们删除了系统给添加的“Helloworld”,那么就需要来修改启动页面过后进入到的游戏主场景界面。在“Groups & Files”中进入DropBombAppDelegate.m,对applicationDidFinishLaunching函数中的runWithScene进行修改,将游戏主场景设为GameScene:
[[CCDirector sharedDirector] runWithScene: [GameScene scene]];
另外,cocos2d默认的屏幕显示方向是横向的,这里需要纵向的屏幕来进行游戏,找到shouldAutorotateToInterfaceOrientation函数,对GAME_AUTOROTATION == kGameAutorotationUIViewController条件下的return值进行修改,将UIInterfaceOrientationIsLandscape改为UIInterfaceOrientationIsPortrait
#elif GAME_AUTOROTATION == kGameAutorotationUIViewController
return ( UIInterfaceOrientationIsPortrait( interfaceOrientation ) );
好了,暂时不必理会DropBombAppDelegate.m中的其他代码,我们还不用动到他们。此时运行项目,在启动页面过后会出现空白页面,那是因为我们还没有给GameScene主场景中添加内容。不要紧,现在马上切换到GameScene中。
1. GameScene类
我们需要在GameScene中以静态方法定义场景节点和层节点,在GameScene.h中添加一个静态方法:
+(id)scene;
然后在GameScene.m中实现这个静态方法:
+(id)scene {
CCScene *scene = [CCScene node];
CCLayer *layer = [GameScene node];
[scene addChild:layer];
return scene;
}
此时,我们的GameScene中就包含了一个CCScene的场景节点和GameScene的层节点(不要忘了GameScene是继承自CCLayer的)。下面就通过实现GameScene的init方法来为主场景添加点内容吧:我们在GameScene的init方法中对游戏的时间、得分和生命值进行了初始化以保证每次切换到主场景时,这些数值都是新的;然后我们对游戏主角、炸弹、金币、游戏背景、加速按钮、得分显示、生命值显示等进行了初始化(注意此处对游戏主角的初始化用到了我们对系统的CCAnimation扩展的方法,需要实现Helper类后才可以使用,稍后再来看Helper的实现方法);此外我们还初始化了炸弹的初始下落速度,并设定了一个循环播放的背景音乐。由于需要对主场景中的变量传递到其他的层中,我们还在init方法中将self赋值给了GameScene的一个静态对象(cocos2d中将这种方法称为“伪单例”)。详细的GameScene的init方法如下,对代码我进行详细的标注,可以进行对照:
-(id)init {
if (self == [super init]) {
NSLog(@"%@,%@",NSStringFromSelector(_cmd),self);
totalTime = 0.0f;
score = 0;
life = 3;
self.isAccelerometerEnabled = YES; //启用加速计
player = [CCSprite spriteWithFile:@"bomber0.png"]; //用bomber图片初始化player精灵
[self addChild:player z:2 tag:1]; //将player精灵添加到场景中
CGSize screenSize = [[CCDirector sharedDirector] winSize]; //获取屏幕大小
float imageHeight = [player texture].contentSize.height; //获取精灵中图片内容的大小
player.position = CGPointMake(screenSize.width*0.5f, imageHeight*0.5f); //初始化player的位置
CCAnimation *anim = [CCAnimation animationWithFile:@"bomber" frameCount:2 delay:0.5f]; //运用给CCAnimation扩展的方法来生成一个动画对象anim
CCAnimate *animate = [CCAnimate actionWithAnimation:anim];
CCRepeatForever *repeate = [CCRepeatForever actionWithAction:animate]; //生成一个重复动作的repeate对象
[player runAction:repeate]; //让player运行这个重复动作
//初始化bomb
[self initBombs];
//初始化coin
[self initCoins];
[self scheduleUpdate]; //用于检测加速度,以随时改变player的位置
//设置一个不断向前滚动的背景
CCSprite *background = [CCSprite spriteWithFile:@"background.png"];
background.position = ccp(0,screenSize.height*3);
background.anchorPoint = ccp(0,1);
[self addChild:background z:-1 tag:2];
CCMoveTo *moveTo = [CCMoveTo actionWithDuration:8 position:CGPointMake(0, screenSize.height)];
CCCallFunc *func = [CCCallFunc actionWithTarget:self selector:@selector(onCallFunc)];
CCSequence *bgSequence = [CCSequence actions:moveTo,func,nil];
CCRepeatForever *bgRepeat = [CCRepeatForever actionWithAction:bgSequence];
[background runAction:bgRepeat];
//设置加速按钮
CCSprite *normal1 = [CCSprite spriteWithFile:@"speed1.png"];
CCSprite *normalSelected = [CCSprite spriteWithFile:@"speed1.png"];
normalSelected.color = ccGRAY;
CCMenuItemSprite *itemNormal = [CCMenuItemSprite itemFromNormalSprite:normal1 selectedSprite:normalSelected];
CCSprite *speed1 = [CCSprite spriteWithFile:@"speed2.png"];
CCSprite *speedSelected = [CCSprite spriteWithFile:@"speed2.png"];
speedSelected.color = ccGRAY;
CCMenuItemSprite *itemSpeed = [CCMenuItemSprite itemFromNormalSprite:speed1 selectedSprite:speedSelected];
CCMenuItemToggle *speedToggle = [CCMenuItemToggle itemWithTarget:self selector:@selector(speedTouched) items:itemNormal,itemSpeed,nil];
CCMenu *menu = [CCMenu menuWithItems:speedToggle,nil];
menu.position = CGPointMake(screenSize.width - [speed1 texture].contentSize.width * 0.5f - 5, screenSize.height - [speed1 texture].contentSize.height * 0.5f - 5);
[self addChild:menu z:102];
//初始化scoreLabel
scoreLabel = [CCLabelTTF labelWithString:@"SCORE:0" fontName:@"Marker Felt" fontSize:23];
scoreLabel.color = ccGREEN;
//scoreLabel.opacity = 160;
scoreLabel.position = CGPointMake(10, screenSize.height - 10);
scoreLabel.anchorPoint = CGPointMake(0, 1.0f);
[self addChild:scoreLabel z:100];
//初始化lifeLabel
lifeLabel = [CCLabelTTF labelWithString:@"LIVES:3" fontName:@"Marker Felt" fontSize:23];
//lifeLabel.color = ccGREEN;
lifeLabel.position = CGPointMake(screenSize.width * 0.6f, screenSize.height - 10);
lifeLabel.anchorPoint = CGPointMake(0, 1.0f);
[self addChild:lifeLabel z:101];
life = 3;
//myBomb *test = [[myBomb alloc] init];
//[self addChild:test z:112 tag:111];
//test.position = ccp(screenSize.width/2,screenSize.height/2);
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"star.mp3" loop:YES]; //播放背景音乐
dropSpeed = 1.0f; //设置默认下落速度为1倍速
sharedAllInMe = self; //将self赋值给共享本层的对象
}
return self;
}
这里的sharedAllInMe是一个指向当前GameScene的静态对象,对于其的定义在GameScene.m中,如下:
static GameScene *sharedAllInMe; //生成一个当前层的静态对象
当然为了在其他的类中可以访问这个静态对象,我们还需要设置一个返回它的静态方法,在GameScene.h中定义:
+(GameScene *)getShared; //生成一个当前层的静态访问方法
并在GameScene.m中实现这个方法:
+(GameScene *)getShared { //返回当前层
return sharedAllInMe;
}
为了防止在其他的类中score被修改,我们需要在GameScene.h中将其定义为只读:
@property (assign,readonly) int score;
我们在设置背景不断向前滚动时采用的方法是创建一个动作序列bgSequence,在这个动作序列的末尾调用函数onCallFunc,将背景的位置重新挪到第一屏的位置(需要保证这个背景的第一屏图像与最后一屏图像一致)。onCallFunc函数的详细如下:
//当背景图片滚动到头时,将其重新放回初始位置
-(void)onCallFunc {
CCNode *node = [self getChildByTag:2]; //获取tag为2的对象作为一个CCNode对象,就是背景图
if ([node isKindOfClass:[CCSprite class]]) {
CCSprite *back1 = (CCSprite *)node;
CGSize size = [[CCDirector sharedDirector] winSize];
back1.position = ccp(0,size.height*3); //将该图片的位置重置为初始位置
}
}
在init中,我们设定了点击加速按钮将会调用的方法speedTouched,对于这个方法我们作一个简单的实现,假设我们有一个二级的速度控制(三级或者更多级别仅需要增加判断):
//点击加速按钮改变下落速度
-(void)speedTouched {
if (dropSpeed == 1.0f) {
dropSpeed = 0.5f;
}
else {
dropSpeed = 1.0f;
}
}
init中还用[self scheduleUpdate]预定了一个每秒调用的函数,以随时通过加速计修正的速度来调整主角的位置,并进行碰撞测试。这个函数的实现如下:
-(void)update:(ccTime)delta {
//获取player的当前位置
CGPoint pos = player.position;
//将player的当前位置根据目前方向的速度增加
pos.x += playerVelocity.x;
//获取player可移动的边界
CGSize screenSize = [[CCDirector sharedDirector] winSize];
float imageWidthHelv = [player texture].contentSize.width * 0.5f;
float leftBorderLimit = imageWidthHelv;
float rightBorderLimit = screenSize.width - imageWidthHelv;
//判断player的当前位置是否超过了边界,并进行相应处理
if (pos.x < leftBorderLimit) {
pos.x = leftBorderLimit;
playerVelocity = CGPointZero;
}
else if (pos.x > rightBorderLimit) {
pos.x = rightBorderLimit;
playerVelocity = CGPointZero;
}
//将处理后的位置信息赋值到player上以改变其位置
player.position = pos;
[self checkForCollision]; //进行bomb和bomber或者coin和bomber的碰撞测试
}
对于碰撞测试的实现函数checkForCollision稍后再作说明,此外这里需要用到三轴陀螺仪来返回x轴方向上的速度,相应的实现如下:
//利用加速计改变player的位置
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
float deceleration = 0.3f; //控制减速的速率,值越小可以越快地改变方向
float sensitivity = 8.0f; //控制对加速计输入的敏感度,值越大对加速计输入越敏感
float maxVelocity = 100; //控制最大的速度
playerVelocity.x = playerVelocity.x * deceleration + acceleration.x * sensitivity; //基于当前加速计的加速度调整速度
if (playerVelocity.x > maxVelocity) { //控制player的速度在最大速度之内
playerVelocity.x = maxVelocity;
}
else if (playerVelocity.x < -maxVelocity) {
playerVelocity.x = -maxVelocity;
}
}
其中playerVelocity是在GameScene.h中定义的一个CGPoint变量,我们在此仅对其进行改变以影响主角的位置。
下面来看在GameScene中对炸弹的控制,首先是对炸弹进行初始化,我们要设定炸弹的初始移动速度,将生成的炸弹放入一个炸弹数组以便于后续的控制,然后将炸弹放置到场景中。initBombs的详细实现如下:
//初始化bombs
-(void)initBombs {
bombMoveDuration = 1.8f;
CGSize screenSize = [[CCDirector sharedDirector] winSize];
CCSprite *tempBomb = [CCSprite spriteWithFile:@"bomb0.png"];
//NSLog(@"bombs------%@",tempBomb);
float imageWidth = [tempBomb texture].contentSize.width; //获取bomb图片的宽度
int numBombs = screenSize.width / imageWidth; //计算在当前屏幕宽度下,可容纳多少个bomb
bombs = [[CCArray alloc] initWithCapacity:numBombs]; //用可容纳的bomb数量来初始化bombs数组
for (int i = 0; i < numBombs; i++) { //在当前场景中添加这些bomb,并将其放入bombs数组
myBomb *bomb1 = [[[myBomb alloc] init] autorelease];
//NSLog(@"bombs------%@",bomb1);
[self addChild:bomb1 z:1 tag:3];
//bomb1.position = CGPointMake([tempBomb texture].contentSize.width*i + [tempBomb texture].contentSize.width*0.5f, screenSize.height + [tempBomb texture].contentSize.height);
[bombs addObject:bomb1];
}
[self resetBombs];
}
resetBombs用于将炸弹移到屏幕外的初始位置,并停止其动作:
-(void)resetBombs { //重设bomb的位置,将bomb的位置全部设置在屏幕顶部以外
CGSize screenSize = [[CCDirector sharedDirector] winSize];
myBomb *tmpBomb = [bombs lastObject]; //生成临时bomb对象
CGSize size = tmpBomb.bombSize;
int numBombs = [bombs count];
for (int i = 0; i < numBombs; i++) {
myBomb *bomb1 = [bombs objectAtIndex:i]; //获取bombs数组中的每个bomb对象
bomb1.position = CGPointMake(size.width*i + size.width*0.5f, screenSize.height + size.height); //将各个bomb对象的位置设置到屏幕顶部以外
//NSLog(@"bombs------%f,%f",bomb1.position.x,bomb1.position.y);
[bomb1 stopAllActions];
}
//将预约的方法取消掉
[self unschedule:@selector(bombUpdates:)];
//用指定的时间间隔调用bomb的更新方法
[self schedule:@selector(bombUpdates:) interval:0.7f];
}
每次resetBombs之后,都会预约一个每隔0.7s执行一次的函数bombUpdates:,并将之前的预约取消。bombUpdates:的作用是在炸弹数组中随机寻找一个没有开始移动的炸弹,并让它开始向屏幕底部移动:
-(void)bombUpdates:(ccTime)delta {
//寻找一只不动的bomb,并让其执行动作序列
for (int i = 0; i < 10; i++) {
//获取bombs数组中的一个bomb对象
int randomBombIndex = CCRANDOM_0_1()*[bombs count];
myBomb *bomb1 = [bombs objectAtIndex:randomBombIndex];
//检测该bomb对象是否没有执行任何动作,如果是则让其执行动作序列
if ([bomb1 numberOfRunningActions] == 0) {
[self runBombMoveSequence:bomb1];
//NSLog(@"update-----%@",bomb1);
break; //保证一次仅有一个bomb执行动作
}
}
}
控制炸弹的移动动作则通过runBombMoveSequence:函数来实现,该函数会在判断炸弹没有与主角碰撞到之后(判断通过isCollision标志位来进行),将炸弹向屏幕底部以设定的bombMoveDuration进行移动:
-(void)runBombMoveSequence:(myBomb *)bomb { //控制bomb的执行动作
if (bomb.isCollision == NO) {
bombMoveDuration = 1.8f * dropSpeed;
//设置bomb的移动终点
CGPoint belowScreenPosition = CGPointMake(bomb.position.x, -bomb.bombSize.height);
//bomb移动动作
CCMoveTo *move = [CCMoveTo actionWithDuration:bombMoveDuration position:belowScreenPosition];
//bomb移动到终点,即屏幕底部以外时,将其重新放回屏幕顶部以外的初始位置
CCCallFuncN *call = [CCCallFuncN actionWithTarget:self selector:@selector(onCallFuncN:)];
//生成动作序列并执行动作
CCSequence *seq = [CCSequence actions:move,call,nil];
[bomb runAction:seq];
//NSLog(@"update-----%f,%f",bomb.position.x,bomb.position.y);
}
}
为了防止炸弹移动到屏幕底部以外仍然向下移动,在炸弹移动的动作序列最后调用函数
onCallFuncN:将炸弹重新放回到屏幕顶部的初始位置:
-(void)onCallFuncN:(id)sender {
if ([sender isKindOfClass:[myBomb class]]) { //检测sender是否是myBomb类,如果是则将其位置重置到屏幕顶部以外的初始位置
myBomb *bomb1 = (myBomb *)sender;
//NSLog(@"update-----%f,%f",bomb1.position.x,bomb1.position.y);
CGPoint pos = bomb1.position;
CGSize screenSize = [[CCDirector sharedDirector] winSize];
pos.y = screenSize.height + bomb1.bombSize.height;
bomb1.position = pos;
}
else {
NSLog(@"sender is not a CCSprite");
}
}
到这里,GameScene中对于炸弹的控制就差不多了。而对于金币的控制,原理与炸弹的控制基本一致,就不详细说了,具体的代码如下:
//初始化coins
-(void)initCoins {
coinMoveDuration = 1.6f; //设置coin的移动速度
CGSize screenSize = [[CCDirector sharedDirector] winSize];
CCSprite *tempCoin = [CCSprite spriteWithFile:@"coin0.png"];
//NSLog(@"coins------%@",tempCoin);
float imageWidth = [tempCoin texture].contentSize.width; //获取coin图片的宽度
int numCoins = screenSize.width / imageWidth; //计算在当前屏幕宽度下,可容纳多少个coin
coins = [[CCArray alloc] initWithCapacity:numCoins]; //用可容纳的coin数量来初始化coins数组
for (int i = 0; i < numCoins; i++) { //在当前场景中添加这些coin,并将其放入coins数组
myCoin *coin1 = [[[myCoin alloc] init] autorelease];
//NSLog(@"coins------%@",coin1);
[self addChild:coin1 z:0 tag:4];
//coin1.position = CGPointMake([tempCoin texture].contentSize.width*i + [tempCoin texture].contentSize.width*0.5f, screenSize.height + [tempCoin texture].contentSize.height);
[coins addObject:coin1];
}
[self resetCoins];
}
-(void)resetCoins { //重设coin的位置,将coin的位置全部设置在屏幕顶部以外
CGSize screenSize = [[CCDirector sharedDirector] winSize];
myCoin *tmpCoin = [coins lastObject]; //生成临时coin对象
CGSize size = tmpCoin.coinSize;
int numCoins = [coins count];
for (int i = 0; i < numCoins; i++) {
myCoin *coin1 = [coins objectAtIndex:i]; //获取coins数组中的每个coin对象
coin1.position = CGPointMake(size.width*i + size.width*0.5f, screenSize.height + size.height); //将各个coin对象的位置设置到屏幕顶部以外
//NSLog(@"coins------%f,%f",coin1.position.x,coin1.position.y);
[coin1 stopAllActions];
}
//将预约的方法取消掉
[self unschedule:@selector(coinUpdates:)];
//用指定的时间间隔调用coin的更新方法
[self schedule:@selector(coinUpdates:) interval:0.7f];
}
-(void)coinUpdates:(ccTime)delta {
//寻找一只不动的coin,并让其执行动作序列
for (int i = 0; i < 10; i++) {
//获取coins数组中的一个coin对象
int randomCoinIndex = CCRANDOM_0_1()*[coins count];
myCoin *coin1 = [coins objectAtIndex:randomCoinIndex];
//检测该coin对象是否没有执行任何动作,如果是则让其执行动作序列
if ([coin1 numberOfRunningActions] == 0) {
[self runCoinMoveSequence:coin1];
//NSLog(@"update-----%@",coin1);
break; //保证一次仅有一个coin执行动作
}
}
}
-(void)runCoinMoveSequence:(myCoin *)coin { //控制coin的执行动作
if (coin.isGet == NO) {
coinMoveDuration = 1.6f * dropSpeed;
//设置coin的移动终点
CGPoint belowScreenPosition = CGPointMake(coin.position.x, -coin.coinSize.height);
//coin移动动作
CCMoveTo *move = [CCMoveTo actionWithDuration:coinMoveDuration position:belowScreenPosition];
//coin移动到终点,即屏幕底部以外时,将其重新放回屏幕顶部以外的初始位置
CCCallFuncN *call = [CCCallFuncN actionWithTarget:self selector:@selector(coinCallFuncN:)];
//生成动作序列并执行动作
CCSequence *seq = [CCSequence actions:move,call,nil];
[coin runAction:seq];
//NSLog(@"update-----%f,%f",coin.position.x,coin.position.y);
}
}
-(void)coinCallFuncN:(id)sender {
if ([sender isKindOfClass:[myCoin class]]) { //检测sender是否是myCoin类,如果是则将其位置重置到屏幕顶部以外的初始位置
myCoin *coin1 = (myCoin *)sender;
//NSLog(@"update-----%f,%f",coin1.position.x,coin1.position.y);
CGPoint pos = coin1.position;
CGSize screenSize = [[CCDirector sharedDirector] winSize];
pos.y = screenSize.height + coin1.coinSize.height;
coin1.position = pos;
}
else {
NSLog(@"sender is not a CCSprite");
}
}
接下来,主要来看一下GameScene中对主角和炸弹以及金币的碰撞测试部分。首先要获取主角、炸弹以及金币的尺寸,这里采用简单的径向测试,因此仅取图片宽度。然后通过图片宽度,计算出主角和炸弹、主角和金币的最大碰撞距离。如果检测两者间的距离小于最大碰撞距离,即认为发生了碰撞。与炸弹发生碰撞后,生命值减少1,播放炸弹爆炸的音效并呈现爆炸效果;与金币发生碰撞后,则播放吃金币的音效并呈现得分+1的效果。在碰撞后均需要将碰撞标志位(isCollision和isGet)设为已碰撞。具体如下:
//碰撞测试
-(void)checkForCollision {
//获取player和bomb、coin的半径
float playerImageSize = [player texture].contentSize.width;
myBomb *tmpBomb = [bombs lastObject];
float bombImageSize = tmpBomb.bombSize.width;
myCoin *tmpCoin = [coins lastObject];
float coinImageSize = tmpCoin.coinSize.width;
float playerImageRadius = playerImageSize * 0.4f;
float bombImageRadius = bombImageSize * 0.4f;
float coinImageRadius = coinImageSize * 0.4f;
float maxCollisionDistance = playerImageRadius + bombImageRadius; //这里的最大碰撞距离和图片形状基本一致
float maxCoinGetDistance = playerImageRadius + coinImageRadius; //这里获取coin和bomber的最大碰撞距离
int numBombs = [bombs count];
int numCoins = [coins count];
for (int i = 0; i < numBombs; i++) {
myBomb *bomb1 = [bombs objectAtIndex:i];
if ([bomb1 numberOfRunningActions] == 0) { //如果此bomb未移动,我们就跳过对其的碰撞测试
continue;
}
float actualDistance = ccpDistance(player.position, bomb1.position); //得到bomb和bomber间的距离
if (actualDistance < maxCollisionDistance && bomb1.isCollision == NO) { //检查是否已碰撞,因为重置这个bomb的位置在延迟0.1s后的函数里进行,而这段时间里碰撞测试函数是一直在执行的,为防止对同一个bomb重复检测碰撞,增加了isCollision的标志位
[bomb1 stopAllActions]; //先停止碰撞的bomb的向下掉落动作
[bomb1 explosionEffect]; //让该bomb呈现爆炸的效果
[[SimpleAudioEngine sharedEngine] playEffect:@"bomb3.wav"];
if (life > 0) {
life--;
[lifeLabel setString:[NSString stringWithFormat:@"LIVES:%i",life]];
}
//NSLog(@"%i",life);
[self schedule:@selector(collisionReset) interval:0.1f]; //延迟执行碰撞后的重置操作
}
//NSLog(@"%@",bomb1.isCollision?@"yes":@"no");
}
for (int i = 0; i < numCoins; i++) {
myCoin *coin1 = [coins objectAtIndex:i];
if ([coin1 numberOfRunningActions] == 0) { //如果此coin未移动,我们就跳过对其的碰撞测试
continue;
}
float actualCoinBomberDistance = ccpDistance(player.position, coin1.position); //得到coin和bomber间的距离
if (actualCoinBomberDistance < maxCoinGetDistance && coin1.isGet == NO) { //检查是否已碰撞,因为重置这个coin的位置在延迟0.5s后的函数里进行,而这段时间里碰撞测试函数是一直在执行的,为防止对同一个coin重复检测碰撞,增加了isGet的标志位
[coin1 stopAllActions]; //先停止碰撞的coin的向下掉落动作
[coin1 getEffect]; //让该coin呈现获取的效果
[[SimpleAudioEngine sharedEngine] playEffect:@"coinsound.mp3"];
score++;
[scoreLabel setString:[NSString stringWithFormat:@"SCORE:%i",score]];
//NSLog(@"%i",life);
[self schedule:@selector(getCoinReset) interval:0.5f]; //延迟执行碰撞后的重置操作
}
//NSLog(@"%@",coin1.isGet?@"yes":@"no");
}
}
发生碰撞后,需要对炸弹和金币进行设置,包括位置重置、动作效果重置以及碰撞标志位的设置,都在相应的设置函数里进行(collisionReset和getCoinReset)。collisionReset主要用于将已碰撞的炸弹放回屏幕顶部,移去爆炸效果,重置已碰撞标志位,并在生命值为0时结束游戏跳转到得分呈现和重新开始界面;getCoinReset主要用于将已吃到的金币+1效果移去,增加得分,重置金币位置以及已碰撞标志。具体如下:
-(void)collisionReset {
//重置bomb位置
for (int i = 0; i < [bombs count]; i++) {
myBomb *bomb1 = [bombs objectAtIndex:i];
//检测该bomb对象是否没有执行任何动作,如果是则让其回到屏幕顶部以外
if ([bomb1 numberOfRunningActions] == 0) {
CGPoint pos = bomb1.position;
CGSize screenSize = [[CCDirector sharedDirector] winSize];
pos.y = screenSize.height + bomb1.bombSize.height;
bomb1.position = pos;
[bomb1 removeExplosion];
}
}
if (life <= 0) {
[[SimpleAudioEngine sharedEngine] stopBackgroundMusic];
[[CCDirector sharedDirector] replaceScene:[CCTransitionFadeTR transitionWithDuration:1.0f scene:[LoadingScene sceneWithTargetScene:TargetSceneSecondScene]]]; //调用载入页面
}
[self unschedule:_cmd];
}
-(void)getCoinReset { //获取金币后重置coin
for (int i = 0; i < [coins count]; i++) {
myCoin *coin1 = [coins objectAtIndex:i];
//检测该coin对象是否没有执行任何动作,如果是则让其回到屏幕顶部以外
if ([coin1 numberOfRunningActions] == 0) {
CGPoint pos = coin1.position;
CGSize screenSize = [[CCDirector sharedDirector] winSize];
pos.y = screenSize.height + coin1.coinSize.height;
coin1.position = pos;
[coin1 removeEffect];
}
}
[self unschedule:_cmd];
}
在GameScene的最后,实现dealloc,将非自动释放的对象释放:
-(void)dealloc {
NSLog(@"%@,%@",NSStringFromSelector(_cmd),self);
[bombs release];
bombs = nil;
[coins release];
coins = nil;
sharedAllInMe = nil;
[super dealloc];
}
在GameScene.m中用到的一些函数和变量需要在GameScene.h中声明,GameScene.h具体如下:
#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "SimpleAudioEngine.h"
#import "LoadingScene.h"
#import "Helper.h"
#import "myBomb.h"
#import "myCoin.h"
@interface GameScene : CCLayer {
CCSprite *player;
CGPoint playerVelocity;
CCArray *bombs;
float bombMoveDuration;
CCArray *coins;
float coinMoveDuration;
CCLabelTTF *scoreLabel;
float totalTime;
int score;
CCLabelTTF *lifeLabel;
int life;
float dropSpeed;
}
@property (assign,readonly) int score;
+(id)scene;
+(GameScene *)getShared; //生成一个当前层的静态访问方法
-(void)onCallFunc; //背景图移动到头后将其重置
-(void)initBombs;
-(void)resetBombs;
-(void)bombUpdates:(ccTime)delta;
-(void)runBombMoveSequence:(myBomb *)bomb;
-(void)onCallFuncN:(id)sender;
-(void)initCoins;
-(void)resetCoins;
-(void)coinUpdates:(ccTime)delta;
-(void)runCoinMoveSequence:(myCoin *)coin;
-(void)coinCallFuncN:(id)sender;
-(void)checkForCollision;
-(void)collisionReset;
-(void)getCoinReset;
-(void)speedTouched;
@end
到这里游戏的主场景GameScene就基本上完成了。接下来,需要实现在GameScene中用到的几个类:myBomb、myCoin、Helper以及两个场景LoadingScene和EndScene。
2. myBomb类
myBomb类中主要实现了炸弹的初始化、爆炸动作以及移除爆炸动作。爆炸动作采用了cocos2d的粒子效果来实现,关于粒子效果网上有很多资料说明,另外也有第三方提供的粒子效果生成器可直接用,这里就不展开了。炸弹的初始化也用到了我们对CCAnimation的扩展方法来播放炸弹在飞行下落过程中的动作。具体实现如下:
-(id)init {
if (self == [super init]) {
bomb = [CCSprite spriteWithFile:@"bomb0.png"]; //用bomber图片初始化player精灵
bombSize = [bomb texture].contentSize;
isCollision = NO;
[self addChild:bomb z:0 tag:1]; //将player精灵添加到场景中
bomb.position = CGPointMake(0,0); //初始化player的位置
CCAnimation *anim = [CCAnimation animationWithFile:@"bomb" frameCount:2 delay:0.5f]; //运用给CCAnimation扩展的方法来生成一个动画对象anim
CCAnimate *animate = [CCAnimate actionWithAnimation:anim];
CCRepeatForever *repeate = [CCRepeatForever actionWithAction:animate]; //生成一个重复动作的repeate对象
[bomb runAction:repeate]; //让player运行这个重复动作
}
return self;
}
-(void)explosionEffect {
CCParticleSystem *system;
system = [CCParticleSun node];
[bomb addChild:system z:0 tag:1];
system.position = ccp(bombSize.width/2,bombSize.height/2);
system.startSize = 38.0f;
system.startSizeVar = 80.0f;
system.endSize = 88.0f;
system.endSizeVar = 80.0f;
isCollision = YES;
}
-(void)removeExplosion {
isCollision = NO;
[bomb removeAllChildrenWithCleanup:YES];
}
-(void)dealloc {
[super dealloc];
}
3. myCoin类
myCoin类与myBomb类相似,不同的是没有采用粒子效果,而是在碰撞时播放一个+1的渐隐动画:
-(id)init {
if (self == [super init]) {
coin = [CCSprite spriteWithFile:@"coin0.png"]; //用coin0图片初始化coin精灵
coinSize = [coin texture].contentSize;
isGet = NO;
[self addChild:coin z:0 tag:1]; //将coin精灵添加到myCoin中
coin.position = CGPointMake(0, 0); //初始化coin的位置
CCAnimation *anim = [CCAnimation animationWithFile:@"coin" frameCount:2 delay:0.5f]; //运用给CCAnimation扩展的方法来生成一个动画对象anim
CCAnimate *animate = [CCAnimate actionWithAnimation:anim];
CCRepeatForever *repeate = [CCRepeatForever actionWithAction:animate]; //生成一个重复动作的repeate对象
[coin runAction:repeate]; //让coin运行这个重复动作
}
return self;
}
-(void)getEffect { //碰撞金币后的效果
[coin stopAllActions]; //先暂停coin本身的所有动作
CCTexture2D * texture =[[CCTextureCache sharedTextureCache] addImage:@"plusone.png"]; //新建一个贴图对象
[coin setTexture:texture]; //将coin的贴图更换为texture贴图对象
CCFadeOut *getAnim = [CCFadeOut actionWithDuration:0.5f]; //+1图片渐隐
[coin runAction:getAnim]; //coin运行+1渐隐的动作
//NSLog(@"%@",coin);
isGet = YES; //将该coin的已获取标志位置为yes
}
-(void)removeEffect { //+1动作完成后对该coin的重置操作
[coin stopAllActions]; //先暂停当前coin执行的所有动作
coin.opacity = 255; //将透明度重置为完全不透明
CCTexture2D * texture =[[CCTextureCache sharedTextureCache] addImage:@"coin0.png"]; //新建一个贴图对象
[coin setTexture:texture];
CCAnimation *initAnim = [CCAnimation animationWithFile:@"coin" frameCount:2 delay:0.5f]; //运用给CCAnimation扩展的方法来生成一个动画对象anim
CCAnimate *initAnimate = [CCAnimate actionWithAnimation:initAnim];
CCRepeatForever *initRepeate = [CCRepeatForever actionWithAction:initAnimate]; //生成一个重复动作的repeate对象
[coin runAction:initRepeate]; //coin运行这个重复动作
isGet = NO;
}
-(void)dealloc {
[super dealloc];
}
4. Helper类
对系统CCAnimation扩展的方法在Helper里实现,cocos2d允许采用类别功能(category)对其系统提供的类进行方法扩展,但仅能添加方法,不能添加类的成员变量。首先在Helper.h里进行类别扩展的声明:
@interface CCAnimation(Helper)
+(CCAnimation *)animationWithFile:(NSString *)name frameCount:(int)frameCount delay:(float)delay; //给CCAnimation类扩展的一个方法,通过动画名称,帧数和每帧延迟生成一个动画对象
@end
在Helper.m里对扩展的类别方法的实现:
@implementation CCAnimation(Helper)
//使用单个文件生成动画
+(CCAnimation *)animationWithFile:(NSString *)name frameCount:(int)frameCount delay:(float)delay {
//把动画帧作为贴图进行加载,然后生成精灵动画帧
NSMutableArray *frames = [NSMutableArray arrayWithCapacity:frameCount];
for (int i = 0; i < frameCount; i++) {
//假设所有动画帧的名字均为“名字+数字.png”的格式
NSString *file = [NSString stringWithFormat:@"%@%i.png",name,i];
CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage:file];
//假设总是使用整个动画帧文件
CGSize textureSize = texture.contentSize;
CGRect textureRect = CGRectMake(0, 0, textureSize.width, textureSize.height);
CCSpriteFrame *frame = [CCSpriteFrame frameWithTexture:texture rect:textureRect];
[frames addObject:frame];
}
//使用所有的精灵动画帧,返回一个动画对象
return [CCAnimation animationWithFrames:frames delay:delay];
}
@end
这样我们就可以通过单张的图片来直接形成myBomb和myCoin的精灵动画了。
5. LoadingScene类
由于在场景之间切换会需要加载资源,消耗时间,因此设置一个载入界面无疑可以减弱加载过程对用户体验的影响。这里的载入界面,我们通过LoadingScene类来实现,通过维护这一个界面,即可实现向各个界面的跳转。首先我们在LoadingScene.h中声明一个枚举类型TargetScenes来列举我们需要涉及载入的界面,同时我们在此保存需要在各个界面间传递的变量,如score。另外,声明一个静态方法sceneWithTargetScene:以供其他需载入的场景调用。具体声明如下:
#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "GameScene.h"
#import "EndScene.h"
typedef enum {
TargetSceneINVALID = 0,
TargetSceneFirstScene, //有任何新的需要载入的场景只需要在此增加新的场景枚举类型就可,如TargetSceneSecondScene。。。
TargetSceneSecondScene,
TargetSceneMAX,
} TargetScenes;
@interface LoadingScene : CCScene {
TargetScenes targetScene_;
int endScore;
}
+(id)sceneWithTargetScene:(TargetScenes)targetScene;
-(id)initWithTargetScene:(TargetScenes)targetScene;
-(void)loadingUpdate:(ccTime)delta;
@end
在LoadingScene.m中,实现sceneWithTargetScene:方法以及LoadingScene的初始化方法。initWithTargetScene:方法中通过在GameScene中设定的静态方法获取到当前的GameScene对象。之后通过targetScene_进行条件判断需载入的目标场景,详细如下:
+(id)sceneWithTargetScene:(TargetScenes)targetScene { //此处的targetScene参数即为想要加载进来的下一个场景
return [[[self alloc] initWithTargetScene:targetScene] autorelease]; //生成一个当前类的自动释放对象,self即是LoadingScene
}
-(id)initWithTargetScene:(TargetScenes)targetScene {
if (self == [super init]) {
targetScene_ = targetScene; //将要加载的目标场景给到全局的枚举变量中,以在update函数中判断并加载对应的场景
GameScene *tmpScene = [GameScene getShared];
endScore = tmpScene.score;
//生成并添加一个载入的文本标签
CCLabelTTF *label = [CCLabelTTF labelWithString:@"Loading..." fontName:@"Marker Felt" fontSize:38];
CGSize size = [[CCDirector sharedDirector] winSize];
label.position = CGPointMake(size.width * 0.5f, size.height * 0.5f);
[self addChild:label];
[self schedule:@selector(loadingUpdate:) interval:2.3f]; //必须在下一帧加载目标场景
}
return self;
}
-(void)loadingUpdate:(ccTime)delta {
[self unscheduleAllSelectors];
switch (targetScene_) { //通过targetScene_这个枚举类型决定加载哪个scene
case TargetSceneFirstScene:
[[CCDirector sharedDirector] replaceScene:[GameScene scene]];
break;
case TargetSceneSecondScene:
[[CCDirector sharedDirector] replaceScene:[EndScene endScene:endScore]];
break;
default:
NSAssert2(nil,@"%@:unsupported TargetScene %i",NSStringFromSelector(_cmd),targetScene_); //使用未指定的枚举类型时发出的警告信息
break;
}
}
这样,我们就可以在任意场景中通过CCDirector的replaceScene方法先切换到LoadingScene,再由LoadingScene过渡到目标场景了。
6. EndScene类
这个层需要呈现的内容并不多,我们只是简单在这里呈现用户的得分,并提供用户一个重新开始的按钮。这里的初始化方法需要带入一个用户得分的参数,由LoadingScene类传递过来:
+(id)endScene:(int)myScore {
return [[[self alloc] initScene:myScore] autorelease];
}
-(id)initScene:(int)myScore {
if (self == [super init]) {
//生成一个分数标签
CCLabelTTF *label = [CCLabelTTF labelWithString:[NSString stringWithFormat:@"Your Score is: %i",myScore] fontName:@"Marker Felt" fontSize:38];
CGSize size = [[CCDirector sharedDirector] winSize];
label.position = CGPointMake(size.width * 0.5f, size.height * 0.5f);
label.anchorPoint = ccp(0.5f,0);
[self addChild:label];
//生成一个重新开始的按钮
[CCMenuItemFont setFontName:@"STHeitiJ-Light"];
[CCMenuItemFont setFontSize:26];
CCMenuItemFont *item1 = [CCMenuItemFont itemFromString:@"Retry!" target:self selector:@selector(redirectToGameScene)];
item1.color = ccGREEN;
CCMenu *menu = [CCMenu menuWithItems:item1,nil];
menu.position = CGPointMake(size.width * 0.5f, size.height * 0.5f - 88.0f);
menu.anchorPoint = ccp(0.5f,1.0f);
[self addChild:menu];
}
return self;
}
-(void)redirectToGameScene {
[[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:1.0f scene:[LoadingScene sceneWithTargetScene:TargetSceneFirstScene]]]; //调用载入页面
}
到这里,整个工程就基本上完成了,剩下的就是添加需要的资源文件了,比如:主角、炸弹、金币等游戏元素在各种状态下的图片,游戏中用到的音效等等。这部分就不再罗嗦了,当然是否能设计出引人入胜的图片和音效是游戏成败的关键,我一直这么觉得~真正的独立游戏开发者应该是一个全才(像韩国开发亡灵杀手的那位大神),继续努力~~~加油!