平台介绍:
win7 + cocos2d-x 3.0 + VS2012
,
语言
C++
游戏开发的时候,我们首先要准备一些素材,譬如图片资源,音效资源等等。资源可以下载一个
android
的
flappybird
的游戏包
apk
并取出其中的游戏资源。这个游戏相对来说还是比较简单的,所涉及的元素也不多,只要熟练的话,很快就可以开发出一个属于你的
flappy bird
。
第一步:创建项目
这个每个版本都有一些不同,具体的还是要根据个人所使用的版本而定。所以在此就不具体展开,就说一下
3.0rc
版本需要注意的地方,这个版本在
cocos
根目录下有个
setup.py
文件,这个适用于配置环境变量的,在
cmd
界面里运行并按照提示设置即可,主要是
android
开发的
SDK
,
NDK
和
ANT
,还有项目创建保存的路径。在根目录下还有一个文件
README.md
,这个文件是开发帮助文件,官方权威教程,可以参考如何创建一个游戏。
第二步:游戏的设计
了解过
cocos2d
的朋友都知道,这个游戏的基本元素是
Director(
导演
)
,
Scene
(场景),
Layer(
画布
)
和
Node(
节点
)
等基本元素组成,所以我们在开始游戏的时候必须先把这些基本元素构建好,这样接下来的编程也就比较简单了。
先来讨论一下
Scene
,对于一般的游戏而言都可以分成
3
个
Scene
,启动游戏的加载
Scene
,运行游戏主逻辑时候的游戏
Scene
,还有就是游戏结束的
Scene
。这三个
Scene
主要是按照功能性来区分的,启动
Scene
主要用于资源的加载,游戏的预处理等,通常有个进度条展示。游戏预处理完后就跳转到游戏
Scene
,这个主要是运行游戏主逻辑的,也是玩家和程序直接交互的界面。游戏结束后,跳到游戏结束
Scene
进行一些游戏的后继处理,譬如分数的统计,等级的提升这些操作。也可以从这里跳转到游戏主
Scene
重新进行游戏。这里的
flappy bird
由于比较简单,所以这里省去了结束
Scene
,直接在游戏主
Scene
中添加一个
Layer
处理分数统计并返回游戏主
Scene
。
1、
启动
Scene
这里有两个
Layer
,一个是
startLayer
,用于进行游戏预处理,这里关键的地方就是加载图片和声效资源。加载图片使用了帧缓存
SpriteFrameCache
和动画缓存
AnimationCache
。这里的图片素材我是先用
photoshop
切割之后使用
TexturePacker
进行打包,生成了整合图片
game.png
和描述文件
game.plist
。如左下图所示,另外这里没有用进度条,而是简单使用右下的图片的渐进方式实现加载。
把资源文件放到游戏项目中的
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();
}
添加声音:
加载完会进入一个游戏准备状态,这里我用了一个welcomeLayer来实现。这个层主要是的元素有背景,小鸟和开始按键。背景和小鸟都是展示而已,按键设定触发事件,也即是切换到游戏主Scene中去。如下图所示:
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);
}
}
当显示的时候我们可以通过设定其实位置,然后按照一定的时间间隔来重新设置
pipe
的
PositionY
即可有,然后当
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,
那么当不同物体碰撞时才会触发检测,其他的参数可以查看源代码中的注释。就这么简单我们就可以看到小鸟会受到重力影响而下落。同时我们也要为水管和地面添加物理属性,原理和上面的方法一样,都是通过设定
physicsBody
并
setPhysicsBody
到制定的精灵上即可。
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));
}
这里要进行的动作有几个,首先计分板飞入屏幕,然后统计当前得分,也就是由
0
到
final 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
的世界,在接下来希望我们共同进步,共同积累,在未来编写一个属于我们自己的游戏。