Cocos2d-x,Cocos

Cocos2dx 3.2 横版过关游戏Brave学习笔记(五)

作者: douxt

这次我们要为游戏增加一些UI元素。

添加血条

原版教程中用了ProgressTimer来实现血条,这个基本可以照搬过来。
ProgressTimer是什么呢?cpp-tests里面的 ActionsProgressTest 里面有若干示例,例如:


以及



我们所需要的比较接近第二种情况,即一个横向的血条,从左到右血量从0%变化到100%。

用一个空血槽图片的Sprite做背景,上面放一个ProgressTimer, 通过设置ProgressTimer的进度来控制血条的长短。
建立一个Progress类来实现:
头文件:

#ifndef __Progress__
#define __Progress__
#include "cocos2d.h"
USING_NS_CC;
 
class Progress : public Sprite
{
public:
    bool init(const char* background, const char* fillname);
    /*
    the inputs are SpriteFrame Names.
    they should be loaded into SpriteFrameCache before calling this.
    */
    static Progress* create(const char* background, const char* fill);
 
    void setFill(ProgressTimer* fill){_fill=fill;}
 
    void setProgress(float percentage){_fill->setPercentage(percentage);}
 
private:
    ProgressTimer* _fill;
};
#endif

其实现为:

#include "Progress.h"
 
bool Progress::init(const char* background, const char* fillname)
{
    this->initWithSpriteFrameName(background);
    ProgressTimer* fill = ProgressTimer::create(Sprite::createWithSpriteFrameName(fillname));
    this->setFill(fill);
    this->addChild(fill);
 
    fill->setType(ProgressTimer::Type::BAR);
    fill->setMidpoint(Point(0,0.5));
    fill->setBarChangeRate(Point(1.0, 0));
    fill->setPosition(this->getContentSize()/2);
    fill->setPercentage(100);
    return true;
}
 
Progress* Progress::create(const char* background, const char* fillname)
{
    Progress* progress = new Progress();
    if(progress && progress->init(background,fillname))
    {
        progress->autorelease();
        return progress;
    }
    else
    {
        delete progress;
        progress = NULL;
        return NULL;
    }
}

init函数需要传入两个SpriteFrameName,所以UI资源需要提前载入:

SpriteFrameCache::getInstance()->addSpriteFramesWithFile("image/ui.plist","image/ui.pvr.ccz");

代码中设置了ProgressTimer的参数,值得注意的有:
setType:设置类型为ProgressTimer::Type::BAR,血条自然是条状类的。
setMidpoint:设置血条的起点为(0,0.5),即左侧的中间点。
setBarChangeRate:设置变化率为(1,0),即在x轴变化率为1,y轴不变化。
setPercentage:设置血条填充率为100,即满血状态。

然后在MainScene::init中加入如下代码:

_progress = Progress::create("player-progress-bg.png","player-progress-fill.png");
    _progress->setPosition(VisibleRect::left().x + _progress->getContentSize().width/2, VisibleRect::top().y - _progress->getContentSize().height/2);
    this->addChild(_progress);

每次需要设置坐标的时候,要通过Director获取窗口尺寸以及原点坐标,有些繁琐,可以利用cpp-tests中的VisibleRect类来简化输入,将源文件和头文件拷贝至Classes下,加入工程,包含头文件即可使用。

运行程序可以看到玩家的血条了。

然后我们还要给敌人增加血条。可以在Player类中添加,但是这样岂不是把玩家头上也加上血条了?
没关系,可以让玩家头上的血条默认隐藏。

在Player中增加私有变量_progress,并在init中进行初始化,增加bool型变量_isShowBar 用来通过角色类型判断是否显示。
Player::init中添加:

    auto size = this->getContentSize();
    _progress = Progress::create("small-enemy-progress-bg.png","small-enemy-progress-fill.png");
    _progress->setPosition( size.width*2/3, size.height + _progress->getContentSize().height/2);
    this->addChild(_progress);
    if(!_isShowBar)
    {
        _progress->setVisible(false);
    }

运行程序便可以看到玩家和敌人的血条都显示了。

增加暂停界面

在右上角增加一个暂停按钮,触摸按钮会暂停游戏,并弹出暂停菜单。从暂停菜单,可以返回开始界面(目前还没有),以及继续游戏。

先从简单的做起,先给界面增加一个暂停的按钮好了。
因为UI资源已经载入SpriteFrameCache,所以我们可以从这里面直接提取SpriteFrame来建立MenuItemImage 对象。但是MenuItemImage好像没有直接用spriteFrame的创建函数,不妨自己写个函数,简化操作。具体实现请下载代码参考。在MainScene::init中加入:

    auto pauseItem = CustomTool::createMenuItemImage("pause1.png", "pause2.png", CC_CALLBACK_1(MainScene::onTouchPause,this));
    pauseItem->setPosition(VisibleRect::right().x - pauseItem->getContentSize().width/2, 
                            VisibleRect::top().y - pauseItem->getContentSize().height/2);

下面定义并实现一下函数MainScene::onTouchPause:

void MainScene::onTouchPause(Ref* sender)
{
    _player->pause();
    _enemy1->pause();
    _enemy2->pause();
    auto layer = PauseLayer::create();
    this->addChild(layer,100);
}

原版教程里一个display.pause()就实现了暂停。
我现在暂时用手动暂停所有角色的方法来模拟。
有暂停就有恢复:

void MainScene::onTouchResume()
{
    _player->resume();
    _enemy1->resume();
    _enemy2->resume();
}

注意:当时第一次写,用的这个暂停方法不合适,尤其是当角色死亡之后对象被回收,这时再这样暂停会引发错误。更好的方法是用Director自带的pause和resume方法。
以后的版本里改正了这个问题。

上面的onTouchPause中创建了一个新的PauseLayer,并通过设置zOrder保证其覆盖在前面。
其实头文件如下:

#ifndef __PauseLayer__
#define __PauseLayer__
#include "cocos2d.h"
 
USING_NS_CC;
 
class PauseLayer : public LayerColor
{
public:
    bool init();
    CREATE_FUNC(PauseLayer);
 
    void addUI();
    void addTouch();
    void home(Ref* obj);
    void back(Ref* obj);
private:
    EventListenerTouchOneByOne* _listener;
};
 
#endif

实现为:

#include "PauseLayer.h"
#include "VisibleRect.h"
#include "CustomTool.h"
//#include "StartScene.h"
#include "MainScene.h"
 
bool PauseLayer::init()
{
    if(!LayerColor::init())
        return false;
 
    this->initWithColor(Color4B(162, 162, 162, 128));
 
    addUI();
    addTouch();
    return true;
}
 
void PauseLayer::addUI()
{
    auto background = Sprite::createWithSpriteFrameName("pause-bg.png");
    background->setPosition(VisibleRect::center());
    this->addChild(background);
 
    auto homeItem = CustomTool::createMenuItemImage("home-1.png","home-2.png",
                                                    CC_CALLBACK_1(PauseLayer::home,this));
 
    auto resumItem = CustomTool::createMenuItemImage("continue-1.png","continue-2.png",
                                                    CC_CALLBACK_1(PauseLayer::back,this));
 
    auto bgSize = background->getContentSize();
    homeItem->setPosition(bgSize.width/3, bgSize.height/2);
    resumItem->setPosition(bgSize.width*2/3,bgSize.height/2);
    auto menu = Menu::create(homeItem, resumItem, NULL);
    menu->setPosition(VisibleRect::leftBottom());
 
    background->addChild(menu);
 
}
 
void PauseLayer::addTouch()
{
    _listener = EventListenerTouchOneByOne::create();
    _listener->onTouchBegan =[&](Touch* touch, Event* event)
    {
        log("PauseLayer::addTouch");
        return true;
    };
    _listener->setSwallowTouches(true);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(_listener, this);
}
 
void PauseLayer::home(Ref* obj)
{
    _eventDispatcher->removeEventListener(_listener);
    this->removeFromParentAndCleanup(true);
    //auto start = StartLayer::createScene();
    //Director::getInstance()->replaceScene(start);
}
 
void PauseLayer::back(Ref* obj)
{
    _eventDispatcher->removeEventListener(_listener);
    auto main = (MainScene*)this->getParent();
    this->removeFromParentAndCleanup(true);
    main->onTouchResume();
}

PauseLayer继承了LayerColor类,通过initWithColor(Color4B(162, 162, 162, 128))实现了一种透明的灰色背景,并在此之上添加了一个精灵模拟弹出窗口,并在窗口上添加了两个按钮,分别是Home按钮,目前功能还没实现,还有一个Resume按钮,按了之后会继续游戏。
另外PauseLayer还将触屏事件进行了拦截,使其不被下层的对象接收到。

运行一下,效果为:



提交一下代码,名为"Note 5 add UI"

Cocos2dx 3.2 横版过关游戏Brave学习笔记(四)

作者: douxt

前几天我把笔记写的差不多了,突然觉得可以发到论坛和大家分享一下,感觉学习cocos2d的新人很多,能相互学习一下还是挺好的。

在发第一个帖子的时候出了点问题,有些文字莫名其妙的成了斜体……我查了一下,原来是代码中有数组用了下标i,这个[ i],被论坛识别为斜体标记了....晕

刚发现上次写的代码存在问题,即在角色正在行走中,如果再次点击,PlayAnimationForever会再次运行,出现了多个动画同时播放的情况。如果想要播放的和正在进行的动画相同,不应该停止然后在重新开始,因为这样会导致不连贯。所以我加了检查,通过getActionByTag检查是否正在进行所需的action,如果正在进行,则不做任何操作。好像原版也有这个问题。

不同的帧动画不应该同时播放,所以在播放一个帧动画之前,可以通过stopActionByTag把所有帧动画都停止掉。

另外,我移除了私有变量_seq,增加了一个枚举用于标记动作Tag。在walkTo中,用检查Tag方法来防止动作的重复进行。之前用Tag停止动作失败的原因,很有可能是不同的动作用了相同的Tag。

接下来我提交一下代码,名为"fix animation superposition" 。哈哈,好像提交代码上瘾了啊。

游戏中的状态机设计

Quick-Cocos2d-x内置了对状态机的支持,所以这里的状态机就要自己想办法了,初步的想法是设计一个状态机对象,然后让Player类持有一个状态机对象。当然也可以让Player继承状态机对象……不过我们先考虑用组合的方法把。

状态机的必备构件:
1.状态(State)
这里的状态有  idle,walking,attacking, dead 等。
先假设他们是互斥的。虽然一边walking一边attacking也是可能的。
2.事件(Event)
可以理解为指令,即要求满足一定条件的状态机改变状态到指定态。
例如
{name="walk", from="idle", to="walking"}
如果令状态机执行这个事件,则当其处于idle状态时,会变化至walking态。
所以状态机对象需要保存所有状态,以及所有的事件,以供使用。
3.动作(Action)
例如在进入dead状态后,角色需要播放dead动画,并移除自身。
每个状态都要提供一个函数如onIdleEnter,在进入这个态时调用,当然也可为空。
按理说退出一个状态也应该调用一个函数,如onIdleExit,不过我们暂时可以不用这个。

状态和事件是否需要单独设计class?如果是class是否要继承Ref?纠结了半天,也写了下Event类和State类,感觉直接用字符串表示状态也是可行的。所以果断删了,直接用字符串。

set<string> _states;用这个保存所有的状态,这里不应该有两个状态名字相同。
map<string, map<string, string>> _events; 用于保存所有的事件,形式为<eventName, <from, to>>
map<string, function<void()>> _onEnters;  保存每个态的回调函数,如果不为空就在进入状态时调用这个函数。
这个函数做什么用呢?当然是状态转换后的行为控制了。例如_onEnters["idle"]可以负责停止所有帧动画的播放。
_onEnters["dead"]让角色播放死亡动画,然后处理后事等等。
然后还需要保存当前状态,前一个状态。

折腾了半天,看了网上的资料,发现状态机也可以挺复杂,也参考了别人的简易状态机,还有状态机的数学语言定义等等……又发现了C++里的map容器可以用unordered_map,他的性能测试,set容器用法,map插入内容的方法……总算弄出一个能用的。

头文件如下:

#ifndef __FSM__
#define __FSM__
 
#include "cocos2d.h"
 
class FSM :public cocos2d::Ref
{
public:
 
    bool init();
    //Create FSM with a initial state name and optional callback function
    static FSM* create(std::string state, std::function<void()> onEnter = nullptr);
     
    FSM(std::string state, std::function<void()> onEnter = nullptr);
    //add state into FSM
    FSM* addState(std::string state, std::function<void()> onEnter = nullptr);
    //add Event into FSM
    FSM* addEvent(std::string eventName, std::string from, std::string to);
    //check if state is already in FSM
    bool isContainState(std::string stateName);
    //print a list of states
    void printState();
    //do the event
    void doEvent(std::string eventName);
    //check if the event can change state
    bool canDoEvent(std::string eventName);
    //set the onEnter callback for a specified state
    void setOnEnter(std::string state, std::function<void()> onEnter);
private:
    //change state and run callback.
    void changeToState(std::string state);
private:
    std::set _states;
    std::unordered_map<std::string,std::unordered_map<std::string,std::string>> _events;
    std::unordered_map<std::string,std::function<void()>> _onEnters;
    std::string _currentState;
    std::string _previousState;
};
 
#endif

现在不妨做个测试,可以先写到init里。

bool FSM::init()
{
    this->addState("walking",[](){cocos2d::log("Enter walking");})
        ->addState("attacking",[](){cocos2d::log("Enter attacking");})
        ->addState("dead",[](){cocos2d::log("Enter dead");});
 
    this->addEvent("walk","idle","walking")
        ->addEvent("walk","attacking","walking")
        ->addEvent("attack","idle","attacking")
        ->addEvent("attack","walking", "attacking")
        ->addEvent("die","idle","dead")
        ->addEvent("die","walking","dead")
        ->addEvent("die","attacking","dead")
        ->addEvent("stop","walking","idle")
        ->addEvent("stop","attacking","idle")
        ->addEvent("walk","walking","walking");
 
    this->doEvent("walk");
    this->doEvent("attack");
    this->doEvent("eat");
    this->doEvent("stop");
    this->doEvent("die");
    this->doEvent("walk");
    return true;
}

在MainScene::init中加入:

auto fsm = FSM::create("idle",[](){cocos2d::log("Enter idle");});

运行输出如下:


复制代码
  1. FSM::doEvent: doing event walk
  2. FSM::changeToState: idle -> walking
  3. Enter walking
  4. FSM::doEvent: doing event attack
  5. FSM::changeToState: walking -> attacking
  6. Enter attacking
  7. FSM::doEvent: cannot do event eat
  8. FSM::doEvent: doing event stop
  9. FSM::changeToState: attacking -> idle
  10. Enter idle
  11. FSM::doEvent: doing event die
  12. FSM::changeToState: idle -> dead
  13. Enter dead
  14. FSM::doEvent: cannot do event walk

第一个walk Event成功,idle -> walking
第二个attack Event成功,walking -> attacking
第三个eat Event失败,因为我们没有定义eat Event
第四个stop Event成功,attacking -> idle
第五个die Event 成功,idle -> dead
第六个walk Event失败,这也是我们期望的,因为死了之后不应该还能行走。

下面应该考虑在player中使用FSM, 可以新建一个私有成员持有一个实例。
在尝试过程中出了点故障,好久才搞定,原来是FSM create之后我没有retain,访问出问题了。
既然要retain,那就别忘了release。

我们先把以前的walkTo改变一下,让他用状态机来实现。

void Player::walkTo(Vec2 dest)
{
    std::function<void()> onWalk = CC_CALLBACK_0(Player::onWalk, this, dest);
    _fsm->setOnEnter("walking", onWalk);
    _fsm->doEvent("walk");
}

即现在是委托"walking"状态的回调函数来进行动作,回调函数是由另一个函数Player::onWalk bind得到的。
这个函数如下:

void Player::onWalk(Vec2 dest)
{
    log("onIdle: Enter walk");
    this->stopActionByTag(WALKTO_TAG);
    auto curPos = this->getPosition();
 
    if(curPos.x > dest.x)
        this->setFlippedX(true);
    else
        this->setFlippedX(false);
 
    auto diff = dest - curPos;
    auto time = diff.getLength()/_speed;
    auto move = MoveTo::create(time, dest);
    auto func = [&]()
    {
        this->_fsm->doEvent("stop");
    };
    auto callback = CallFunc::create(func);
    auto seq = Sequence::create(move, callback, nullptr);
    seq->setTag(WALKTO_TAG);
    this->runAction(seq);
    this->playAnimationForever(0);
}

这个函数和原来的walkTo基本一样除了:

auto func = [&]()
    {
        this->_fsm->doEvent("stop");
    };

这里的回调函数会使用状态机,将角色回到idle状态,而idle的回调函数会停止播放动画。
另外在上面的代码中有一句:
->addEvent("walk","walking","walking");
这个的作用是允许在从walking状态转换到walking状态,当点击屏幕时,walk的目的发生变化,即使在walking中也应该即刻改变目标。

现在的情况好像和之前一样,不一样的是现在用的是状态机。

然后 我做了一个名为Note 4 的commit.

Cocos2dx 3.2 横版过关游戏Brave学习笔记(三)

作者: douxt



原版的第二篇包含了动画和触摸响应。动画部分我费了好大工夫才搞定,所以就先把那部分当作第二篇了。另外我发现原来的教程基本上不会大段贴代码,只是把道理讲清楚,代码可以自己下载了慢慢看。我觉得这样很好,但有时候用代码说话比描述更清晰。

为了方便进行,代码做了些小改动,如将HelloWorld改名为Main,player,enemy1改为私有变量_player,_enemy1,将USING_NS_CC从HelloWorld的cpp文件移动到h文件等等,具体细节可以在:
https://github.com/douxt/Brave_cpp 查看


点击 commits 可以查看所有历史版本以及改动


点击<>便可以查看并下载这个版本的代码。

触摸响应

下一步给MainScene这个Layer添加触摸响应。
先从简单的开始,即触摸屏幕后,player角色会边播放行走动画,并移动到点击的屏幕位置。

给MainScene增加一个EventListenerTouchOneByOne* 类型的_listener_touch
给MainScene.h增加触摸响应函数:

bool onTouchBegan(Touch* touch, Event* event);
private:
EventListenerTouchOneByOne* _listener_touch;

并在 MainScene::init 里增加:

    _listener_touch = EventListenerTouchOneByOne::create();
    _listener_touch->onTouchBegan = CC_CALLBACK_2(MainScene::onTouchBegan,this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(_listener_touch, this);

触摸响应函数实现为:

bool MainScene::onTouchBegan(Touch* touch, Event* event)
{
    Vec2 pos = this->convertToNodeSpace(touch->getLocation());
    _player->walkTo(pos);
//    log("MainScene::onTouchBegan");
    return true;
}

这里调用了Player类的walkTo函数,还没定义,所以在Player头文件和源文件中定义一下。

void Player::walkTo(Vec2 dest)
{
    //stop current moving action, if any.
    if(_seq)
        this->stopAction(_seq);
     
    auto curPos = this->getPosition();
 
    //flip when moving backward
    if(curPos.x > dest.x)
        this->setFlippedX(true);
    else
        this->setFlippedX(false);
 
    //calculate the time needed to move
    auto diff = dest - curPos;
    auto time = diff.getLength()/_speed;
    auto move = MoveTo::create(time, dest);
    //lambda function
    auto func = [&]()
    {
        this->stopAllActions();
        _seq = nullptr;
    };
    auto callback = CallFunc::create(func);
    _seq = Sequence::create(move, callback, nullptr);
 
    this->runAction(_seq);
    this->playAnimationForever(0);
}

我曾经想用stopActionTag函数来停止指定的动作,不知为何竟然没成功。这里我索性将动作保存到私有变量_seq中,初始化为nullptr, 并在移动结束的回调中将其赋值为nullptr。
这样如果_seq不为空,则运动正在进行,需要把这个动作停下,否则可能出现多个MoveTo叠加的情况,这不是想要的结果。

然后通过现在位置和目标位置横坐标的比较,决定是否在X方向反转角色。接着根据动作计算所需时间。然后创建MoveTo动作,并定义了一个回调函数func,这是个lambda函数,具体资料可以在网上查到。

然后建立回调动作,将移动和回调组合为序列,然后运行这个序列,并开始播放这个角色的第一个动画walk.

现在运行一下,发现角色会向着点击的方向移动,移动到目的地后,动画会停止播放。

现在的版本为:

https://github.com/douxt/Brave_cpp/tree/51b653b01b5427d985e10483499487d450af31c2

Cocos2dx 3.2 横版过关游戏Brave学习笔记(二)

作者: douxt



动画

这次我们要让人物动起来。
首先要将精灵帧缓存中的帧组合成动画,然后将这些动画放到动画缓存中。
需要播放的时候直接从缓存中拿出来用即可。


目前这个项目里用到的帧的格式为:  player1-1-1.png, 即:角色-动作编号-动画帧编号.png
对于player1,有walk,attack,dead,hit,skill动作,每个动作的帧数可以不同,这里分别为,4,4,4,2,4
对于enemy1,有walk,attack,dead,hit,帧数分别为3,3,3,2
相关信息可以在Resources下的image/role.plist里看到。

对于不同的角色类型,加载动画时略参数略有不同,现在我们给Player类增加一些参数,记录这些不同之处,并在初始化函数中,根据角色类型的不同来给这些参数赋值。
首先建立一个字符串,记录动画的前缀,如PLAYER类型的角色_name值为"player1"。
还需要一个整数记录动作的数目,一个字符串数组记录动作的名称,以及一个整数数组记录每个动作的帧数。

在类Player的末端加入:

private:
    PlayerType _type;  
    std::string _name;
    int _animationNum; 
    std::vector _animationFrameNum;
    std::vector _animationNames;

然后修改InitWithPlayerType函数,使之如下:

bool Player::initWithPlayerType(PlayerType type)
{
    std::string sfName = "";
    _type = type;
    int animationFrameNum[5] ={4, 4, 4, 2, 4};
    int animationFrameNum2[5] ={3, 3, 3, 2, 0};
 
    //setup according to PlayerType
    switch(type)
    {
    case PlayerType::PLAYER:
        sfName = "player1-1-1.png";
        _name = "player1";
        _animationNum = 5;
        _animationFrameNum.assign(animationFrameNum, animationFrameNum + 5);
        break;
    case PlayerType::ENEMY1:
        sfName = "enemy1-1-1.png";
        _name = "enemy1";
        _animationNum = 4;
        _animationFrameNum.assign(animationFrameNum2, animationFrameNum2 + 5);
        break;
    case PlayerType::ENEMY2:
        sfName = "enemy2-1-1.png";
        _name = "enemy2";
        _animationNum = 4;
        _animationFrameNum.assign(animationFrameNum2, animationFrameNum2 + 5);
        break;
    }
    this->initWithSpriteFrameName(sfName);
    std::string animationNames[] = {"walk", "attack", "dead", "hit", "skill"};
    _animationNames.assign(animationNames, animationNames + 5);
    //load animation
    this->addAnimation();
    return true;
}

现在给Player类增加载入动画函数Player::addAnimation(),相同动画的载入只需要进行一次,所以载入前先确定所需动画还没有被载入。

void Player::addAnimation()
{
    // check if already loaded
    auto animation = AnimationCache::getInstance()->getAnimation(String::createWithFormat("%s-%s",_name.c_str(),
                        _animationNames[0])->getCString());
    if(animation)
        return;
     
    for(int i1=0; i1<_animationNum; i1++)     {         auto animation = Animation::create();         animation->setDelayPerUnit(0.2f);
        //put frames into animation
        for(int j = 0; j< _animationFrameNum[i1] ; j++)         {             auto sfName =String::createWithFormat("%s-%d-%d.png",_name.c_str(), i1+1, j+1)->getCString();
            animation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName(sfName));
        }
        // put the animation into cache
        AnimationCache::getInstance()->addAnimation(animation, String::createWithFormat("%s-%s",_name.c_str(), 
                    _animationNames[i1].c_str())->getCString());
    }
}

现在加入一个根据动作编号进行指定动作的函数,查看动画是否载入成功。

void Player::playAnimationForever(int index)
{
    if(index = _animationNum)
    {
        log("illegal animation index!");
        return;
    }
    auto str = String::createWithFormat("%s-%s",_name.c_str(), _animationNames[index].c_str())->getCString();
    auto animation = AnimationCache::getInstance()->getAnimation(str);
    auto animate = Animate::create(animation);
    this->runAction(RepeatForever::create(animate));
}

在HelloWorld::init()的末端加入如下语句,然后运行。

//add player
    Player* player = Player::create(Player::PlayerType::PLAYER);
    player->setPosition(origin.x + player->getContentSize().width/2, origin.y + visibleSize.height/2);
    this->addChild(player);
 
 
    //add enemy1
    Player* enemy1 = Player::create(Player::PlayerType::ENEMY1);
    enemy1->setPosition(origin.x + visibleSize.width - player->getContentSize().width/2, origin.y + visibleSize.height/2);
    this->addChild(enemy1);
 
    //test animation
    player->playAnimationForever(1);
    enemy1->playAnimationForever(1);

结果如下:



用GitHub来管理源代码是个不错的选择。
项目的代码经常会进行删减,随着笔记的进行,最后的代码可能和前期相比面目全非了,但是使用GitHub可以让你访问任意历史版本。
我刚刚进行了一个名为“Note2”的commit,如果有人想下载代码,可以点下面链接,点下载Zip即可。
https://github.com/douxt/Brave_cpp/tree/8752cc055601800eb10ed73d9ea757993dfa352a
即使以后更新了,这个历史版本还是可以访问。

注意:代码中没有包含cocos2d目录,可以将其他项目的cocos2d目录拷贝到项目目录下。

Cocos2dx 3.2 横版过关游戏Brave学习笔记(一)

作者: douxt

最近看了一下《使用Quick-Cocos2d-x搭建一个横版过关游戏》这个教程。当然这个游戏的名字叫做Brave

我不太懂lua,但感觉代码意思基本能看出来,就想用C++重新写一下这个示例,一方面了解一下lua,另一方面还可以熟悉一下用C++的开发。本人小白,免不了会犯些愚蠢的错误,不过犯了错只要能发现就是进步。

环境:
Cocos2dx 3.2
VS2012
Win32平台

资源及源码:
https://github.com/douxt/Brave_cpp   (不包含cocos2d文件夹,大概4.3MB)
点击 Download Zip 即可下载。
强烈建议自己新建项目,可以避免一些版本相关的错误。

目录

Cocos2dx 3.2  横版过关游戏Brave学习笔记(一)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(二)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(三)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(四)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(五)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(六)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(七)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(八)

1.创建工程

打开命令行,输入:

cocos new -l cpp -d e:/projects/   Brave_cpp

这个命令会帮你创建一个新的HelloWorld工程,而且把整个cocos2d的源码给你拷了一份放在工程下面。

运行一下这个工程,什么,竟然报错了。果断关闭所有VS窗口,重新打开运行,发现竟然正常运行了。
将资源文件复制到Resource文件夹下面备用。
删除掉init中按钮,精灵,标签等代码,让程序变成只显示一个黑屏。

给游戏添加角色

添加背景:

加入代码,加完后init函数内容如下:

bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    
    Size visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
   
    Sprite* background = Sprite::create("image/background.png");
    background->setPosition(origin + visibleSize/2);
    this->addChild(background);

    return true;
}

这样应该就可以显示出背景了。咦,Vec2类型竟然可以直接与Size类型相加?Size可以支持除法?这是怎么一回事呢?看一下Size源码,Size类定义了除法运算,Vec2难道定义了对Size的加法?不对,Vec2里没有关于Size的加法运算符。那就是Size可以自动转化成Vec2了?看到Size中有个如下的定义:

public:
    operator Vec2() const
    {
        return Vec2(width, height);
    }

精确的含义我没查到,不过应该可以理解为:如果有人把Size当作Vec2,就按这个方法给他一个Vec2.

冥冥之中自有天意,本来应该是一时的手误竟然背后还隐藏着道理……另外,Size还重载了=操作符,可以用Vec2给Size赋值。

导入精灵帧资源
继续在init函数中加入:

 SpriteFrameCache::getInstance()->addSpriteFramesWithFile("image/role.plist","image/role.pvr.ccz");

这个资源里包含了一些玩家、敌人动作的精灵帧,需要提前导入缓存。

显示玩家和怪物

玩家应该是一个类,可以继承自Sprite。原文中为Player建立了一个类,为Enemy1也建立了一个类,然后在后面出现Enemy2的时候又建立了一个类。
我觉得这几个类可以用一个类代替,因为他们属性十分类似,目前只是贴图不同,仅仅因为这个就建立这么多类不太划算。
添加的Player.h如下:

#ifndef __Player__
#define __Player__
#include "cocos2d.h"
USING_NS_CC;

class Player : public Sprite
{
public:
 enum PlayerType
 {
  PLAYER,
  ENEMY1,
  ENEMY2
 };

 bool initWithPlayerType(PlayerType type);

 static Player* create(PlayerType type);


};

#endif

目前先考虑三类角色,player , enemy1, enemy2。创建时,用枚举类型指定是哪一种。

Player.cpp 如下:

#include "Player.h"

bool Player::initWithPlayerType(PlayerType type)
{
 std::string spName = "";
 switch(type)
 {
 case PlayerType::PLAYER:
  spName = "player1-1-1.png";
  break;
 case PlayerType::ENEMY1:
  spName = "enemy1-1-1.png";
  break;
 case PlayerType::ENEMY2:
  spName = "enemy2-1-1.png";
  break;
 }
 this->initWithSpriteFrameName(spName);
 return true;
}


Player* Player::create(PlayerType type)
{
 Player* player = new Player();
 if(player && player->initWithPlayerType(type))
 {
  player->autorelease();
  return player;
 }
 else
 {
  delete player;
  player = NULL;
  return NULL;
 }
}

可以看出,其核心就是根据不同的角色类型,用不同的SpriteFrameName来初始化精灵。

继续在HelloWorld的init类中加入:

 Player* player = Player::create(Player::PlayerType::PLAYER);
 player->setPosition(origin.x + player->getContentSize().width/2, origin.y + visibleSize.height/2);
 this->addChild(player);

 Player* enemy1 = Player::create(Player::PlayerType::ENEMY1);
 enemy1->setPosition(origin.x + visibleSize.width - player->getContentSize().width/2, origin.y + visibleSize.height/2);
 this->addChild(enemy1);

运行程序,结果如下:



刚开始用git,发现项目目录下好多文件是不需要提交的,比如lib文件什么的。虽说可以靠只add需要的文件来实现,但系统总是有提示。
在项目目录下建立一个.gitignore文件,可以使其忽略没必要上传的文件。
例如:


  1. *.[oa]
  2. *.lib
  3. *.sdf
  4. *.opensdf
  5. cocos2d/
  6. .git*
  7. proj.win32/Debug.win32/
  8. proj.android/
  9. proj.ios_mac/
  10. proj.linux/
  11. proj.wp8-xaml/

这样提交,上传就感觉顺多了。

Cocos2d-x塔防游戏_贼来了10——选择关卡

原创: 任珊


Cocos2d-x塔防游戏_贼来了1——基础知识储备
Cocos2d-x塔防游戏_贼来了2——地图的创建加载
Cocos2d-x塔防游戏_贼来了3——进攻的敌人
Cocos2d-x塔防游戏_贼来了4——创建炮塔
Cocos2d-x塔防游戏_贼来了5——触摸响应
Cocos2d-x塔防游戏_贼来了6——触摸响应2
Cocos2d-x塔防游戏_贼来了7——数据管理与碰撞检测
Cocos2d-x塔防游戏_贼来了8——批量添加敌人
Cocos2d-x塔防游戏_贼来了9——关卡数据

综述

目前我们的塔防游戏已经算是有模有样了,接下来在最后一部分的代码中,我们将为这款贼来了游戏锦上添花,为它添加选关功能、预加载功能、音乐音效以及一些不同的游戏场景和粒子渲染效果。

该游戏的完整代码已更新,大家可以点击这里进行下载。

本章教程我们将会先教大家实现以下内容:

  • 滑动式UI界面。左右滑动界面实现不同“场景”的切换。
  • 关卡解锁模式和选关功能。根据玩家所选择的关卡进入不同游戏场景,当第 N 关成功过关的时候,解锁第 N+1 关。

滑动式UI界面

UI界面能直观的指引玩家进行游戏操作,好的UI设计可以吸引玩家,提升一款游戏的品味。所以本款游戏将采用交互性很强的滑动式UI界面,同时结合一些粒子特效,为用户提供更友好的界面设计。

那如何实现滑动式的UI啦!其实在Cocos2d-x 3.0中已经封装了可供用户实现滚动操作的ScrollView控件类,但它的功能还不是特别强大,使用起来不是很理想,因为它只可以实现简单的滑动效果,不能满足我们的需要(滑动一下刚好滑动到指定的下一个位置),所以这里我们必须重写一个类来实现滚动操作,设计思路如下:

我们定义一个继承于Layer的LevelLayer类,用它来作为滑动页面的容器层,其可视化大小固定。再定义一个类,继承于Node,根据使用需要依次把这个类的对象作为LevelLayer类的子节点添加到其上,间隔宽度(高度)为LevelLayer的宽(高)。在LevelLayer中实现触摸监听,如果是滑动事件,就执行滚屏的操作,并且在触摸事件完成后跳转到当前子节点的位置;如果是点击事件,则交由当前子节点处理。最后再定义一个场景,把滚动层添加到其上。层级关系如下:

容器层

下面我们先来看看如何定义作为滑动页面的容器层(LevelLayer)。先看代码:

class LevelLayer: public Layer
{
private:    
    int pageNode;
    int curPageNode;
    Point touchDownPoint;
    Point touchUpPoint;
    Point touchCurPoint;
    float WINDOW_WIDTH;
    float WINDOW_HEIGHT;
    void goToCurrNode();

public:
    LevelLayer();
    ~LevelLayer();

    virtual bool init();
    static cocos2d::Scene* createScene();
    CREATE_FUNC(LevelLayer);

    void addNode(Node *level);
    void menuCloseCallback(Ref* pSender);
    bool onTouchBegan(Touch *pTouch, Event  *pEvent);
    void onTouchMoved(Touch *pTouch, Event  *pEvent);
    void onTouchEnded(Touch *pTouch, Event  *pEvent);  
};

在该类中pageNode属性是LevelLayer层中总共的子页数,curPageNode表示当前显示的第几个页节点,0表示第1页,1表示第2页,依次类推。属性touchDownPoint、touchUpPoint、touchCurPoint分别用来记录触摸屏幕的按下点、触摸屏幕抬起点和当前触摸点,我们将通过它们计算并控制LevelLayer层的滚动。WINDOW_WIDTH、WINDOW_HEIGHT是LevelLayer的固定宽高。

goToCurrNode方法在触摸事件完成后根据当前滑动偏移量跳转到当前子页面,addNode方法用于向LevelLayer中添加子节点。下面是它们的实现方法:

void LevelLayer::addNode(Node *level)
{
    if (level)
    {
        level->setContentSize(Size::Size(WINDOW_WIDTH, WINDOW_HEIGHT));
        level->setPosition(Point(WINDOW_WIDTH * pageNode, 0));
        this->addChild(level);
        pageNode++;
    }
}

void LevelLayer::goToCurrNode()
{
    this->runAction(MoveTo::create(0.4f, Point::Point(-curPageNode * WINDOW_WIDTH, 0)));
}

addNode方法添加一个节点到LevelLayer上。节点的大小固定,位置依次向后排开,所以当有多个节点添加到LevelLayer时,它们就像排队照相的人一样,谁站在布景幕的位置,谁就能被显示在照相机里。每添加一个人,记录总数的pageNode就加1。
跳转的概念其实就是让这些排队照相的人向前或向后移动到指定的位置,所以这里我们只需要让整个队伍(也就是LevelLayer层)runAction就能实现移动。

在LevelLayer中最重要的是我们必须实现触摸监听,所以下面来一起看看其触摸回调函数如何实现吧。

bool LevelLayer::onTouchBegan(Touch *touch, Event  *event)
{
    touchDownPoint = touch->getLocation();
    touchCurPoint = touchDownPoint;
    return true;
}

void LevelLayer::onTouchMoved(Touch *touch, Event  *event)
{
    Point touchPoint = touch->getLocation();
    auto currX = this->getPositionX() + touchPoint.x - touchCurPoint.x;
    Point posPoint = Point::Point(currX, getPositionY());
    auto dis= fabsf(touchPoint.x - touchCurPoint.x);
    if (dis >= SHORTEST_SLIDE_LENGTH ) {
        this->setPosition(posPoint);
    }
    touchCurPoint = touchPoint;

}

void LevelLayer::onTouchEnded(Touch *touch, Event  *event)
{
    touchUpPoint = touch->getLocation();
    auto dis= touchUpPoint.getDistance(touchDownPoint);
    auto sprite1 =Sprite::createWithSpriteFrameName("page_mark1.png");
    auto width = sprite1->getContentSize().width;
    if (dis >= SHORTEST_SLIDE_LENGTH )
    {
        int offset = getPositionX() - curPageNode * (-WINDOW_WIDTH);
        if (offset > width) {
            if (curPageNode > 0) {
                --curPageNode;
                Sprite *sprite =  (Sprite *)LevelScene::getInstance()->getChildByTag(888);
                sprite->setPosition(Point(sprite->getPositionX()-width,sprite->getPositionY()));
            }
        }
        else if (offset < -width) {
            if (curPageNode < (pageNode - 1)) {
                ++curPageNode;
                Sprite *sprite =  (Sprite *)LevelScene::getInstance()->getChildByTag(888);
                sprite->setPosition(Point(sprite->getPositionX()+width,sprite->getPositionY()));
            }
        }
        goToCurrNode();
    }
}

在开始触摸屏幕时,记录下触摸按下点和当前触摸点;在屏幕上移动时,记录下当前的触摸点并计算滑动的距离,如果滑动的距离超过我们给出的最短滑动长度,那么我们就设置LevelLayer的位置到移动的地方;在结束触摸时,同样记录抬起的点并计算按下点与抬起点的之间的距离,当这一距离大于给定的SHORTEST_SLIDE_LENGTH时,根据具体情况跳转到其他位置,同时设置游动标记的位置。
result2

子选项

本游戏中一共有三页关卡选择项,在每页选择项上又有六个小的按钮。这里玩家选择哪个按钮,就会进入哪个关卡。因为每页选择项的背景图片,以及它的子按钮所代表的关卡数都是不一样的,所有我们需要充分的考虑到这些因素,抽象出一个高耦合的类模块来实现它。
接下来我们将来看看这个关卡选择页面(LevelSelectPage)的实现方法,其继承于Node,下面是其定义:

class LevelSelectPage: public Node
{
public:

    bool initLevelPage(const std::string& bgName, int level);
    static LevelSelectPage* create(const std::string& bgName, int level);
    void menuStartCallback(Ref* pSender);
};

LevelSelectPage类很简单,create是创建LevelSelectPage对象的静态方法,而initLevelPage方法则会初始化LevelSelectPage对象的背景,菜单等属性。
当玩家点了LevelSelectPage上的按钮时,将会触发menuStartCallback按钮回调函数。

我们先来看看LevelSelectPage的初始化,代码如下:

bool LevelSelectPage::initLevelPage(const std::string& bgName, int level)
{
    if (!Node::init())
    {
        return false;
    }
    // 1
    auto size = Director::getInstance()->getWinSize();
    auto sprite =Sprite::create(bgName);
    sprite->setPosition(Point(size.width / 2,size.height / 2));
    addChild(sprite,-2);
    // 2
    Vector menuItemVector;
    auto buttonSize = Sprite::create("card_1.png")->getContentSize();
    auto gap = buttonSize.width / 4;
    auto startWidth =( size.width -  LEVEL_ROW* buttonSize.width - (LEVEL_ROW - 1)*gap ) /2 ;
    auto startHeight = ( size.height + (LEVEL_COL - 1) * gap + buttonSize.height * LEVEL_COL ) /2 - buttonSize.height;
    for ( int row = 0; row < LEVEL_COL; row++ )
    {
        auto height = startHeight - ( buttonSize.height + gap ) * row ;
        for ( int col = 0; col < LEVEL_ROW; col++)
        {
            auto width = startWidth + ( buttonSize.width + gap ) * col ;
            auto item = MenuItemImage::create(
                                              "card_1.png",
                                              "card_2.png",
                                              "card_3.png",
                                              CC_CALLBACK_1(LevelSelectPage::menuStartCallback, this));
            item->setAnchorPoint(Point(0 ,0));
            item->setPosition(Point(width, height));
            item->setTag(row * LEVEL_ROW + col + level * LEVEL_ROW * LEVEL_COL);
            menuItemVector.pushBack( item );
            auto levelNum = UserDefault::getInstance()->getIntegerForKey("levelNum");

            if(levelNum < row * LEVEL_ROW + col + level * LEVEL_ROW * LEVEL_COL)
            {
                item->setEnabled(false);
            }
        }
    }
    auto levelMenu = Menu::createWithArray(menuItemVector);
    levelMenu->setPosition(Point::ZERO);
    this->addChild(levelMenu, -1);

    return true;
}
  1. 根据参数给的图片名,创建选项页的背景。
  2. 创建一组按钮项,依次排开。这里需要注意的是,我们为每个按钮都设置了一个标记,通过这个标记我们可以知道玩家选择的是哪个关卡。

在按钮回调函数中我们会获取玩家所选择按钮的标记值,再根据该值读取响应的关卡数据。代码如下:

void LevelSelectPage::menuStartCallback(Ref* pSender)
{
    auto button = (Sprite *)pSender;
    SimpleAudioEngine::getInstance()->playEffect(FileUtils::getInstance()->fullPathForFilename("sound/button.wav").c_str(), false);

    char buffer[20] = { 0 };
    sprintf(buffer, "levelInfo_%d.plist", button->getTag());
    std::string strName =  buffer;
    UserDefault::getInstance()->setStringForKey("nextLevelFile", strName);

    Scene *scene = Scene::create();
    auto layer = LevelInfoScene::create();
    scene->addChild(layer);
    auto sence = TransitionFade::create(0.5, scene);
    Director::getInstance()->replaceScene(sence);
}

下面分别是第0关和第1关的关卡:

游戏中还有一点要提的是,我们通过UserDefault数据来判断游戏解锁关卡数、游戏当前要进入关卡的关卡名,以及已通关关卡的分数(星星数)。所有在程序中大家会看到在很多地方都在set键值,get键值,这里就不一一解释了,看到程序应该都能明白。

OK,现在我们就可以创建一个场景(LevelScene),把LevelLayer层和这些子选项按层级依次加到里面了,详见代码。

当然为了美观,我们在场景中加入了如同下雪一样的粒子特效,关于该效果的实现可阅读使用Cocos2d-x和粒子编辑器实现“天天爱消除”场景特效一文。

Cocos2d-x塔防游戏_贼来了9——关卡数据

原创: 任珊

Cocos2d-x塔防游戏_贼来了1——基础知识储备
Cocos2d-x塔防游戏_贼来了2——地图的创建加载
Cocos2d-x塔防游戏_贼来了3——进攻的敌人
Cocos2d-x塔防游戏_贼来了4——创建炮塔
Cocos2d-x塔防游戏_贼来了5——触摸响应
Cocos2d-x塔防游戏_贼来了6——触摸响应2
Cocos2d-x塔防游戏_贼来了7——数据管理与碰撞检测
Cocos2d-x塔防游戏_贼来了8——批量添加敌人
Cocos2d-x塔防游戏_贼来了10——选择关卡
Cocos2d-x塔防游戏_贼来了11——完善游戏

读取游戏关卡数据

关卡设计对于塔防游戏是必须的,通过关卡设计玩家可以尝试各种不同风格和难度的游戏。它是游戏的重要组成部分,游戏的节奏、难度等级等方面很大程度上要依靠关卡来控制。而在各关中,敌人波数、敌人个数、地图、金币等等信息都是不同的。如果每个关卡都循规蹈矩的重构代码,那它的重用率将会很高,程序的藕合度也将很低,因此,这里我们很有必要把这些数据信息收集起来统一管理。这样,在游戏场景中我们就可以在不同的关卡中重用相同的一套逻辑了。

但是,现在另一个问题又出现了,我们该如何储存和处理这些数据啦?

我们可以把游戏中的数据分为静态数据和动态数据两种:

  • 动态数据是指游戏运行和运营过程中不断变动的数据,这些数据会随着玩家在游戏世界中执行各种行为的不同而发生改变,如本游戏中的分数。一般简单的数据可以使用Cocos2d-x中的UserDefault来进行动态数据的存储,大型的数据则会更倾向于用SQLite来进行存储。所以,在开发过程中应该根据需求来选择数据存储方案。
  • 静态数据则是程序中的只读数据,如资源名,敌人起始血量,起始金币数等等。然而,为了达到最佳的游戏效果或方便测试,这些数据在开发过程中可能是经常变动的。所以为了便于修改,一般会把这些数据放到外部文件中进行保存,杜绝硬编码。

现在回到游戏,我们第一步需要做的是抽象出一组有关关卡信息的静态数据,然后把它们写到文件中,便于读取。

如果是简单数据的读取,我们除了使用常用的格式之外,我们还可以用Cocos2d-x最常用的plist来读取。plist是基于XML的纯文本格式,随便找个文本编辑器就可以编辑。当然,如果你使用的是OS X系统,那在XCode中可以直接创建和编辑plist文件。下面我们就来和大家共同学习一下plist。

根据本游戏关卡的特征,我们抽象出了包括如下所示的一系列数据:



把这些关卡数据写入plist文件后,第二步就可以设计一个类来解析读取数据了。要解析plist文件可以参考Cocos2d-x类库中的SpriteFrameCache类和ParticleSystem类,它们使用ValueMap类来对plist文件进行操作。下图是创建好的plist文件:



说了那么多,接下来我们还是来看看代码吧。如下所示:

class LoadLevelinfo: public Ref
{
public:    
    ~LoadLevelinfo();
    static LoadLevelinfo * createLoadLevelinfo(const std::string& plistpath);

    bool initPlist(const std::string& plistpath);
    void readLevelInfo();
    void clearAll();   
private:
    ValueMap resources;
    ValueMap levelInfo;
};

变量resources是关卡待加载的资源数据,levelInfo是关卡信息数据。initPlist方法根据plist文件路径加载并读取游戏相关数据,readLevelInfo则是读取并保存plist文件中所有属性的值。而这些值都被保存在GameManger中,如下就是GameManger中增加的属性,它们基本上都是用来存储从plist文件中解析的关卡数据的。

    CC_SYNTHESIZE(int, money, Money);
    CC_SYNTHESIZE(int, groupNum, GroupNum);
    CC_SYNTHESIZE(std::string, curMapName, CurMapName);
    CC_SYNTHESIZE(std::string, currLevelFile, CurrLevelFile);
    CC_SYNTHESIZE(std::string, nextLevelFile, NextLevelFile);
    CC_SYNTHESIZE(bool, isFinishedAddGroup, IsFinishedAddGroup);
    CC_SYNTHESIZE(std::string, curBgName, CurBgName);

接下来是initPlist和readLevelInfo的实现方法,如下所示:

bool LoadLevelinfo::initPlist(const std::string& plistpath)
{ 
    bool bRet = false;
    do
    {
        // 1
        std::string fullPath = FileUtils::getInstance()->fullPathForFilename(plistpath);
        ValueMap dict = FileUtils::getInstance()->getValueMapFromFile(fullPath);  
        // 2    
        resources = dict["resources"].asValueMap();
        levelInfo = dict["levelInfo"].asValueMap();   
        bRet = true;
    }
    while (0);
    return bRet;
}

void LoadLevelinfo::readLevelInfo()
{
    GameManager *instance = GameManager::getInstance();
    // 3
    auto money =   levelInfo["money"].asFloat();
    instance->setMoney(money);
    auto currlevel =   levelInfo["currlevel"].asString();
    instance->setCurrLevelFile(currlevel);
    auto nextlevel =   levelInfo["nextlevel"].asString();
    instance->setNextLevelFile(nextlevel);

    ValueMap& groupDict = levelInfo["group"].asValueMap();
    auto groupTotle = groupDict.size();
    instance->setGroupNum(groupTotle);

    for (auto iter = groupDict.begin(); iter != groupDict.end(); ++iter)
    {
        ValueMap& group = iter->second.asValueMap();
        std::string spriteFrameName = iter->first;
        auto type1Num = group["type1Num"].asInt();
        auto type2Num = group["type2Num"].asInt();
        auto type3Num = group["type3Num"].asInt();
        auto type1Hp = group["type1Hp"].asInt();
        auto type2Hp = group["type2Hp"].asInt();
        auto type3Hp = group["type3Hp"].asInt();

        GroupEnemy* groupEnemy = GroupEnemy::create()->initGroupEnemy(type1Num, type1Hp, type2Num, type2Hp, type3Num, type3Hp);
        instance->groupVector.pushBack(groupEnemy);
    }

    auto curMapName =   resources["map"].asString();
    instance->setCurMapName(curMapName);
    auto curBgName =   resources["image"].asString();
    instance->setCurBgName(curBgName);
}
  1. plistpath是.plist文件的相对路径,这里通过FileUtils类获得给定文件名的完整路径,再把该文件中的内容(类型为Dictionary)加载到ValueMap的对象中保存。
  2. 放到Map中即可用Map的方法读取键为”id"的值是多少,分别读取dict对象中键为”resources",”levelInfo"的值,它们的类型依旧是Dictionary,所以依旧将其内容转换到ValueMap对象中保存。

  3. 根据plist文件的属性和层次特征,一层一层的遍历获得相应类型的键值,再把它们存储到GameManager中。
  4. 根据从plist文件中获得的敌人信息创建一波敌人(groupEnemy),并把它插入groupVector向量统一管理。

判断游戏是否结束

判断是否过关

当最后一波敌人添加完后,变量isSuccessful将被置为true。这也说明了玩家游戏已经顺利过关,该跳转到下一个界面了。

在update函数体中添加如下的代码段,实现最终分数的评比和场景跳转。

    if(isSuccessful)
    {
        isSuccessful = false;
        auto star = 0;
        auto playHp = this->getPlayHpPercentage();

        if( playHp > 0 && playHp <= 30){ star = 1;}         else if(playHp > 30 && playHp <= 60 ){ star = 2;}         else if(playHp > 60 && playHp <= 100 ){ star = 3;}         if( star > UserDefault::getInstance()->getIntegerForKey(instance->getCurrLevelFile().c_str()))
        {
            UserDefault::getInstance()->setIntegerForKey(instance->getCurrLevelFile().c_str(), star);
        }

        instance->clear();
        // 应该跳转到成功界面,这里暂时显示如下文字
        Size winSize = Director::getInstance()->getWinSize();
        auto putOutLabel = Label::createWithBMFont("fonts/boundsTestFont.fnt", "Congratulations!");
        putOutLabel->setPosition(Point(winSize.width / 2, winSize.height / 2 ));
        putOutLabel->setScale(4);
        this->addChild(putOutLabel);
    }

该段代码将根据玩家剩余血量来评定分数,当血量在60到100之间时,玩家将得到三颗星;当在30到60之间时,则为两颗星;在0到30之间时就只会得到一颗星了。

正如前面所说,像游戏分数这样简单的动态数据我们使用UserDefault来进行存储即可,所以,我们把过关后的分数用UserDefault存储起来。它的键名是从plist中读取的,为了能清楚地分便,所以设为了该关数据的文件名。

判断是否失败

当敌人移动到最后一个路径点的时候,这也意味着该敌人成功的攻克了玩家的防守,它取得了胜利。所以我们需要为每个敌人都添加一条是否成功进入玩家阵地的属性,并在敌人的nextPoint()方法中加上如下的判断。

CC_SYNTHESIZE(bool, enemySuccessful, EnemySuccessful);
Node* EnemyBase::nextPoint()
{
    int maxCount = this->pointsVector.size();
    pointCounter++;
    if (pointCounter < maxCount  ){         auto node =this->pointsVector.at(pointCounter);
        return node;
    }
    else{
        setEnemySuccessful(true);
    }
    return NULL;
}

每当有敌人攻克防守时,玩家的血量就会相应的减少,当玩家血量减少到0时,游戏失败,跳转到下一个界面。实现该方法的enemyIntoHouse函数我们依旧把它放在update函数体中,这样程序会逐帧检测游戏是否失败。

void PlayLayer::enemyIntoHouse()
{
    auto enemyVector = instance->enemyVector;
    for (int i = 0; i < enemyVector.size(); i++)     {         auto enemy = enemyVector.at(i);         if( enemy->getEnemySuccessful())
        {
            instance->enemyVector.eraseObject(enemy);
            enemy->removeFromParent();
            auto playHp = getPlayHpPercentage() - 10;
            if(playHp > 0){
                setPlayHpPercentage(playHp);
                playHpBar->setPercentage(playHp);
            }
            else{
                instance->clear();
                // 应该跳转到失败界面
                this->removeAllChildren();
                Size winSize = Director::getInstance()->getWinSize();
                auto putOutLabel = Label::createWithBMFont("fonts/boundsTestFont.fnt", "Game Over");
                putOutLabel->setPosition(Point(winSize.width / 2, winSize.height / 2 ));
                putOutLabel->setScale(4);
                this->addChild(putOutLabel);
            }
        }
    }
}

添加工具栏

这里工具栏指游戏场景上方的图形化信息提示栏。为游戏添加工具栏可以更直观的观察到游戏的信息动态,如游戏金币数、当前波数、总波数等等信息。所以,它是很有必要的。

void PlayLayer::initToolLayer()
{
    auto size = Director::getInstance()->getWinSize();
    toolLayer = Layer::create();
    addChild(toolLayer);
    // 工具栏背景图片
    auto spritetool = Sprite::createWithSpriteFrameName("toolbg.png");
    spritetool->setAnchorPoint(Point(0.5f, 1));
    spritetool->setPosition (Point(size.width / 2, size.height));
    toolLayer->addChild(spritetool);   
    // 金币数
    money = instance->getMoney();
    moneyLabel = Label::createWithBMFont("fonts/bitmapFontChinese.fnt", " ");
    moneyLabel->setPosition(Point(spritetool->getContentSize().width / 8, spritetool->getContentSize().height / 2));
    moneyLabel->setAnchorPoint(Point(0, 0.5f));
    auto moneyText = std::to_string(money);
    moneyLabel->setString(moneyText);
    spritetool->addChild(moneyLabel);   
    // 玩家血量条
    playHpBar = ProgressTimer::create(Sprite::createWithSpriteFrameName("playhp.png"));
    playHpBar->setType(ProgressTimer::Type::BAR);
    playHpBar->setMidpoint(Point(0, 0.4f));
    playHpBar->setBarChangeRate(Point(1, 0));
    playHpBar->setPercentage(playHpPercentage);
    playHpBar->setPosition(Point(spritetool->getContentSize().width / 5 *4  , spritetool->getContentSize().height / 2));
    spritetool->addChild(playHpBar);
    // 玩家得分标尺  
    auto star = Sprite::createWithSpriteFrameName("playstar.png");
    star->setPosition(Point(spritetool->getContentSize().width / 5 *4 , spritetool->getContentSize().height / 2));
    spritetool->addChild(star); 
    // 当前波数
    int groupTotal = instance->getGroupNum();
    groupLabel = Label::createWithBMFont("fonts/bitmapFontChinese.fnt", " ");
    groupLabel->setPosition(Point(spritetool->getContentSize().width / 8 * 3, spritetool->getContentSize().height / 2 ));
    groupLabel->setAnchorPoint(Point(0.5f , 0.5f));
    auto groupInfoText = std::to_string(groupCounter + 1);
    groupLabel->setString(groupInfoText);
    spritetool->addChild(groupLabel);
    // 总波数
    groupTotalLabel = Label::createWithBMFont("fonts/bitmapFontChinese.fnt", " ");
    groupTotalLabel->setPosition(Point(spritetool->getContentSize().width / 2 , spritetool->getContentSize().height / 2 ));
    groupTotalLabel->setAnchorPoint(Point(0.5f , 0.5f));
    auto groupTotalText = std::to_string(groupTotal);
    groupTotalLabel->setString(groupTotalText);
    spritetool->addChild(groupTotalLabel);
}

Cocos2d-x塔防游戏_贼来了8——批量添加敌人

原创: 任珊


Cocos2d-x塔防游戏_贼来了1——基础知识储备
Cocos2d-x塔防游戏_贼来了2——地图的创建加载
Cocos2d-x塔防游戏_贼来了3——进攻的敌人
Cocos2d-x塔防游戏_贼来了4——创建炮塔
Cocos2d-x塔防游戏_贼来了5——触摸响应
Cocos2d-x塔防游戏_贼来了6——触摸响应2
Cocos2d-x塔防游戏_贼来了7——数据管理与碰撞检测
Cocos2d-x塔防游戏_贼来了9——关卡数据
Cocos2d-x塔防游戏_贼来了10——选择关卡

综述

上一章结束为止,我们的场景中都还只有一个移动的敌人,这样的游戏是非常乏味无聊的,它简直不像是一个游戏。所以接下来我们将添加更多的敌人,并加上分数系统,使我们的塔防游戏看起来更像一个真正的游戏、完整的游戏。

关于本部分教程的代码,你可以先去这里:https://github.com/iTyran/thiefTD/tree/part3下载。

运行该部分demo,你将看到如下所示的效果图:

从图中我们可以看到,本部分塔防游戏的功能已经是比较完善的了。在此部分游戏中,敌人会一个接一个连续的出现,而且是分时分批分类型依次进入场景的。不同类型敌人的生命值、移动速度等等都是不一样的,每批敌人的数量也是不相同的。同时,本游戏还拥有了完善的得分体系和购买机制。
得分体系:游戏刚开始的时候,玩家拥有满级的生命值(100),所以它拥有三颗星的分数,不过每当有敌人顺利通过时,玩家的生命值就会减少10,游戏会根据当前剩余的生命值判断玩家的分数(比如:生命值小于30时,玩家就只有一颗星的分数了)。当有10个敌人通过时,玩家就彻底失败了,游戏也就此结束。
购买机制:玩家射杀敌人获取金币,购买炮塔消耗金币。

同前面各章一样,由于本部分代码涉及的内容信息较多,所以本章教程会先教大家实现一个接一个的添加敌人。下一章则会实现游戏信息的读取。

GroupEnemy(一波敌人类)

在塔防游戏中有个很重要的概念就是:一波敌人。玩过植物大战僵尸、保卫萝卜等塔防游戏的玩家对这个名词肯定不会陌生。
同样地,我们的游戏也会这样设计,设计一个包含了一整波敌人种类、个数、生命值等信息的类。通俗点说就是:这个类存储了一波敌人的数据信息,它决定了一波敌人里Thief的个数和生命值;Pirate的个数和生命值;Bandit的个数和生命值。

定义这样的一个类可以帮助我们更加方便的设计出更多样的阵容和不同攻击强度的敌人战队。这样的话,在后续设计关卡的时候也会有很大的帮助。这个类我们把它叫做群组类(GroupEnemy)。下面就跟着我们来创建一个这样的类吧。

class GroupEnemy: public cocos2d::Node
{
public:

    virtual bool init();
    GroupEnemy* initGroupEnemy(int type1Total, int type1Hp, int type2Total, int type2Hp, int type3Total, int type3Hp );
    CREATE_FUNC(GroupEnemy);

    CC_SYNTHESIZE(int, type1Total, Type1Total);
    CC_SYNTHESIZE(int, type2Total, Type2Total);
    CC_SYNTHESIZE(int, type3Total, Type3Total);
    CC_SYNTHESIZE(int, type1Hp, Type1Hp);
    CC_SYNTHESIZE(int, type2Hp, Type2Hp);
    CC_SYNTHESIZE(int, type3Hp, Type3Hp);
    CC_SYNTHESIZE(int, enemyTotal, EnemyTotal);
    CC_SYNTHESIZE(bool, isFinishedAddGroup, IsFinishedAddGroup);

};

在该类中,参数type1Total,type2Total,type3Total分别代表了小偷,土匪,海盗三种敌人的数目,type1Hp,type2Hp,type3Hp代表了它们的生命值,而enemyTotal则表示总敌人的总个数,isFinishedAddGroup用于判断该波敌人是否全部都添加到了场景。

GroupEnemy* GroupEnemy::initGroupEnemy(int type1Total, int type1Hp, int type2Total, int type2Hp, int type3Total, int type3Hp)
{
    this->type1Total = type1Total;
    this->type2Total = type2Total;
    this->type3Total = type3Total;
    this->type1Hp = type1Hp;
    this->type2Hp = type2Hp;
    this->type3Hp = type3Hp;
    this->enemyTotal = type1Total + type2Total + type3Total;
    this->isFinishedAddGroup = false;
    return this;
}

initGroupEnemy方法用于初始化本波敌人信息。

挨个添加敌人

现在,GroupEnemy类我们已经创建好了,接下来就让我们回到PlayLayer中,在这儿我们将完成一项丰功伟业:在游戏中添加一波又一波的敌人。不过在此之前,还是先让我们来整理整理思路吧。

在游戏中,我们会先创建并初始化不同的GroupEnemy,并会把这些创建好的GroupEnemy添加到GameManager对象的一个向量中,方便统一管理。这和管理敌人、子弹的方法一样。
接着我们会一波一波的添加敌人,当一波敌人被全部添加,并且这波的敌人数量为0(被消灭或顺利过关)时,再接着添加下一波、再下一波......。
添加每波的敌人时,我们会根据这波敌人给出的信息依次创建不同种类,不同数量,不同生命值的敌人。这样一来,我们就实现了一个接一个添加敌人的功能。

在GameManager中管理着我们的游戏信息,

游戏中管理GroupEnemy对象的向量是Vector groupVector;,我们可以通过遍历这个向量来获取当前波和下一波的敌人信息。下面是实现方法:

GroupEnemy* PlayLayer::currentGroup()
{
    GroupEnemy* groupEnemy;
    if(!instance->groupVector.empty() )
    {
        groupEnemy = (GroupEnemy*)instance->groupVector.at(groupCounter);
    }
    else
        groupEnemy =NULL;   
    return groupEnemy;
}

GroupEnemy* PlayLayer::nextGroup()
{  
    if (groupCounter < GroupToTal - 1)
    {
        groupCounter++;
    }
    else
    {
        isSuccessful = true;
    }
    GroupEnemy* groupEnemy = (GroupEnemy*)instance->groupVector.at(groupCounter);
    return groupEnemy;

}

这里我们通过变量groupCounter获得当前波和下一波敌人信息。向量groupVector中第groupCounter项就是当前波的敌人信息,当我们想获取下一波敌人信息的时候,我们就递增groupCounter的值。当groupCounter达到游戏的总波数时,证明游戏已经添加完所有的敌人,玩家挺过了所以进攻的敌人。

接下来让我们来看看添加敌人的方法,如下所示。logic是一个需要定时执行的函数。它会由定时器函数schedule(SEL_SCHEDULE selector, float interval) 方法启用,interval是每次调用的间隔时间。

void PlayLayer::logic(float dt)
{
    // 1
    GroupEnemy* groupEnemy = this->currentGroup();
    if(groupEnemy == NULL){ return; }
    // 2
    if(groupEnemy->getIsFinishedAddGroup() ==true  && instance->enemyVector.size() == 0 && groupCounter < instance->getGroupNum())
    {
        groupEnemy = this->nextGroup();
    }
    // 3
    this->addEnemy();
}
  1. 获取当前波次的敌人信息,如果为空,则返回。
  2. 当当前波的敌人全部都添加到了场景,并且此时场景中敌人数量变为0(被消灭或顺利过关)时,添加下一波的敌人。
  3. 添加敌人。也就是说,只要groupEnemy不为空,那么每dt秒就会执行一次addEnemy方法来添加敌人。
void PlayLayer::addEnemy()
{
    GameManager *instance = GameManager::getInstance();

    GroupEnemy* groupEnemy = this->currentGroup();
    if(groupEnemy == NULL)
    {
        return;
    }
    // 1
    auto restEnemyNum = groupEnemy->getEnemyTotal();
    if( restEnemyNum <= 0){
        groupEnemy->setIsFinishedAddGroup(true);
        return;
    }
    // 2
    restEnemyNum--;
    groupEnemy->setEnemyTotal(restEnemyNum);
    // 3
    EnemyBase* enemy = NULL;
    if(groupEnemy->getType1Total() > 0){
        enemy = Thief::createThief(pointsVector, groupEnemy->getType1Hp());
        groupEnemy->setType1Total(groupEnemy->getType1Total() - 1);
    }
    else if(groupEnemy->getType2Total() > 0){
        enemy = Pirate::createPirate(pointsVector, groupEnemy->getType2Hp());
        groupEnemy->setType2Total(groupEnemy->getType2Total() - 1);
    }
    else if(groupEnemy->getType3Total() > 0){
        enemy = Bandit::createBandit(pointsVector, groupEnemy->getType3Hp());
        groupEnemy->setType3Total(groupEnemy->getType3Total() - 1);
    }
    // 4
    this->addChild(enemy, 10);
    instance->enemyVector.pushBack(enemy);
}

addEnemy方法会取出当前波敌人的信息挨个添加敌人,下面是该函数的具体讲解。

  1. 判断当前波剩余的敌人(待添加)总数是否为0,如果为0,则表示该波敌人全部都被添加到了场景。
  2. 因为该函数会添加一个敌人,敌人的剩余总数也会减少,所以此处需要重新设置剩余敌人的总数。
  3. 根据当前波敌人信息,依次添加Thief,Pirate和Bandit。添加相应敌人的同时需要减少它的总数值。
  4. 把创建好的敌人添加到场景,并且添加到敌人列表中。

下节内容将讲解如何设置关卡数据。

Cocos2d-x塔防游戏_贼来了7——数据管理与碰撞检测

原创: 任珊

Cocos2d-x塔防游戏_贼来了1——基础知识储备
Cocos2d-x塔防游戏_贼来了2——地图的创建加载
Cocos2d-x塔防游戏_贼来了3——进攻的敌人
Cocos2d-x塔防游戏_贼来了4——创建炮塔
Cocos2d-x塔防游戏_贼来了5——触摸响应
Cocos2d-x塔防游戏_贼来了6——触摸响应2
Cocos2d-x塔防游戏_贼来了8——批量添加敌人
Cocos2d-x塔防游戏_贼来了9——关卡数据
Cocos2d-x塔防游戏_贼来了10——选择关卡

前面的章节中我们已经提到过如何实现碰撞检测,本着认真负责的态度,这章我们就将落实到具体的行动上。不过在实现碰撞检测之前,我们会先看看怎样存储敌人,子弹等检测需要的数据。最后会完善一下游戏的功能。总而言之,本章是一篇大杂烩教程,擦屁股的教程,之前教程中漏讲的在这里都会一一讲到。

管理游戏数据

本游戏使用了向量Vector和数组来存储并管理场景中的敌人、子弹、炮塔,以及后面会讲到的一整批敌人信息等等数据。这些变量全局唯一,且时刻变换。例如敌人:此刻场景中有1个敌人,但下一秒可能这个敌人就被射死了或者突破重围闯入了玩家阵地,也有可能下一秒起点处又出来了一个新的敌人,这些情况都是随时可能发生的。

又因为这些数据会应用于多个类中,所以这里我们设计了一个管理游戏数据的单例模式类,意图是保证其他类可以同时访问这个类的数据。

单例模式也称为单件模式、单子模式,可能是使用最广泛的设计模式。它保证了一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。以下就是我们管理游戏数据的单例类的定义:

class GameManager
{
public:

    Vector enemyVector;
    Vector bulletVector;
    Vector towerVector;

    static GameManager* getInstance();   
private:
    static GameManager * instance;
};

《设计模式》一书中给出了一种比较认可的单例模式的实现方法:用一个public的静态方法来获取类的唯一实例,这个实例则是一个private的静态指针变量。在GameManager类中,instance就是这一唯一的实例,通过getInstance()方法可以获取到它。方法如下:

GameManager* GameManager::instance;
GameManager* GameManager::getInstance()
{
    if (instance == NULL)
        instance = new GameManager();
    return instance;
}

了解更多关于单例模式的讲解,请阅读Cocos2d-x设计模式发掘之一:单例模式一文。

小结:GameManager类的实现决定了整个游戏框架拥有良好的结构,我们将通过GameManager类获取游戏数据信息和关卡信息,为全局访问提供保障。

碰撞检测

本游戏的碰撞检测使用了最传统的方法——通过遍历子弹向量和敌人向量,检测两个对象是否相交。在碰撞检测时,会根据检测情况添加特效并销毁相应对象。

这里的检测碰撞并不难,关键点在于检测区域的获取和碰撞后的数据处理。对于不同层次上的节点来说,碰撞检测时必须把它们的坐标位置映射到同一个层中,否则可能无法精准的检测。正如在本游戏中,我们是把子弹作为炮塔的一部分添加到了炮塔精灵上的,所以它的坐标位置是相对于炮塔的,这样子弹与敌人就不在同一个父节点上,检测时如果不处理就会出问题。

说了这么多废话后,我们还是回到正题,来看看下面具体的实现方法吧。

void PlayLayer::CollisionDetection()
{
    GameManager *instance = GameManager::getInstance();  
    auto bulletVector = instance->bulletVector;
    auto enemyVector = instance->enemyVector;
    if(bulletVector.empty() || enemyVector.empty() ){
        return;
    }
    // 1 
    Vector enemyNeedToDelete;
    Vector bulletNeedToDelete;
    // 2
    for (int i = 0; i < bulletVector.size(); i++)
    {
        auto  bullet = bulletVector.at(i);
        auto  bulletRect = Rect(bullet->getPositionX()+bullet->getParent()->getPositionX()-bullet->getContentSize().width/2,
                                bullet->getPositionY()+bullet->getParent()->getPositionY()-bullet->getContentSize().height/2,
                                bullet->getContentSize().width,
                                bullet->getContentSize().height );
        // 3
        for (int j = 0; j < enemyVector.size(); j++)
        {
            auto enemy = enemyVector.at(j);
            auto enemyRect = enemy->sprite->getBoundingBox();
            // 4
            if (bulletRect.intersectsRect(enemyRect))
            {
                enemyNeedToDelete.pushBack(enemy);
                bulletNeedToDelete.pushBack( bullet);
                // 5
                break;
            }
        }
        // 6
        for (EnemyBase* enemyTemp : enemyNeedToDelete)
        {
            enemyTemp->enemyExpload();
            instance->enemyVector.eraseObject(enemyTemp);
        }
        enemyNeedToDelete.clear();
    }
    // 7
    for (const auto& bulletTemp : bulletNeedToDelete)
    {
        instance->bulletVector.eraseObject(bulletTemp);
        bulletTemp->removeFromParent();
    }
    bulletNeedToDelete.clear();
}
  1. 定义待删除子弹和敌人的临时向量变量,当有子弹或敌人需要被删除时,就会把它们插入到这些向量中。
  2. 遍历子弹向量,计算子弹的占地范围,其中子弹的Rect时要把它的坐标值映射到场景层(PlayerLayer)中,保证能与敌人在同一子节点上。
  3. 遍历敌人向量,计算敌人的范围。getBoundingBox函数用于获得经过缩放和旋转之后的外框盒大小。
  4. 判断子弹与敌人是否有交集。如果相交,则把该敌人和子弹添加到待删除的列表中。
  5. 只要击中敌人,就跳出循环,这也意味着一个bullet只能射击一个敌人。
  6. 销毁待删除列表中的敌人,并把该敌人从enemyVector中移除,最后清理enemyNeedToDelete。enemyExpload方法将在播放了爆炸效果后销毁敌人,这章后半部分会讲解。
  7. 从bulletVector中移除待删除列表中的子弹并销毁最后清理bulletNeedToDelete。

完善敌人功能

目前为止,我们的游戏已经添加了敌人、炮塔,并且已经能射杀敌人了。抛开只有一个敌人的问题来看,这里还有一个缺陷就是这里的敌人一枪毙命,死的也太容易了。所以,我们还需要完善一下敌人的功能,让它更经打一些。并且,我们还将为敌人添加代表血量的进度条和爆炸效果。

为了实现以上所说的功能,需要在EnemyBase类中添加以下属性:

CC_SYNTHESIZE(int, maxHp, MaxHp);
CC_SYNTHESIZE(int, currHp, CurrHp);
CC_SYNTHESIZE(float, hpPercentage, HpPercentage);
CC_SYNTHESIZE_READONLY(ProgressTimer*, hpBar, HpBar);

添加血条

添加敌人的血条我们用进度条实现。游戏开发中难免会用到进度条,Cocos2d-x也为我们封装了进度条,虽然不太理想,但要实现基本的一些功能还是很方便的。下面就来向大家介绍下在Cocos2d-x中如何使用进度条ProgressTimer。

void EnemyBase::createAndSetHpBar()
{
    // 1
    hpBgSprite = Sprite::createWithSpriteFrameName("hpBg1.png");
    hpBgSprite->setPosition(Point(sprite->getContentSize().width / 2, sprite->getContentSize().height ));
    sprite->addChild(hpBgSprite);
    // 2
    hpBar = ProgressTimer::create(Sprite::createWithSpriteFrameName("hp1.png"));
    hpBar->setType(ProgressTimer::Type::BAR);
    hpBar->setMidpoint(Point(0, 0.5f));
    hpBar->setBarChangeRate(Point(1, 0));
    hpBar->setPercentage(hpPercentage);
    hpBar->setPosition(Point(hpBgSprite->getContentSize().width / 2, hpBgSprite->getContentSize().height / 3 * 2 ));
    hpBgSprite->addChild(hpBar);
}
  1. 添加血条背景图片。
  2. 添加血条进度条。
    进度条ProgressTimer有两种类型:一种是环形,一种是条形(包括vertical 和 horizontal),所以使用进度条时需要指明它是哪种类型。setMidpoint方法设置进度条的起始点,(0,y)表示最左边,(1,y)表示最右边,(x,1)表示最上面,(x,0)表示最下面。

    setBarChangeRate方法用来设置进度条变化方向的,如果不用变化的方向,则设置该方向为0,否则设置为1。所以(1,0)表示横方向,(0,1)表示纵方向。

    ProgressTimer有一个很最要的percentage属性。它代表了当前进度条的进度值。如果要让一个进度条正常的显示出来,那么percentage的值必须大于0。setPercentage方法能设置ProgressTimer的percentage值。

我们会在子弹与敌人的碰撞检测中不断的更新当前敌人血量的进度值和生命值,当敌人的生命值小于等于0时,再移除敌人。如下所示修改CollisionDetection方法。

    if (bulletRect.intersectsRect(enemyRect))
    {
        auto currHp = enemy->getCurrHp();               currHp--;
        enemy->setCurrHp( currHp );
        auto currHpPercentage = enemy->getHpPercentage();
        auto offHp = 100 / enemy->getMaxHp();
        currHpPercentage -= offHp;

        if(currHpPercentage < 0){
            currHpPercentage = 0;
        }
        enemy->setHpPercentage(currHpPercentage);
        enemy->getHpBar()->setPercentage(currHpPercentage);
        if(currHp <= 0)
        {
            enemyNeedToDelete.pushBack(enemy);
        }
        bulletNeedToDelete.pushBack( bullet);
        break;
    }

爆炸效果

在游戏中,移除死亡角色之前,一般都会伴有牛逼的爆炸效果。本游戏也不例外,我们将在移除敌人之前,播放一则爆炸动画,方法如下:

void Thief::enemyExpload()
{
    hpBgSprite->setVisible(false);
    sprite->stopAllActions();
    unschedule(schedule_selector(Thief::changeDirection));
    // 修整爆炸动画的位置,因为它比其他状态都要大
    sprite->setAnchorPoint(Point(0.5f, 0.25f));
    sprite->runAction(Sequence::create(Animate::create(AnimationCache::getInstance()->getAnimation("explode1"))
                                       ,CallFuncN::create(CC_CALLBACK_0(EnemyBase::removeFromParent, this))
                                       , NULL));
}

在该方法中,我们会先暂停敌人所有的动作,并停止方向检测。然后再让敌人播放爆炸动画,播放完后再将它移除。

小结

为了让敌人看起来不那么单一,在第二部分的Demo中我们还添加了另外两种类型的敌人,分别是Pirate和Bandit。它们同Thief类似,只有轻微的差别,所以教程中就不再做过多的描述。

下一章教程我们将围绕第三部分Demo一一讲解,下图是该部分的效果图,敬请关注。

?>