使用cocos2d-x制作My Flappy Bird

时间:2023-02-08 15:42:31

平台介绍:win7 + cocos2d-x 3.0 + VS2012语言C++

    游戏开发的时候,我们首先要准备一些素材,譬如图片资源,音效资源等等。资源可以下载一个androidflappybird的游戏包apk并取出其中的游戏资源。这个游戏相对来说还是比较简单的,所涉及的元素也不多,只要熟练的话,很快就可以开发出一个属于你的flappy bird


第一步:创建项目

    这个每个版本都有一些不同,具体的还是要根据个人所使用的版本而定。所以在此就不具体展开,就说一下3.0rc版本需要注意的地方,这个版本在cocos根目录下有个setup.py文件,这个适用于配置环境变量的,在cmd界面里运行并按照提示设置即可,主要是android开发的SDKNDKANT,还有项目创建保存的路径。在根目录下还有一个文件README.md,这个文件是开发帮助文件,官方权威教程,可以参考如何创建一个游戏。


第二步:游戏的设计

    了解过cocos2d的朋友都知道,这个游戏的基本元素是Director(导演)Scene(场景),Layer(画布)Node(节点)等基本元素组成,所以我们在开始游戏的时候必须先把这些基本元素构建好,这样接下来的编程也就比较简单了。

    先来讨论一下Scene,对于一般的游戏而言都可以分成3Scene,启动游戏的加载Scene,运行游戏主逻辑时候的游戏Scene,还有就是游戏结束的Scene。这三个Scene主要是按照功能性来区分的,启动Scene主要用于资源的加载,游戏的预处理等,通常有个进度条展示。游戏预处理完后就跳转到游戏Scene,这个主要是运行游戏主逻辑的,也是玩家和程序直接交互的界面。游戏结束后,跳到游戏结束Scene进行一些游戏的后继处理,譬如分数的统计,等级的提升这些操作。也可以从这里跳转到游戏主Scene重新进行游戏。这里的flappy bird由于比较简单,所以这里省去了结束Scene,直接在游戏主Scene中添加一个Layer处理分数统计并返回游戏主Scene


使用cocos2d-x制作My Flappy Bird


1、 启动Scene

    这里有两个 Layer ,一个是 startLayer ,用于进行游戏预处理,这里关键的地方就是加载图片和声效资源。加载图片使用了帧缓存 SpriteFrameCache 和动画缓存 AnimationCache 。这里的图片素材我是先用 photoshop 切割之后使用 TexturePacker 进行打包,生成了整合图片 game.png 和描述文件 game.plist 。如左下图所示,另外这里没有用进度条,而是简单使用右下的图片的渐进方式实现加载。

使用cocos2d-x制作My Flappy Bird         使用cocos2d-x制作My Flappy Bird           



把资源文件放到游戏项目中的Resources目录下。加载图片演示比较简单,直接创建精灵并加载图片即可,如下:

auto loadingSprite = Sprite::create("loading.png");

加载game.png的时候我们使用异步加载的方式,每加载一个Texture就回调一次函数,通常可以在回调函数中实现进度条的加载,这里只进行简单如下:

Director::getInstance()->getTextureCache()->addImageAsync("game.png", CC_CALLBACK_1(StartLayer::loadingCallBack, this));
/////////////////////////////////////////////
void StartLayer::loadingCallBack(Texture2D* texture)
{
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("game.plist", texture);
preLoadResoure();

auto scene = WelcomeLayer::createScene();
TransitionScene* transition = TransitionFade::create(2.0f, scene);
Director::getInstance()->replaceScene(transition);
}

利用动画缓存预加载动画资源,从 spriteFrameCache 中读取响应的帧来构成动画 birdAnimation ,并保存到 AnimationCache 中,使用的时候直接按名字获取即可

void StartLayer::preLoadResoure()
{
//prelaod the bird animation
auto birdAnimation = Animation::create();
birdAnimation->setDelayPerUnit(0.1f);
birdAnimation->addSpriteFrame(SpriteFrameCache::getInstance()->spriteFrameByName("bird_0_0.png"));
birdAnimation->addSpriteFrame(SpriteFrameCache::getInstance()->spriteFrameByName("bird_0_1.png"));
birdAnimation->addSpriteFrame(SpriteFrameCache::getInstance()->spriteFrameByName("bird_0_2.png"));
AnimationCache::getInstance()->addAnimation(birdAnimation,"birdAnimation");

UserData::getInstance()->saveUserData("bestScore",0);
GameAudio::getInstance();
}

添加声音:

       添加声音可以通过SimpleAudioEngine来完成,不过这个要区分好不同平台所要求的格式是不一样的,为了方便在WIN32上面调试,这里采用MP3的音频格式。这里写了一个GameAudio的类来继承SimpleAudioEngine,并在初始化的时候预加载音频资源,也就是调用simpleAudioEngine中的preloadEffect来预加载声效,假如有背景音乐就调用preloadBackgroundMusic,还有其他一些设置声音大小的这里不说明,最后在要使用的地方直接调用playEffect(“filename”)即可。

    加载完会进入一个游戏准备状态,这里我用了一个welcomeLayer来实现。这个层主要是的元素有背景,小鸟和开始按键。背景和小鸟都是展示而已,按键设定触发事件,也即是切换到游戏主Scene中去。如下图所示:

使用cocos2d-x制作My Flappy Bird



2、 游戏主Scene

2.1 背景的加载

    这个比较简单。就是简单加载一个图片而已,我用一个BackgroundLayer把它实现,继承于Layer,然后重载init,在里面实现图片加载,这里有一个白天和黑夜的图片,加载的时候可以通过获取当前时间来控制加载哪张图片。使用的时候在需要背景的地方实例化,并使用addChild函数添加到Scene上就可以了。

2.2 小鸟的实现

    这个小鸟的翅膀是会摆动的,其实这个就是帧动画的播放,我们创建一个精灵来runAction前面我们在帧动画缓冲区中已经缓冲的小鸟帧动画就可以了,如下所示:

 //initialize the bird sprite
birdSprite = Sprite::create();
this->addChild(birdSprite);
//get the bird animation and run it
auto birdAnimate = Animate::create(AnimationCache::getInstance()->animationByName("birdAnimation"));
birdSprite->runAction(RepeatForever::create(birdAnimate));

除了摆动翅膀,小鸟还有一个上下游动的动作,我们可以通过动作类moveTo来实现。并使用reverse来实现moveTo的逆运动,就会有上下飘动的效果拉。


2.3 水管的出现

     一组水管由管口向上和向下两个组成,不同组水管之间间隔是一样的,但是管口向上和管口向下的水管出现在屏幕上的比例是不一样的而已。这里我用Pipe类来实现,这个类继承Node,每个pipe就是一个Node,然后创建不同管口方向的水管Sprite,添加到pipe中去,设置好不同管的间隔即可。如下所示:

pipeDown = Sprite::createWithSpriteFrameName("pipe_down.png");
pipeUp = Sprite::createWithSpriteFrameName("pipe_up.png");
pipeDown->setPosition(0, winSize.height);
pipeUp->setPosition(0, pipeDown->getPosition().y-pipeDown->getContentSize().height-Pipe_Distance);
this->addChild(pipeDown, 0, PIPE_DOWN);
this->addChild(pipeUp, 0, PIPE_UP);

玩过这款游戏的朋友都知道,同一时刻只有最多只有两组pipe出现在屏幕上,每一组的不同只是pipe的位置不一样而已。也就是说我们可以只创建两个pipe,这里用vector来管理。如下:

void GameLayer::createPipe()
{
Size winSize = Director::getInstance()->getWinSize();
for(int i = 0; i < pipeCount; i++)
{
Pipe* pipe = Pipe::create();
pipe->setTag(PIP_NEW);
pipe->setPosition(Point(winSize.width + (i+1)*Pipe_Interval, getRandomY()));
//pipe->setPosition(Point(winSize.width*0.5+i*50, getRandomY()));
this->addChild(pipe);
pipeVector.pushBack(pipe);
}
}

当显示的时候我们可以通过设定其实位置,然后按照一定的时间间隔来重新设置pipePositionY即可有,然后当PositionY达到一定数值的时候再重置,当然重置的时候还要随机生成pipe在屏幕上的位置。

	//move the pipe
for(auto pip : pipeVector)
{
float test = pip->getPosition().x-1.0f;
pip->setPositionX(test);
if(pip->getPositionX() < (-Pipe_Width))
{
Size winSize = Director::getInstance()->getWinSize();
pip->setPositionX(winSize.width + Pipe_Width);
pip->setPositionY(getRandomY());
pip->setTag(PIP_NEW);
}
}

2.3 地面的滚动

    地面的滚动其实是创建两个加载同一张land.png所生成的精灵,然后设定他们的位置,通过定时器来修改他们的位置信息,从而实现滚动的效果,如下所示。

    land1 = Sprite::createWithSpriteFrameName("land.png");
land1->setAnchorPoint(Point::ZERO);
land1->setPosition(Point::ZERO);
this->addChild(land1, 2);
//land2同上所示。
//move the land
land1->setPositionX(land1->getPosition().x-1.0f);
land2->setPositionX(land1->getPosition().x + land1->getContentSize().width-4.0f);
if(land2->getPositionX() == 0)
{
land1->setPosition(Point::ZERO);
}

到这一步,我们就可以在主界面中看到我们的主要元素了,但是他们都是简单的演示,并没有交互,因此接下来我们需要把他们连接起来。


2.4 物理世界的加入

    相对于以前的版本,3.0之后我们使用物理世界更加简单。。。。。。。。。查资料添加

首先我们在 GameScene 中初始化物理世界
Scene::initWithPhysics()

在开发过程中我们可以使用下面这条语句来帮组我们调试,这样可以把我们设定的物理对象用红色线包裹起来。等完成游戏后再注释掉。

this->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);

然后可以添加一些物理参数,这里只需要改变重力向量即可。

this->getPhysicsWorld()->setGravity(Vect(0,-600));

这里的重力参数可以改变游戏的难度。当然这个就由个人喜好设定拉。然后我们要让GameLayer也继承这个物理世界,那么就要在GameLayer.h中添加

cocos2d::PhysicsWorld* m_world;
void setPhyWorld(cocos2d::PhysicsWorld* world){m_world = world;}

并且在实例化GameLayer的时候执行如下代码即可:

gameLayer->setPhyWorld(this->getPhysicsWorld())

这里我们以小鸟为例,为它添加一个物理属性。

void bird::setBirdPhysics()
{
auto birdBody = PhysicsBody::createCircle(BIRD_RADIUS);
birdBody->setDynamic(true);
birdBody->setContactTestBitmask(1);
birdBody->setGravityEnable(false);
birdSprite->setPhysicsBody(birdBody);
}

    这里的有个关键参数是setContactTestBitmask,必须设置成true,那么当不同物体碰撞时才会触发检测,其他的参数可以查看源代码中的注释。就这么简单我们就可以看到小鸟会受到重力影响而下落。同时我们也要为水管和地面添加物理属性,原理和上面的方法一样,都是通过设定physicsBodysetPhysicsBody到制定的精灵上即可。


2.5 碰撞检测

    其实这个使用起来很简单,就是override监听事件,并添加到分发事件中去即可。这里我们只需要override监听事件onContactBegin即可,如下所示:

 auto contactListener = EventListenerPhysicsContact::create();
contactListener->onContactBegin = CC_CALLBACK_1(GameLayer::onContactBegin, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this);

我们需要进行什么处理,就添加到onContactBegin函数体中,如下所示:

bool GameLayer::onContactBegin(PhysicsContact& contact)
{
gameOver();
return true;
}
就这样碰撞检测就搞掂了,是不是很简单....

2.6 触摸层的添加

    当然我们要进行人机交互,就必须要实现一个触摸层。这个实现也是一种事件分发,override我们要实现的触摸事件,然后添加到事件分发器中即可。如下:

auto touchListener = EventListenerTouchAllAtOnce::create();
touchListener->onTouchesBegan = CC_CALLBACK_2(TouchLayer::onTouchesBegan, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);

当然到底触摸发生什么效果还是我们自己添加的,如下:

void TouchLayer::onTouchesBegan(const std::vector<Touch*>&touches, Event* event)
{
_touchDelegator->onTouch();
}

这个 _touchDelegator GameLayer TouchLayer 交互的代理,实现起来很简单,就是先在 TouchLayer.h 中写一个代理类
class TouchDelegate{public:	virtual void onTouch()=0;};

然后在TouchLayer中继承这个接口,添加

CC_SYNTHESIZE(TouchDelegate*, _touchDelegator, TouchDelegator);

override这个onTouch函数,这里就是实现点击一下小鸟会上升一点,如下:

void GameLayer::onTouch()
{
GameAudio::getInstance()->playEffect("sfx_wing.ogg");
if(gameStatus == GAME_START)
{
Vect curVelocity = bird::getInstance()->getBirdSprite()->getPhysicsBody()->getVelocity();
bird::getInstance()->getBirdSprite()->getPhysicsBody()->setVelocity(Vect(0,MIN(200,500+curVelocity.y)));
}
else if(gameStatus == GAME_READY)
{
getReady->runAction(FadeOut::create(0.5f));
tapToStart->runAction(FadeOut::create(0.5f));
getReady->removeFromParent();
tapToStart->removeFromParent();
createPipe();
bird::getInstance()->getBirdSprite()->getPhysicsBody()->setVelocity(Vect(0,200));
bird::getInstance()->getBirdSprite()->getPhysicsBody()->setGravityEnable(true);
gameStatus = GAME_START;
}

else;
}

这里还涉及到游戏的状态而触发不同的事件,当游戏为start时,那么点击就是小鸟跳一下,当为ready时,点击就会开始游戏。当然还需要关键的一步,就是必须在实例化GameLayer的时候执行

touchLayer->setTouchDelegator(gameLayer);

到这里,我们的游戏就可以玩起来拉,不过还差的就是游戏结束的统计和设定一个接口重新回到游戏中来。


2.7 游戏数据统计和储存

    在游戏运行中,每当小鸟越过一组水管,当前所取得的分数也要显示在屏幕上。这里就要对当前的水管和小鸟的位置进行比较。我们在让水管从游戏右方初始化进入游戏界面的时候,给水管设置标签NEW,并对于vector中的水管进行轮询,当标签为NEW则与小鸟的位置比较,所示小鸟已经越过该组水管,则设置该组水管标签为PASS。如下所示:

void GameLayer::updateScore()
{
for(auto pip : pipeVector)
{
if(pip->getTag()==PIP_NEW)
{
if(pip->getPositionX() < bird::getInstance()->getPositionX())
{
GameAudio::getInstance()->playEffect("sfx_point.ogg");
score++;
pip->setTag(PIP_PASS);
_scoreDelegator->showCurrentScore(score);
}
}
}
}

    更新score变量,并且通过分数代理与ScoreLayer进行交互,改变分值的显示。这里的代理实现的方式和TouchLayer那里实现的方式一样,就是通过接口的思想。

    ScoreLayer类主要是用于显示分数的变化。当游戏结束时候,依次弹出GameOver,记分牌和菜单栏。另外这里还用两个vector来预存两组数字图片帧,方便生成不同的数字精灵来显示当前的分数。

游戏过程中的分值显示如下。按照位数调整位置,显示不同的数字精灵。

void ScoreLayer::showCurrentScore(int score)
{
numberPlace = 0;
scoreNode->removeAllChildren();
Size winSize = Director::getInstance()->getWinSize();
while(score)
{
iterator = score%10;
numberSprite = Sprite::createWithSpriteFrame(numberVector.at(iterator));
numberSprite->setPosition(Point(-NUMBER_INTERVAL*numberPlace,0));
scoreNode->addChild(numberSprite);
numberPlace++;
score /= 10;
}
scoreNode->setPosition(winSize.width*0.5+NUMBER_INTERVAL*numberPlace/2, winSize.height*0.9);
}

游戏结束的时候,弹出GameOver

void ScoreLayer::showGameOverSprite()
{
Size winSize = Director::getInstance()->getWinSize();
gameOverSprite = Sprite::createWithSpriteFrameName("gameOver.png");
gameOverSprite->setPosition(Point(winSize.width*0.5, winSize.height*0.8));
this->addChild(gameOverSprite);
auto fadeIn = FadeIn::create(1.0f);
CallFunc* action = CallFunc::create(std::bind(&ScoreLayer::showCounterPanel, this));
gameOverSprite->runAction(Sequence::create(fadeIn,action,NULL));

}

上面的回调函数弹出计分板

void ScoreLayer::showCounterPanel()
{
Size winSize = Director::getInstance()->getWinSize();
counterPanel = Sprite::createWithSpriteFrameName("ScorePanel.png");
counterPanel->setPosition(Point(winSize.width*0.5, -counterPanel->getContentSize().height));
this->addChild(counterPanel);

auto flyTo = MoveTo::create(1.0f, Point(winSize.width*0.5, winSize.height*0.6));
CallFunc* action = CallFunc::create(std::bind(&ScoreLayer::showMenu, this));
CallFunc* ShowScore = CallFunc::create(std::bind(&ScoreLayer::showScore, this));
CallFunc* ShowBestScore = CallFunc::create(std::bind(&ScoreLayer::showBestScore, this));
CallFunc* ShowOther = CallFunc::create(std::bind(&ScoreLayer::showOther, this));
GameAudio::getInstance()->playEffect("sfx_swooshing.ogg");
counterPanel->runAction(Sequence::create(flyTo, ShowScore,ShowBestScore,ShowOther, action, NULL));
}

这里要进行的动作有几个,首先计分板飞入屏幕,然后统计当前得分,也就是由0final score进行演示,这里使用到定时器进行数字的更新

void ScoreLayer::showScore()
{
this->schedule(schedule_selector(ScoreLayer::countScore),0.02f);
}
数字的更新和之前的差不多,如下:
void ScoreLayer::countScore(float dt){if(counter<= finalScore){intscore = counter;if(counterPanel->getChildByTag(SCORE_COUNTER)){ counterPanel->removeChildByTag(SCORE_COUNTER, true);}numberPlace= 0;SizepanelSize = counterPanel->getContentSize();do {    iterator= score%10;    scoreSprite= Sprite::createWithSpriteFrame(counterVector.at(iterator));    scoreSprite->setPosition(Point((panelSize.width-30-SCORE_INTERVAL*numberPlace),panelSize.height*0.6));    counterPanel->addChild(scoreSprite,0, SCORE_COUNTER);    numberPlace++;    score/= 10; }while(score); counter++;}else{    this->unschedule(schedule_selector(ScoreLayer::countScore));}}

    计算完后要unschedule来停止定时器。接着比较当前分数和已取得过的最佳分数做比较,若没变,则直接当前分数,若超过记录中的最佳分数,则存储最佳分数。这里的存储可以使用cocos2d提供的UserDefault,使用很简单,可以创建一个类UserData实现存取即可,如下:

void UserData::saveUserData(const char* key, int value)
{
UserDefault::getInstance()->setIntegerForKey(key, value);
}
int UserData::readUserData(const char* key)
{
return UserDefault::getInstance()->getIntegerForKey(key);
}

具体可以上网看看教程,很快就能实现。

当取得新的记录时,我们要弹出一个 NEW 字样的精灵并赋予一些动态效果来表示我们已经取得了新纪录。
void ScoreLayer::showNewSprite(){ 	if(getNewRecord)	{	   Size panelSize = counterPanel->getContentSize();	   auto newSprite = Sprite::createWithSpriteFrameName("newRecord.png");	   newSprite->setPosition(Point((panelSize.width-60) ,panelSize.height*0.25));	   counterPanel->addChild(newSprite);	   newSprite->setRotation(-30);	   auto scale = ScaleBy::create(1.0f, 2.0f);	   auto reverse = scale->reverse();	   auto seq = Sequence::create(scale, reverse, NULL);	   newSprite->runAction(Repeat::create(seq, 3));	}}
还有就是根据当前的得分情况来赋予 medal ,这个都是根据个人的喜好设定的。如下:
void ScoreLayer::showMedal(){	if(finalScore < 10)	{		medalSprite = Sprite::createWithSpriteFrameName("bronze_medal.png");	}	else if(finalScore < 50)	{	    medalSprite = Sprite::createWithSpriteFrameName("silver_medal.png");	}	else	{	    medalSprite = Sprite::createWithSpriteFrameName("gold_medal.png");	}	medalSprite->setPosition(Point(48, counterPanel->getContentSize().height*0.48));	counterPanel->addChild(medalSprite);	auto scale = ScaleBy::create(0.5f, 2.0f);	auto reverse = scale->reverse();	auto blink = Blink::create(0.5f, 2);	medalSprite->runAction(Sequence::create(scale, reverse, blink, NULL));}

最后是弹出菜单,实现返回到游戏主循环,也就是简单的场景切换而已,如下:

void ScoreLayer::restartCallBack(Object* psender)
{
/*this->removeChild(gameOverSprite, false);
this->removeChild(menu, false);
Size winSize = Director::getInstance()->getWinSize();
counterPanel->setPosition(winSize.width*0.5, -counterPanel->getContentSize().height);*/
Scene* gameScene = GameScene::create();
TransitionScene* transition = TransitionFade::create(1.0f, gameScene);
Director::getInstance()->replaceScene(gameScene);
}

到此为止,简单地完成了一个flappy bird游戏如下图所示。

使用cocos2d-x制作My Flappy Bird使用cocos2d-x制作My Flappy Bird使用cocos2d-x制作My Flappy Bird

     当然还有很多可以改善的地方。第一次写这类型的博客,发现思路上还是有点乱的,不过以后慢慢改进拉。这个游戏作为一个入门的例子,带领我们进入cocos2d的世界,在接下来希望我们共同进步,共同积累,在未来编写一个属于我们自己的游戏。