手把手教你制作那个风靡的flappy bird小游戏(一)

103 篇文章 0 订阅
53 篇文章 0 订阅

前一阵子很火的一款手游flappybird,不知道大家有没有玩到“根本停不下来”的程度,不过刚刚学会了cocos2d和obj-c的我准备拿这款游戏练练手,熟练一下自己对cocos2d的使用。

首先我们当然还是要秉承一贯的风格,自己做美工。当然,为了尊重原作,我们在网上搜了一些游戏截图,参照着绘制完成。当然我也简单的学习了一下像素画的绘制方法和技巧,有兴趣的朋友可以看一下我的另一篇博文中的介绍。

开发用的环境是Xcode 5.1.1,语言是object-c。

好了,闲话不多说,先来图片素材:

bird-01-04.png(每个都是68×48):

2014年06月12日 - 远行的风 - 风的驿站 2014年06月12日 - 远行的风 - 风的驿站 2014年06月12日 - 远行的风 - 风的驿站 2014年06月12日 - 远行的风 - 风的驿站

bg-buildings.png(640×98):

2014年06月12日 - 远行的风 - 风的驿站

bg-trees.png(640×72):

2014年06月12日 - 远行的风 - 风的驿站

bg-cloud.png(640×205):

2014年06月12日 - 远行的风 - 风的驿站

bg-bottom.png(32×128):

2014年06月12日 - 远行的风 - 风的驿站

tube-body.png(105×700):

2014年06月12日 - 远行的风 - 风的驿站

tube-lower.png(115×55):

2014年06月12日 - 远行的风 - 风的驿站

tube-upper.png(115×55):

2014年06月12日 - 远行的风 - 风的驿站

由于上传图片大小限制,我们在后面标注上实际尺寸,如果图片大小有不一致的,以后边括号中的尺寸为准。

 

接着我们就准备开始coding了。

在动键盘之前,我们依然是先划分工程结构。程序大致上由几个主要元素构成(我们不详细制作开始界面和GameOver的界面),首先是那只flappy的鸟,以及鸟需要穿过的水管,还有背景,分数等,当然音乐神马的就不算了。好了,这些确定好之后,我们先把大体的类创建出来。额,好吧,我们还没有创建工程,由于这里不打算用Box2D中的物理引擎来做模拟,所以我们创建cocos2d2.0的工程就可以了,Device类型选择iphone,我们想要最终的程序发布为手机版本。

创建好工程后,我们只保留AppDelegate.h和AppDelegate.m,删掉其余的.h和.m文件。然后我们创建一个GameLayer类,继承自CCLayer,作为我们的游戏场景。

接着我们创建Bird类,Tube类和ParallaxBackground类,这三个类均派生自CCNode。这样我们程序的基本骨架就大致搭建完成了。

接着我们为GameLayer添加一个静态方法:

+ (id)scene;

方法定义:

+ (id)scene{

    CCScene*scene = [CCScene node];

    GameLayer*gameLayer = [GameLayer node];

    [sceneaddChild:gameLayer];

    

    returnscene;

}

接着我们修改AppDelegate类中下面这个方法:

-(void)directorDidReshapeProjection:(CCDirector*)director

{

      if(director.runningScene== nil) {

           [directorrunWithScene: [GameLayer scene]];

      }

}

编译通过。

 

接着我们来制作滚动背景。对于滚动背景,思路是对于每一个滚动元素,使用两张相同的元素图片交替在屏幕中滚动,当一张离开屏幕后立即被放置到另一张的右侧(屏幕外)继续滚动。

我们在ParallaxBackground类中添加如下方法:

- (void)initStaticBackground{

    CCLayerColor*bgcolor = [CCLayerColor layerWithColor:ccc4(113, 197, 207, 255)];

    [selfaddChild:bgcolor z:-1];

    

    screenWidth= [[CCDirector sharedDirector] winSize].width;

    

    CCSprite*cloud = [CCSprite spriteWithFile:@"bg-cloud.png"];

    cloud.anchorPoint= CGPointZero;

    cloud.position= ccp(0, 50);

    [selfaddChild:cloud z:3 tag:BG_CLOUD];

    

    CCSprite*cloud1 = [CCSprite spriteWithFile:@"bg-cloud.png"];

    cloud1.anchorPoint= CGPointZero;

    cloud1.position= ccp(screenWidth-1, 50);

    [selfaddChild:cloud1 z:3 tag:BG_CLOUD1];

    

    CCSprite*buildings = [CCSprite spriteWithFile:@"bg-buildings.png"];

    buildings.anchorPoint= CGPointZero;

    buildings.position= ccp(0, 71);

    [selfaddChild:buildings z:4 tag:BG_BUILDINGS];

    

    CCSprite*buildings1 = [CCSprite spriteWithFile:@"bg-buildings.png"];

    buildings1.anchorPoint= CGPointZero;

    buildings1.position= ccp(screenWidth-1, 71);

    [selfaddChild:buildings1 z:4 tag:BG_BUILDINGS1];

    

    CCSprite*trees = [CCSprite spriteWithFile:@"bg-trees.png"];

    trees.anchorPoint= CGPointZero;

    trees.position= ccp(0, 50);

    [selfaddChild:trees z:5 tag:BG_TREES];

    

    CCSprite*trees1 = [CCSprite spriteWithFile:@"bg-trees.png"];

    trees1.anchorPoint= CGPointZero;

    trees1.position= ccp(screenWidth-1, 50);

    [selfaddChild:trees1 z:5 tag:BG_TREES1];

}

screenWidth是在类中定义的一个成员,用来存储屏幕宽度,提高效率。我们为背景添加了一个CCLayerColor作为其背景色,接着我们将背景的树,房子和云的精灵对象都放置到场景中,每一个元素都对应了两个精灵,一个在屏幕中,一个在屏幕外,形成循环滚动的效果。

注意我们这里没有将bg-bottom.png,也就是画面最下方的地面元素添加到滚动背景中,我们是这样考虑的:在小鸟向前飞行的时候,场景中的管子和地面是同速向画面左侧移动的,背景也是向左侧滚动的,我们现在将背景抽象成了单独的类,这样这个类有自己独立的update循环,而我们的GameLayer也有自己独立的update循环,两个循环是不能同步的,所以为了保证管子和地面是同速移动,我们必须将他们放在一起,也就是都放到GameLayer中(因为管子在GameLayer中,要做碰撞判断)。另外还有一个原因,就是地面要将下边的管子长处来的部分遮住,这样管子就要在地面和背景之间,所以如果将地面也放到ParallaxBackground中,就无法做到这一点了。

接着,我们在ParallaxBackground的初始化方法里添加上initStaticBackground的调用,然后在GameLayer的初始化方法中添加下面的代码:

ParallaxBackground* background =[ParallaxBackground node];

[self addChild:background z:1];

 

注:上面代码中用到的枚举结构:

typedef enum{

    BG_BUILDINGS,

    BG_TREES,

    BG_CLOUD,

    BG_BUILDINGS1,

    BG_TREES1,

    BG_CLOUD1

} BG_ELEMENTES;

 

完成后,得到效果如下:

2014年06月12日 - 远行的风 - 风的驿站

 

接着我们为ParallaxBackground添加下面的方法:

- (void)moveElement:(CCNode*)nodemovedLength:(float) length{

    CGPointcurPos = node.position;

    if(curPos.x - length < -screenWidth) {

        [nodesetPosition:ccp(curPos.x - length + screenWidth * 2 - 2, curPos.y)];

    }else{

        [nodesetPosition:ccp(curPos.x - length, curPos.y)];

    }

}

这个方法用来移动背景元素,在移动的时候会进行判断,如果元素移出了背景,就将其放置到右侧屏幕外以备下一次滚动。

然后实现update方法:

- (void)update:(ccTime)delta{

    floattreeMove = screenWidth/20*delta;

    floatbuildingMove = treeMove/2;

    floatcloudMove = treeMove/3;

    [selfmoveElement:[self getChildByTag:BG_TREES] movedLength:treeMove];

    [selfmoveElement:[self getChildByTag:BG_TREES1] movedLength:treeMove];

    [selfmoveElement:[self getChildByTag:BG_BUILDINGS] movedLength:buildingMove];

    [selfmoveElement:[self getChildByTag:BG_BUILDINGS1] movedLength:buildingMove];

    [selfmoveElement:[self getChildByTag:BG_CLOUD] movedLength:cloudMove];

    [selfmoveElement:[self getChildByTag:BG_CLOUD1] movedLength:cloudMove];

}

update方法中,不同的元素以不同的速率进行移动,移动范围根据帧的时间(delta)而计算,这样保证匀速移动。然后我们在init方法中调用scheduleUpdate方法。

现在运行我们就能够看到背景层的滚动效果了。

 

我们继续来制作管子和地面。flappybird中,鸟每次要飞过一组管子,一组管子由两个管子组成,上下相对,管口距离为定值。所以我们定义的时候,一个tube对象也是一组管子,管口之间的空隙的位置在一个区间内随机生成,我们在Tube类中加入下面的成员:

CCSprite* tubeBodyUpper;

CCSprite* tubeCapUpper;

CCSprite* tubeBodyLower;

CCSprite* tubeCapLower;

float screenWidth;

float scale;

上面四个对象分别对应上下两个管子的管口和管身,最后一个scale用来调整retina屏和非retina屏的比例关系。

接着我们添加初始化方法,这里我们定义下面的初始化方法:

- (id)initWithPosition:(float)xPosition{

    if (self =[super init]){

        screenWidth= [[CCDirector sharedDirector] winSize].width;

        srand(time(nil));

        tubeCapUpper= [CCSprite spriteWithFile:@"tube-upper.png"];

        tubeCapUpper.anchorPoint= CGPointZero;

        tubeCapLower= [CCSprite spriteWithFile:@"tube-lower.png"];

        tubeCapLower.anchorPoint= CGPointZero;

        scale= [tubeCapUpper contentSize].width / 115;

        tubeBodyUpper= [CCSprite spriteWithFile:@"tube-body.png"];

        tubeBodyUpper.anchorPoint= CGPointZero;

        tubeBodyLower= [CCSprite spriteWithFile:@"tube-body.png"];

        tubeBodyLower.anchorPoint= ccp(0, 0.3f);

        [selfreset:xPosition];

        [selfaddChild:tubeBodyUpper];

        [selfaddChild:tubeBodyLower];

        [selfaddChild:tubeCapUpper];

        [selfaddChild:tubeCapLower];

    }

    

    returnself;

}

初始化的时候需要传入一个x位置值,用来设置管子的初始位置。

reset方法定义如下:

- (void)reset:(float) x{

    float y =-CCRANDOM_0_1() * 420 * scale + 100 * scale;

    tubeBodyLower.position= ccp(x, y);

    tubeCapLower.position= ccp(x - 2, y + 460 * scale);

    tubeCapUpper.position= ccp(x - 2, y + 735 * scale);

    tubeBodyUpper.position= ccp(x, y + 790 * scale);

}

用来设置每个管子的位置。

我们在GameLayer中添加下面的成员:

float scale;

Tube* tube1;

Tube* tube2;

Tube* tube3;

然后在初始化方法中添加下面的语句:

scale = [[CCSpritespriteWithFile:@"bg-bottom.png"] contentSize].height / 128;

int startPos = 100 * scale;

tube1 = [[[Tube alloc]initWithPosition:startPos] autorelease];

tube2 = [[[Tube alloc]initWithPosition:startPos + scale * 355] autorelease];

tube3 = [[[Tube alloc]initWithPosition:startPos + scale * 710] autorelease];

[self addChild:tube1 z:2];

[self addChild:tube2 z:2];

[self addChild:tube3 z:2];

完成后我们便以运行一下,发现已经能够看到初始化好的管子了:

2014年06月12日 - 远行的风 - 风的驿站

 

接着我们在GameLayer的初始化方法中将地面对象添加上:

CGRect repeatRect = CGRectMake(0,0, screenWidth, scale * 128);

ccTexParams params = {

    GL_LINEAR,

    GL_LINEAR,

    GL_REPEAT,

    GL_REPEAT

};

 

CCSprite* bottom = [CCSpritespriteWithFile:@"bg-bottom.png" rect:repeatRect];

bottom.anchorPoint = CGPointZero;

bottom.position = CGPointZero;

[bottom.texturesetTexParameters:&params];

[self addChild:bottom z:5tag:BG_BOTTOM];

 

CCSprite* bottom1 = [CCSpritespriteWithFile:@"bg-bottom.png" rect:repeatRect];

bottom1.anchorPoint = CGPointZero;

bottom1.position =ccp(screenWidth-1, 0);

[bottom1.texturesetTexParameters:&params];

[self addChild:bottom1 z:5tag:BG_BOTTOM1];

这里用到了重复纹理,通过不断重复当前的地面纹理来铺满整个地面区域。

用到的枚举定义如下:

typedef enum{

    BG_BOTTOM,

    BG_BOTTOM1,

    BIRD_TAG,

    ScoreLabelTag,

    MsgLabelTag,

    HighScoreLabelTag

} GAMESCENE_OBJECTS;

完成后,地面已经制作完成了。

然后我们为Tube类添加下面两个方法:

- (void)move:(float) length{

    [selfmoveTubeElement:length tubeElement:tubeBodyUpper];

    [selfmoveTubeElement:length tubeElement:tubeCapLower];

    [selfmoveTubeElement:length tubeElement:tubeCapUpper];

    [selfmoveTubeElement:length tubeElement:tubeBodyLower];

}

 

- (void)moveTubeElement:(float)length tubeElement:(CCSprite*) tubeElement{

    floatcapWidth = [tubeCapUpper contentSize].width;

    CGPointposition = tubeElement.position;

    position.x= position.x - length;

    if(position.x < -capWidth) {

        position.x+= 1065 * scale;

    }

    tubeElement.position= position;

}

其中move方法用来同步移动一组管子的4个元素,moveTubeElement方法用来移动管子的管口或者管身,当这个元素消失在屏幕外的时候,将这个元素向右移动三个管子的距离(1065px),这样我们就重复利用3个管子来模拟无穷无尽的管子了。

接着我们在GameLayer中添加方法:

- (void)moveElement:(CCNode*)nodemovedLength:(float) length{

    CGPointcurPos = node.position;

    if(curPos.x - length < -screenWidth) {

        [nodesetPosition:ccp(curPos.x - length + screenWidth * 2 - 2, curPos.y)];

    }else{

        [nodesetPosition:ccp(curPos.x - length, curPos.y)];

    }

}

用这个方法来移动地面元素(和ParallaxBackground方法中的相同,不多解释了)。

然后在GameLayer的update方法中添加:

float bottomMove =screenWidth/4*delta;

[self moveElement:[selfgetChildByTag:BG_BOTTOM] movedLength:bottomMove];

[self moveElement:[selfgetChildByTag:BG_BOTTOM1] movedLength:bottomMove];

    

[tube1 move:bottomMove];

[tube2 move:bottomMove];

[tube3 move:bottomMove];

然后在初始化方法中调用scheduleUpdate方法。

现在编译运行一下,可以看到管子和地面的运动了。

 

好了,第一部分先到这里,在下一篇中我们来制作完成flappybird。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值