【Cocos2d-x实例】 Flappy Bird游戏

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/50421039

好长一段时间没有关注Cocos2d-x引擎了,不知不觉中Cocos2d-x已经更新到3.8了。为了体验一把新特性,我花了几天时间温习了一下,并把自己以前用2.X版本做的Flappy Bird小游戏重写了一遍,然后写下这篇文章记录一下。

游戏源码放在GitHub上,需要的可以到这里下载传送门。

时间仓促,难免会由很多地方考虑不周,请多多包涵。下面就进入主题:

资源收集和导入

要制作游戏,我们首先要有图片、声音等资源。对于Flappy Bird这款游戏,网上已经有很多相关的资源包可供下载,如果没有找到,也可以到上面提供的GitHub自行下载。另外,获取资源后还需要把这些小图片打包成plist文件。这些过程就不一一描述。

资源加载界面的实现

一个游戏中最耗内存的就是图片、声音等资源,如果一次性地加载大量资源会影响游戏运行的流畅性,这一点再移动设备上更是如此。所有一般游戏都会再进入游戏场景前预先加载所需要的资源,让其运行起来更加流畅。当然,我们今天要做的这款Flappy Bird虽然小,远远达不到消耗完收集内存的地步。但是,麻雀虽小,五脏俱全,为了体现游戏的完整性,我们还是增加一个资源载入场景,把图片、声音预先加载进内存。

如果你还不知道Cocos2d-x如何异步加载资源,可以先看看我以前的一篇: Cocos2d-x 3.X 异步加载plist图片资源博客

有一点不同的是:在今天我们制作的这款小游戏中,除了加载图片,我们还在addImageAsync的回调函数中把音效和主角动画也加载进内存。源码如下:

#include "LoadingScene.h"
#include "SimpleAudioEngine.h"
#include "WelcomeScene.h"
#include "resource.h"

USING_NS_CC;
using namespace CocosDenshion;

Scene* LoadingLayer::createScene()
{
    auto scene = Scene::create();
    auto layer = LoadingLayer::create();
    scene->addChild(layer);
    return scene;
}

bool LoadingLayer::init()
{
    if (! Layer::init())
        return false;

    // 异步加载图片
    Director::getInstance()->getTextureCache()->addImageAsync("Atlas.png", CC_CALLBACK_1(LoadingLayer::loadingCallback, this));

    return true;
}

void LoadingLayer::loadingCallback(Texture2D *texture)
{
    // 加载精灵帧
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile("Atlas.plist", texture);

    Size visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
    auto splashSprite = Sprite::createWithSpriteFrameName(splash);
    splashSprite->setPosition(origin.x + visibleSize.width / 2, origin.y + visibleSize.height / 2);
    this->addChild(splashSprite);

    // 加载音频资源
    auto audioEngine = SimpleAudioEngine::getInstance();
    audioEngine->preloadEffect(die_effect);
    audioEngine->preloadEffect(hit_effect);
    audioEngine->preloadEffect(touch_effect);
    audioEngine->preloadEffect(swooshing_effect);
    audioEngine->preloadEffect(wing_effect);

    // 创建并缓存动画效果
    createAndCacheBirdAnimation();

    // 跳转到Welcome Scene
    auto scene = TransitionFade::create(1.0f, WelcomeLayer::createScene());
    Director::getInstance()->replaceScene(scene);
}

void LoadingLayer::createAndCacheBirdAnimation()
{
    auto animationCache = AnimationCache::getInstance();
    auto spriteFrameCache = SpriteFrameCache::getInstance();

    // 黄色小鸟动画
    Animation *yellowBirdAnimation = Animation::create();
    yellowBirdAnimation->setDelayPerUnit(1 / 10.0f);
    yellowBirdAnimation->addSpriteFrame(spriteFrameCache->getSpriteFrameByName(yellow_bird_1));
    yellowBirdAnimation->addSpriteFrame(spriteFrameCache->getSpriteFrameByName(yellow_bird_2));
    yellowBirdAnimation->addSpriteFrame(spriteFrameCache->getSpriteFrameByName(yellow_bird_3));
    animationCache->addAnimation(yellowBirdAnimation, "yellow_bird_animation");

    // 蓝色小鸟动画
    Animation *blueBirdAnimation = Animation::create();
    blueBirdAnimation->setDelayPerUnit(1 / 10.0f);
    blueBirdAnimation->addSpriteFrame(spriteFrameCache->getSpriteFrameByName(blue_bird_1));
    blueBirdAnimation->addSpriteFrame(spriteFrameCache->getSpriteFrameByName(blue_bird_2));
    blueBirdAnimation->addSpriteFrame(spriteFrameCache->getSpriteFrameByName(blue_bird_3));
    animationCache->addAnimation(blueBirdAnimation, "blue_bird_animation");

    // 红色小鸟动画
    Animation *redBirdAnimation = Animation::create();
    redBirdAnimation->setDelayPerUnit(1 / 10.0f);
    redBirdAnimation->addSpriteFrame(spriteFrameCache->getSpriteFrameByName(red_bird_1));
    redBirdAnimation->addSpriteFrame(spriteFrameCache->getSpriteFrameByName(red_bird_2));
    redBirdAnimation->addSpriteFrame(spriteFrameCache->getSpriteFrameByName(red_bird_3));
    animationCache->addAnimation(redBirdAnimation, "red_bird_animation");
}

这样,在以后的游戏场景中,我们就可以直接从SpriteFrameCache、AnimationCache等缓存类中加载资源了。

游戏欢迎界面的实现

每一个游戏在进入后都会先到达欢迎界面,我们也来效仿一下。

我们的游戏界面主要由以下几个元素组成:

游戏背景

说白了就是一张图片,原版《Flappy Bird》游戏中有两种不同风格的背景图,一张是“白天”背景图,一张是“黑夜”背景图。我们也依葫芦画瓢,根据当前的时间来显示不同的背景图片,这样看起来是不是有趣多了。哈哈…

我们先写一个工具函数,获取当前时间并判断当前是白天还是黑夜,放在TimeUtils.cpp文件中:

#include "TimeUtils.h"
#include "time.h"
#include "cocos2d.h"

USING_NS_CC;

bool isDayTime()
{
    time_t timep = time(nullptr);
    struct tm *p = localtime(&timep);
    int curHour = p->tm_hour;
    if (6 <= curHour && curHour <= 17)
        return true;
    return false;
}

有了它,我们就可以判断什么时候加载“白天”背景图,什么时候加载“黑夜”背景图了:

    Sprite *bgSprite = nullptr;
    // 根绝白天/黑夜显示不同的背景图
    if (isDayTime())
        bgSprite = Sprite::createWithSpriteFrameName(bg_day);
    else
        bgSprite = Sprite::createWithSpriteFrameName(bg_night);
    bgSprite->setPosition(origin.x + visibleSize.width / 2, origin.y + visibleSize.height / 2);
    this->addChild(bgSprite);

滚动的地面

怎么让地面运动起来呢?很简单,我们可以创建两张一模一样的“地面”图片,把它们相邻放在场景中。然后…然后…用一个调度器不断地往左边移动图片位置,当地一张图片移动到屏幕之外就将这两张图片“复位”也就是恢复到原来的位置,然后又重新开始移动…这样周而复始,地面就动起来啦。实现如下:

1、添加两张地面图片,并开始scheduler:

  // 添加地面图片
    mLandSpriteA = Sprite::createWithSpriteFrameName(land);
    mLandSpriteA->setAnchorPoint(Vec2::ZERO);
    mLandSpriteA->setPosition(Vec2::ZERO);
    this->addChild(mLandSpriteA);
    mLandSpriteB = Sprite::createWithSpriteFrameName(land);
    mLandSpriteB->setAnchorPoint(origin);
    mLandSpriteB->setPosition(origin.x + mLandSpriteA->getContentSize().width, origin.y);
    this->addChild(mLandSpriteB);

    this->schedule(schedule_selector(WelcomeLayer::moveLandSprite), 0.01f);

2、通过周期性移动两张图片X轴的坐标让图片动起来:

void WelcomeLayer::moveLandSprite(float dt)
{
    mLandSpriteA->setPositionX(mLandSpriteA->getPositionX() - 1.0f);
    mLandSpriteB->setPositionX(mLandSpriteA->getPositionX() + mLandSpriteA->getContentSize().width - 1.0f);

    if (mLandSpriteB->getPositionX() <= 0)
        mLandSpriteA->setPositionX(0);
}

下面我们在游戏场景中也需要这样做,在游戏场景中也需要这样做,在游戏场景中也需要这样做…重要的事情说三遍…

其它元素

我们还有title、copy-right、开始按钮、小鸟动画需要添加。前两者就是一个精灵,之间创建然后添加到场景中就可以了,按钮的实现也很简单。小鸟动画比较“复杂”,我们就再下面讲吧。

小鸟精灵类

我们的游戏主角-小鸟在欢迎场景和游戏场景中都有出现。在游戏场景中,小鸟在不停地挥动翅膀,我们称之为Idle状态,在游戏场景中小鸟还有两个其它的状态:一个是一边飞行一边手受重力影响“下沉”的状态,一个是死亡后从屏幕中消失的状态。这里我们需要封装一下小鸟这个精灵类,具体实现可以参考源码,这里只是简单介绍一下:

小鸟颜色

在游戏素材中,我们又红蓝黄三种不同颜色的小鸟图片,每次创建小鸟时,我们随机选择一种颜色。新建一个继承自Sprite的Bird类,在其构造函数中随机选取颜色:

Bird::Bird()
{
    // 随机产生不同颜色的小鸟
    int type = int(CCRANDOM_0_1() * 10) % 3;
    switch (type) {
        case 0:
            mImageName = yellow_bird_1;
            mType = kYellowBird;
            break;
        case 1:
            mImageName = blue_bird_1;
            mType = kBlueBired;
            break;
        case 2:
            mImageName = red_bird_1;
            mType = kRedBird;
            break;
        default:
            CCASSERT(type == 0 || type == 1 || type == 2, "type取值不合法!");
            break;
    }
}

重写create和init方法

Sprite的子类一般都要自定义create方法,并重写相应的init方法。


Bird* Bird::create()
{
    Bird *pRet = new(std::nothrow) Bird();
    if (pRet && pRet->initWithSpriteFrameName(pRet->getImageName()))
    {
        pRet->autorelease();
        return pRet;
    }
    else
    {
        delete pRet;
        pRet = nullptr;
        return nullptr;
    }
}

bool Bird::initWithSpriteFrameName(const std::string& spriteFrameName)
{
    if (! Sprite::initWithSpriteFrameName(spriteFrameName))
        return false;

    // 初始化
    ActionInterval *up = MoveBy::create(0.4f, Vec2(0, 8));
    ActionInterval *down = up->reverse();
    Sequence *swingSeq = Sequence::create(up, down, nullptr);
    mSwingAction = RepeatForever::create(swingSeq);

    // 从缓存中获取小鸟动画
    auto animationCache = AnimationCache::getInstance();
    Animation *birdAnimation = nullptr;
    switch (mType) {
        case kYellowBird:
            birdAnimation = animationCache->getAnimation("yellow_bird_animation");
            break;
        case kBlueBired:
            birdAnimation = animationCache->getAnimation("blue_bird_animation");
            break;
        case kRedBird:
        default:
            birdAnimation = animationCache->getAnimation("red_bird_animation");
            break;
    }
    Animate *animate = Animate::create(birdAnimation);
    mIdleAction = RepeatForever::create(animate);

    return true;
}

三种不同状态

上面我们分析过,小鸟有三种不同的状态,每一种状态对应不同的Action。

void Bird::idel()
{
    this->runAction(mIdleAction);
    this->runAction(mSwingAction);
}

void Bird::fly()
{
    this->stopAction(mSwingAction);
}

void Bird::die()
{
    this->stopAllActions();
}

游戏分层

在之前的游戏场景Scene中,都只有一个层Layer。但在一个游戏场景中我们不可能吧所有的游戏逻辑都写在一个层中。这样会导致这个Layer过于臃肿,也会导致代码过于耦合,不利于游戏的开发和维护。

一般而言,游戏至少会有4层:
+ 背景层:负责背景的现实
+ 逻辑层(游戏层):处理游戏逻辑
+ 状态层:现实游戏的数据、状态等信息,如得分、血量等
+ 触摸层:处理触摸事件,与玩家进行交互

今天制作的这款游戏比较简单,我们把整个游戏场景分为三层,把触摸层和游戏层合并,即:
+ BackgroundLayer:背景层
+ GameLayer:游戏+触摸层
+ StatusLayer:状态层

把三层添加到游戏场景GameScene中:

bool GameScene::init()
{
    if (! Scene::initWithPhysics())
        return false;

    // 方便调试
    // this->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
    this->getPhysicsWorld()->setGravity(Vec2(0, -900));

    // 加入背景层
    auto backgroundLayer = BackgroundLayer::create();
    this->addChild(backgroundLayer);

    // 加入游戏逻辑层
    auto gameLayer = GameLayer::create();
    this->addChild(gameLayer);

    // 加入数据状态层
    auto statusLayer = StatusLayer::create();
    this->addChild(statusLayer);

    return true;
}

关于各层的实现,大家可以参看源码。

层与层之间的通信

最后,我们还需要讲一下在游戏场景中,层与层之间的通信。比如,在游戏层中,小鸟安全通过了一对水管,需要通知状态层将玩家得分加1,那么,游戏层如何通知状态层呢?最简单的方法就是让游戏层持有状态层实例,也就是再游戏层中保存一个StatusLayer实例。但是…但是…今天我们不使用这种方法。

Cocos2-x3.X后推出了EventListenerCustom和EventDispatcher接口可以让我们很方便的实现自定义事件。就拿游戏层通知状态层将玩家得分加1这一功能来讲讲。

1、我们现在游戏层注册score_change_event事件

// 分数改变事件监听
    auto changeScoreListener = EventListenerCustom::create("change_score_event", CC_CALLBACK_1(GameLayer::scoreChangeEventCustom, this));
    _eventDispatcher->addEventListenerWithFixedPriority(changeScoreListener, 1);

响应函数如下:

void GameLayer::scoreChangeEventCustom(EventCustom *event)
{

}

2、在状态层中,我们注册同样的score_change_event事件

 // 分数改变事件监听
    auto changeScoreListener = EventListenerCustom::create("change_score_event", CC_CALLBACK_1(StatusLayer::scoreChangeCallback, this));
    _eventDispatcher->addEventListenerWithFixedPriority(changeScoreListener, 1);

响应函数:

void StatusLayer::scoreChangeCallback(EventCustom *event) { log("StatusLayer::scoreChangeCallback");
    mCurScore++;
    char tmp[10];
    sprintf(tmp, "%d", mCurScore);
    mScoreLabel->setString(tmp);
}

3、当小鸟安全经过水管后,我们dispatch自定义事件

// 安全经过水管,分数加1
                _eventDispatcher->dispatchCustomEvent("change_score_event");

这样,无论是游戏层的scoreChangeEventCustom函数还是状态层的scoreChangeCallback函数都将被调用。是不是很神奇!!!除此之外,我们还可以传递自定义消息给回调函数,这里就不一一展开了,又兴趣的同学可以看看Cocos2d-x自带的test项目。

今天我们就简单得介绍如何利用Cocos2d-x3.8制作Flappy Bird游戏。源码在https://github.com/xiejingfa/FlappyBird。

你可能感兴趣的:(游戏,实例,cocos2d-x)