最近,一只8比特位像素的小鸟霸占了IOS免费游戏排行榜的第一名,这款《Flappy Bird》游戏可谓是一夜爆红,简单并不粗糙的画面、超级玛丽游戏中的绿色通道、眼神有些呆滞的小鸟和几朵白云便构成了游戏的一切。不过可惜的是,如此受欢迎的笨鸟又在一夜之间火速下架了,让很多还没来得及被《Flappy Bird》虐过的玩家千方百计下载《Flappy Bird》,不过,就算如此也没关系,一只囧鸟倒下了,立马会有千千万万只高仿真山寨货崛起,下面,就和大家分享一款类似《Flappy Bird》的游戏Demo。
源代码下载地址:
效果图:

1 项目介绍
为了使项目的代码结构清晰,好的前期规划是很有必要的,下图是该游戏工程的主要类结构。先从整体看一下,项目的组织结构,然后会对其中内部实现做些必要的解说。

游戏共有3个场景,主菜单场景、游戏场景、结束场景,其中主菜单场景和结束场景都很简单,下面的介绍中将一笔带过,我们的重点会放到游戏场景。下图是该游戏的逻辑场层关系图:

2 前期准备
2.1 分辨率适配
为了适应移动终端的各种分辨率大小和屏幕宽高比,一款游戏能够很好的支持多屏幕多分辨率是必须的。打开AppDelegate.cpp文件添加如下代码:以方便我们在设计游戏时,能够更好的适应不同的运行环境。
1 2 3 4 5 6 7 | <code>
pEGLView->setDesignResolutionSize(320, 480, kResolutionFixedHeight);
std::vector<std::string> searchPath;
searchPath.push_back( "h960" );
pDirector->setContentScaleFactor(960 / 480);
CCFileUtils::sharedFileUtils()->setSearchPaths(searchPath);</code>
|
setDesignResolutionSize()设计分辨率大小及模式,setContentScaleFactor()内容缩放因子,setSearchPaths()设置资源搜索路径。如果不是很了解分辨率的适配,推荐阅读Cocos2d-x 多分辨率适配完全解析。
2.2 Tiled Map的应用
在该游戏中的地图利用瓦片地图编辑器(Tiled Map Editor)编辑制作,它可保存为TMX格式的文件,可以被Cocos2d-x很好的支持。瓦片地图(Tile Map)不但生成简单,并且可以灵活的用于引擎中。不论你的游戏是角色扮演游戏, 平台动作游戏或仿打砖块游戏,这些游戏地图都可以使用开源的瓦片地图编辑器Tiled Map Editor生成。在像《Flappy Bird》之类的游戏中,地图运用Tiled Map在合适不过了。【cocos2d-x官方文档】瓦片地图 Tiled Map
将tmx文件加载到游戏中需要用CCTMXTiledMap类,方法很简单,这部分我们将在介绍游戏背景层的时候做详细的讲解。
3 主菜单场景
打开AppDelegate.cpp文件,在applicationDidFinishLaunching()函数中设置第一个启动的游戏场景为主菜单场景:
1 2 | <code>CCScene *pScene = MenuLayer::scene();
pDirector->runWithScene(pScene);</code>
|
在场景中添加背景图片和按钮,截图如下:

4 游戏场景
游戏的主场景中包括了物理世界层(PhysicWorldLayer)、暂停层(PauseLayer)、分数层(ScoreLayer)和结束场景层(GameOverLayer),在物理世界层中又加入了游戏背景层(BackgroundLayer)。其中物理世界层(PhysicWorldLayer)是整个游戏的主要逻辑层,也是游戏的灵魂。其余几个层都非常简单,这里不做详细的讲解。
4.1 物理世界层
4.1.1 chipmunk
该游戏中采用chipmunk引擎来模拟物理世界的运行。
代表Chipmunk空间的对象是cpSpace,在这个空间里可容纳cpBody, cpShape,cpPolyShape等,它基本等同于Box2D里面的World。cpBody表示刚体,可在刚体上添加cpShape,刚体具有质量,转动惯量,位置,线性速度,加速度,角度,角速度,角加速度等属性;cpShape决定刚体的碰撞外形。
chipmunk使用的一般流程:
- 构建chipmunk的空间(cpSpace),在这个空间内,我们放置需要的刚体(cpBody),并且可以为刚体设置它的形状(cpShape),刚体和形状都有它们的属性。
- 更新空间的状态:在update函数中调用cpSpaceStep方法,计算空间内刚体的位置坐标,角度等等属性值,引擎会根据值重绘精灵。
我们可以通过下面的方式来创建一个cpSpace。切换到头文件(这里是:PhysicWorldLayer.h),做如下更改:
1 2 | <code>#include "chipmunk.h"
cpSpace *space;</code>
|
在这里,我们只需要引入chipmunk头文件,然后声明一个实例变量来记录Chipmunk空间,再在PhysicWorldLayer.cpp 文件中创建物理空间,并添加精灵和层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <code> void PhysicWorldLayer::createPhysicWorld()
{
cpInitChipmunk();
space = cpSpaceNew();
space->gravity = cpv(0, -300);
cocos2d::extension::CCPhysicsDebugNode* debugLayer = cocos2d::extension::CCPhysicsDebugNode::create(space);
addChild(debugLayer, 100);
wallBottom = cpSegmentShapeNew( this ->space->staticBody,
cpv(0, 50),
cpv(4294967295, 50),
0);
wallBottom->e = 0;
cpSpaceAddStaticShape( this ->space, wallBottom);
this ->wallBottom->collision_type = 3;
this ->wallBottom->sensor = false ;
cpShapeSetUserData(wallBottom, this );
CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile( "sprites.plist" );
spriteSheet = CCSpriteBatchNode::create( "sprites.png" );
addChild(spriteSheet, 100);
bird = Bird::create(space);
spriteSheet->addChild(bird,10);
backgroundLayer = BackgroundLayer::create(spriteSheet,space);
addChild(backgroundLayer);
}</code>
|
使用Chipmunk方法之前,第一件必须做的事情是调用cpInitChipmunk方法。然后,你可以调用cpSpaceNew()方法来创建一个新的Chipmunk虚拟空间,将其存放到实例变量space中。gravity设置Chipmunk空间内的重力,这里cpv()的参数分别是:x轴方向重力分量,y轴方向重力分量。
CCPhysicsDebugNode是为了让开发者方便调试而设计的一个类,将它设置为显示之后,在场景内定义的精灵的碰撞形状块就能显示出来了,如下图所示。当然,在游戏发布的时候应该删去。

cpSegmentShapeNew用来建立一个段状形状,它的第一个参数用的space的staticBody来创建一个static shape,作为关卡的物理环境。这个body不需要加入到space中,否则会受重力作用。但这个body的shape需要加入到space中,用作碰撞检测。这里cpSegmentShapeNew创建了一个长度很长的段状横线,用来作为物理空间中的地面。
cpSpaceAddStaticShape将这个shape作为静态刚体添加到space中去。shape的collision_type属性,为自定义的一个类型,这里把地面的collision_type设为3。
加载精灵纹理。CCSpriteFrameCache一般用来处理plist文件,该文件对应一张包含多个精灵的大图。CCSpriteBatchNode用于批处理绘制精灵,可以提高精灵的绘制效率。plist文件可以使用TexturePacker制作。如下图所示:

在物理世界层中加入笨鸟精灵。
添加背景层,背景层中包含了游戏地图和管道。
- 模拟结束后,在析构函数中要调用pSpaceFree进行内存释放,即:
cpSpaceFree(space);
4.1.2 Bird笨鸟类
现在来看Bird类,它继承于CCPhysicsSprite类,从名称上就可以看出来这个CCPhysicsSprite类是与物理引擎相关的类,它定义一个物理类绑定的精灵,并且继承至CCSprite精灵类。
显然地,在《Flappy Bird》的世界中,笨鸟也应是一个刚体。
这里可以通过使用cpBodyNew方法来生成代表笨鸟的动态(可移动的)刚体,这个方法需要两个参数:质量(mass)和惯性力矩(moment of inertia)。惯性力矩决定着刚体移动时遇到的阻力,它可以通过cpMomentForBox方法来获取,cpMomentForBox以刚体的质量和笨鸟的尺寸作为参数。如下代码所示:
1 2 3 4 5 6 7 8 | <code> void Bird::initBody()
{
CCSize size = CCDirector::sharedDirector()->getWinSize();
body = cpBodyNew(1.0f, cpMomentForBox(1.0f, this ->runningSize.width, this ->runningSize.height));
body->p = cpv(100, 50 + size.height / 2);
body->v = cpv(100, 0);
cpSpaceAddBody(space, body);
}</code>
|
body->p设置刚体重心的坐标;
body->v设置刚体重心的速度;
cpSpaceAddBody(space, body)将物体添加到空间内,这样物体才能受到空间重力的影响。
1 2 3 4 5 6 7 | <code> void Bird::initShape()
{
shape = cpBoxShapeNew(body, runningSize.width, runningSize.height);
shape->e = 0;
shape->u = 0;
cpSpaceAddShape(space, shape);
}</code>
|
cpBoxShapeNew给刚体定义一个盒子形状;
shape->e设置形状的弹性系数,也就是物体碰撞到这个形状的反弹力度;
shape->u设置形状的摩擦系数;cpSpaceAddShape(space, shape)将形状作为活动物体添加到空间里。
初始化刚体和形状后,接下来需要把Bird精灵关联到刚体中去,调用setCPBody(body);
函数实现。
Bird是一个动画精灵,所以应实现它的动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <code> void Bird::initAction()
{
CCArray *animFrames = CCArray::create();
for ( int i = 1; i < 4; i++)
{
CCString *name = CCString::createWithFormat( "wugui_%d.png" ,i);
CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(name->getCString());
animFrames->addObject(frame);
}
CCAnimation *animation = CCAnimation::createWithSpriteFrames(animFrames, 0.1);
runningAction =CCRepeatForever::create(CCAnimate::create(animation));
runningAction->retain();
}</code>
|
4.1.3 Bar管道类
Bar类同Bird类相似,都继承于CCPhysicsSprite类,在游戏中也是刚体。只是,管道是由不同的部分组成的,我们必须分情况来创建它。下面的函数是Bar的一个构造函数,它可创建管道的特定部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <code>Bar::Bar(CCSpriteBatchNode *spriteSheet,cpSpace* node, CCPoint position, int barTag)
{
this ->space = node;
if (barTag == 0) {
this ->initWithSpriteFrameName( "top.png" );
}
else if (barTag == 1){
this ->initWithSpriteFrameName( "button.png" );
}
else if (barTag == 2){
this ->initWithSpriteFrameName( "topdown.png" );
}
else if (barTag == 3){
this ->initWithSpriteFrameName( "buttonup.png" );
}
pipeSize = getContentSize();
body = cpBodyNewStatic();
body->p = cpv(position.x, position.y);
setCPBody(body);
shape = cpBoxShapeNew(body, pipeSize.width, pipeSize.height);
cpSpaceAddStaticShape(space, shape);
spriteSheet->addChild( this ,2);
this ->shape->collision_type = 2;
this ->shape->sensor = false ;
cpShapeSetUserData(shape, this );
}</code>
|
在游戏中,把Bar设置为静态的刚体,因为Bar在游戏中不用受到重力的影响,它和地面一样,只参与碰撞检测。
4.1.4 对象管理类
ObjectManager类用于管理场景中的Bar对象。
在Tiled Map Editor中打开游戏中用到的tmx文件,你会发现,它们都有一层命名为center的图层,在这个图层中放置了一些不规则的小矩形,我们将会利用这些小矩形来计算并加载一根根完整的管道。如下图所示:

"center"是tmx文件中一个图层的名字。
注意: tmx地图文件的大小是1920*960,但我们在前面的分辨率适配中设置了内容缩放因子,即pDirector->setContentScaleFactor(960 / 480),所以整个场景中的内容都缩小了一倍。相应地,下面函数中的值也都缩小了一半。
管道由如下的4部分资源构成:
资源图片中top.png的标签值被设为0,button.png的标签值为1,topdown.png的标签值为2 buttonup.png的标签值为3。
创建管道
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | <code>#define Delta 128
void ObjectManager::initObjectOfMap( int mapIndex, float mapWidth)
{
int x = sPosition.x;
int y1 = sPosition.y - Delta / 2;
int y2 = sPosition.y + Delta / 2;
CCLog( "y1 = %d" , y1);
int num = (y1 - 64) / 32 + 1;
if (num > 1)
{
upbar = Bar::create(pSpriteSheet, space, ccp(x, y1), 0);
upbar->setTag(mapIndex);
objects->addObject(upbar);
for ( int i = 1; i < num - 1; i++)
{
buttonbar = Bar::create(pSpriteSheet, space, ccp(x, y1 - 32 * i), 1);
buttonbar->setTag(mapIndex);
objects->addObject(buttonbar);
}
if (y1 - 32 * (num - 1) <= 80) {
buttonbar = Bar::create(pSpriteSheet, space, ccp(x, 80), 1);
buttonbar->setTag(mapIndex);
objects->addObject(buttonbar);
CCLog( "height < 96" );
}
else
{
buttonbar = Bar::create(pSpriteSheet, space, ccp(x, y1 - 32 * (num - 1)), 1);
buttonbar->setTag(mapIndex);
objects->addObject(buttonbar);
CCLog( "height >= 96" );
buttonbar = Bar::create(pSpriteSheet, space, ccp(x, 80), 1);
buttonbar->setTag(mapIndex);
objects->addObject(buttonbar);
}
}
else
{
upbar = Bar::create(pSpriteSheet, space, ccp(x, 80), 0);
upbar->setTag(mapIndex);
objects->addObject(upbar);
}
int num2 = (464 - y2) / 32 + 1;
if (num2 > 1)
{
down_upbar = Bar::create(pSpriteSheet, space, ccp(x, y2), 2);
down_upbar->setTag(mapIndex);
objects->addObject(down_upbar);
for ( int i = 1; i < num2 - 1 ; i++)
{
down_buttonbar = Bar::create(pSpriteSheet, space, ccp(x, y2 + 32 * i), 3);
down_buttonbar->setTag(mapIndex);
objects->addObject(down_buttonbar);
}
if (y2 + 32 * (num2 - 1) >= 464) {
down_buttonbar = Bar::create(pSpriteSheet, space, ccp(x, 464), 3);
down_buttonbar->setTag(mapIndex);
objects->addObject(down_buttonbar);
CCLog( "height >= 464 num2 = %d" , num2);
}
else
{
down_buttonbar = Bar::create(pSpriteSheet, space, ccp(x, y2 + 32 * (num2 - 1)), 3);
down_buttonbar->setTag(mapIndex);
objects->addObject(down_buttonbar);
down_buttonbar = Bar::create(pSpriteSheet, space, ccp(x, 464), 3);
down_buttonbar->setTag(mapIndex);
objects->addObject(down_buttonbar);
CCLog( "num2 = %d" , num2);
}
}
else
{
down_upbar = Bar::create(pSpriteSheet, space, ccp(x, 464), 2);
down_upbar->setTag(mapIndex);
objects->addObject(down_upbar);
}
}</code>
|
以地面部分的管道为例,它的基本原理如下图所示: 
图中sPosition点是从center层中获得的小矩形的坐标点,以sPosition点为中心,向上向下各减去64的长度,分别得到上管道和下管道的贴图坐标初始位置,即height=y2和y1处。y1到y2的距离为上下管道口之间的固定位置,可调。
地面部分的高度为64,各部分管道资源的大小都为32*32。
y1`是最后一节管道的Y坐标值,当这个值小于80(地面部分的高度 + 管道资源的宽度 /2)时,将在 y=80 处创建最后一节管道。
移除管道
为了不让我们的游戏越跑越卡,我们需要移除“跑出”了视线范围的管道。下面是实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <code> void ObjectManager::removeObjectOfMap( int mapIndex)
{
CCArray *toBeRemove = CCArray::create();
CCObject *obj = NULL;
CCPhysicsSprite* sprite = NULL;
CCARRAY_FOREACH( this ->objects, obj)
{
sprite = (CCPhysicsSprite *)obj;
if (mapIndex == sprite->getTag()) {
toBeRemove->addObject(sprite);
sprite->removeFromParent();
}
}
this ->objects->removeObjectsInArray(toBeRemove);
CCLog( "remove all objects" );
}</code>
|
4.1.5 背景层
在背景层中我们需要添加地图和管道。
游戏中地图是tmx格式的文件,它的加载需要调用CCTMXTiledMap类的一些方法。在本游戏中通过三张不同的tmx地图文件来创建这个游戏的地图,也就是要用到三个CCTMXTiledMap对象。
1 2 3 4 5 6 7 8 9 10 11 | <code>map00 = CCTMXTiledMap::create( "bg0.tmx" );
map00->setPosition(ccp(0, 0));
addChild(map00);
map01 = CCTMXTiledMap::create( "bg1.tmx" );
map01->setPosition(ccp(bgWidth, 0));
addChild(map01);
map02 = CCTMXTiledMap::create( "bg2.tmx" );
map02->setPosition(ccp(bgWidth * 2, 0));
addChild(map02);</code>
|
把这三个CCTMXTiledMap对象依次拼起来,再通过更新视角的方法就可以达到地图循环滚动的效果了。后面我们会详细介绍。
CCTMXTiledMap是一个CCNode节点,你可以设置它的坐标和比例等。它的子集是一些层,你可以通过该对象的objectGroupNamed()方法来获得子集层,它会为你返回一个特殊的CCTMXObjectGroup对象。如:
1 | <code>CCTMXObjectGroup* topBar = map->objectGroupNamed( "center" );</code>
|
4.1.6 重载触屏事件
因为我们需要不断控制点击屏幕来调节小鸟的飞行高度和降落速度,所以在这里我们需要重载触屏事件。 在PhysicWorldLayer.h中添加