前一阵子很火的一款手游flappybird,不知道大家有没有玩到“根本停不下来”的程度,不过刚刚学会了cocos2d和obj-c的我准备拿这款游戏练练手,熟练一下自己对cocos2d的使用。
首先我们当然还是要秉承一贯的风格,自己做美工。当然,为了尊重原作,我们在网上搜了一些游戏截图,参照着绘制完成。当然我也简单的学习了一下像素画的绘制方法和技巧,有兴趣的朋友可以看一下我的另一篇博文中的介绍。
开发用的环境是Xcode 5.1.1,语言是object-c。
好了,闲话不多说,先来图片素材:
bird-01-04.png(每个都是68×48):
bg-buildings.png(640×98):
bg-trees.png(640×72):
bg-cloud.png(640×205):
bg-bottom.png(32×128):
tube-body.png(105×700):
tube-lower.png(115×55):
tube-upper.png(115×55):
由于上传图片大小限制,我们在后面标注上实际尺寸,如果图片大小有不一致的,以后边括号中的尺寸为准。
接着我们就准备开始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;
完成后,得到效果如下:
接着我们为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];
完成后我们便以运行一下,发现已经能够看到初始化好的管子了:
接着我们在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:¶ms];
[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:¶ms];
[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。