开发环境
开发环境:(1)windows 7 64位(2)vs2015(3)python2.7.10
开发语言:c++
游戏引擎:cocos2d-x-3.16下载地址:http://download.cocos.com/Cocos2D-X/cocos2d-x-3.16.zip
项目源码托管在Github:https://github.com/smiger/FlappyBird
一、项目创建
使用cocos new创建新项目,命令行模式下进入cocos2d-x-3.16\tools\cocos2d-console\bin目录下,使用cocos new FlappyBird -p com.game.flappybird -l cpp -d E:\Projects命令创建Flappybird项目
![](http://www.tengewang.cn/wp-content/uploads/2018/03/QQ%E6%88%AA%E5%9B%BE20180301112209-300x101.png)
-p:包名
-l :编程语言
-d:项目存储路径
二、用vs2015打开FlappyBird项目
进入E:\Projects\FlappyBird\proj.win32目录,打开FlappyBird.sln
先不急着码代码,生成该解决方案,这个过程会编译cocos引擎的代码,时间会久一点
生成解决方案成功后,运行项目
加载资源
一、用TexturePacker做成flappy_packer.png合图
TexturePacker工具:链接:https://pan.baidu.com/s/1htqSysG 密码:xvsi
flappy图片资源:链接:https://pan.baidu.com/s/1jM2_yMjkBBcNSl43i0f4Ig 密码:4h0v
![flappy_packer.png](http://www.tengewang.cn/wp-content/uploads/image/2018/0307/1520434227286314.png)
二、cocos2dx提供加载纹理的接口
1 | Director::getInstance()->getTextureCache()->addImageAsync( "flappy_packer.png" , CC_CALLBACK_1(LoadingScene::loadingCallBack, this )); |
TextureCache这个类里面的addImageAsync的方法便是一个异步加载图片纹理的方法,第一个参数是图片的名字,第二个参数便是图片异步加载完成之后需要调用的回调函数
flappy_packer.png是放在image文件夹下的,在AppDelegate.cpp下有将image添加到搜索路径,所以直接写文件名就可以了
1 2 3 | FileUtils::getInstance()->addSearchPath( "fonts" ); FileUtils::getInstance()->addSearchPath( "image" ); FileUtils::getInstance()->addSearchPath( "sounds" ); |
loadingCallback回调函数加载帧动画和声音文件后跳转到欢迎界面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void LoadingScene::loadingCallBack(Texture2D *texture){ //预加载帧缓存纹理 SpriteFrameCache::getInstance()->addSpriteFramesWithFile( "flappy_packer.plist" , texture); // After loading the texture , preload all the sound SimpleAudioEngine::getInstance()->preloadEffect( "sfx_die.ogg" ); SimpleAudioEngine::getInstance()->preloadEffect( "sfx_hit.ogg" ); SimpleAudioEngine::getInstance()->preloadEffect( "sfx_point.ogg" ); SimpleAudioEngine::getInstance()->preloadEffect( "sfx_swooshing.ogg" ); SimpleAudioEngine::getInstance()->preloadEffect( "sfx_wing.ogg" ); // After load all the things, change the scene to new one //auto scene = HelloWorld::createScene(); auto scene = WelcomeScene::create(); TransitionScene *transition = TransitionFade::create(1, scene); Director::getInstance()->replaceScene(transition); } |
欢迎界面
一、欢迎界面比较简单,由背景、标题、鸟、开始按钮、地面构成
1、背景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //通过时间判断是白天还是晚上加载不同的图片资源 time_t t = time (NULL); tm * lt = localtime (&t); int hour = lt->tm_hour; //create the background image according to the current time; Sprite *background; if (hour >= 6 && hour <= 17){ background = Sprite::createWithSpriteFrameName( "bg_day.png" ); } else { background = Sprite::createWithSpriteFrameName( "bg_night.png" ); } background->setAnchorPoint(Point::ZERO); background->setPosition(Point::ZERO); this ->addChild(background); |
2、开始按钮
1 2 3 4 5 6 7 8 9 10 | //add the start-menu to the current scene Sprite *startButton = Sprite::createWithSpriteFrameName( "button_play.png" ); Sprite *activeStartButton = Sprite::createWithSpriteFrameName( "button_play.png" ); activeStartButton->setPositionY(5); auto menuItem = MenuItemSprite::create(startButton,activeStartButton,NULL,CC_CALLBACK_1(WelcomeLayer::menuStartCallback, this )); menuItem->setPosition(Point(origin.x + visiableSize.width/2 ,origin.y + visiableSize.height*2/5)); //将menuItem菜单项加入到Menu菜单中 auto menu = Menu::create(menuItem,NULL); menu->setPosition(Point(origin.x ,origin.y)); this ->addChild(menu,1); |
3、地面
地面的移动是通过两张图片拼接循环移动的效果
1 2 3 4 5 6 7 8 9 10 11 12 | // Add the land this ->land1 = Sprite::createWithSpriteFrameName( "land.png" ); this ->land1->setAnchorPoint(Point::ZERO); this ->land1->setPosition(Point::ZERO); this ->addChild( this ->land1); this ->land2 = Sprite::createWithSpriteFrameName( "land.png" ); this ->land2->setAnchorPoint(Point::ZERO); this ->land2->setPosition( this ->land1->getContentSize().width - 2.0f, 0); this ->addChild( this ->land2); this ->schedule(schedule_selector(WelcomeLayer::scrollLand), 0.01f); |
4、挥动翅膀的小鸟
1 2 3 4 5 6 7 | //create a bird and set the position in the center of the screen this ->bird = BirdSprite::getInstance(); this ->bird->createBird(); this ->bird->setTag(BIRD_SPRITE_TAG); this ->bird->setPosition(Point(origin.x + visiableSize.width / 2,origin.y + visiableSize.height*3/5 - 10)); this ->bird->idle(); this ->addChild( this ->bird); |
小鸟作为单独的类会在下一节讲解
小鸟类的实现
一、小鸟的状态
1 2 3 4 5 | typedef enum { ACTION_STATE_IDLE, //准备状态 ACTION_STATE_FLY, //飞行状态 ACTION_STATE_DIE //死亡状态 } ActionState; |
二、小鸟相关操作
1、初始化随机创建一种颜色的鸟createBirdByRandom
2、小鸟状态的切换changeState
3、创建小鸟的动画createAnimation
三、3种不同状态的分析
初始化时创建了小鸟的2个动作
翅膀的挥动idleAction
小鸟的上下移动swingAction
1 2 3 4 5 6 7 8 9 | // init idle status //create the bird animation Animation* animation = this ->createAnimation( this ->birdNameFormat.c_str(), 3, 10); Animate* animate = Animate::create(animation); this ->idleAction = RepeatForever::create(animate); // create the swing action ActionInterval *up = CCMoveBy::create(0.4f,Point(0, 8)); ActionInterval *upBack= up->reverse(); this ->swingAction = RepeatForever::create(Sequence::create(up, upBack, NULL)); |
1、idle空闲状态
1 2 3 4 5 6 | void BirdSprite::idle() { if (changeState(ACTION_STATE_IDLE)) { this ->runAction(idleAction); this ->runAction(swingAction); } } |
2、fly飞行状态
点击开始按钮,游戏开始后,小鸟由idle状态切换到fly状态
由于游戏用到了box2d物理引擎,所以fly状态除了挥动翅膀还要受到重力的作用
1 2 3 4 5 6 | void BirdSprite::fly() { if (changeState(ACTION_STATE_FLY)) { this ->stopAction(swingAction); this ->getPhysicsBody()->setGravityEnable( true ); } } |
3、die死亡状态
当小鸟碰到水管后,进入死亡状态
1 2 3 4 5 | void BirdSprite::die() { if (changeState(ACTION_STATE_DIE)) { this ->stopAllActions(); } } |
控制层与游戏层
一、控制层
在OptionLayer层中,我们实现了触摸事件的监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | bool OptionLayer::init() { if (Layer::init()){ auto dispatcher = Director::getInstance()->getEventDispatcher(); auto listener = EventListenerTouchAllAtOnce::create(); listener->onTouchesBegan = CC_CALLBACK_2(OptionLayer::onTouchesBegan, this ); dispatcher->addEventListenerWithSceneGraphPriority(listener, this ); return true ; } else { return false ; } } void OptionLayer::onTouchesBegan( const std::vector<Touch*>& touches, Event *event) { this ->delegator->onTouch(); } |
在头文件OptionLayer.h中定义了delegator
1 | CC_SYNTHESIZE(OptionDelegate*, delegator, Delegator); |
还有OptionDelegate类
1 2 3 4 5 | class OptionDelegate { public : virtual void onTouch() = 0; }; |
二、控制层与游戏层的关系
游戏层GameLayer和控制层OptionLayer通过代理OptionDelegate来进行通讯
游戏层继承OptionDelegate实现onTouch()方法
1 2 3 | class GameLayer : public Layer , public OptionDelegate{ ... }; |
在游戏主场景GameScene中将两者联系起来
1 2 3 4 5 6 | // Add operation layer to control the game auto optionLayer = OptionLayer::create(); if (optionLayer) { optionLayer->setDelegator(gameLayer); this ->addChild(optionLayer); } |
三、游戏层
1、游戏状态
游戏有三种状态:准备、开始、结束
1 2 3 4 5 | typedef enum _game_status { GAME_STATUS_READY = 1, GAME_STATUS_START, GAME_STATUS_OVER } GameStatus; |
2、添加小鸟
在上一节中我们已经完成了小鸟类的封装,我们先在GameLayer.h中声明这只小鸟
设置物理引擎的相关属性并添加到游戏层中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Add the bird this ->bird = BirdSprite::getInstance(); this ->bird->createBird(); PhysicsBody *body = PhysicsBody::create(); body->addShape(PhysicsShapeCircle::create(BIRD_RADIUS)); body->setCategoryBitmask(ColliderTypeBird); body->setCollisionBitmask(ColliderTypeLand & ColliderTypePip); body->setContactTestBitmask(ColliderTypeLand | ColliderTypePip); body->setDynamic( true ); body->setLinearDamping(0.0f); body->setGravityEnable( false ); this ->bird->setPhysicsBody(body); this ->bird->setPosition(origin.x + visiableSize.width*1/3 - 5,origin.y + visiableSize.height/2 + 5); this ->bird->idle(); this ->addChild( this ->bird); |
游戏进行过程当中,每次点击屏莫的时候,给小鸟设置一个向上的初速度:
1 | this ->bird->getPhysicsBody()->setVelocity(Vect(0, 260) |
3、水管的添加
水管的添加,主要是通过createPips()函数来生成水管
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 | void GameLayer::createPips() { // Create the pips for ( int i = 0; i < PIP_COUNT; i++) { Size visibleSize = Director::getInstance()->getVisibleSize(); Sprite *pipUp = Sprite::createWithSpriteFrameName( "pipe_up.png" ); Sprite *pipDown = Sprite::createWithSpriteFrameName( "pipe_down.png" ); Node *singlePip = Node::create(); // bind to pair pipDown->setPosition(0, PIP_HEIGHT + PIP_DISTANCE); singlePip->addChild(pipDown, 0, DOWN_PIP); singlePip->addChild(pipUp, 0, UP_PIP); singlePip->setPosition(visibleSize.width + i*PIP_INTERVAL + WAIT_DISTANCE, this ->getRandomHeight()); auto body = PhysicsBody::create(); auto shapeBoxDown = PhysicsShapeBox::create(pipDown->getContentSize(),PHYSICSSHAPE_MATERIAL_DEFAULT, Point(0, PIP_HEIGHT + PIP_DISTANCE)); body->addShape(shapeBoxDown); body->addShape(PhysicsShapeBox::create(pipUp->getContentSize())); body->setDynamic( false ); body->setContactTestBitmask( true ); singlePip->setPhysicsBody(body); singlePip->setTag(PIP_NEW); this ->addChild(singlePip); this ->pips.push_back(singlePip); } } |
一组水管由上下2半根组成,用vector容器添加两组管道,每次出现在屏幕中的管子只有两组,当一组消失在屏幕范围内则重设置其横坐标
1 2 3 4 5 6 7 8 9 10 | // move the pips for ( auto singlePip : this ->pips) { singlePip->setPositionX(singlePip->getPositionX() - 2); if (singlePip->getPositionX() < -PIP_WIDTH) { singlePip->setTag(PIP_NEW); Size visibleSize = Director::getInstance()->getVisibleSize(); singlePip->setPositionX(visibleSize.width); singlePip->setPositionY( this ->getRandomHeight()); } } |
4、碰撞检查
在GameLayer初始化的时候,我们创建了一个碰撞监听:
1 2 3 | auto contactListener = EventListenerPhysicsContact::create(); contactListener->onContactBegin = CC_CALLBACK_1(GameLayer::onContactBegin, this ); this ->getEventDispatcher()->addEventListenerWithSceneGraphPriority(contactListener, this ); |
一旦出现碰撞,则进入游戏结束状态
1 2 3 4 | bool GameLayer::onContactBegin(PhysicsContact& contact) { this ->gameOver(); return true ; } |
游戏状态层
游戏状态层StatusLayer也是分为3种状态,在不同的阶段显示相应的画面
一、状态层的三种状态
1、游戏准备
![image.png](http://www.tengewang.cn/wp-content/uploads/image/2018/0307/1520434506254095.png)
2、游戏进行
![image.png](http://www.tengewang.cn/wp-content/uploads/image/2018/0307/1520434545693803.png)
3、游戏结束
![image.png](http://www.tengewang.cn/wp-content/uploads/image/2018/0307/1520434556185707.png)
二、不同状态的处理函数
1、OnGameStart
当用户在游戏准备状态点击屏幕后,则执行OnGameStart函数
1 2 3 | void StatusLayer::onGameStart(){ this ->showStartStatus(); } |
2、onGamePlaying
游戏进行过程中,状态层主要负责显示实时得分
1 2 3 4 | void StatusLayer::onGamePlaying( int score){ auto scorestr = __String::createWithFormat( "%d" , score)->getCString(); scoreSprite->setString(scorestr); } |
其中游戏过程中展示的分数采用TextAtlas控件,先将数字做成一张合图
![num.png](http://www.tengewang.cn/wp-content/uploads/image/2018/0307/1520434885849310.png)
然后使用ui::TextAtlas::create创建,参数1:展示字符,参数2:做成的合图,参数3:每个字符所占宽度,参数4:高度,参数5:开始字符
1 2 3 4 5 | TextAtlas* TextAtlas::create( const std::string &stringValue, const std::string &charMapFile, int itemWidth, int itemHeight, const std::string &startCharMap) |
1 | scoreSprite = ui::TextAtlas::create( "0" , "num.png" ,30, 44, "0" ); |
3、onGameEnd
在游戏结束的时候,需要显示游戏结束画面,在这个过程当中,包括一些动画的显示,菜单的显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void StatusLayer::onGameEnd( int curScore, int bestScore){ this ->showOverStatus(curScore,bestScore); } void StatusLayer::showOverStatus( int curScore, int bestScore) { this ->currentScore = curScore; this ->bestScore = bestScore; if (curScore > bestScore){ this ->bestScore = curScore; this ->isNewRecord = true ; } else { this ->isNewRecord = false ; } this ->removeChild(scoreSprite); this ->blinkFullScreen(); } |