原创作品,转载请标明: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;
}
}
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这一功能来讲讲。
// 分数改变事件监听
auto changeScoreListener = EventListenerCustom::create("change_score_event", CC_CALLBACK_1(GameLayer::scoreChangeEventCustom, this));
_eventDispatcher->addEventListenerWithFixedPriority(changeScoreListener, 1);
响应函数如下:
void GameLayer::scoreChangeEventCustom(EventCustom *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);
}
// 安全经过水管,分数加1
_eventDispatcher->dispatchCustomEvent("change_score_event");
这样,无论是游戏层的scoreChangeEventCustom函数还是状态层的scoreChangeCallback函数都将被调用。是不是很神奇!!!除此之外,我们还可以传递自定义消息给回调函数,这里就不一一展开了,又兴趣的同学可以看看Cocos2d-x自带的test项目。
今天我们就简单得介绍如何利用Cocos2d-x3.8制作Flappy Bird游戏。源码在https://github.com/xiejingfa/FlappyBird。