《Cocos2d-x3.x游戏开发之旅》学习

编程知识 更新时间:2023-04-19 00:40:57

1.addEventListenerWidthSceneGraphPriority函数,这个函数的两个参数作用如下:

  •    EventListener *listener:事件监听对象,当触摸事件发生时通过它来回调;
  •    Node *node:绑定的对象,当node对象被释放时,监听事件的注册也会被取消,同时,有多个触摸事件发生时(比如几个按钮叠加在一起),会根据node层次优先回调(越在上面的对象越先回调);

   addEventListenerWithFixedPriority,也是用于注册监听事件,但这个函数需要手动指定触摸事件回调的优先级,并且需要手动取消监听事件。一帮情况下,我们使用addEventListenerWidthSceneGraphPriority就可以了。

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    
    Size visibleSize = Director::getInstance()->getVisibleSize();

    /* 创建两个精灵,相互有重叠的部分 */
    
    Sprite *sp1 = Sprite::create("sprite1.png");
    sp1->setPosition(Point(visibleSize.width * 0.5f, visibleSize.height * 0.5f));
    this->addChild(sp1);

    Sprite *sp2 = Sprite::create("sprite2.png");
    sp2->setPosition(Point(visibleSize.width * 0.5f, visibleSize.height * 0.5f));
    this->addChild(sp2);

    auto listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan = [](Touch *touch, Event *event) {
        /* 注册监听事件时绑定了一个Node对象,在这里就可以取出这个对象 */
        auto target = static_cast<Sprite *>(event->getCurrentTarget());
        Point pos = Director::getInstance()->convertToGL(touch->getLocationInView());

        /* 判断单击的坐标是否在精灵的范围内 */
        if (target->getBoundingBox().containsPoint(pos)) {
            /* 设置精灵的透明度为100 */
            target->setOpacity(100);
            return true;
        }

        return false;
    };
    listener->onTouchEnded = [](Touch *touch, Event *event) {
        /* 恢复精灵的透明度 */
        auto target = static_cast<Sprite *>(event->getCurrentTarget());
        target->setOpacity(255);
    };
    /* 注册监听事件,绑定精灵1 */
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, sp1);

    /*注册监听事件,绑定精灵2,这里要注意,listener对象复制了一份*/
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener->clone(), sp2);
    return true;
}

      分步骤来讲解:

       a) 创建两个精灵,让这两个精灵刚好有部分位置是重叠的;

       b) 创建EventListenerTouchOneByOne监听事件;

       c) 在onTouchBegan函数里获取事件绑定的精灵对象,判断单击的坐标是否在精灵的范围内,是的话,则修改精灵的透明度为100;

       d) 调用addEventListenerWithSceneGraphPriority函数分别添加两个精灵的监听事件;

     通常我们要求在单击重叠部位的时候,只有上面的按钮能获得响应。要实现这个效果,很简单,我们给listener调用一个函数即可:

     .listener->setSwallowTouches(true);

    setSwallowTouches函数用于设置是否吞没事件。

2.多点触控

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    auto listener = EventListenerTouchAllAtOnce::create();
    listener->onTouchesBegan = [](const std::vector<Touch *>& touches, Event *event) {};
    listener->onTouchesMoved = [](const std::vector<Touch *>& touches, Event *event) {};
    listener->onTouchesEnded = [](const std::vector<Touch *>& touches, Event *event) {};
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
    return true;
}

 

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    Label *logText1 = Label::create("", "Arial", 24);
    logText1->setPosition(Point(400, 280));
    this->addChild(logText1, 1, 1);

    Label *logText2 = Label::create("", "Arial", 24);
    logText3->setPosition(Point(400, 100));
    this->addChild(logText3, 1, 3);
    auto listener = EventListenerTouchAllAtOnce::create();

    listener->onTouchesBegan = [&](const std::vector<Touch *>& touches, Event *event) {
        auto logText = (Label *)this->getChildByTag(1);
        int num = touches.size();
        logText->setString(Value(num).asString() + " Touches:");
    };

    listener->onTouchesMoved = [&](const std::vector<Touch *>& touches, Event *event) {
        auto logText = (Label *)this->getChildByTag(2);
        int num = touches.size();
        std::string text = Value(num).asString() + " Touches:";
        for (auto &touch : touches) {
            auto location = touch->getLocation();
            text += "[touchID" + Value(touch->getID()).asString() + "],";
        }
        logText->setString(text);
    };

    listener->onTouchesEnded = [&](const std::vector<Touch *>& touches, Event *event) {
        auto logText = (Label *)this->getChildByTag(3);
        int num = touches.size();
        logText->setString(Value(num).asString() + " Touches:");
    };

    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
    return true;
}    

3.<LittleRunner>的主要功能如下:

  •    主角一直往前跑,实际上是地图在滚动;
  •    跑动过程中会有很多金币飞向主角,主角被金币打中会扣血;
  •    扣血有动画效果;
  •    按下Jump按钮,主角可以向上弹跳;

   游戏包含的开发要点:

  •      地图无限滚动;
  •      精灵动作的使用;
  •      Cocostudio UI使用:按钮、标签、进度条;
  •      简单的碰撞检测;
  •      update函数的应用.

  3.1 创建实体基类

      Entity.h文件:

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

class Entity : public Node {
    public:
        Entity();
        ~Entity();
        Sprite *getSprite();   /* 获取精灵对象 */
        void bindSprite(Sprite *sprite); /* 绑定精灵对象 */
    private:
        Sprite *m_sprite;
};

#endif

      Entity.cpp文件

#include "Player.h"
#include "Entity.h"
Entity::Entity() {
    m_sprite = NULL;
}
Entity::~Entity() {
   
}
Sprite *Entity::getSprite() {
    return this->m_sprite;
}
void Entity::bindSprite(Sprite *sprite) {
    this->m_sprite = sprite;
    this->addChild(m_sprite);
}

    3.2 创建主角类

       Player.h文件

#ifndef __Player_H__
#define __Player_H__

#include "cocos2d.h"
#include "Entity.h"
using namespace cocos2d;

#define JUMP_ACTION_TAG 1
class Player : public Entity {
    public:
        Player();
        ~Player();
        CREATE_FUNC(Player);
        virtual bool init();
};

#endif

        Player.cpp文件

#include "Player.h"

Player::Player() {}
Player::~Player() {}

bool Player::init() {
    return true;
}

      3.3 创建游戏场景

         TollgateScene.h文件

#ifndef __TollgateScene_H__
#define __TollgateScene_H__

#include "cocos2d.h"
using namespace cocos2d;

class Player;
class TollgateScene : public Layer {
    public:
        static Scene *createScene();
        virtual bool init();
        CREATE_FUNC(TollgateScene);
    private:
        void initBG();   //初始化关卡背景
    private:
        Sprite *m_bgSprite1; //背景精灵1
        Sprite *m_bgSprite2; //背景精灵2

        Player *m_player; //主角
};

#endif

         TollgateScene.cpp文件:

bool TollgateScene::init() {
    if (!Layer::init()) { return false; }
    
    Size visibleSize = Director::getInstance()->getVisibleSize();

    /* 游戏标题图片 */
    Sprite *titleSprite = Sprite::create("title.png");
    titleSprite->setPosition(Point(visibleSize.width / 2, visibleSize.height - 50));
    this->addChild(titleSprite, 2);

    /* 创建主角 */
    m_player = Player::create();
    m_player->bindSprite(Sprite::create("sprite.png"));
    m_player->setPosition(Point(200, visibleSize.height / 4));
    this->addChild(m_player, 3);

    /* 初始化背景图片 */
    initBG();

    return true;
}

void TollgateScene::initBG() {
    Size visibleSize = Director::getInstance()->getVisibleSize();

    m_bgSprite1 = Sprite::create("tollgateBG.jpg");
    m_bgSprite1->setPosition(Point(visibleSize.width / 2, visibleSize.height / 2));
    this->addChild(m_bgSprite1, 0);

    m_bgSprite2 = Sprite::create("tollgateBG.jpg");
    m_bgSprite2->setPosition(Point(visibleSize.width+visibleSize.width/2, visibleSize.height/2));
    m_bgSprite2->setFlippedX(true); // 水平翻转精灵
    this->addChild(m_bgSprite2, 0);
}
}

       注意:调用addChild函数时,使用了第二个参数,这个参数代表对象的绘制层次,数值越大,对象层次越高(越迟被绘制),背景图片应该设成较低层次,否则会把主角挡住。

    3.4 修改游戏窗口大小

       打开main.cpp,找到下面的代码:

if (!glview) {
    glview = GLView::create("LittleRunner");
    glview->setFrameSize(480, 320);
    director->setOpenGLView(glview);
}

     3.5 我们要尽量避免使用多线程,Cocos2d-x的Node对象提供了一个update函数,在游戏运行的每一帧(也就是主线程的每一个循环)都会调用update函数。前提是我们允许它调用。

        程序默认是不会调用Node对象的update函数的,要开启调用update函数的功能,只需要一句代码:this->scheduleUpdate()。

     3.6 打开TollgateScene.cpp文件,在init函数的最后加上一句代码:

bool TollgateScene::init() {
    /* 省略了很多代码 */
    this->scheduleUpdate();
    return true;
}

      scheduleUpdate函数只是开启了这个功能,我们还必须重写Node的update函数。现在打开TollgateScene.h头文件,增加一句代码:

class TollgateScene : public Layer {
    public:
        /* 又省略了很多代码 */
        virtual void update(float delta);
};

    然后在TollgateScene.cpp文件里实现update函数,如下:

void TollgateScene::update(float delta) {
    log("update");
}

      3.7 让地图滚动起来,修改TollgateScene的update函数,如下:

void TollgateScene::update(float delta) {
    int posX1 = m_bgSprite1->getPositionX(); //背景地图1的X坐标
    int posX2 = m_bgSprite2->getPositionX(); //背景地图2的X坐标

    int iSpeed = 1;    //地图滚动速度
    
    /* 两张地图向左滚动(两张地图是相邻的,所以要一起滚动,否则会出现空隙) */
    posX1 -= iSpeed;
    posX2 -= iSpeed;

    m_bgSprite1->setPositionX(posX1);
    m_bgSprite2->setPositionX(posX2);
}

        效果图:

     

    还需要加上另外的代码,如下:

void TollgateScene::update(float delta) {
    int posX1 = m_bgSprite1->getPositionX(); //背景地图1的X坐标
    int posX2 = m_bgSprite2->getPositionX(); //背景地图2的X坐标
    
    int iSpeed = 1; //地图滚动速度
    
    /* 两张地图向左滚动(两张地图是相邻的,所以要一起滚动,否则会出现空隙)*/
    posX1 -= iSpeed;
    posX2 -= iSpeed;

    /* 地图大小 */
    Size mapSize = m_bgSprite1->getContentSize();

    /* 当第1个地图完全离开屏幕时,第2个地图刚好完全出现在屏幕上,这时候就让第1个地图紧贴在第2个地图后面 */
    if (posX1 <= -mapSize.width / 2) {
        posX1 = mapSize.width + mapSize.width / 2;
    }

    /* 同理,当第2个地图完全离开屏幕时,第1个地图刚好完全出现在屏幕上,这时候就让第2个地图紧贴在第1个地图后面 */
    if (posX2 <= -mapSize.width / 2) {
        posX2 = mapSize.width + mapSize.width / 2;
    }

    m_bgSprite1->setPositionX(posX1);
    m_bgSprite2->setPositionX(posX2);
}
    

      3.8 创建跳跃按钮

       TollgateScene.h文件:

class TollgateScene : public Layer {
    private:
        /* 前面省略了很多代码 */
        void loadUI(); //加载UI
        void jumpEvent(Ref *, TouchEventType type);
};

        TollgateScene.cpp文件:

void TollgateScene::loadUI() {
    /* 加载UI */
    auto UI = cocostudio::GUIReader::getInstance()->widgetFromJsonFile("LitterRunnerUI_1.ExportJson");
    this->addChild(UI);
    
    /* 获取控件对象 */
    auto jumpBtn = (Button *)Helper::seekWidgetByName(UI, "JumpBtn");

    /* 添加单击监听 */
    jumpBtn->addTouchEventListener(this, toucheventselector(TollgateScene::jumpEvent));
}

void TollgateScene::jumpEvent(Ref *, TouchEventType type) {
    switch (type) {
        case TouchEventType::TOUCH_EVENT_ENDED:
            m_player->jump();
            break;
    }
}

          两个函数很简单,loadUI负责加载UI文件,并且监听按钮的一个单击事件,在单击事件jumpEvent中调用主角的jump函数,然后主角就能跳起来了。

         现在我们开始给主角赋予跳跃的能力,给Player类添加一些变量和函数,如下:

      Player.h文件:

class Player : public Entity {
    public:
        Player();
        ~Player();
        CREATE_FUNC(Player); //create函数
        virtual bool init();
    public:
        void jump(); //跳跃函数
    private:
        bool m_isJumping; //标记主角是否正在跳跃
};

        Player.cpp文件

Player::Player() {
    /*初始化主角跳跃标记为false,一定不能忘记这一步 */
    m_isJumping = false;
}
Player::~Player() {
}
bool Player::init() {
    return true;
}

void Player::jump() {
    if (getSprite() == NULL) {
        return;
    }

    /* 如果主角还在跳跃中,则不重复执行 */
    if (m_isJumping) {
        return;
    }

    /* 标记主角为跳跃状态 */
    m_isJumping = true;

    /* 创建跳跃动作: 原地跳跃,高度为250像素,跳跃一次 */
    auto jump = JumpBy::create(2.0f, Point(0, 0), 250, 1);

    /* 创建回调动作,跳跃结束后修改m_isJumping标记为false */
    auto callFunc = CallFunc::create([&])(){
        m_isJumping = false;
    });
    
    /* 创建连续动作 */
    auto jumpActions = Sequence::create(jump, callFunc, NULL);

    /* 执行动作 */
    this->runAction(jumpActions);
}

       

     3.9 加入怪物

       Monster.h文件

class Monster : public Entity {
    public:
        Monster();
        ~Monster();
        CREATE_FUNC(Monster);
        virtual bool init();
    public:
        void show(); //显示怪物
        void hide(); //隐藏怪物
        void reset(); //重置怪物数据
        bool isAlive(); //是否活动状态
    private:
        bool m_isAlive;
};

        Monster.cpp文件:

Monster::Monster() {
    m_isAlive = false;
}
Monster::~Monster() {}
bool Monster::init() {
    return true;
}

void Monster::show() {
    if (getSprite() != NULL) {
        setVisible(true); /*设置可见*/
        m_isAlive = true; /*标记怪物为活动状态*/
    }
}

void Monster::hide() {
    if (getSprite() != NULL) {
        setVisible(false); /*设置不可见*/
        reset();           /*重置怪物数据*/
        m_isAlive = false; /*标记怪物为非活动状态*/
    }
}

void Monster::reset() {
    if (getSprite() != NULL) {
        /* 初始化怪物坐标*/
        setPosition(Point(800+CCRANDOM_0_1()*2000, 200-CCRANDOM_0_1() * 100));
    }
}

bool Monster::isAlive() {
    return m_isAlive;
}

       3.10 创建怪物管理器

           MonsterManager.h文件

#ifndef __MonsterManger_H__
#define __MonsterManger_H__

#include "cocos2d.h"
#include "Monster.h"
USING_NS_CC;

#define MAX_MONSTER_NUM 10 //怪物最大数量
class MonsterManger : public Node {
    public:
        CREATE_FUNC(MonsterManager);
        virtual bool init();

        virtual void update(float dt); /*重写update函数*/
    private:
        void createMonsters(); /*创建怪物对象*/
    private:
        Vector<Monster*> m_monsterArr; /*存放怪物对象列表*/
};
#endif

           MonsterManager.cpp文件:

#include "MonsterManager.h"
#include "Player.h"
#include "Monster.h"

bool MonsterMangager::init() {
    createMonsters();   /*创建怪物缓存*/
    this->scheduleUpdate();  /*开启update函数调用*/
    return true;
}

void MonsterMangager::createMonsters() {
    Monster *monster = NULL;
    Sprite *sprite = NULL;

    for (int i=0; i<MAX_MONSTER_NUM; i++) {
        /*创建怪物对象*/
        monster = Monster::create();
        monster->bindSprite(Sprite::create("monster.png"));
        monster->reset();

        /*添加怪物对象*/
        this->addChild(monster);
        
        /*保存怪物对象到列表中,方便管理*/
        m_monsterArr.pushBack(monster);
    }
}

void MonsterManager::update(float dt) {
}

           现在,我们在TollgateScene.cpp的init函数最后面添加几句代码,让怪物管理器生效,如下:

#include "MonsterManager.h"   

bool TollgateScene::init() {
    /*省略了很多代码*/
       
    /*创建怪物管理器*/
    MonsterManager *monsterMgr = MonsterManager::create();
    this->addChild(monsterMgr, 4);

    return true;
}

         我们的怪物管理器还要增加一个功能,就是不断地改变怪物的坐标。修改MonsterManager.cpp的update函数,如下所示:

void MonsterManager::update(float dt) {
    for (auto monster : m_monsterArr) {
        if (monster->isAlive()) {
            /*如果怪物处于活动状态*/
            monster->setPositionX(monster->getPositionX() - 4);

            /*如果怪物X坐标小于0,则表示已经超出屏幕范围,隐藏怪物*/
            if (monster->getPositionX() < 0) {
                monster->hide();
            }
        } else {
            /*怪物处于非活动状态,让怪物出场吧*/
            monster->show();
        }
    }
 }

            效果如下:

             

         3.11 怪物碰撞检测

            如果怪物碰到主角,则扣主角的血

            给Player类加点东西,如下代码:

class Player : public Entity {
    /* 这里省略了很多代码 */
    public:
        void jump();    //跳跃函数
        void hit();     //玩家受伤害
        int getiHP();
    private:
        bool m_isJumping;   //标记主角是否正在跳跃
        int m_iHP;   //主角血量
};

            再来看Player.cpp是如何实现这些函数的,如下:

Player::Player() {
    /*初始化主角跳跃标记为false,一定不能忘记这一步*/
    m_isJumping = false;
    
    /*初始化血量*/
    m_iHP = 1000;
}

void Player::hit() {
    if (getSprite() == NULL) {
        return;
    }

    m_iHP -= 15;
    if (m_iHP < 0) {
        m_iHP = 0;
    }
}

int Player::getiHP() {
    return this->m_iHP;
}

          接着,我们要给Monster添加一个检测碰撞的函数,如下所示:

         Monster.h文件:

class Monster : public Entity {
    public:
        /*这里省略了很多代码*/
        /*检查碰撞*/
        bool isCollideWithPlayer(Player *player);
};

           Monster.cpp文件

bool Monster::isCollideWithPlayer(Player *player) {
    /*获取碰撞检查对象的boundingBox*/
    Rect entityRect = player->getBoundingBox();
    Point monsterPos = getPosition();
    /*判断boundingBox和怪物中心点是否有交集*/
    return entityRect.containsPoint(monsterPos);
}

          最后修改MonsterManager的update函数,以及新增一个bindPlayer函数,如下代码:

         MonsterManager.h文件

class MonsterManager : public Node {
    /*这里省略了很多代码*/
    public:
        /*绑定玩家对象*/
        void bindPlayer(Player *player);
    private:
        Player *m_player; /*玩家对象*/
};

         MonsterManager.cpp文件

void MonsterManager::update(float dt) {
    /*这里省略了很多代码*/
    /*如果怪物X坐标小于0,则表示已经超出屏幕范围,隐藏怪物*/
    if (monster->getPositionX() < 0) {
        monster->hide();
    }
    /*判断怪物是否碰撞玩家*/
    else if (monster->isCollideWithPlayer(m_player)) {
        m_player->hit();
        monster->hide();
    }
    /*这里省略很多代码*/
}

void MonsterManager::bindPlayer(Player *player) {
    m_player = player;
}

         TollgateScene.cpp文件

bool TollgateScene::init() {
    /*这里省略了很多代码*/
    /*创建怪物管理器*/
    MonsterManager *monsterMgr = MonsterManager::create();
    this->addChild(monsterMgr, 4);
    monsterMgr->bindPlayer(m_player);

    return true;
}

         3.12 怪物碰不到主角

            修改Entity.cpp的bindSprite函数,如下:

void Entity::bindSprite(Sprite *sprite) {
    this->m_sprite = sprite;
    this->addChild(m_sprite);

    Size size = m_sprite->getContentSize();
    m_sprite->setPosition(Point(size.width * 0.5f, size.height * 0.5f));
    this->setContentSize(size);
}

            而Sprite的中点(在嘴巴的下方,下图中用3个箭头指示)在Entity的左下角,于是,在金币和实体检测碰撞时,是以实体的宽、高和坐标为准的,这样就会造成碰撞的位置“不准确”。

             

         3.13 增加主角受伤时的动作

            修改Player.cpp的hit函数,如下:

void Player::hit() {
    if (getSprite() == NULL) {
        return;
    }

    /*扣血飘字特效*/
    FlowWord *flowWord = FlowWord::create();
    this->addChild(flowWord);
    flowWord->showWord("-15", getSprite()->getPosition());

    m_iHP -= 15;
    if (m_iHP < 0) {
        m_iHP = 0;
    }

    /* 创建几种动作对象*/
    auto backMove = MoveBy::create(0.1f, Point(-20, 0));
    auto forwardMove = MoveBy::create(0.1f, Point(20, 0));
    auto backRotate = RotateBy::create(0.1f, -5, 0);
    auto forwardRotate = RotateBy::create(0.1f, 5, 0);

    /* 分别组合成两种动作 */
    auto backActions = Spawn::create(backMove, backRotate, NULL);
    auto forwardActions = Spawn::create(forwardMove, forwardRotate, NULL);

    auto actions = Sequence::create(backActions, forwardActions, NULL);

    stopAllActions(); /*先停止所有正在执行的动作*/
    resetData();   /*重置数据*/
    runAction(actions); /*执行动作*/
}

          3.14 最后还有个resetData函数,如下:

void Player::resetData() {
    if (m_isJumping) {
        m_isJumping = false;
    }
    setPosition(Point(200, 140);
    setScale(1.0f);
    setRotation(0);
}

                 

      3.15 创建分数标签、血量等属性对象

        

     这里只需要给TollgateScene类加东西,如下:

          头文件:

class TollgateScene : public Layer {
    /*忽略了很多代码*/
    private:
        Int m_iScore;   //分数
        Text *m_scoreLab;  //分数标签
        LoadingBar *m_hpBar;  //血量条
};

          我们需要修改一下TollgateScene的loadUI函数,如下:

void TollgateScene::loadUI() {
    /*这里省略了一些代码*/
    /*获取控件对象*/
    auto jumpBtn = (Button *)Helper::seekWidgetByName(UI, "JumpBtn");
    m_scoreLab = (Text *)Helper::seekWidgetByName(UI, "scoreLab");
    m_hpBar = (LoadingBar *)Helper::seekWidgetByName(UI, "hpProgress");
    /*这里省略了一些代码*/
}

          让分数标签和血量条起作用,我们稍微修改一下TollgateScene的update函数即可:

void TollgateScene::update(float delta) {
    /*这里继续省略了很多代码*/
    /*增加分数*/
    m_iScore += 1;

    /*修改分数标签*/
    m_scoreLab->setText(Value(m_iScore).asString());

    /*修改血量进度*/
    m_hpBar->setPercent(m_player->getiHP() / 1000.0f * 100);
}

       

4.修改HelloWorldScene的init函数,如下代码:

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

    /*创建很多个小若精灵*/
    for (int i=0; i<14100; i++) {
        Sprite *xiaoruo = Sprite::create("sprite0.png");
        xiaoruo->setPosition(Point(CCRANDOM_0_1()*480, 120 + CCRANDOM_0_1()*300));
        this->addChild(xiaoruo);
    }
}

     4.1 3.0新功能---Auto-batching

         (1) 需确保精灵对象拥有相同的TextureId(精灵表单spritesheet);

         (2) 确保它们都使用相同的材质和混合功能;

         (3) 不再把精灵添加SpriteBatchNode上。

    4.2 修改HelloWorldScene的init函数,代码如下:

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

    /*创建很多个精灵*/
    for (int i=0; i<14100; i++) {
        Sprite *xiaoruo = Sprite::create("sprite0.png");
        xiaoruo->setPosition(Point(CCRANDOM_0_1()*480, 120+CCRANDOM_0_1()*300));
        this->addChild(xiaoruo);
    
        xiaoruo = Sprite::create("sprite1.png");
        xiaoruo->setPosition(Point(CCRANDOM_0_1()*480, 120+CCRANDOM_0_1()*300));
        this->addChild(xiaoruo);
    }
}

     

      怎么才算是“连续的对象”,最简单的解释就是:

  •          如果节点具有相同的globalZOrder值,则是连续的;
  •          否则,如果节点具有相同的localZOrder值,则是连续的;
  •          否则,如果节点具有相同的orderOfArrival值,则是连续的;
  •          连续的节点还必须使用相同的纹理(简单地说就是相同的图片)。

      我们来看HelloWorldScene的init函数,代码如下:

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

    /*创建很多很多个精灵*/
    for (int i=0; i<14100; i++) {
        Sprite *xiaoruo = Sprite::create("sprite0.png");
        xiaoruo->setPosition(Point(CCRANDOM_0_1()*480, 120+CCRANDOM_0_1()*300));
        this->addChild(xiaoruo);
        xiaoruo->setGlobalZOrder(1);

        xiaoruo = Sprite::create("sprite1.png");
        xiaoruo->setPosition(Point(CCRANDOm_0_1()*480, 120+CCRANDOM_0_1()*300));
        this->addChild(xiaoruo);
        xiaoruo->setGlobalZOrder(2);
    }
}

       4.3 修改HelloWorldScene的init函数,如下所示:

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

    /*创建一个精灵,它比较文雅*/
    auto sprite1 = Sprite::create("sprite1.png");
    sprite1->setPosition(Point(240, 160));
    this->addChild(sprite1);
    
    /*创建一个精灵,它比较霸气*/
    auto sprite2 = Sprite::create("sprite2.png");
    sprite2->setPosition(Point(200, 160));
    this->addChild(sprite2);
}

          

    4.4  继续修改,如下:

bool HelloWorld::init() {
    /*这里省略了很多代码*/
    sprite1->setLocalZOrder(2);
    sprite2->setLocalZOrder(1);
}

        

    4.5  新增一个Layer类。

        SecondLayer.h文件:

#ifndef __SecondLayer_H__
#define __SecondLayer_H__

#include"cocos2d.h"
USING_NS_CC;

class SecondLayer : public Layer {
    public:
        SecondLayer();
        ~SecondLayer();
        CREATE_FUNC(SecondLayer);
        virtual bool init();
};

#endif

         SecondLayer.cpp文件

#include "SecondLayer.h"
SecondLayer::SecondLayer() {
}

SecondLayer::~SecondLayer() {
}

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

    auto sprite3 = Sprite::create("sprite3.png");

    sprite3->setPosition(Point(240, 160));
    this->addChild(sprite3);

    return false;
}

       最后,把这个layer也添加到HelloWorldScene场景里,修改createScene函数,如下代码:

#include "SecondLayer.h"  //别忘了包含头文件

Scene *HelloWorld::createScene() {
    auto scene = Scene::create();

    auto layer = HelloWorld::create();
    Scene->addChild(layer);

    auto secondLayer = SecondLayer::create();
    scene->addChild(secondLayer);

    return scene;
}

        

     4.6 修改HelloWorldScene的init函数,如下代码所示:

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

    /* 创建批次渲染对象,并添加到场景里*/
    SpriteBatchNode *batchNOde = SpriteBatchNode::create("sprite.png");
    this->addChild(batchNode);

    /*创建很多个小若精灵*/
    for (int i=0; i< 999; i++) {
        Sprite *xiaoruo = Sprite::create("sprite.png");
        xiaoruo->setPosition(Point(CCRANDOM_0_1()*480, 120+CCRANDOM_0_1() * 200));

        /*将精灵添加到batchNode对象*/
        batchNode->addChild(xiaoruo);
    }
    return true;
}

      

     4.7 Texture纹理

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

    Sprite *sp1 = Sprite::createWithSpriteFrame(
                    SpriteFrame::create("sprite.png", Rect(0, 0, 60, 50)));
    Sprite *sp2 = Sprite::create("sprite.png");

    sp1->setPosition(Point(100, 200));
    sp2->setPosition(Point(250, 200));
    this->addChild(sp1);
    this->addChild(sp2);

    /*获取两个精灵的纹理对象*/
    Texture2D *t1 = sp1->getTexture();
    Texture2D *t2 = sp2->getTexture();

    return true;
}

      

5.用打包前的图片创建动画

     HelloWorldScene.h文件:

class HelloWorld : public cocos2d::Layer {
    public:
        /*省略了很多代码*/
    private:
        /*用打包前图片创建动画*/
        cocos2d::Animate *createAnimate1();
};

     HelloWorldScene.cpp文件:

cocos2d::Animate *HelloWorld::createAnimate1() {
    int iFrameNum = 15;
    SpriteFrame *frame = NULL;
    Vector<SpriteFrame *> frameVec;

    /*用一个列表保存所有SpriteFrame对象*/
    for (int i=1; i<=iFrameNum; i++) {
        /*用每一张图盘创建SpriteFrame对象*/
        frame = SpriteFrame::create(StringUtils::format("run%d.png", i), Rect(0, 0, 130, 130));
        frameVec.pushBack(frame);
    }

    /*使用SpriteFrame列表创建动画对象*/
    Animation *animation = Animation::createWithSpriteFrames(frameVec);
    animation->setLoops(-1);
    animation->setDelayPerUnit(0.1f);

    /*将动画包装成一个动作*/
    Animate *action = Animate::create(animation);
    return action;
}

       

      创建动画的步骤一般有3步:

        (1) 创建一组SpriteFrame对象,每张动画图片为一个SpriteFrame对象;

        (2) 用这组SpriteFrame对象创建一个Animation对象,该对象包含了动画所需的一些配置信息;

        (3) 我们要利用Animation对象创建一个Animate对象,Animate其实也是一个动作。精灵直接调用runAction函数即可执行Animate动画。

          我们修改一下HelloWorldScene的init函数,测试一下,代码如下:

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    /*先创建一个精灵*/
    Sprite *runSp = Sprite::create("run1.png");
    runSp->setPosition(Point(200, 200));
    this->addChild(runSp);

    /*动画也是动作,精灵直接执行动画动作即可*/
    runSp->runAction(createAnimate1());
    return true;
}

         

       在创建了Animation对象后,要设置动画的属性,代码如下:

/*使用SpriteFrame列表创建动画对象*/
Animation *animation = Animation::createWithSpriteFrames(frameVec);
animation->setLoops(-1);
animation->setDelayPerUnit(0.1f);

         setLoops函数用于设置动画的播放次数,将setLoops的参数设为-1,就代表循环播放动画。

6.用打包后的图片创建动画

    HelloWorldScene.h文件:

class HelloWorld : public cocos2d::Layer {
    public:
        /*这里省略很多代码*/
    private:
        /*用打包前图片创建动画*/
        cocos2d::Animate *createAnimate1();

        /*用打包后的图片创建动画*/
        cocos2d::Animate *createAnimate2();
};

    HelloWorldScene.cpp文件:

cocos2d::Animate *HelloWorld::createAnimate2() {
    /*加载图片帧到缓存池*/
    SpriteFrameCache *frameCache = SpriteFrameCache::getInstance();
    frameCache->addSpriteFramesWithFile("boys.plist", "boys.png");

    int iFrameNum = 15;
    SpriteFrame *frame = NULL;
    Vector<SpriteFrame *> frameVec;

    /*用一个列表保存所有SpriteFrame对象*/
    for (int i=1; i<=iFrameNum; i++) {
        /*从SpriteFrame缓存池中获取SpriteFrame对象*/
        frame = frameCache->getSpriteFrameByName(StringUtils::format("run%d.png", i));
        frameVec.pushBack(frame);
    }

    /*使用SpriteFrame列表创建动画对象*/
    Animation *animation = Animation::createWithSpriteFrames(frameVec);
    animation->setLoops(-1);
    animation->setDelayPerUnit(0.1f);

    /*将动画包装成一个动作*/
    Animation *action = Animate::create(animation);
    
    return action;
}

7.动画创建辅助类

   新建一个类,命名为AnimationUtil,头文件代码如下:

#ifndef __AnimationUtil_H__
#define __AnimationUtil_H__

#include "cocos2d.h"
USING_NS_CC;

class AnimationUtil {
    public:
        /*根据文件名字前缀创建动画对象*/
        static Animation *createWithSingleFrameName(const char *name, float delay, int iLoops);

        /*根据文件名字前缀创建动画对象,指定动画图片数量*/
        static Animation *createWithFrameNameAndNum(const char *name, int iNum, float delay, int iLoops);
};

#endif

 

Animation *AnimationUtil::createWithSingleFrameName(const char *name, float delay, int iLoops) {
    SpriteFrameCache *cache = SpriteFrameCache::getInstance();

    Vector<SpriteFrame *> frameVec;
    SpriteFrame *frame = NULL;
    int index = 1;
    
    do {
        frame = cache->getSpriteFrameByName(StringUtils::format("%s%d.png", name, index++));

        /*不断地获取SpriteFrame对象,直到获取的值为NULL*/
        if (frame == NULL) {
            break;
        }

        frameVec.pushBack(frame);
    } while(true);

    Animation *animation = Animation::createWithSpriteFrames(frameVec);
    animation->setLoops(iLoops);
    animation->setRestoreOriginalFrame(true);
    animation->setDelayPerUnit(delay);

    return animation;
}

Animation *AnimationUtil::createWithFrameNameAndNum(const char *name, int iNum, float delay, int iLoops) {
    SpriteFrameCache *cache = SpriteFrameCache::getInstance();
    
    Vector<SpriteFrame *> frameVec;
    SpriteFrame *frame = NULL;
    int index = 1;
    for (int i=0; i<=iNum; i++) {
        frame = cache->getSpriteFrameByName(StringUtils::format("%s%d.png", name, i));
        if (frame == NULL) {
            break;
        }
        frameVec.pushBack(frame);
    }

    Animation *animation = Animation::createWithSpriteFrames(frameVec);
    animation->setLoops(iLoops);
    animation->setRestperOriginalFrame(true);
    animation->setDelayPerUnit(delay);

    return animation;
}

        测试一下,修改HelloWorldScene的init函数,如下:

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

    /*先创建一个精灵*/
    Sprite *runSp = Sprite::create("run1.png");
    runSp->setPosition(Point(200, 200));
    this->addChild(runSp);

    /*加载图片帧到缓存池*/
    SpriteFrameCache *frameCache = SpriteFrameCache::getInstance();
    frameCache->addSpriteFramesWithFile("boys.plist", "boys.png");

    /*用辅助工具创建动画*/
    Animation *animation = AnimationUtil::createWithSingleFrameName("run", 0.1f, -1);
    //Animation *animation = AnimationUtil::createWithFrameNameAndNum("run", 15, 0.1f, -1);

    /*动画也是动作,精灵直接执行动画动作即可*/
    runSp->runAction(Animate::create(animation));

    return true;
}

        使用的步骤如下:

        (1) 加载图片帧到缓存池,因为一张打包好的图片往往不是一种动画,所以最好不要在创建Animation对象的函数时才加载图片帧。

        (2) 调用AnimationUtil的函数创建Animation对象。

        (3) 创建Animate对象,精灵执行该动作即可。

8.《跑跑跑》

   8.1 创建跑步场景

       TollgateScene.h文件:

#ifndef _TollgateScene_H_
#define _TollgateScene_H_

#include "cocos2d.h"
using namespace cocos2d;

class TollgateScene : public Layer {
    public:
        static Scene *createScene();
        CREATE_FUNC(TollgateScene);
        virtual bool init();
};
#endif

         TollgateScene.cpp文件:

Scene *TollgateScene::createScene() {
    auto scene = Scene::create();
    auto layer = TollgateScene::create();
    scene->addChild(layer);
    return scene;
}
bool TollgateScene::init() {
    if (!Layer::init()) { return false; }

    /*加载Tiled地图,添加到场景中*/
    TMXTiledMap *map = TMXTiledMap::create("level01.tmx");

    this->addChild(map);
    return true;
}
    

   8.2 创建实体类和主角类

      Entity.h文件: 

#ifndef _Entity_H_
#define _Entity_H_

#include "cocos2d.h"
using namespace cocos2d;
class Entity : public Node {
    public:
        /*绑定精灵对象*/
        void bindSprite(Sprite *sprite);
    protected:
        Sprite *m_sprite;
};

#endif

       Entity.cpp文件:

#include "Entity.h"

void Entity::bindSprite(Sprite *sprite) {
    m_sprite = sprite;
    this->addChild(m_sprite);
}

       Player.h文件

#ifndef _Player_H_
#define _Player_H_

#include "Entity.h"
class Player : public Entity {
    public:
        CREATE_FUNC(Player);
        virtual bool init();
        void run();
};

#endif

      Player.cpp文件

bool Player::init() {
    return true;
}

void Player::run() {
}

      玩家有了,我们把它加到地图里。打开TollgateScene.cpp文件,修改init方法,如下代码:

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

    /*加载Tiled地图,添加到场景中(这部分代码没贴出来)*/
    
    addPlayer(map);  /*加载玩家*/
    return true;
}

       addPlayer函数如下:

void TollgateScene::addPlayer(TMXTiledMap *map) {
    Size visibleSize = Director::getInstance()->getVisibleSize();

    /*创建精灵*/
    Sprite *playerSprite = Sprite::create("player.png");

    /*将精灵绑定到玩家对象上*/
    Player *mPlayer = Player::create();
    mPlayer->bindSprite(playerSprite);
    mPlayer->run();

    /*设置玩家坐标*/
    mPlayer->setPosition(Point(100, visibleSize.height / 2));

    /*将玩家添加到地图*/
    map->addChild(mPlayer);
}

     效果图:

             

   8.3 继续打开TollgateScene.cpp文件,修改addPlayer函数,如下:

/*加载对象层*/
    TMXObjectGroup *objGroup = map->getObjectGroup("objects");

    /*加载玩家坐标对象*/
    ValueMap playerPointMap = objGroup->getObject("PlayerPoint");
    float playerX = playerPointMap.at("x").asFloat();
    float playerY = playerPointMap.at("y").asFloat();

    /*设置玩家坐标*/
    mPlayer->setPosition(Point(playerX, playerY));

    8.4 让主角跑

        首先给Player类增加一个函数,代码如下:

void Player::run() {
    SpriteFrameCache *frameCache = SpriteFrameCache::getInstance();
    frameCache->addSpriteFramesWithFile("boys.plist", "boys.png");

    SpriteFrame *frame = NULL;
    Vector<SpriteFrame *> frameList;

    /*创建精灵帧对象,添加到列表里*/
    for (int i=1; i<=15; i++) {
        frame = frameCache->getSpriteFrameByName(StringUtils::format("run%d.png", i));
        frameList.pushBack(frame);
    }

    /*根据精灵帧对象创建动画对象*/
    Animation *animation = Animation::createWithSpriteFrames(frameList);
    animation->setLoops(-1); //循环播放
    animation->setDelayPerUnit(0.08f);  //每帧播放间隔

    /*创建动画动作*/
    Animate *animate = Animate::create(animation);
    
    /*精灵执行动作*/
    m_sprite->runAction(animate);
}

         效果:

           

    8.5 添加角色控制器

       Controller.h文件:

#ifndef _Controller_H_
#define _Controller_H_

#include "cocos2d.h"
#include "ControllerListener.h"

using namespace cocos2d;
class Controller : public Node {
    public:
        /*设置监听对象*/
        void setControllerListener(ControllerListener *controllerListener);
    protected:
        ControllerListener *m_controllerListener;
};

#endif

      Controller.cpp文件

void Controller::setControllerListener(ControllerListener *controllerListener) {
    this->m_controllerListener = controllerListener;
}

    ControllerListener就是将要被控制的对象,比如主角,只要继承了ControllerListener接口,就能够被控制器控制。

       ControllerListener.h头文件:

#ifndef _ControllerListener_H_
#define _ControllerListener_H_

#include "cocos2d.h"
using namespace cocos2d;
class ControllerListener {
    public:
        /*设置目标坐标*/
        virtual void setTagPosition(int x, int y) = 0;

        /*获取目标坐标*/
        virtual Point getTagPosition() = 0;
};

#endif

       8.6 主角移动控制器

          SimpleMoveController.h文件

#ifndef _SimpleMoveController_H_
#define _SimpleMoveController_H_

#include "cocos2d.h"
#include "Controller.h"
using namespace cocos2d;

class SimpleMoveController : public Controller {
    public:
        CREATE_FUNC(SimpleMoveController);
        virtual bool init();
        virtual void update(float dt);

        /*设置移动速度*/
        void setiSpeed(int iSpeed);
    private:
        int m_iSpeed;
};

#endif

           SimpleMoveController.cpp文件

bool SimpleMoveController::init() {
    this->m_iSpeed = 0;
    
    /*每一帧都要调用update函数,所以要这样设置*/
    this->scheduleUpdate();

    return true;
}
void SimpleMoveController::update(float dt) {
    if (m_controllerListener == NULL) {
        return;
    }
    /*增加移动对象的X坐标值*/
    Point pos = m_controllerListener->getTagPosition();
    pos.x += m_iSpeed;
    m_controllerListener->setTagPosition(pos.x, pos.y);
}

void SimpleMoveController::setiSpeed(int iSpeed) {
    this->m_iSpeed = iSpeed;
}

           我们需要修改Entity类:

       Entity.h文件:

#include "ControllerListener.h"
#include "Controller.h"
class Entity : public Node, public ControllerListener {
    public:
        /*绑定精灵对象*/
        void bindSprite(Sprite *sprite);

        /*设置控制器*/
        void setController(Controller *controller);

        /*实现SimpleMoveListener接口的方法*/
        virtual void setTagPosition(int x, int y);
        virtual Point getTagPosition();
    protected:
        Sprite *m_sprite;
        Controller *m_controller;
};

       我们还为Entity新增了一个方法,那就是setController.

void Entity::setController(Controller *controller) {
    this->m_controller = controller;
    m_controller->setControllerListener(this);
}

void Entity::setTagPosition(int x, int y) {
    setPosition(Point(x, y));
}

Point Entity::getTagPosition() {
    return getPosition();
}

      TollgateScene.cpp的addPlayer函数:

#include "SimpleMoveController.h" 

void TollgateScene::addPlayer(TMXTiledMap *map) {
    /*省略了很多很多代码*/
    /*--------------创建玩家简单移动控制器--------------*/
    SimpleMoveController *simpleMoveControll = SimpleMoveController::create();

    /*设置移动速度*/
    simpleMoveControll->setiSpeed(1);

    /*控制器要添加到场景中才能让update被调用*/
    this->addChild(simpleMoveControll);

    /*设置控制器到主角身上*/
    mPlayer->setController(simpleMoveControll);
}

     现在主角就会一直往前跑了,效果:

           

      8.7 让地图随着主角滚动

          为Player类增加一个函数,如下:

void Player::setViewPointByPlayer() {
    if (m_sprite == NULL){
        return;
    }
    Layer *parent = (Layer *)getParent();

    /*地图方块数量*/
    Size mapTiledNum = m_map->getMapSize();

    /*地图单个格子大小*/
    Size tiledSize = m_mpa->getTileSize();

    /*地图大小*/
    Size mapSize = Size(
        mapTiledNum.width * tiledSize.width,
        mapTiledNum.height * tiledSize.height);

    /*屏幕大小*/
    Size visibleSize = Director::getInstance()->getVisibleSize();

    /*主角坐标*/
    Point spritePos = getPosition();

    /*如果主角坐标小于屏幕的一半,则取屏幕中点坐标,否则取主角的坐标*/
    float x = std::max(spritePos.x, visibleSize.width / 2);
    float y = std::max(spritePos.y, visibleSize.height / 2);

    /*如果X、Y的坐标大于右上角的极限值,则取极限值的坐标(极限值是指不让地图超出屏幕造成出现黑边的极限坐标*/
    x = std::min(x, mapSize.width - visibleSize.width / 2);
    y = std::min(y, mapSize.height - visibleSize.height / 2);

    /*目标点*/
    Point destPos = Point(x, y);
    
    /*屏幕中点*/
    Point centerPos = Point(visibleSize.width / 2, visibleSize.height / 2);

    /*计算屏幕中点和所要移动的目的点之间的距离*/
    Point viewPos = centerPos - destPos;

    parent->setPosition(viewPos);
}

          这个函数的功能是让地图所在图层以主角为中心进行移动,也就是让事件的焦点停留在主角身上,屏幕随着主角移动。

     然后,Player要重写父类的setTagPosition函数,代码如下:

void Player::setTagPosition(int x, int y) {
    Entity::setTagPosition(x, y);

    /*以主角为中心移动地图*/
    setViewPointByPlayer();
}

          在头文件中加入函数声明:

virtual void setTagPosition(int x, int y);

        再次修改Player类,代码如下:

     Player.h文件:

class Player : public Entity {
    public:
        /*省略了很多代码*/
        void setTiledMap(TMXTiledMap *map);
    private:
        TMXTiledMap *m_map;
};

    Player.cpp文件

void Player::setTiledMap(TMXTiledMap *map) {
    this->m_map = map;
}

     打开TollgateScene的addPlayer函数,在创建Player对象之后,再调用Player的setTiledMap函数,如下:

void TollgateScene::addPlayer(TMXTiledMap *map) {
    /*省略了一些代码*/
    
    /*将精灵绑定到玩家对象上*/
    Player *mPlayer = Player::create();
    mPlayer->bindSprite(playerSprite);
    mPlayer->run();
    mPlayer->setTiledMap(map);

    /*省略了很多代码*/
}

       地图会有一些细细的黑边,可能滚动的时候特别明显,在代码中加入:

Director::getInstance()->setProjection(Director::Projection::_2D);

        

      8.8 三方移动控制器

        ThreeDirectionController.h文件:

#include "Controller.h"
#include "cocos2d.h"
using namespace cocos2d;

class ThreeDirectionController : public Controller {
    public:
        CREATE_FUNC(ThreeDirectionController);
        virtual bool init();
        virtual void update(float dt);

        /*设置X方向的移动速度*/
        void setiXSpeed(int iSpeed);

        /*设置Y方向的移动速度*/
        void setiYSpeed(int iSpeed);

    private:
        int m_iXSpeed;
        int m_iYSpeed;

        /*注册屏幕触摸事件*/
        void registerTouchEvent();
};

         ThreeDirectionController.cpp文件:

#include "ThreeDirectionController.h" 

bool ThreeDirectionController::init() {
    this->m_iXSpeed = 0;
    this->m_iYSpeed = 0;

    /*注册屏幕触摸事件*/
    registerTouchEvent();

    /*开启update函数的调用*/
    this->scheduleUpdate();
    return true;
}

void ThreeDirectionController::update(float dt) {
    if (m_controllerListener == NULL) {
        return;
    }

    /*让移动对象在X和Y方向上增加坐标*/
    Point curPos = m_controllerListener->getTagPosition();
    curPos.x += m_iXSpeed;

    m_controllerListener->setTagPosition(curPos.x + m_iXSpeed, curPos.y + m_iYSpeed);
}

void ThreeDirectionController::setiXSpeed(int iSpeed) {
    this->m_iXSpeed = iSpeed;
}

void ThreeDirectionController::setiYSpeed(int iSpeed) {
    this->m_iYSpeed = iSpeed;
}

void ThreeDirectionController::registerTouchEvent() {
    auto listener = EventListenerTouchOneByOne::create();
    
    listener->onTouchBegan = [](Touch *touch, Event *event) {
        return true;
    };

    listener->onTouchMoved = [&](Touch *touch, Event *event) {
        /*获取单击坐标,基于Cocos2d-x*/
        Point touchPos = Director::getInstance()->convertToGL(touch->getLocationInView());

        /*被控制对象的坐标*/
        Point pos = m_controllerListener->getTagPosition();

        /*判断是向上移动还是向下移动*/
        int iSpeed = 0;
        if (touchPos.y > pos.y) {
            iSpeed = 1;
        } else {
            iSpeed = -1;
        }

        setiYSpeed(iSpeed);
    };

    listener->onTouchEnded = [&](Touch *touch, Event *event) {
        /*停止Y坐标上的移动*/
        setiYSpeed(0);
    };

    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}

        打开TollgateScene.cpp的addPlayer函数,将SimpleMoveController替换为ThreeDirectionController,代码如下:

#include "ThreeDirectionController.h"
void TollgateScene::addPlayer(TMXTiledMap *map) {
    /*又忽略了很多代码*/
    
    /* ---------------创建玩家移动控制器-------------*/
    ThreeDirectionController *threeMoveControll = ThreeDirectionController::create();
    threeMoveControll->setiXSpeed(1);
    threeMoveControll->setiYSpeed(0);

    /*控制器要添加到场景中才能获得update事件*/
    this->addChild(threeMoveControll);

    /*设置控制器到主角身上*/
    mPlayer->setController(threeMoveControl);
}

         为Player添加一个函数tileCoordForPosition,代码如下:

     Player.h文件:

class Player : public Entity {
    /*省略了很多代码*/
    private:
        /*标记主角是否碰撞了障碍物,在反弹中*/
        bool isJumping;

        /*检测碰撞的地图层*/
        TMXLayer *meta;

        /*将像素坐标转换为地图格子坐标*/
        Point tileCoordForPosition(Point pos);
};

     Player.cpp文件:

Point Player::tileCoordForPosition(Point pos) {
    Size mapTiledNum = m_map->getMapSize();
    Size tiledSize = m_map->getTileSize();

    int x = pos.x / tiledSize.width;

    /*Cocos2d-x的默认Y坐标是由下至上的,所以要做一个相减操作*/
    int y = (700 - pos.y) / tiledSize.height;

    /*格子坐标从零开始计算*/
    if (x > 0) {
        x -= 1;
    }
    if (y > 0) {
        y -= 0;
    }

    return Point(x, y);
}

        再次打开Player类,修改setTiledMap函数,代码如下:

void Player::setTiledMap(TMXTiledMap *map) {
    this->m_map = map;
    
    /*保存meta图层的引用*/
    this->meta = m_map->getLayer("meta");
    this->meta->setVisible(false);
}

         最后一步,修改Player.cpp的setTagPosition函数:

void Player::setTagPosition(int x, int y) {
    /*----------------判断前面是否不可通行------------*/
    /*取主角前方的坐标*/
    Size spriteSize = m_sprite->getContentSize();
    Point dstPos = Point(x + spriteSize.widht / 2, y);

    /*获得当前主角前方坐标在地图中的格子位置*/
    Point tiledPos = tileCoordForPosition(Point(dstPos.x, dstPos.y));

    /*获取地图格子的唯一标识*/
    int tiledGid = meta->getTileGIDAt(tiledPos);

    /*不为0,代表存在这个格子*/
    if (tiledGid != 0) {
        /*获取该地图格子的所有属性,目前我们只有一个Collidable属性
        格子是属于meta层的,但同时也是属于整个地图的,所以在获取格子
        的所有属性时,通过格子唯一标识在地图中取得*/
        Value properties = m_map->getPropertiesForGID(tiledGid);

        /*取得格子的collidate属性值*/
        Value prop = properties.asValueMap().at("Collidable");

        /*判断Collidable属性是否为true,如果是,则不让玩家移动*/
        if (prop.asString()pare("true") == 0) {
            return;
        }
    }

    Entity::setTagPosition(x, y);

    /*以主角为中心移动地图*/
    setViewPointByPlayer();
}

            

     8.9 当遇到障碍物时,不是让主角停止前进,而是让主角向后弹,如代码所示:

/*判断Collidate属性是否为true,如果是,不让玩家移动*/
if (prop.asString()pare("true") == 0 && isJumping == false) {
    isJumping = true;

    auto jumpBy = JumpBy::create(0.5f, Point(-100, 0), 80, 1);
    CallFunc *callfunc = CallFunc::create([&](){
        /*恢复状态*/
        isJumping = false;
    });

    /*执行动作,碰撞到障碍物时的反弹效果*/
    auto actions = Sequence::create(jumpBy, callFunc, NULL);
    this->runAction(actions);
}
        

      8.10 添加能吃的物品以及胜利条件

         打开Player.cpp的setTagPosition函数,如下:

Value properties = m_map->getPropertiesForGID(tiledGid);

ValueMap propertiesMap = properties.asValueMap();

if (propertiesMap.find("Collidable") != propertiesMap.end()) {
    /*取得格子的Collidable属性值*/
    Value prop = propertiesMap.at("Collidable");
    /*判断Collidable属性是否为true,如果是,则不让玩家移动*/
    if (prop.asString()pare("true") == 0 && isJumping == false) {
        /*这里面的代码没变,所以省略*/
    }
}
if(propertiesMap.find("food") != propertiesMap.end()) {
    /*取得格子的food属性值,判断是否为true,如果是,则让格子上的物体消失*/
    Value prop = properties.asValueMap().at("food");
    if (prop.asString()pare("true") == 0) {
        /*从障碍物层清除当前格子的物体*/
        TMXLayer *barrier = m_map->getLayer("barrier");
        barrier->removeTileAt(tiledPos);
    }
}

           最后,依旧修改Player的setTagPosition函数,代码如下:

if (propertiesMap.find("win") != propertiesMap.end()) {
    Value prop = properties.asValueMap().at("win");
    if (prop.asString()pare("true") == 0) {
        /*取得格子的win属性值,判断是否为true,如果是,则游戏胜利,跳转到胜利场景*/
        Director::getInstance()->replaceScene(WinScene::createScene());
    }
}

            我们判断地图格子的win属性是否为true,如果是,则代表主角达到终点,切换到胜利场景,如下图所示:

     

9.scheduleUpdate和update

    HelloWorldScene的init函数:

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    this->scheduleUpdate();
    return true;
}

     HelloWorldScene.h文件:

class HelloWorld : public cocos2d::Layer {
    public:
        static cocos2d::Scene *createScene();
        virtual bool init();
        CREATE_FUNC(HelloWorld);

        /*重写update函数*/
        virtual void update(float dt);
};

      this->scheduleUpdate()函数是为了把当前节点(如Layer)添加到队列里,只要把节点添加到队列里(或许是其他结构,总之可以存放节点),那么这个节点就会在游戏运行的每一帧被调用一次update函数。

      9.1 不调用update函数,调用自己的函数

          先为HelloWorldScene添加一个函数,代码如下:

         HelloWorldScene.h文件 

class HelloWorld : public cocos2d::Layer {
    public:
        static cocos2d::Scene *createScene();
        virtual bool init();
        CREATE_FUNC(HelloWorld);
        
        void mutUpdate(float dt); /*自定义的update函数*/
};

         HelloWorldScene.cpp文件:

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

    /*指定每帧执行自定义的函数*/
    this->schedule(schedule_selector(HelloWorld::mutUpdate));

    return true;
}

void HelloWorld::mutUpdate(float dt) {
    log("MutUpdate");
}

          Cocos2d-x在指定回调函数时都使用*selector的形式,比如我们要知道schedule的回调函数,则使用schedule_seelctor,要指定按钮的回调函数则使用callfunc_selector等。

          常用的selector如下:

  •            schedule_selector: 常用于定义定时器回调函数,函数无参数;
  •            callfunc_selector: 常用于定义动作回调函数,函数无参数;
  •            callfuncN_selector: 常用于定义动作回调函数,函数带一个Node *参数;
  •            callfuncND_selector:常用于定义动作回调函数,函数带一个Node *参数和一个void *参数(任意类型);
  •            menu_selector:常用于定义菜单回调函数,带一个CCObject*参数。

         9.2 真正的定时器

           修改一下schedule的参数就可以了,如下代码:

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

    /*指定每帧执行自定义的函数*/
    this->schedule(schedule_selector(HelloWorld::mutUpdate), 2.0f);
    return true;
}
void HelloWorld::mutUpdate(float dt) {
    log("MutUpdate dt = %f", dt);
}

             输出:

             

         9.3 让一切都停下来-----unSchedule

            HelloWorldScene.h文件:

class HelloWorld : public cocos2d::CCLayer {
    public:
        static cocos2d::Scene *createScene();
        virtual bool init();
        CREATE_FUNC(HelloWorld);

        virtual void update(float dt); /*默认的update函数*/
        
        void mutUpdate(float dt); /*自定义的update函数*/
};

             HelloWorldScene.cpp文件:

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

    /*指定每帧执行update函数*/
    this->scheduleUpdate();

    /*指定每帧执行自定义的函数,指定每隔n秒执行一次*/
    this->schedule(schedule_selector(HelloWorld::mutUpdate), 2.0f);
    return true;
}

void HelloWorld::mutUpdate(float dt) {
}

void HelloWorld::update(float dt) {
    log("update");
    this->unscheduleUpdate();
}

           如果想停止调用自定义的update函数,代码如下:

void HelloWorld::mutUpdate(float dt) {
    log("Mutupdate dt = %f", dt);
    this->unschedule(schedule_selector(HelloWorld::mutUpdate));
}

          要想停止所有的update函数,只需要一句代码:this->unscheduleAllSelectors()。

       9.4 scheduleOnce和回调函数

          HelloWorldScene.h文件:

class HelloWorld : public cocos2d::CCLayer {
    public:
        static cocos2d::Scene *createScene();
        virtual bool init();
        CREATE_FUNC(HelloWorld);

        /*回调函数,西红柿烤好了*/
        void cookFinish(float dt);
};

           HelloWorldScene.cpp文件

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

    /*指定若干秒后执行一次函数*/
    this->scheduleOnce(schedule_selector(HelloWorld::cookFinish), 2.0f);
    return true;
}

void HelloWorld::cookFinish(float dt) {
    log("cookFinish!");
}

       9.5 更准确地计时----不会变慢的时间

          TimeCounter.h文件:

#include "cocos2d.h"
USING_NS_CC;
class TimeCounter : public Node {
    public:
        CREATE_FUNC(TimeCounter);
        virtual bool init();

        virtual void update(float dt);

        void start();   /*开始计时*/
        float getfCurTime();   /*获取当前时间*/
    private:
        float m_fTime;
};

          TimeCounter.cpp文件:

#include "TimeCounter.h"
bool TimeCounter::init() {
    return true;
}
void TimeCounter::update(float dt) {
    m_fTime += dt;
}
float TimeCounter::getfCurTime() {
    return m_fTime;
}

void TimeCounter::start() {
    m_fTime = 0;
    this->scheduleUpdate();
}

       来看看执行情况,修改HelloWorldScene类,代码如下:

      HelloWorldScene.h文件:

#include "TimeCounter.h"
class HelloWorld : public cocos2d::Layer {
    public:
        static cocos2d::Scene *createScene();
        virtual bool init();
        CREATE_FUNC(HelloWorld);

        void logic(float dt);
        void doSomething(float dt);
    private:
        TimeCounter *m_timeCounter;
};

       HelloWorldScene.cpp文件:

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    /*创建计时器,要添加到场景*/
    m_timeCounter = TimeCounter::create();
    this->addChild(m_timeCounter);

    /*开始计时*/
    m_timeCounter->start();

    this->schedule(schedule_selector(HelloWorld::logic), 1.0f);
    this->schedule(schedule_selector(HelloWorld::doSomething));
    return true;
}

void HelloWorld::logic(float dt) {
    log("%f", m_timeCounter->getfCurTime());
}

void HelloWorld::doSomething(float dt) {
    for (int i=0; i<9999999; i++) {
    }
}

       结果如下:

      

     9.6 给TimeCounter做进一步的强化,让它可以在我们规定的时间做我们预设的事情。

       CallbakcTimeCounter.h文件:

class CallbackTimeCounter : public Node {
    public:
        CREATE_FUNC(CallbackTimeCounter);
        virtual bool init();

        virtual void update(float dt);

        /*开始计时,指定回调时间和回调函数*/
        void start(float fCBTime, std::function<void()> func);
    private:
        float m_fTime; /*用于计时*/
        float m_fCBTime; /*回调的时间*/
        bool m_isCounting; /*标记是否正在计时*/
    
        std::funuction<void()> m_func; /*回调函数*/
};

    这次我们的定时器是这样工作的:

  •    不断调用update函数,根据m_isCounting标记,判断是否要计时;
  •    发现到了指定时间后,就回调m_func函数;
  •    使用方式是,调用start函数,传入指定时间,以及要回调的函数(lambda匿名函数)

      CallbackTimeCounter.cpp文件:

#include "CallbackTimeCounter.h"

/*在调用start函数之前都不开始计时;然后调用scheduleUpdate开启update函数的调用*/
bool CallbackTimeCounter::init() {
    m_isCounting = false;
    this->scheduleUpdate();
    return true;
}

/*如果m_isCounting为true,则执行计时,使用m_fTime来累计总时间。
当m_fTime达到或超过指定时间时,就回调m_func函数,并结束计时状态(设置m_isCounting为false)*/
void CallbackTimeCounter::update(float dt) {
    if (m_isCounting == false) {
        return;
    }

    m_fTime += dt;

    /*到达时间,回调函数*/
    if (m_fTime >= m_fCBTime) {
        m_func();
        m_isCounting = false;
    }
}

void CallbackTimeCounter::start(float fCBTime, std::function<void()> func) {
    m_fCBTime = fCBTime;
    m_fTime = 0;
    m_func = func;
    m_isCounting = true;
}

     9.7 NotificationCenter常用的几个函数及其参数的说明如下:

       (1) addObserver(订阅消息)

       (2) removeObserver(取消订阅消息)

       (3) postNotification(发布消息)

            const std::string &name:消息名称

       (4) postNotification(发布消息)

            const std::string &name:消息名称

            Ref *sender: 传递的数据

        消息订阅不仅仅能用于同一个Layer下的对象,它最强大的功能在于可以跨越不同的Layer进行消息订阅和发布。

        来看看两个Layer之间如何进行消息订阅和发布,创建一个新的类,命名为OtherLayer,代码如下:

       OtherLayer.h文件:

class OtherLayer : public Layer {
    public:
        CREATE_FUNC(OtherLayer);
        virtual bool init();
    private:
        /*接收test消息的回调函数*/
        void testMsg(Ref *pData);
};

      OtherLayer.cpp文件:

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

    /*订阅消息类型为test的消息,不传递消息*/
    NotificationCenter::getInstance()->addObserver(
            this,
            callfuncO_selector(OtherLayer::testMsg),
            "test",
            NULL);
    
    return true;
}

void OtherLayer::testMsg(Ref *pData) {
    log("testMsg in OtherLayer");
}

          我们新建一个继承了layer的类,并且该类订阅了test消息。我们把HelloWorldScene的testMsg函数删除,并且把订阅消息的代码也删除,同时修改scene函数,代码如下:

     

#include "OtherLayer.h"

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

    /*在添加一个layer*/
    auto otherLayer = OtherLayer::create();
    scene->addChild(otherLayer);

    return scene;
}
bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    
    /*3秒后发布test的消息*/
    this->schedule(schedule_selector(HelloWorld::sendMsg), 3.0f);
    return true;
}

void HelloWorld::sendMsg(float dt) {
    /*发布test消息,不传递数据*/
    NotificationCenter::getInstance()->postNotification("test", NULL);
}
   

       再次运行项目,3秒后,将看到以下日志输出:

testMsg in OtherLayer

        我们在HelloWorldScene里发布消息,在OtherLayer里接收消息,这次就能够表现出消息订阅的优势了。再具体一点,比如游戏中的战斗,角色死亡的消息也可以被订阅,当角色死亡后,那些订阅了这个消息的主体就能做出一些处理(比如怪物停止攻击

弹出死亡提示界面,等等)。

    9.8 用lambda函数来实现自己的观察者吧

        NotifyUtil.h文件:

class NotifyUtil : public Ref {
    public:
        static NotifyUtil *getInstance();
        CREATE_FUNC(NotifyUtil);
        virtual bool init();

        /*订阅消息*/
        void addObserver(const std::string &sMsgName, std::function<void(Ref*)> func);

        /*发布消息*/
        void postNotification(const std::string &sMsgName, Ref *data);
    private:
        static NotifyUtil *m_NotifyUtil;

        /*看起来很复杂,其实很简单*/
        std::map<std::string, std::vector<std::function<void(Ref*)>>> m_funcMap;
};

         NotifyUtil.cpp文件:

#include "NotifyUtil.h"
NotifyUtil *NotifyUtil::m_NotifyUtil = NULL;

NotifyUtil *NotifyUtil::getInstance() {
    if (m_NotifyUtil == NULL) {
        m_NotifyUtil = NotifyUtil::create();
        m_NotifyUtil->retain();
    }
    return m_NotifyUtil;
}
bool NotifyUtil::init() {
    return true;
}

void NotifyUtil::addObserver(const std::string &sMsgName, std::function<void(Ref*)> func) {
    /*查找是否有已经存在该消息的回调列表*/
    if (m_funcMap.find(sMsgName) != m_funcMap.end()) {
        /*已经存在该回调列表(换句话说,已经有人订阅过同样的消息 */
        std::vector<std::function<void(Ref*)>> &funcList = m_funcMap.at(sMsgName);

        /*将新的订阅者添加到回调列表里*/
        funcList.push_back(func);
    } else {
        /*不存在该回调列表(换句话说,已经有人订阅过同样的消息*/
        std::vector<std::function<void(Ref*)>> funcList;

        /*将新建的列表保存到map中*/
        m_funcMap[sMapName] = funcList;
    }
}

void NotifyUtil::postNotification(const std::string &sMsgName, Ref *data) {
    /*查找是否有人订阅过该消息*/
    if (m_funcMap.find(sMsgName) != m_funcMap.end()) {
        /*取得回调列表*/
        std::vector<std::function<void(Ref*)>> funcList = m_funcMap.at(sMsgName);

        /*遍历列表,回调函数,并传递数据*/
        for (auto func : funcList) {
            func(data);
        }
    }
}

   来测试一下,修改HelloWorldScene的Init函数,代码如下:

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

    NotifyUtil::getInstance()->addObserve("LikeHer", [](Ref *data) {
        log("Recv Msg:%s", data);
    });
    
    NotifyUtil::getInstance()->addObserve("LikeHer", [](Ref *data) {
        log("Gongxi Gongxi~");
    });

    NotifyUtil::getInstance()->addObserve("LikeHer", [](Ref *data) {
        log("ai yo, bu cuo o~");
    });

    NotifyUtil::getInstance()->postNotification("LikeHer", (Ref *J) "hehe");
    return true;
}

   调用NotifyUtil的addObserver函数订阅了3次消息,并且都是"LikeHer"消息,也就是有3个订阅者对这个消息感兴趣。

  最后,调用postNotification函数发布“LikeHer”消息,同时传递“hehe”字符串数据

  看到以下输出:

Recv Msg:here
Gongxi Gongxi~
ai yo, bu cuo o~

  因为NotifyUtil是单例类,所以postNotification函数在任何类里调用都能成功发布消息,在游戏开发中,消息派发几乎是不可缺少的。

10.当你把一个对象作为成员变量时,并且没有把对象addChild到另外一个对象时,就需要调用retain函数。

     最后,一定要记住,必须要调用了对象的autorelease函数之后,retain和release函数才会生效,否则,一切都是徒劳。

    因此,建议使用create的方式创建对象,Cocos2d-x的类大部分都具有create函数,create函数里会在创建对象后调用autorelease函数,代码如下:

Sprite *Sprite::create(const std::string& filename) {
    Sprite *sprite = new Sprite();
    if (sprite && sprite->initWithFile(filename)) {
        sprite->autorelease();
        return sprite;
    }
    CC_SAFE_DELETE(sprite);
    return nullptr;
}

     10.1 看看UserDefault用于保存数据的函数,代码如下:

/* 保存布尔类型的数据*/
void setBoolForKey(const char *pKey, bool value);
/* 保存整形类型的数据*/
void setIntegerForKey(const char *pKey, int value);
/*保存浮点型类型的数据*/
void setFloatForKey(const char *pKey, float value);
/*保存双精度浮点型类型的数据*/
void setDoubleForKey(const char *pKey, double value);
/*保存字符串类型的数据*/
void setStringForKey(const char *pKey, const std::string & value);

    10.2 看看UserDefault用于读取数据的函数,代码如下:

/*获取布尔类型的数据*/
bool getBoolForKey(const char *pKey);
bool getBoolForKey(const char *pKey, bool defaultValue);
/*获取整形类型的数据*/
int getIntegerForKey(const char *pKey);
int getIntegerForKey(const char *pKey, int defaultValue);
/*获取浮点型类型的数据*/
float getFloatForKey(const char *pKey);
float getFloatForKey(const char *pKey, float defaultValue);
/*获取双精度浮点型类型的数据*/
double getDoubleForKey(const char *pKey);
double getDoubleForKey(const char *pKey, double defaultValue);
/*获取字符串类型的数据*/
std::string getStringForKey(const char *pKey);
std::string getStringForKey(const char *pKey, const std::string & defaultValue);

   10.3 举例说明SaveData:

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    /*保存角色名称*/
    UserDefault::getInstance()->setStringForKey("ActorName", "mutou");

    /*读取角色名称*/
    std::string actorName = UserDefault::getInstance()->getStringForKey("ActorName", "none");
    log("ActorName = %s", actorName.c_str());
    return true;
}

     用调试模式运行项目,我们看到一下日志输出:

mutou

    在运行了一次项目后,ActorName的数据已经保存了,我们可以把setStringForKey这句代码注释掉,再次运行项目,依旧能够获取到ActorName的数据。

  10.4 编写字符串工具类

    StringUtil.h文件:

class StringUtil : public Ref {
public:
    static StringUtil *getInstance();
    virtual bool init();

    /*用分隔符分割字符串,结果存放到一个列表中,列表中的对象为Value*/
    ValueVector split(const char *srcStr, const char *sSep);
private:
    static StringUtil *m_StringUtil;
};

   StringUtil.cpp文件:

#include "StringUtil.h"
StringUtil *StringUtil::m_StringUtil = NULL;
StringUtil *StringUtil::getInstance() {
    if (m_StringUtil == NULL) {
        m_StringUtil = new StringUtil();
        if (m_StringUtil && m_StringUtil->init()) {
            m_StringUtil->autorelease();
            m_StringUtil->retain();
        } else {
            CC_SAFE_DELETE(m_StringUtil);
            m_StringUtil = NULL;
        }
    }

    return m_StringUtil;
}

bool StringUtil::init() {
    return true;
}
ValueVector StringUtil::split(const char *srcStr, const char *sSep) {
    ValueVector stringList;

    int size = strlen(srcStr);

    /*将数据转换为字符串对象*/
    Value str = Value(srcStr);

    int startIndex = 0;
    int endIndex = 0;
    endIndex = str.asString().find(sSep);

    std::string lineStr;
    /*根据换行符拆分字符串,并添加到列表中*/
    while (endIndex > 0) {
        lineStr = str.asString().substr(startIndex, endIndex); /*截取一行字符串*/
        stringList.push_back(Value(lineStr)); /*添加到列表*/
        str = Value(str.asString().substr(endIndex + 1, size)); /*截取剩下的字符串*/
    
        endIndex = str.asString().find(sSep);
    }

    /*剩下的字符串也添加到列表*/
    if (str.asString()pare("") != 0) {
        stringList.push_back(Value(str.asString()));
    }

    return stringList;
}

   拆分的原理是,从字符串中找到第一个分隔符所在的位置,然后截取该位置之前的字符串,将截取到的字符串保存到ValueVector中。再次从剩余的字符串里执行相同的步骤,直到找不到分隔符为止。

   看看StringUtil是否正常工作,修改HelloWorldScene的init函数,代码如下:

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

    /*拆分字符串*/
    auto strsList = StringUtil::getInstance()->split("Mutou,Xiaoruo,Cocos2d-x,Csv", ",");

    /*测试输出结果*/
    for (auto value : strsList) {
        log("value=%s", value.asString().c_str());
    }

    return true;
}

    运行项目,看到以下输出:

Mutou
Xiaoruo
Cocos2d-x
Csv

   10.5 loadFile函数的存放文件数据的逻辑:

  •      有一个Map类型的mCsvMap变量,存放一个CsvData对象和Csv文件名的对应关系:mCsvMap.insert(sPath, csvData);
  •      CsvData存放Csv文件每一行的数据,每一行的数据又由一个ValueVector列表保存;
  •      ValueVector列表保存的是一个个Value对象(内容是字符串),如:ID、Name、Level、HP、MP、HappyValue。

      反过来,加载了Csv文件之后,读取文件数据的情况就是这样:

  •    根据文件名sPath从mCsvMap中获取一个CsvData对象;
  •    CsvData对象保存了Csv文件每一行的数据;
  •    如果要获取Csv文件第一行的数据,则取得CsvData的第一行,取出来的值又是一个ValueVector列表;
  •    ValueVector里保存了第一行的所有数据,这些数据以Value类型保存.

    10.6 getValue函数用于获取Csv文件的某行某列的数据,逻辑很简单,现货区某行的数据,再从该行数据获取某列的值。

Value CsvUtil::getValue(int iRow, int iCol, const char *csvFilePath) {
    auto csvData = mCsvMap.at(csvFilePath); /*取出Csv文件对象*/
    
    /*如果配置文件的数据不存在,则加载配置文件*/
    if (csvData == nullptr) {
        loadFile(csvFilePath);
        csvData = mCsvMap.at(csvFilePath);
    }

    ValueVector rowVector = csvData->getSingleLineData(iRow); /*获取第iRow行数据*/
    Value colValue = rowVector.at(iCol);    /*获取第iCol列数据*/
    
    return colValue;
}

         修改HelloWorldScene的init函数,代码如下: 

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

    const char *sPath = "Monster.csv";     /*测试读取Csv文件*/
    CsvUtil::getInstance()->loadFile(sPath); /*加载文件*/
    
    /*获取第一个怪物的名字*/
    Value firstMonsterName = CsvUtil::getInstance()->getValue(2, 1, sPath);

    /*获取第二个怪物的HP值*/
    Value secMonsterHP = CsvUtil::getInstance()->getValue(3, 3, sPath);

    log("firstMonsterName = %s", firstMonsterName.asString().c_str());
    log("secMonsterHP = %s", secMonsterHP.asString().c_str());

    return true;
}

        在确定CSV文件的编码格式为UTF-8之后,输出如下:

     firstMonsterName = 笨木头

     secMonsterHP = 250

    10.7 CsvUtil剩余的函数代码如下:

CsvUtil *CsvUtil::m_CsvUtil = NULL;
CsvUtil *CsvUtil::getInstance() {
    if (m_CsvUtil == NULL) {
        m_CsvUtil = new CsvUtil();
        if (m_CsvUtil && m_CsvUtil->init()) {
            m_CsvUtil->autorelease();
            m_CsvUtil->retain();
        } else {
            CC_SAFE_DELETE(m_CsvUtil);
            m_CsvUtil = NULL;
        }
    }
    return m_CsvUtil;
}
bool CsvUtil::init() {
    return true;
}
const std::string CsvUtil::get(int iRow, int iCol, const char *csvFilePath) {
    Value colValue = getValue(iRow, iCol, csvFilePath);
    
    return colValue.asString();
}

const int CsvUtil::getInt(int iRow, int iCol, const char *csvFilePath) {
    Value colValue = getValue(iRow, iCol, csvFilePath);
    return colValue.asInt();
}
const float CsvUtil::getFloat(int iRow, int iCol, const char *csvFilePath) {
    Value colValue = getValue(iRow, iCol, csvFilePath);
    return colValue.asFloat();
}
const bool CsvUtil::getBool(int iRow, int iCol, const char *csvFilePath)
{
    Value colValue = getValue(iRow, iCol, csvFilePath);
    return colValue.asBool();
}

      10.8 读取JSON文件

      修改HelloWorldScene的init函数,代码如下:

     

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

    readJson();
    return true;
}

void HelloWorld::readJson() {
    Json::Reader reader;
    Json::Value root;

    std::string data = FileUtils::getInstance()->getStringFromFile("test1.json");

    if (reader.parse(data, root, false) == true) {
        log("id=%d", root["id"].asInt());
        log("name=%s", root["name"].asCString());
        log("IQ=%f", root["IQ"].asDouble());
    }
}

      进一步解释readJson数据:

  •        Json::Reader,这个类就是用来解析JSON文件的,很重要。
  •        Json::Value,这个类代表了JSON的一段数据。
  •        调用FileUtils的getStringFromFile("test1.json"),FileUtils用来加载文件,返回字符串。
  •        最重要的步骤来了,调用Reader的parse函数开始解析JSON文件,解析的结果会保存到root对象中(Json::Value类      型),root对象就作为JSON的根节点了,所有的数据都可以通过root来获得。
  •        目前我们的test1.json只有一个数据段,所以root就是唯一的节点, 要读取它的字段值很简单,像获取数组的值一样直接调用root["id"]即可,然后根据值的类型进行转换,如root["id"].asInt()。

            调试项目运行,输出:

%d=1
name=mutou
IQ=99.500000

         10.9 读取嵌套结构的JSON文件

             test2.json文件内容如下:

{
    "%d":1,
    "name":"mutou",
    "IQ":99.5,
    "msg":{ "money":999999, "say":"hehe" }
}

            修改HelloWorldScene的init函数,代码如下:

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

    readChildJson();

    return true;
}

void HelloWorld::readChildJson() {
    Json::Reader reader;
    Json::Value root;

    std::string data = FileUtils::getInstance()->getStringFromFile("test2.json");

    if (reader.parse(data, root, false) == true) {
        log("id=%d", root["id"].asInt());
        log("name=%s", root["name"].asCString());
        log("IQ=%f", root["IQ"].asDouble());

        log("msg value money=%d", root["msg"]["money"].asInt());
        log("msg value say=%s", root["msg"]["say"].asCString());
    }
}

       运行输出:

id=1
name=mutou
IQ=99.500000
msg value money=999999
msg value say=hehe

          10.10 读取数组结构的JSON文件

              test3.json文件:

[
    {"id":1, "model":"monster1.png", "atk":190},
    {"id":2, "model":"monster2.png", "atk":10},
    {"id":3, "model":"monster3.png", "atk":650}
]

          继续修改HelloWorldScene的init函数,代码如下:

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

    readArrayJson();
    return true;
}

void HelloWorld::readArrayJson() {
    Json::Reader reader;
    Json::Value root;

    std::string data = FileUtils::getInstance()->getStringFromFile("test3.json");
    
    if (reader.parse(data, root, false) == true) {
        int iNum = root.size();
        for (int i=0; i<iNum; i++) {
            log("id=%d", root[i]["id"].asInt());
            log("model=%s", root[i]["model"].asCString());
            log("atk=%d", root[i]["atk"].asInt());
        }
    }
}

          10.11 输出JSON文件

            继续修改HelloWorldScene的init函数,代码如下:

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    
    writeJson();
    return true;
}

void HelloWorld::writeJson() {
    Json::Value root;
    Json::FastWriter writer;

    root["name"] = "Who";
    root["IQ"] = 999;

    std::string json_file = writer.write(root);

    FILE *file = fopen("testWrite.json", "w");
    fprintf(file, json_file.c_str());
    fclose(file);
}

         解释如下:

  •     准备一个Json::Value对象,root;
  •     往root里存放数据;
  •     使用Json::FastWriter的write函数把root里的数据转换为JSON格式的字符串;
  •     创建一个文件,把JSON格式的字符串写到文件里。

11.有限状态机

     11.1.实现简单有限状态机的类

       Mutou类:

enum EnumState {
    enStateWriteCode,   /*状态:写代码*/
    enStateWriteArticle,  /*状态: 写教程*/
    enStateRest,          /*状态:休息*/
};

class Mutou : public Node {
public:
    CREATE_FUNC(Mutou);
    virtual bool init();

    EnumState enCurState;     /*当前状态*/
    bool isTire();         /*判断是否写代码写累了*/
    bool isWantToWriteArticle();   /*是否想写教程*/

    void writeCode();   /*写代码*/
    void writeArticle();  /*写教程*/
    void rest();    /*休息*/

    void changeState(EnumState enState); /*切换状态*/

    virtual void update(float dt);
};

    Mutou.cpp文件:

bool Mutou::init() {
    this->scheduleUpdate();
    return true;
}
void Mutou::changeState(EnumState enState) {
    this->enCurState = enState;
}
bool Mutou::isTire() {
    /*每次问木头累不累,他都会说:累*/
    return true;
}
bool Mutou::isWantToWriteArticle() {
    /*有10%的概率想写教程(好懒)*/
    float ran = CCRANDOM_0_1();
    if (ran < 0.1f) {
        return true;
    }
    return false;
}
void Mutou::writeCode() {
    log("mutou is writing Code.");
}
void Mutou::writeArticle() {
    log("mutou is writing article");
}
void Mutou::rest() {
    log("mutou is resting.");
}
void Mutou::update(float dt) {
    /*判断在每一种状态下应该做什么事情*/
    switch(enCurState) {
    case enStateWriteCode:
        /*如果累了就休息,并且切换到修改状态*/
        if (isTrue()) {
            rest();
            changeState(enStateRest);
        }
        break;
    case enStateWriteArticle:
        /*如果累了就休息,并且切换到休息状态*/
        if (isTire()) {
            rest();
            changeState(enStateRest);
        }
        break;
    case enStateWriteArticle:
        /*如果累了就休息,并且切换到休息状态*/
        if (isTire()) {
            rest();
            changeState(enStateRest);
        }
        break;
    case enStateRest:
        /*一定的概率写代码,一定的概率写教程,并且切换到相应的状态*/
        if (isWantToWriteArticle()) {
            writeArticle();
            changeState(enStateWriteArticle);
        } else {
            writeCode();
            changeState(enStateWriteCode);
        }
        break;
    }
}

        然后修改HelloWorldScene的init函数,代码如下:

/*一定不要忘了包含头文件*/
#include "Mutou.h"
bool HelloWorld::init() {
    if (!Layer::init()) { return false; }

    /* 新建木头角色*/
    Mutou *mMutou = Mutou::create();

    /*初始化木头的状态为休息*/
    mMutou->changeState(enStateRest);

    this->addChild(mMutou);

    return true;
}

       运行程序看到以下输出:

      mutou is writing Code.

      mutou is resting.

      mutou is writing Code.

      mutou is resting.

      mutou is writing article.

      mutou is resting.

      mutou is writing Code.

      mutou is resting.

      mutou is writing Code.

      11.2 用状态模式实现有限状态机

         Mutou类:

void Mutou::update(float dt) {
    /*判断在每一种状态下应该做什么事情*/
    switch(enCurState) {
    case enStateWriteCode:
        /*如果累了就休息,并且切换到休息状态*/
    case enStateWriteArticle:
        /*如果累了就休息,并且切换到休息状态*/
    case enStateRest:
        /*一定的概率写代码,一定的概率写教程,并且切换到相应的状态*/
    }
}

       update函数会先判断木头当前所在的状态,然后再去执行相对应的逻辑。

        但是,要是Mutou类有好多状态,那这个函数也太庞大了!

            (1)  状态基类

             首先,我们需要一个状态基类,它只有一个抽象方法execute,表示要执行一件事情,至于执行什么事情,由它的子类来决定。

              State类:

class MutouT;
class State {
public:
    virtual void execute(MutouT *mutou) = 0;
};

               State类作为接口类使用,execute函数是某个状态下要执行的行为,MutouT类将在后面解释.

            (2)  状态类

              StateWriteCode.h文件:

Class MutouT;
class StateWriteCode : public State {
public:
    virtual void execute(Mutou *mutou);
};

               StateWriteCode.cpp文件:

void StateWriteCode::execute(MutouT *mutou) {
    /*如果累了就休息,并且切换到休息状态*/
    if (mutou->isTire()) {
        mutou->rest();
        mutou->changeState(new StateRest());
    }
}

               StateWriteArticle.h文件:

class StateWriteArticle : public State {
public:
    virtual void execute(MutouT *mutou);
};

                StateWriteArticle.cpp文件:

void StateWriteArticle::execute(MutouT *mutou) {
    /*如果累了就休息,并切换到休息状态*/
    if (mutou->isTire()) {
        mutou->rest();
        mutou->changeState(new StateRest());
    }
}

                StateRest类:

                  StateRest.h文件:

class StateRest : public State {
public:
    virtual void execute(MutouT *mutou);
};

                   StateRest.cpp文件:

void StateRest::execute(MutouT *mutou) {
    /*一定的概率写代码,一定的概率写教程,并且切换到相应的状态*/
    if (mutou->isWantToWriteArticle()) {
        mutou->writeArticle();
        mutou->changeState(new StateWriteArticle());
    } else {
        mutou->writeCode();
        mutou->changeState(new StateWriteCode());
    }
}

           (3) 新的木头类

               MutouT.h文件:

class MutouT : public Node {
public:
    CREATE_FUNC(MutouT);
    virtual bool init();

    bool isTire();     /*判断是否写代码累了*/
    bool isWantToWriteArticle();   /*是否想写教程*/
    void writeCode();     /*写代码*/
    void writeArticle();   /*写教程*/
    void rest();          /*休息*/
    void changeState(State *state);   /*切换状态*/

    virtual void update(float dt);
private:
    State *mCurState;    /*存放当前状态类*/
};

           MutouT.cpp文件:

bool MutouT::init() {
    mCurState = NULL;
    this->scheduleUpdate();
    return true;
}
bool MutouT::isTire() {
    /*每次问木头累不累,他都会说:累*/
    return true;
}
bool MutouT::isWantToWriteArticle() {
    /*有10%的概率想写教程(好懒)*/
    float ran = CCRANDOM_0_1();
    if (ran < 0.1f) {
        return true;
    }
    return false;
}
void MutouT::writeCode() {
    log("mutou is writing Code.");
}
void MutouT::writeArticle() {
    log("mutou is writing article.");
}
void MutouT::rest() {
    log("mutou is resting.");
}
void MutouT::changeState(State *state) {
    CC_SAFE_DELETE(mCurState);
    mCurState = state;
}
void MutouT::update(float dt) {
    mCurState->execute(this);
}

          修改HelloWorld的init函数,代码如下:

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    /*新建木头2角色*/
    MutouT *mMutou = MutouT::create();

    /*初始化木头的状态为休息*/
    mMutou->changeState(new StateRest());
    this->addChild(mMutou);

    return true;
}

           运行后输出:

            mutou is writing Code.

            mutou is resting.

            mutou is writing article.   

            mutou is resting.

            mutou is writing article.

            mutou is resting.

            mutou is writing Code.     

            mutou is resting.

            mutou is writing Code. 

            mutou is resting.

            mutou is writing Code.

                  这就是简单地利用了多态,神奇的地方就在execute这个函数,这个函数会根据不同的情况切换MutouT的当前状态。

        11.3 真正的状态机来了

             (1) 创建状态机类

                 MutouTFSM.h文件:

class MutouTFSM : public Node {
public:
    static MutouTFSM *createWithMutouT(MutouT *mutou);
    bool initWithMutouT(MutouT *mutou);

    virtual void update(float dt);
    void changeState(I_State *state); /*切换状态*/
private:
    I_State *mCurState; /*存放当前状态类*/
    MutouT *mMutou;   /*木头对象*/
};

               现在,所有和状态有关的操作都放在MutouTFSM状态机类,MutouT类只要专心地做它该做的情况就好了(休息、写代码、写教程)。

               MutouTFSM.cpp文件:

MutouTFSM *MutouTFSM::createWithMutouT(MutouT *mutou) {
    MutouTFSM *fsm = new MutouTFSM();

    if (fsm && fsm->initWithMutouT(mutou)) {
        fsm->autorelease();
    } else {
        CC_SAFE_DELETE(fsm);
        fsm = NULL;
    }

    return fsm;
}
bool MutouTFSM::initWithMutouT(MutouT *mutou) {
    this->mCurState = NULL;
    this->mMutou = mutou;
    return true;
}
void MutouTFSM::changeState(I_State *state) {
    CC_SAFE_DELETE(mCurState);
    this->mCurState = state;
}
void MutouTFSM::update(float dt) {
    this->mCurState->execute(mMutou);
}

           (2) 被释放的木头类

              有了MutouTFSM类之后,MutouT类就能够得到释放了,来看看新的MutouT类代码:

class MutouT : public Node {
public:
    CREATE_FUNC(MutouT);
    virtual bool init();
        
    bool isTire();       /*判断是否写代码写累了*/
    bool isWantToWriteArticle();   /*是否想写教程*/
    void writeCode();        /*写代码*/
    void writeArticle();     /*写教程*/
    void rest();             /*休息*/

    MutouTFSM *getFSM();    /*获取状态机对象*/
    
    virtual void update(float dt);
private:
    MutouTFSM *mFSM; /*木头状态机*/
};

               再来看看MutouT的实现,代码如下:

bool MutouT::init() {
    mFSM = MutouTFSM::createWithMutouT(this);
    mFSM->retain();    
    this->scheduleUpdate();
    return true;
}
bool MutouT::isTrue() {
    /*每次问木头累不累,他都会说:累*/
    return true;
}
bool MutouT::isWantToWriteArticle() {
    float ran = CCRANDOM_0_1();
    if (ran < 0.1f) {
        return true;
    }
    return false;
}
void MutouT::writeCode() {
    log("mutou is writing Code.");
}
void MutouT::writeArticle() {
    log("mutou is writing article.");
}
void MutouT::rest() {
    log("mutou is resting.");
}
MutouTFSM *MutouT::getFSM() {
    return this->mFSM;
}
void MutouT::update(float dt) {
    this->mFSM->update(dt);
}

              唯一变化的是,原本通过调用MutouT类的changeState函数来切换状态,而现在是通过调用MutouTFSM类的changeState函数来切换状态,代码如下:

void StateWriteArticle::execute(MutouT *mutou) {
    /*如果累了就休息,并且切换到休息状态*/
    if (mutou->isTire()) {
        mutou->rest();
        mutou->getFSM()->changeState(new StateRest());
    }
}
void StateWriteCode::execute(MutouT *mutou) {
    /*如果累了就休息,并且切换到休息状态*/
    if (mutou->isTire()) {
        mutou->rest();
        mutou->getFSM()->changeState(new StateRest());
    }
}
void StateRest::execute(MutouT *mutou) {
    /*一定的概率写代码,一定的概率写教程,并且切换到相应的状态*/
    if (mutou->isWantToWriteArticle()) {
        mutou->writeArticle();
        mutou->getFSM()->changeState(new StateWriteArticle());
    } else {
        mutou->writeCode();
        mutou->getFSM()->changeState(new StateWriteCode());
    }
}

          最后,再次修改HelloWorldScene的init函数,代码如下:

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

    /*新建木头2角色*/
    MutouT *mMutou = MutouT::create();

    /*初始化木头的状态为休息*/
    mMutou->getFSM()->changeState(new StateRest());
    this->addChild(mMutou);

    return true;
}

         运行并输出:

       mutou is writing article.

       mutou is resting.

       mutou is writing Code.

       mutou is resting.

       mutou is writing Code.

       mutou is resting.

       mutou is writing Code.

         11.4 事件驱动

             (1) 彻底抛弃update函数--新的状态机类

                 MutouTFSM类

                  MutouTFSM.h文件:

class MutouTFSM : public Node {
public:
    ~MutouTFSM();
    static MutouTFSM *createWithMutouT(MutouT *mutou);
    bool initWithMutouT(MutouT *mutou);

    void changeState(State *state);   /*切换状态*/
private:
    void onRecvWantToRest(Ref *obj);
    void onRecvWantToWriteCode(Ref *obj);
    void onRecvWantToWriteArticle(Ref *obj);

    I_State *mCurState;   /*存放当前状态类*/
    MutouT *mMutou;   /*木头对象*/
};

                 MutouTFSM.cpp文件:

#define NOTIFY NotificationCenter::getInstance()
MutouTFSM::~MutouTFSM() {
    NOTIFY->removeAllObservers(this);
}

MutouTFSM *MutouTFSM::createWithMutouT(MutouT *mutou) {
    MutouTFSM *fsm = new MutouTFSM();

    if (fsm && fsm->initWithMutouT(mutou)) {
        fsm->autorelease();
    } else {
        CC_SAFE_DELETE(fsm);
        fsm = NULL;
    }

    return fsm;
}

bool MutouTFSM::initWithMutouT(MutouT *mutou) {
    this->mCurState = NULL;
    this->mMutou = mutou;

    /*订阅消息*/
    NOTIFY->addObserver(this, callfuncO_selector(MutouTFSM::onRecvWantToRest),
                        StringUtils::toString(en_Msg_WantToRest), NULL);
    NOTIFY->addObserver(this, callfuncO_selector(MutouTFSM::onRecvWantToWriteCode),
                        StringUtils::toString(en_Msg_WantToWriteCode), NULL);
    NOTIFY->addObserver(this, callfuncO_selector(MutouTFSM::onRecvWantToWriteArticle),
                        StringUtils::toString(en_Msg_WantToWriteArticle), NULL);
    return true;
}

void MutouTFSM::changeState(State *state) {
    CC_SAFE_DELETE(mCurState);
    this->mCurState = state;
}

void MutouTFSM::onRecvWantToRest(Ref *obj) {
    /*将当前事件传递给具体的状态类*/
    this->mCurState->execute(mMutou, en_Msg_WantToRest);
}

void MutouTFSM::onRecvWantToWriteCode(Ref *obj) {
    /*将当前事件传递给具体的状态类*/
    this->mCurState->execute(mMutou, en_Msg_WantToWriteCode);
}

void MutouTFSM::onRecvWantToWriteArticle(Ref *obj) {
    /*将当前事件传递给具体的状态类*/
    this->mCurState->execute(mMutou, en_Msg_WantToWriteArticle);
}

               其中,宏定义:

                  #define NOTIFY NotificationCenter::getInstance()   

            在收到消息后,状态机是怎么处理的。和以前一样,调用当前状态的execute方法,但这次多了一个参数,那就是刚刚提到的消息类型,我们需要新建一个枚举类,EnumMsgType,代码如下:

#ifndef _EnumMsgType_H_
#define _EnumMsgType_H_
enum EnumMsgType {
    en_Msg_WantToRest,   /*需要休息*/
    en_Msg_WantToWriteCode,  /*需要写代码*/
    en_Msg_WantToWriteArticle,   /*需要写教程*/
};
#endif

                 (2) 更智能的状态类

                   首先要修改的是状态类的基类,代码如下:

#include "EnumMsgType.h"
class MutouT;
class State {
public:
    virtual void execute(MutouT *mutou, EnumMsgType enMsgType) = 0;
};

                   看一下3种状态类的execute函数的新实现,代码如下:

void StateRest::execute(MutouT *mutou, EnumMsgType enMsgType) {
    switch(enMsgType) {
    case en_Msg_WantToWriteCode:
        mutou->writeCode();
        mutou->getFSM()->changeState(new StateWriteCode());
        break;
    case en_Msg_WantToWriteArticle:
        mutou->writeArticle();
        mutou->getFSM()->changeState(new StateWriteArticle());
        break;
    }
}
void StateWriteArticle::execute(MutoT *mutou, EnumMsgType enMsgType) {
    switch(enMsgType) {
    case en_Msg_WantToRest:
        mutou->rest();    
        mutou->getFSM()->changeState(new StateRest());
        break;
    }
}

void StateWriteCode::execute(MutouT *mutou, EnumMsgType enMsgType) {
    switch(enMsgType) {
    case en_Msg_WantToRest:
        mutou->rest();
        mutou->getFSM()->changeState(new StateRest());
        break;
    }
}

void StateWriteCode::execute(MutouT *mutou, EnumMsgType enMsgType) {
    switch(enMsgType) {
    case en_Msg_WantToRest:
        mutou->rest();
        mutou->getFSM()->changeState(new StateRest());
        break;
    }
}

           每个状态类的execute函数已经不需要再主动去判断木头当前在做什么或者想做什么或者是什么状况。它只要知道当前发生了什么事件,只关心它所关心的事件,在特定的事件下改变木头的状态即可。

         测试一下,修改HelloWorld的init函数,代码如下:

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

    /*新建木头2角色*/
    MutouT *mMutou = MutouT::create();

    /*初始化木头的状态为休息*/
    mMutou->getFSM()->changeState(new StateRest());

    this->addChild(mMutou);

    /*模拟事件的发生*/
    auto notify = NotificationCenter::getInstance();
    notify->posNotification(StringUtils::toString(en_Msg_WantToWriteCode));
    notify->posNotification(StringUtils::toString(en_Msg_WantToRest));
    notify->posNotification(StringUtils::toString(en_Msg_WantToWriteArticle));
    notify->posNotification(StringUtils::toString(en_Msg_WantToRest));
    notify->posNotification(StringUtils::toString(en_Msg_WantToWriteArticle));
    notify->posNotification(StringUtils::toString(en_Msg_WantToRest));
    notify->posNotification(StringUtils::toString(en_Msg_WantToWriteCode));
    notify->posNotification(StringUtils::toString(en_Msg_WantToRest));

    return true;
}

             运行并输出:

mutou is writing Code.
mutou is resting.
mutou is writing article.
mutou is resting.
mutou is writing article.
mutou is resting.
mutou is writing Code.
mutou is resting.

               描述一个场景:

          一个拥有超能力的木头,它所经过的地方周围的物体都会弹开,于是,它一边走,周围的物体一边弹开。十分美妙的场景。

              这依旧可以使用有限状态机来实现,木头经过的时候可以发出"我来了,我的能力范围是方圆10m"的消息,然后周围的物体订阅了这个消息,在接收到这个消息时,就由静止状态改变为弹射状态,然后物体就执行弹射动作。

12.强大的Lua

     C++和Lua的通信流程,如图所示:

     

      (1) C++想获取Lua的myName字符串的值,所以它把myName放到Lua堆栈(栈顶),以便Lua能看到。

      (2) Lua从堆栈(栈顶)中获取myName,此时栈顶再次变为空。

      (3) Lua拿着这个myName去Lua全局表查找myName对应的字符串。

      (4) 全局表返回一个字符串"beauty girl".

      (5) Lua把取得的"beauty girl"字符串当道堆栈(栈顶).

      (6) C++可以从Lua堆栈中取得"beauty girl"。

    12.1 开始使用

         HelloLua.h文件:

extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
};

class HelloLua : public Layer {
public:
    CREATE_FUNC(HelloLua);
    virtual bool init();
    static Scene *createScene();
};

       HelloLua.cpp文件:

#include "HelloLua.h"
Scene *HelloLua::createScene() {
    auto scene = Scene::create();
    auto layer = HelloLua::create();
    scene->addChild(layer);
    return scene;
}
bool HelloLua::init() {
    lua_State *pL = lua_open();
    luaopen_base(pL);
    luaopen_math(pL);
    luaopen_string(pL);

    /*1.执行Lua脚本,返回0代表成功*/
    int err = luaL_dofile(pL, "helloLua.lua");
    log("open : %d", err);

    /*2.重置栈顶索引*/
    lua_settop(pL, 0);  //为了确认让栈顶的索引置为0,因为我们操作栈时是根据所以你来操作的。
    lua_getglobal(pL, "myName"); //把myName放到了栈中,然后Lua就会通过myName去全局表中查找,找到myName对应的字符串"beauty girl", 再放到栈中。

    /*3.判断栈顶的值的类型是否为String,返回非0值代表成功*/
    int isstr = lua_isstring(pL, 1);
    log("isstr = %d", isstr);

    /*4.获取栈顶的值*/
    if (isstr != 0) {
        const char *str = lua_tostring(pL, 1);
        log("getStr = %s", str);
    }


    lua_close(pL);
    return true;
}

   运行输出:

      open : 0

      isstr = 1

      getStr = beauty girl

      12.2 GetTableData.

          创建HelloLua类,代码如下:

bool HelloLua::init() {
    /*初始化*/
    lua_State *pL = lua_open();
    luaopen_base(pL);

    /*执行脚本*/
    luaL_dofile(pL, "helloLua.lua");

    /*重置栈顶元素*/
    lua_settop(pL, 0);

    /*取得table变量,在栈顶*/
    lua_getglobal(pL, "helloTable");

    /*将C++的字符串放到Lua的栈中,此时栈顶变为"name",helloTable对象变为栈底*/
    lua_pushstring(pL, "name");

    /*从table对象寻找"name"对应的值(table对象现在在索引为-2的栈中,也就是当前的栈底)取得对应值之后,将值放回栈顶*/
    lua_gettable(pL, -2);

    /*现在表的name对应的值已经在栈顶了,直接取出即可*/
    const char *sName = lua_tostring(pL, -1);
    log("name = %s", sName);
    lua_close(pL);
    return true;
}

    

           运行并输出:

    name = mutou

       12.3 C++调用Lua函数

           helloLua.lua文件

myName = "beauty girl"
helloTable = {name = "mutou", IQ = 125}

function helloAdd(num1, num2) 
    return (num1 + num2)
end

           HelloLua类:

bool HelloLua::init() {
    lua_state *pL = lua_open();
    luaopen_base(pL);

    /*执行脚本*/
    luaL_dofile(pL, "hellolua.lua");

    /*重置栈顶元素*/
    lua_settop(pL, 0);

    /*把helloAdd函数对象放到栈中*/
    lua_getglobal(pL, "helloAdd");

    /*把函数所需要的参数入栈*/
    lua_pushnumber(pL, 10);
    lua_pushnumber(pL, 5);
    
    /*执行函数,第一个参数表示函数的参数个数,第二个参数表示函数返回值个数
     *Lua会先去堆栈取出参数,然后再取出函数对象,开始执行哈数
     */
    lua_call(pL, 2, 1);

    int iResult = lua_tonumber(pL, -1);
    log("iResult = %d", iResult);
    return true;
}

            简述一下步骤:

              (1) 执行脚本。

              (2) 将helloAdd函数放到栈中:lua_getglobal(pL, "helloAdd").

              (3) helloAdd有2个参数,我们要把参数传递给Lua,所以2个参数都要放到栈里。

              (4) 第2和第3步已经把函数所需要的数据都放到栈里了,接下来只要告诉Lua去栈里取数据,然后执行函数,调用lua_call即可。

           运行并输出:

     iResult = 15

         12.4 Lua调用C++的函数

              HelloLua.h文件:

class HelloWorld : public Layer {
public:
    CREATE_FUNC(HelloLua);
    virtual bool init();
    static Scene *createScene();

    static int getNumber(int num);
};

---------------------
int HelloLua::getNumber(int num) {
    log("getNumber num = %d", num);
    return num + 1;
}

            在HelloLua.h中添加声明:

        static int cpp_GetNumber(lua_State *pL);

             HelloLua.cpp文件:

int HelloLua::cpp_GetNumber(lua_State *pL) {
    /*从栈顶中取一个值*/
    int num = (int)lua_tonumber(pL, 1);

    /*调用getNumber函数,将返回值入栈*/
    lua_pushnumber(pL, getNumber(num));

    /*返回值个数,getNumber只有一个返回值,所以返回1*/
    return 1;
}

              Lua和C++只能通过堆栈通信,Lua是不可能直接调用getNumber函数的,所以我们建立一个cpp_GetNumber函数作为中介。cpp_GetNumber函数有一个lua_State *pL参数,有了这个参数,C++就能从Lua的堆栈中取值了。

               (1) Lua脚本里会调用cpp_GetNumber函数。

               (2) 当cpp_GetNumber被调用时,一切又回到C++对Lua的操作,栈顶里会存放函数所需要的参数,取出来用就可以。

               (3) Lua调用cpp_GetNumber之后,需要一个结果,当然,这个结果同样只能存放在栈里,所以理所当然地要把getNumber的结果入栈。

               (4) cpp_GetNumber返回了一个值,这个值不是函数的执行结果,而是getNumber需要返回值的个数(Lua支持多个返回值的函数)。

               (5) Lua会从栈中取出函数的执行结果。

            现在,开始使用这两个函数,修改HelloLua的init函数,代码如下:

bool HelloLua::init() {
    lua_State *pL = lua_open();
    luaopen_base(pL);

    /*C++的函数和封装函数都必须是静态的,不知道可不可以不是静态的?当然不可以*/
    lua_register(pL, "cpp_GetNumber", cpp_GetNumber);
    luaL_dofile(pL, "helloLua.lua");
    lua_close(pL);
    return true;
}

            最后还要修改helloLua.lua脚本文件:

       local num = cpp_GetNumber(10);

            运行输出:

       getNumber num = 10

          Lua可以通过lua_register将C++的静态函数注册到Lua中,这样Lua就可以直接调用C++的函数。

          为什么cpp_GetNumber函数为什么非得是静态的,很简单,如果不是静态函数,就必须在对象被创建之后才能调用。在Lua中是不能也不会去再次创建一个HelloLua对象的,因此,注册的函数必须是静态的。

13.自己写UI模块

    13.1 UI模块设计思路图

           

    13.2 UI的XML配置文件

       看看规定的XML配置文件的格式,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<MMWinRoot>
    <MMWin><!-- 窗口 -->
        <enWinType>MMNormalWin</enWinType>
        <bg>tollgate/msgBg.jpg</bg>
        <x>0</x>
        <y>450</y>
        <MMCld><!-- 文字标签 -->
            <enWinType>MMLabel</enWinType>
            <text>Hello</text>
            <fontSize>25</fontSize>
            <x>7</x>
            <y>40</y>
        </MMCld>
    </MMWin>
</MMWinRoot>

       我们定义三种主要的标签:

  •         MMWinRoot: 根节点,每个XML配置文件只有一对根节点;
  •         MMWin: 代表一个父窗口,通常一个XML配置文件只有一个父窗口,因为我们一般会把各种UI窗口分开写到不同XML文件里,方便管理;
  •         MMCld: 代表一个子窗口,子窗口同样也可以包含子窗口。
  •         enWinType:这是一个很重要的标签,它代表控件的类型,比如窗口、按钮、标签等类型;
  •         bg:  代表控件的背景图片路径,通常用在窗口和按钮控件上;
  •         x:   代表控件的X坐标;
  •         y:   代表控件的Y坐标。
  •         text:  文本内容,通常只用在文字标签控件;
  •         fontSize:  字体大小,同样只用在文字标签控件。

      13.3 我们来读一个XML文件,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<MMWinRoot>
    <MMWin id=10>
        <enWinType>MMNormalWin</enWinType>
    </MMWin>
</MMWinRoot>

           修改HelloWorldScene类,代码如下:

class TiXmlElement;
class HelloWorld : public cocos2d::CCLayer {
public:
    virtual bool init();
    static cocos2d::CCScene *scene();
    CREATE_FUNC(HelloWorld);
private:
    void loadXmlTest1(const char *sPath);   //XML读取测试1,Hello TinyXML
    void loadXmlEle(TiXmlElement *rootElement);  //读取普通的XML配置文件
};

           新增了两个函数,代码如下:

#include "tinyxml\tinyxml.h"

void HelloWorld::loadXmlTest1(const char *sPath) {
    TiXmlDocument *xmlDoc = new TiXmlDocument();

    /*读取XML文件*/
    Data fileData = FileUtils::getInstance()->getDataFromFile(sPath);

    /*开始解析XML*/
    xmlDoc->Parse((const char *)fileData.getBytes());

    /*获取XML根节点*/
    TiXmlElement *rootElement = xmlDoc->RootElement();

    /*开始读取XML各个标签*/
    loadXmlEle(rootElement);

    /*删除对象*/
    delete xmlDoc;
}

        解释以下loadXmlTest1函数的流程:

       (1) 创建一个TiXmlDocument对象,命名为xmlDoc,这个对象可以理解为XML文件的对象或者管理器。

       (2) 使用FileUtils的getDataFromFile函数获取XML文件的数据,返回Data对象,命名为fileData。

       (3) 调用TiXmlDocument对象的Parse函数解析XML文件数据,经过解析之后,XML文件的节点就被保存为一个个对象,也就是我们常说的节点。

       (4) 节点使用TiXmlDocument对象保存,为了开始读取所有的节点对象,首先要获取根节点对象,通过TiXmlDocument的RootElement函数可以获取根节点对象。

       (5) 有了根节点对象,就可以开始读取所有的节点了。

       (6) 由于xmlDoc是"new"出来的对象,并且没有参与Cocos2d-x的内存管理机制,因此,在使用完之后,要调用delete释放对象。

        我们来看看loadXmlEle函数的实现,代码如下:

void HelloWorld::loadXmlEle(TiXmlElement *rootElement) {
    /*取得根节点的第一个字标签对象*/
    TiXmlElement *cldElement = rootElement->FirstChildElement();

    /*打印标签的名字和标签的id属性*/
    log("%s id=%s", cldElement->Value(), cldElement->Attribute("id"));

    /*再取得标签的第一个子对象*/
    cldElement = cldElement->FirstChildElement();

    /*打印标签的名字和标签的值*/
    log("%s:%s", cldElement->Value(), cldElement->GetText());
}

        修改HelloWorldScene的init函数,代码如下:

bool HelloWorld::init() {
    if (!Layer::init()) {
        return false;
    }
    loadXmlTest1("test1.xml");

    return true;
}

        运行后输出:

       MMWin id=10

       enWinType:MMNormalWin

      13.4 看下面的XML配置文件:

<?xml version="1.0" encoding="utf-8"?>
<MMWinRoot>
    <MMWin><!-- 窗口 -->
        <enWinType>MMNormalWin</enWinType>
        <bg>tollgate/msgBg.jpg</bg>
        <x>0</x>
        <y>450</y>
        <MMCld><!-- 区域1 -->
            <enWinType>MMDiv</enWinType>
            <x>7</x>
            <y>40</y>
            <MMCld><!-- 标签1 -->
                <enWinType>MMLabel</enWinType>
                <text>label1</text>
                <fontSize>25</fontSize>
            </MMCld>
            <MMCld><!-- 标签2 -->
                <enWinType>MMLabel</enWinType>
                <text>label2</text>
                <fontSize>25</fontSize>
            </MMCld>
            <MMCld><!-- 标签2 -->
                <enWinType>MMLabel</enWinType>
                <text>label2</text>
            </MMCld>
        </MMCld>
    </MMWin>
</MMWinRoot>

          给HelloWorldScene新增两个函数,代码如下:

class HelloWorld : public cocos2d::CCLayer {
    /*这里省略了很多代码*/
        
    void loadXmlTest2(const char *sPath);   //XML读取测试2,读取MM控件的XML
    void loadXmlEleMMWin(TiXmlElement *rootElement);  //读取MM控件的XML配置文件
};

          loadXmlTest2函数:

void HelloWorld::loadXmlTest2(const char *sPath) 
{
    TiXmlDocument *xmlDoc = new TiXmlDocument();
    Data fileData = FileUtils::getInstance()->getDataFromFile(sPath); /*读取XML文件*/
    xmlDoc->Parse((const char *)fileData.getBytes());  /*开始解析XML*/
    TiXmlElement *rootElement = xmlDoc->RootElement();   /*获取XML根节点*/
    /*开始读取XML各个标签*/
    loadXmlEleMMWin(rootElement);

    delete xmlDoc; /*删除对象*/
}

-------------------------
void HelloWorld::loadXmlEleMMWin(TiXmlElement *rootElement) {
    TiXmlElement *cldElement = rootElement->FirstChildElement();

    while(cldElement != NULL) {
        /*某些节点的内容为空,所以不获取它的内容(但是内容有子节点)*/
        if (cldElement->GetText() != NULL) {
            log("%s:%s", cldElement->Value(), cldElement->GetText());
        }

        /*如果有子节点,则继续解析,并且添加到当前节点的子节点列表*/
        else if (cldElement->FirstChildElement() != NULL) {
            loadXmlEleMMWin(cldElement);
        }

        /*下一个同级节点*/
        cldElement = cldElement->NextSiblingElement();
    }
}

             修改HelloWorldScene的init函数,代码如下:

bool HelloWorld::init() {
    if (!Layer::init()) {
        return false;
    }
    loadXmlTest2("test2.xml");
    return true;
}

             运行并输出:

         enWinType:MMNormalWin

         bg:tollgate/msgBg.jpg

         x:0

         y:450

         enWinType:MMDiv

         x:7

         y:40

         enWinType:MMLabel

         text:label1

         fontSize:25

         enWinType:MMLabel

         text:label2

         fontSize:25

       13.5 XML标签节点对象

           MMWinXmlData的头文件,代码如下:

class MMWinXmlData : public Ref {
public:
    CREATE_FUNC(MMWinXmlData);
    virtual bool init();

    /*添加子节点*/
    void addCldXmlData(MMWinXmlData *cldXmlData);

    /*获取子节点列表<MMWinXmlData*> */
    Vector<MMWinXmlData *> getCldXmlDataList();

    bool isHasChild();  /*是否有子节点*/
private:
    Vector<MMWinXmlData *> mCldXmlDataList;  /*子节点列表*/
    
    CC_PRIVATE_BOOL(m_isNone, None);   /*标记本身是否为空节点*/
    CC_PRIVATE(EnumWinType, mEnWinType, EnWinTYpe);  /*控件类型*/
    CC_PRIVATE(int, m_iX, iX);     /*X坐标*/
    CC_PRIVATE(int, m_iY, iY);    /*Y坐标*/
};

           CC_PRIVATE和CC_PRIVATE_BOOL宏,代码如下:

#define CC_PRIVATE(varType, varName, funName) \
private: varType varName; \
public: varType get##funName(void) const { return varName; } \
public: void set##funName(varType var) { varName = var; }

/*创建bool私有变量,包括get和set方法*/
#define CC_PRIVATE_BOOL(varName, funcName) \
private: bool varName; \
public: bool is##funcName(void) const { return varName; } \
public: void set##funcName(bool var) { varName = var; }

           最后新建一个枚举类,创建一个头文件,命名为EnumWinType,代码如下:

/*控件类型字符串*/
#define WINType_C_en_Win_None "MMNone"     //无
#define WINType_C_en_Win_NormalWin "MMNormalWin"   //普通窗口
#define WINType_C_en_Win_Label     "MMLabel"     //标签

enum EnumWinType {
    en_Win_None,
    en_Win_NormalWin,   /*普通窗口*/
    en_Win_Label,       /*普通标签*/
};

          修改HelloWorldScene,新增两个函数,代码如下:

class MMWinXmlData;
class TiXmlElement;
class HelloWorld : public cocos2d::Layer {
public:
    virtual bool init();
    static cocos2d::Scene *createScene();
    CREATE_FUNC(HelloWorld);

    /*创建自定义的标签属性对象*/
    MMWinXmlData *createXmlData(TiXmlElement *xmlElement);

    /*设置某个具体的标签*/
    void setWinXmlData(MMWinXmlData *xmlData, const char *sName, const char *sText);
};

          来看看createXmlData函数,用于将XML配置文件转换为我们的MMWinXmlData对象,代码如下:

MMWinXmlData *HelloWorld::createXmlData(TiXmlElement *xmlElement) {
    MMWinXmlData *xmlData = MMWinXmlData::create();
    TiXmlElement *cldElement = xmlElement->FirstChildElement();

    log("%s %d: --------------------", __FILE__, __LINE__);

    /*默认节点为空节点*/
    xmlData->setNone(true);

    while( cldElement != NULL) {
        /*MMCld节点的内容为空,所以不获取它的内容,但是它有子节点*/
        if (cldElement->GetText() != NULL) {    
            xmlData->setNone(false);

            /*给节点赋值*/
            setWinXmlData(xmlData, cldElement->Value(), cldElement->GetText());

            log("%s %d:[%s]-[%s]", __FILE__, __LINE__, cldElement->Value(), cldElement->GetText());
        } else {
            /*如果有子节点,则继续解析,并且添加到当前节点的子节点列表*/
            if (cldElement->FirstChildElement() != NULL) {
                xmlData->addCldXmlData(createXmlData(cldElement));
            }
        }

        /*下一个同级节点*/
        cldElement = cldElement->NextSiblingElement();
    }

    return xmlData;
}

         看看setWinXmlData函数如何给MMWinXmlData对象赋值的,代码如下:

void HelloWorld::setWinXmlData(MMWinXmlData *xmlData, const char *sName, const cahr *sText) {
    if (strcmp(sName, "enWinType") == 0) {
        if (strcmp(sText, WINType_C_en_Win_None) == 0) {
            xmlData->setEnWinType(en_Win_None);
        } else if (strcmp(sText, WINType_C_en_Win_NormalWin) == 0) {
            xmlData->setEnWinType(en_Win_NormalWin);
        } else if (strcmp(sText, WINType_C_en_Win_Label) == 0) {
            xmlData->setEnWinType(en_Win_Label);
        }
    } else if (strcmp(sName, "x") == 0) {
        xmlData->setiX(atoi(sText));
    } else if (strcmp(sName, "y") == 0) {
        xmlData->setiY(atoi(sText));
    }
}

         三个参数的含义:

  •          MMWinXmlData *xmlData: 自定义的节点对象;
  •          const char *sName: 标签节点的名字,如"enWinType";
  •          const char *sText: 标签节点的内容,如"MMNormalWin"。

        测试一下,修改HelloWorldScene的init函数,代码如下:

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    TiXmlDocument *xmlDoc = new TiXmlDocument();

    /*开始解析XML*/
    Data fileData = FileUtils::getInstance()->getDataFromFile("test.xml");
    xmlDoc->Parse((const char *)fileData.getBytes());

    TiXmlElement *rootElement = xmlDoc->RootElement();

    /*只有这里是新知识,根据标签根节点对象,创建MMWinXmlData对象*/
    MMWinXmlData *xmlData = createXmlData(rootElement->FirstChildElement());

    delete xmlDoc;

    return true;
}

         13.6 XML标签节点属性设置器

          MMWinXmlData仅仅用来保存标签节点数据,不进行任何逻辑处理,用另外一个类去处理各种逻辑,职责分明,便于维护和扩展。

          MMWinXmlDataSetting.h文件:

#ifndef __MM_WIN_XML_DATA_SETTING_H__
#define __MM_WIN_XML_DATA_SETTING_H__

/*XML文件的节点名称*/
#define XML_VALUE_enWinType "enWinType"   //控件类型
#define XML_VALUE_x "x"                   //X坐标
#define XML_VALUE_y "y"                   //Y坐标

#include "cocos2d.h"
#include "MMWinXmlData.h"

USING_NS_CC;

/*普通set函数,为了减少重复工作
例子:void setXMLiX(MMWinXmlData *xmlData, const char *sText); */
#define MM_SET_XML(funName) \
public: void setXml##funName(MMWinXmlData *xmlData, const char *sText);

class MMWinXmlDataSetting : public Ref {
public:
    CREATE_FUNC(MMWinXmlDataSetting);
    virtual bool init();

    /*给XML data对象赋值*/
    void setWinXmlData(MMWinXmlData *xmlData, const char *sName, const char *sText);
public:
    /* --------------------根据XML中的节点字符串内容赋值-------------*/
    MM_SET_XML(EnWinType);  /*设置控件类型*/
    MM_SET_XML(iX);   /*设置X坐标*/
    MM_SET_XML(iY);   /*设置Y坐标*/
};
#endif

           MMWinXmlDataSetting的实现,代码如下:

void MMWinXmlDataSetting::setXmlEnWinType(MMWinXmlData *xmlData, const char *sText) 
{
    if (strcmp(sText, WINType_C_en_Win_None) == 0) {
        xmlData->setEnWinType(en_Win_None);
    } else if (strcmp(sText, WINType_C_en_Win_NormalWin) == 0) {
        xmlData->setEnWinType(en_Win_NormalWin);
    } else if (strcmp(sText, WINType_C_en_Win_Label) == 0) {
        xmlData->setEnWinType(en_Win_Label);
    }
}

void MMWinXmlDataSetting::setXmliX(MMWinXmlData *xmlData, const char *sText) 
{
    xmlData->setiX(atoi(sText));
}

void MMWinXmlDataSetting::setXmliY(MMWinXmlData *xmlData, const char *sText) {
    xmlData->setiY(atoi(sText));
}

void MMWinXmlDataSetting::setWinXmlData(MMWinXmlData *xmlData, cosnt char *sName, const char *sText) {
    if (strcmp(sName, XML_VALUE_enWinType) == 0) {
        setXmlEnWinType(xmlData, sText);
    } else if (strcmp(sName, XML_VALUE_x) == 0) {
        setXmliX(xmlData, sText);
    } else if (strcmp(sName, XML_VALUE_y) == 0) {
        setXmliY(xmlData, sText);
    }
}

           修改HelloWorldScene的setWinXmlData函数,代码如下:

void HelloWorld::setWinXmlData(MMWinXmlData *xmlData, const char *sName, const char *sText) {
    /*为xmlData对象的属性赋值*/
    MMWinXmlDataSetting *winXmlDataSetting = MMWinXmlDataSetting::create();
    winXmlDataSetting->setWinXmlData(xmlData, sName, sText);
}

      13.7 创建控件

            在HelloWorldScene增加一个函数,代码如下:

Node *HelloWorld::createWins(MMWinXmlData *xmlData) {
    EnumWinType enWinType = xmlData->getEnWinType();
    Point pos = CCPointMake(xmlData->getiX(), xmlData->getiY());

    /*根据控件类型创建不同类型的控件*/
    Node *win = NULL;
    switch(enWinType) {
    case en_Win_NormalWin:
        win = Sprite::create("bg.png");
        win->setPosition(pos);
        break;
    case en_Win_Label:
        win = Label::create("Label", "Arial", 35);
        win->setPosition(pos);
        break;
    }

    /*创建子控件*/
    if (win != NULL && xmlData->isHasChild()) {
        auto childList = xmlData->getCldXmlDataList();
        for (auto xmlData : childList) {
            win->addChild(createWins(xmlData));
        }
    }

    return win;
}

              测试一下,修改HelloWorldScene的init函数,代码如下:

bool HelloWorld::init() {
    if (!Layer::init()) { return false; }
    TiXmlDocument *xmlDoc = new TiXmlDocument();

    /*开始解析XML*/
    Data fileData = FileUtils::getInstance()->getDataFromFile("test.xml");
    xmlDoc->Parse((const char *)fileData.getBytes());

    TiXmlElement *rootElement = xmlDoc->RootElement();

    MMWinXmlData *xmlData = createXmlData(rootElement->FirstChildElement());

    /*创建控件*/
    Node *win = createWins(xmlData);
    this->addChild(win);

    delete xmlDoc;
    return true;
}

             接着编写一个XML文件,命名为test.xml,保存到项目的resources目录,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<MMWinRoot>
    <MMWin><!-- 窗口 -->
        <enWinType>MMNormalWin</enWinType>
        <x>250</x>
        <y>150</y>
        <MMCld><!-- 标签1 -->
            <enWinType>MMLabel</enWinType>
            <x>50</x>
            <y>140</y>
        </MMCld>
        <MMCld><!-- 标签2 -->
            <enWinType>MMLabel</enWinType>
            <x>200</x>
            <y>140</y>
        </MMCld>
    </MMWin>
</MMWinRoot>

           效果图:

               

    13.8 组件

         (1) 控件基类---MMBase

            MMBase类:

class MMBase : public Node {
public:
    MMBase();
    ~MMBase();

private:
    CC_PRIVATE(int, m_ID, ID);    /*控件ID*/
    CC_PRIVATE(int, m_iOrder, iOrder);   /*层次*/
    CC_PRIVATE(EnumWinType, mEnWinType, EnWinType);  /*控件类型*/
    CC_PRIVATE_BOOL(m_isHasParentWin, HasParentWin);  /*是否有父控件*/
    CC_PRIVATE_BOOL(m_isHasChildWin, HasChildWin); /*是否有子控件*/
};

         MMBase的实现,代码如下:

MMBase::MMBase()
    : m_ID(-1),
    m_iOrder(1),
    m_isHasParentWin(false),
    m_isHasChildWin(false) {
}

MMBase::~MMBase() {
}

       (2) 普通窗口控件-----MMNormalWin

         MMNormalWin类,代码如下:

class MMNormalWin : public MMBase {
public:
    MMNormalWin();
    ~MMNormalWin();

    CREATE_FUNC(MMNormalWin);
    virtual bool init();

    virtual void setAnchorPoint(const Point &anchorPoint);

    void setBG(const char *sPath);   /*设置窗口背景图片*/
private:
    Sprite *m_sprite;   /*用一个精灵作为窗口的表现*/
};

        MMNormalWin的实现,代码如下:

MMNormalWin::MMNormalWin() {
    m_sprite = NULL;
}

MMNormalWin::~MMNormalWin() {}

bool MMNormalWin::init() {
    MMBase::init();
    return true;
}

void MMNormalWin::setBG( const char *sPath) {
    if (m_sprite != NULL) {
        this->removeChild(m_sprite);
    }
    
    m_sprite = Sprite::create(sPath);
    this->addChild(m_sprite);
    
    Size size = m_sprite->getContentSize();
    m_sprite->setPosition(Point(size.width * 0.5f, size.height * 0.5f));
    this->setContentSize(size);
}

void MMNormalWin::setAnchorPoint(const Point &anchorPoint) {
    Node::setAnchorPoint(anchorPoint);

    /*child的描点也要设置*/
    m_sprite->setAnchorPoint(anchorPoint);
}

       (3) 标签控件----MMLabel

          MMLabel.h文件:

class MMLabel : public MMBase {
public:
    MMLabel();
    ~MMLabel();

    CREATE_FUNC(MMLabel);
    virtual bool init();

    virtual void setAnchorPoint(const Point &anchorPoint);

    /*设置标签内容*/
    void setsText(const char *sText);

    /*设置标签内容,内容为数字*/
    void setsText(int iValue);

    /*设置标签文字大小*/
    void setiFontSize(int iFontSize);

    /*设置标签文字颜色*/
    void setColorRGB(int r, int g, int b);
private:
    Label *m_label;
};

         来看看MMLabel的实现:

MMLabel::MMLabel() {
    m_label = NULL;
}
MMLabel::~MMLabel() {
}

bool MMLabel::init() {
    MMBase::init();
    
    m_label = Label::create("", "Arial", 24);
    this->addChild(m_label);

    return true;
}
void MMLabel::setsText(const char *sText) {
    m_label->setString(sText);
}

void MMLabel::setsText(int iValue) {
    setsText(StringUtils::toString(iValue).c_str());
}

void MMLabel::setiFontSize(int iFontSize) {
    m_label->setSystemFontSize(iFontSize);
}

void MMLabel::setAnchorPoint(const Point &anchorPoint) {
    Node::setAnchorPoint(anchorPoint);
    m_label->setAnchorPoint(anchorPoint);
}

void MMLabel::setColorRGB(int r, int g, int b) 
{
    m_label->setColor(Color3B(r, g, b));
}

      (4) 测试新的控件类

          修改HelloWorldScene的createWins函数,代码如下:

Node *HelloWorld::createWins(MMWinXmlData *xmlData) 
{
    EnumWinType enWinType = xmlData->getEnWinType();
    Point pos = Point(xmlData->getiX(), xmlData->getiY());

    /*根据控件类型创建不同类型的控件*/
    MMBase *win = NULL;
    switch(enWinType) {
    case en_Win_NormalWin:
        win = MMNormalWin::create();
        ((MMNormalWin *)win)->setBG("bg.png");
        win->setPosition(pos);
        break;
    case en_Win_Label:
        win = MMLabel::create();
        ((MMLabel *)win)->setsText("MMLabel");
        win->setPosition(pos);
        break;
    }

    /*创建子控件*/
    if (win != NULL && xmlData->isHasChild()) {
        auto childList = xmlData->getCldXmlDataList();
        for (auto xmlData : childList) {
            win->addChild(createWins(xmlData));
        }
    }

    return win;
}

        然后编写一个xml文件,命名为test.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<MMWinRoot>
    <MMWin><!-- 窗口 -->
        <enWinType>MMNormalWin</enWinType>
        <x>50</x>
        <y>20</y>
        <MMCld><!-- 标签1 -->
            <enWinType><MMLabel</enWinType>
            <x>100</x>
            <y>140</y>
        </MMCld>
    </MMWin>
</MMWinRoot>

         效果如下:

     

     (5) MMWinManager控件管理器

        MMWinManager.h文件:

class MMWinManager : public Ref {
public:
    MMWinManager();
    ~MMWinManager();

    static MMWinManager *getInstance();
    virtual bool init();

    /*获取桌面*/
    MMDesktopWin *getDesktopWin();

    /*获取控件生成器*/
    MMWinSystem *getWinSystem();
public:
    /*根据XML文件,创建控件*/
    MMBase *createWinsFromXML(const char *sXmlPath);
private:
    /*读取XML元素,生成一个XML控件树形结构对象*/
    MMWinXmlData *createXmlData(TiXmlElement *xmlElement);

    /*为XML data对象赋值*/
    void setWinXmlData(MMWinXmlData *xmlData, const char *sName, const char *sText);
private:
    static MMWinManager *mWinManager;
private:
    /*控件顶级桌面*/
    MMDesktopWin *mDesktopWin;

    /*控件生成器*/
    MMWinSystem *mWinSystem;

    /*xml属性结构设置器*/
    MMWinXmlDataSetting *mWinXmlDataSetting;
};

           我们重点看看createWinsFromXML函数,该函数用于读取XML配置文件并生成控件,它将替代之前HelloWorldScene的createWins函数:

           cpp文件代码如下:

MMWinManager *MMWinManager::mWinManager = NULL;
MMWinManager::MMWinManager() {
}

MMWinManager::~MMWinManager() {
    CC_SAFE_RELEASE_NULL(mWinXmlDataSetting);
    CC_SAFE_RELEASE_NULL(mWinSystem);
    CC_SAFE_RELEASE_NULL(mDesktopWin);
}

MMWinManager *MMWinManager::getInstance() {
    if (mWinManager == NULL) {
        mWinManager = new MMWinManager();
        if (mWinManager && mWinManager->init()) {
            mWinManager->autorelease();
        } else {
            CC_SAFE_DELETE(mWinManager);
            mWinManager = NULL;
        }
    }
    return mWinManager;
}

bool MMWinManager::init() {
    mWinXmlDataSetting = MMWinXmlDataSetting::create();
    mWinXMlDataSetting->retain();

    mDesktopWin = MMDesktopWin::create();
    mDesktopWin->retain();

    mWinSystem = MMWinSystem::create(mDesktopWin);
    mWinSystem->retain();

    return true;
}

MMBase *MMWinManager::createWinsFromXML(const char *sXmlPath) {
    TiXmlDocument *xmlDoc = new TiXmlDocument();
    
    TiXmlElement *rootElement = xmlDoc->RootElement();
    MMWinXmlData *xmlData = createXmlData(rootElement->FirstChildElement());
    TiXmlElement *rootElement = xmlDoc->RootElement();
    MMWinXmlData *xmlData  = createXmlData(rootElement->FirstChildElement());

    delete xmlDoc;
    
    /*生成控件*/
    MMBase *baseWin = mWinSystem->createWinsByXmlData(xmlData, false);

    if (baseWin->isHasChildWin() == false) {
        mWinSystem->dressPropertiesByType(baseWin, xmlData);
    }

    /*添加父控件到桌面中*/
    mDesktopWin->addChild(baseWin, baseWin->getiOrder());
    return baseWin;
}

MMWinXmlData *MMWinManager::createXmlData(TiXmlElement *xmlElement) {
    MMWinXmlData *xmlData = MMWinXmlData::create();
    TiXmlElement *cldElement = xmlElement->FirstChildElement();

    xmlData->setNone(true);  /*默认节点为空节点*/
    
    while(cldElement != NULL) {
        /*MMCld节点的内容为空,所以不获取它的内容,但是它有子节点*/
        if (cldElement->GetText() != NULL) {
            xmlData->setNone(false);

            /*给节点赋值*/
            setWinXmlData(xmlData, cldElement->Value(), cldElement->GetText());
        } else {
            /*如果有子节点,则继续解析,并且添加到当前节点的子节点列表*/
            if (cldElement->FirstChildElement() != NULL) {
                xmlData->addCldXmlData(createXmlData(cldElement));
            }
        }

        /*下一个同级节点*/
        cldElement = cldElement->NextSiblingElement();
    }

    return xmlData;
}

void MMWinManager::setWinXmlData(MMWinXmlData *xmlData, const char *sName, const char *sText) {
    mWinXmlDataSetting->setWinXmlData(xmlData, sName, sText);
}

MMDesktopWin *MMWinManager::getDesktopWin() {
    return this->mDesktopWin;
}

MMWinSystem *MMWinManager::getWinSystem() {
    return this->mWinSystem;
}

            创建控件的流程如下:

            (1) 创建一个XML配置文件。

            (2) 调用MMWinManager的createWinsFromXML函数。

            (3) 读取XML文件,创建MMWinXmlData对象。

            (4) 用MMWinXmlData对象调用MMWinSystem的createWinsByXmlData函数创建控件。

            (5) 将创建的控件添加到MMDesktopWin.

     (6) MMWinSystem控件系统

         MMWinSystem.h头文件:

class MMWinSystem : public Ref {
public:
    MMWinSystem();
    ~MMWinSystem();

    static MMWinSystem *create(MMDesktopWin *desktopWin);
    virtual bool init(MMDesktopWin *desktopWin);

    /*根据XML结构数据创建控件
      它有可能创建很多个控件,但最终都会添加到一个父控件里
      换句话说,XML文件里只允许出现一个最高父控件,不能出现同级别的父控件*/
    MMBase *createWinsByXmlData(MMWinXmlData *xmlData, bool isHasParent);

    /*根据控件类型给控件设置属性(就像穿衣服一样)*/
    void dressPropertiesByType(MMBase *mmWin, MMWinXmlData *xmlData);

    /*创建一个唯一ID*/
    int createUniqueID();
private: /*------属性 -------*/
    /*桌面*/
    MMDesktopWin *mDesktopWin;

    /*控件工厂*/
    MMWinBaseFactory *mWinFactory;

    /*控件属性加工厂*/
    MMWinPropertyFactory *mWinPropertyFactory;

    /*控件ID*/
    int m_iWinID;

private: /*-------------方法 -------------*/
    /*根据XML结构数据创建一个控件*/
    MMBase *createWinByXmlData(MMWinXmlData *xmlData);

    /*根据控件类型创建一个控件*/
    MMBase *createWinByType(EnumWinType enWinType);
};

       MMWinSystem稍微有点复杂,它的主要职责就是调用控件工厂生成控件,并且产生控件的唯一ID。

       看看createWinsByXmlData函数,代码如下:

MMBase *MMWinSystem::createWinsByXmlData(MMWinXmlData *xmlData, bool isHasParent) {
    /*规定只能有一个MMBase,XML中生成的所有控件的最终父控件都是这个惟一的MMBase*/
    MMBase *baseWin = NULL;
    
    if (xmlData->isNone() == false) {
        baseWin = createWinByXmlData(xmlData);
    }

    if (xmlData->isHasChild()) {
        if (baseWin != NULL) {
            baseWin->setHasChildWin(true);

            /*如果没有父控件,代表自身是父控件,父控件要在子控件之前设置属性*/
            if (isHasParent == false) {
                /*根据控件类型给控件设置属性,父控件要在子控件之前设置属性*/
                dressPropertiesByType(baseWin, xmlData);
            }
        }

        auto cldXmlDataList = xmlData->getCldXmlDataList();

        for (auto cldXmlData : cldXmlDataList) {
            MMBase *mmWin = createWinsByXmlData(cldXmlData, true);
            
            baseWin->addChild(mmWin);
            mmWin->setHasParentWin(true);
            
            /*根据控件类型给控件设置属性(如果没有父控件,代表自身是父控件,父控件已经设置过属性,不重复设置) */
            if (mmWin->isHasParentWin() == true) {
                dressPropertiesByType(mmWin, cldXmlData);
            }
        }
    }
    return baseWin;
}

         看看createWinByXmlData函数,代码如下:

MMBase *MMWinSystem::createWinByXmlData(MMWinXmlData *xmlData) 
{
    assert(xmlData && "createWinByXmlData:xmlData is NULL!");

    EnumWinType enWinType = xmlData->getEnWinType();

    /*根据控件类型创建控件*/
    MMBase *win = createWinByType(enWinType);

    return win;
}

MMBase *MMWinSystem::createWinByType(EnumWinType enWinType) {
    /*从控件工厂创建一个控件*/
    return mWinFactory->createWinByType(enWinType);
}

       (7) MMWinDesktop控件顶层桌面

             MMWinDekstop.h文件:

class MMDesktopWin : public MMBase {
public:
    MMDesktopWin();
    ~MMDesktopWin();

    CREATE_FUNC(MMDesktopWin);
    virtual bool init();
public:
    /*添加一个控件*/
    void addWin(MMBase *mmWin);

    /*根据ID获取控件*/
    MMBase *getWinByID(int ID);

    /*删除所有控件*/
    void removeAllWins();
private:
    /*存放所有控件的字典<MMBase, CCInteger> */
    Map<int, MMBase *> mWinDict;
};

          MMWinDesktop用于存放所有控件的引用,方便查找和管理。拥有添加控件、查找控件、删除控件的功能。

          MMWinDesktop.cpp文件:

MMDesktopWin::MMDesktopWin() {
}

MMDesktopWin::~MMDesktopWin() {
}
bool MMDesktopWin::init() {
    return true;
}
void MMDesktopWin::addWin(MMBase *mmWin) {
    assert(mmWin && "addWin:mmWin is NULL!");

    int iWinID = mmWin->getID();

    /*如果已经存在该ID的控件,则先删除*/
    if (mWinDict.at(iWinID) != nullptr) {
        mWinDict.erase(iWinID);
    }

    /*添加控件到字典中,方便索引*/
    mWinDict.insert(iWinID, mmWin);
}

MMBase *MMDesktopWin::getWinByID(int ID) {
    return mWinDict.at(ID);
}

void MMDesktopWin::removeAllWins() {
    mWinDict.clear();

    this->removeAllChildrenWithCleanup(true);
}

              MMDesktopWin就像一个桌子,我们把所有的控件都丢到桌子上,每个控件都有一个ID,要找哪个控件就从桌子上找。

      (8) 抽象工厂之MMWinBaseFactory

          MMWinBaseFactory.h文件:

class MMWinBaseFactory : public Ref {
public:
    MMBase *createWinByType(EnumWinType enWinType);
protected:
    /*由子类负责创建控件*/
    virtual MMBase *createWin(EnumWinType enWinType) = 0;
};

         MMWinBaseFactory.cpp文件:

MMBase *MMWinBaseFactory::createWinByType(EnumWinType enWinType)
{
    /*从子类件工厂创建一个控件*/
    MMBase *mmWin = createWin(enWinType);

    /*给控件设置一个唯一ID(必须大于0)*/
    mmWin->setID(MMWinManager::getInstance()->getWinSystem()->createUniqueID());

    /*每一个控件都要添加到desktop中*/
    MMWinManager::getInstance()->getDesktopWin()->addWin(mmWin);

    return mmWin;
}

      (9) 控件工厂之MMWinFactory

       MMWinFactory.h文件:

class MMWinFactory : public MMWinBaseFactory 
{
public:
    CREATE_FUNC(MMWinFactory);
    virtual bool init();

protected:
    virtual MMBase *createWin(EnumWinType enWinType);
};

       MMWinFactory继承了MMWinBaseFactory,因此它必须实现createWin函数。这就是使用抽象工厂的好处,我们可以有很多不同的工厂,可以用不同的方式生成控件,而这种差异就是由createWin函数来实现的。

     MMWinFactory.cpp文件:

bool MMWinFactory::init()
{
    return true;
}

MMBase *MMWinFactory::createWin(EnumWinType enWinType) 
{
    MMBase *win = NULL;
    switch(enWinType) {
    case en_Win_None:
        break;
    case en_Win_NormalWin:
        win = MMNormalWin::create();
        break;
    case en_Win_Label:
        win = MMLabel::create();
        break;
    }

    if (win != NULL) {
        win->setEnWinType(enWinType);
    }

    return win;
}

      (10) 装饰工厂之MMWinProperityFactory

         MMWinProperityFactory.h文件,代码如下:

class MMWinPropertyFactory : public Ref {
public:
    CREATE_FUNC(MMWinPropertyFactory);
    virtual bool init();
public:
    /*给控件设置属性(穿衣服)*/
    void dressPropertiesByType(MMBase *mmWin, MMWinXmlData *xmlData);

private:
    /*设置控件公共属性,所有控件都必须设置*/
    void dressBaseProperties(MMBase *mmWin, MMWinXmlData *xmlData);

private:
    void dressMMNormalWin(MMNormalWin *mmNormalWin, MMWinXmlData *xmlData);
    void dressMMLabel(MMLabel *mmLabel, MMWinXmlData *xmlData);
};

        再看看MMWinProperityFactory的dressPropertiesByType函数的实现,代码如下:

void MMWinPropertyFactory::dressPropertiesByType(MMBase *mmWin, MMWinXmlData *xmlData) {
    /*根据控件类型设置独特属性*/
    switch(mmWin->getEnWinType()) {
    case en_Win_None:
        break;
    case en_Win_NormalWin:
        dressMMNormalWin((MMNormalWin *)mmWin, xmlData);
        break;
    case en_Win_Label:
        dressMMLabel((MMLabel *)mmWin, xmlData);
        break;
    }

    /*设置基础属性*/
    dressBaseProperties(mmWin, xmlData);
}

          该函数要做两个处理:

            (1) 根据控件类型分别调用不同控件的装饰函数,每种控件都有一个对应的装饰函数,因为不同的空间可能有不同的属性。

            (2) 调用控件的公共装饰函数,不同的控件仍然有相同的属性,如X、Y坐标,所以把相同属性的设置工作放到一个函数里。

         再看看其他的装饰函数,代码如下:

void MMWinPropertyFactory::dressBaseProperties(MMBase *mmWin, MMWinXmlData *xmlData)
{
    /*如果有父控件,则取父控件的宽高*/
    MMBase *mmParent = NULL;
    if (mmWin->isHasParentWin()) {
        mmParent = (MMBase *)mmWin->getParent();
    }
    mmWin->setPositionX(xmlData->getiX()); //X坐标
    mmWin->setPositionY(xmlData->getiY()); //Y坐标
}

void MMWinPropertyFactory::dressMMNormalWin(MMNormalWin *mmNormalWin, MMWinXmlData *xmlData) {
    mmNormalWin->setBG("bg.jpg");
}

void MMWinPropertyFactory::dressMMLabel(MMLabel *mmLabel, MMWinXmlData *xmlData) {
    mmLabel->setsText("Label Test!");
}

     (11) 运行项目

         修改HelloWorldScene的init函数,代码如下:

bool HelloWorld::init() {
    bool bRet = false;

    do {
        /*创建控件*/
        MMBase *win = MMWinManager::getInstance()->createWinsFromXML("test.xml");

        /*将顶级桌面添加到场景*/
        this->addChild(MMWinManager::getInstance()->getDesktopWin());
        bRet = true;
    } while (0);

    return bRet;
}

         效果如下:

           

14.《卡牌塔防》(上篇)

     最终效果图:

     

    主要功能包括:

       <1> 内置关卡编辑器                 <2> CocoStudio UI编辑器使用:使用CocoStudio UI编辑器编写游戏UI。

       <3> 怪物灵活配置                     <4> 怪我种类Csv配置

       <5> 英雄种类Csv配置               <6> 英雄升级功能

       <7> 怪物血量条                         <8> 炮塔操作按钮

       <9> 属性刷新                             <10> 场景管理器

       <11> 文件读取: 读取Csv文件、读取XML文件    <12> 国际化支持

       <13> 移动控制器:控制怪物和子弹移动的各种控制器

       <14> 怪物管理器:负责管理所有怪物对象

       <15> 英雄管理器:负责管理所有英雄(也就是炮塔)对象。

   14.1 炮台坐标编辑器

        TowerPosEditorScene.h文件:

class TowerPosEditorScene : public Layer {
public:
    TowerPosEditorScene();
    ~TowerPosEditorScene();

    static Scene *createScene();
    virtual bool init();
    CREATE_FUNC(TowerPosEditorScene);
};

        TowerPosEditorScene.cpp文件:

TowerPosEditorScene::TowerPosEditorScene() {
}

TowerPosEditorScene::~TowerPosEditorScene() {
}

Scene *TowerPosEditorScene::scene() {
    auto scene = Scene::create();
    auto layer = TowerPosEditorLayer::create();
    scene->addChild(layer, 1);
    return scene;
}

bool TowerPosEditorScene::init() {
    if (!Layer::init()) {
        return false;
    }
    return true;
}

         PosBase类(炮台坐标类),代码如下:

         PosBase.h文件:

class PosBase : public Layer {
public:
    PosBase();
    ~PosBase();

    static PosBase *create(Point pos);
    static PosBase *create(Point pos, bool isDebug);
    bool init(Point pos);
    bool init(Point pos, bool isDebug);

    CC_SYNTHESIZE(Point, m_pos, Pos);

    virtual bool isClickMe(Point pos); /*判断坐标是否进入范围*/
    void setDebug(bool isDebug);    /*开启或关闭调试模式*/
protected:
    bool m_isDebug;  /*是否为调试状态*/
};

       PosBase.cpp文件:

PosBase::PosBase() {
    m_pos = Point(0, 0);
    m_isDebug = false;
}

PosBase::~PosBase() {
}

PosBase *PosBase::create(Point pos) {
    PosBase *tPos = new PosBase();
    
    if (tPos && tPos->init(pos)) {
        tPos->autorelease();
    } else {
        CC_SAFE_DELETE(tPos);
    }
    return tPos;
}

PosBase *PosBase::create(Point pos, bool isDebug) {
    PosBase *tPos = new PosBase();

    if (tPos && tPos->init(pos, isDebug)) {
        tPos->autorelease();
    } else {
        CC_SAFE_DELETE(tPos);
    }
    return tPos;
}

bool PosBase::init(Point pos) {
    bool bRet = false;
    
    do {
        setPos(pos);
        bRet = true;
    } while(0);

    return bRet;
}

bool PosBasae::init(Point pos, bool isDebug) {
    bool bRet = false;
    do {
        CC_BREAK_IF(! init(pos));

        m_isDebug = isDebug;

        bRet = true;
    } while (0);
    return bRet;
}

bool PosBase::isClickMe(Point pos) {
    return false;
}

void PosBase::setDebug(bool isDebug) {
    this->m_isDebug = isDebug;
}

          PosBase只是一个基类,现在我们需要一个炮台坐标类,创建一个类继承PosBase,命名为TowerPos,代码如下:

#define RADIUS 32
class TowerPos : public PosBase {
public:
    TowerPos();
    ~TowerPos();
    static TowerPos *create(Point pos);
    static TowerPos *create(Point pos, bool isDebug);
    bool init(Point pos);
    bool init(Point pos, bool isDebug);

    virtual bool isClickMe(Point pos) override; /*判断坐标是否进入范围*/
        
    void draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated);
private:
    void onDraw(const kmMat4 &transform, bool transformUpdated);
    CustomCommand _customCommand;
};

         draw函数是为了调试用的,我们在配置关卡时,要设置坑的位置,当然就是屏幕上需要设置坑的位置单击一下了,但是如果单击之后没有任何表现,那我们怎么知道点了哪里呢?因此,我们在编辑关卡时需要把坐标的位置画出来,这就是调试模式。

      我们来看看draw函数,代码如下:

void TowerPos::draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated) {
    if (m_isDebug) {
        _customCommand.init(_globalZOrder);
        _customCommand.func = CC_CALLBACK_0(TowerPos::onDraw,
                                    this, transform, transformUpdated);
        renderer->addCommand(&_customCommand);
    }
}

void TowerPos::onDraw(const kmMat4 &transform, bool transformUpdated) {
    kmGLPushMatrix();
    kmGLLoadMatrix(&transform);

    glLineWidth(5.0f); //设置画笔粗细
    
    /*绘制矩形*/
    Point srcPos = Point(m_pos.x - RADIUS, m_pos.y + RADIUS);
    Point destPos = Point(m_pos.x + RADIUS, m_pos.y - RADIUS);
    DrawPrimitives::drawRect(srcPos, destPos);

    glLineWidth(1);   //恢复画笔粗细
    kmGLPopMatrix();   //结束绘制
}

      看看isClickMe函数,用于判断某个坐标是否进入TowerPos的矩形范围,代码如下:

bool TowerPos::isClickMe(Point pos) {
    Point srcPos = Point(m_pos.x - RADIUS, m_pos.y + RADIUS);
    Point destPos = Point(m_pos.x + RADIUS, m_pos.y - RADIUS);

    if (pos.x >= srcPos.x && pos.x <= destPos.x && pos.y <= srcPos.y && pos.y >= destPos.y) {
        return true;
    }

    return false;
}

      看看TowerPos剩余的几个函数,代码如下:

TowerPos::TowerPos() {
    m_pos = Point(0, 0);
    m_isDebug = false;
}

TowerPos::~TowerPos() {
}

TowerPos *TowerPos::create(Point pos) {
    TowerPos *tPos = new TowerPos();
    if (tPos && tPos->init(pos)) {
        tPos->autorelease();
    } else {
        CC_SAFE_DELETE(tPos);
    }
    return tPos;
}

TowerPos *TowerPos::create(Point pos, bool isDebug) {
    TowerPos *tPos = new TowerPos();
    if (tPos && tPos->init(pos, isDebug)) {
        tPos->autorelease();
    } else {
        CC_SAFE_DELETE(tPos);
    }

    return tPos;
}

bool TowerPos::init(Point pos) {
    bool bRet = false;
    do {
        CC_BREAK_IF(! PosBase::init(pos));
        bRet = true;
    } while(0);
    return bRet;
}

bool TowerPos::init(Point pos, bool isDebug) {
    bool bRet = false;
    do {
        CC_BREAK_IF(! PosBase::init(pos, isDebug));
        bRet = true;
    } while(0);
    return bRet;
}

        测试一下TowerPos类,我们修改一下TowerPosEditorScene的scene函数,代码如下:

Scene *TowerPosEditorScene::createScene() {
    auto scene = Scene::create();

    //auto layer = TowerPosEditorLayer::create();
    //scene->addChild(layer, 1);

    TowerPos *pos = TowerPos::create(Point(200, 200), true);
    scene->addChild(pos);
    return scene;
}

      我们创建了一个TowerPos对象,并且将其添加到场景里。

      最后,把游戏的初始启动场景设置为TowerPosEditorScene(在AppDelegate里设置).

      效果如下:

   

    真正的炮台坐标编辑器

     TowerPosEditorLayer包含以下功能:单机屏幕任意位置会添加炮台坐标对象,单击已存在的炮台坐标对象则删除对象,最后生成配置文件。

     TowerPosEditorLayer.h文件:

class TowerPosEditorLayer : public Layer {
public:
    TowerPosEditorLayer();
    ~TowerPosEditorLayer();

    CREATE_FUNC(TowerPosEditorLayer);
    virtual bool init();
private:
    Vector<PosBase *> m_towerPosList; /*存放所有塔的坐标对象*/
    int m_iCurLevel;   /*当前关卡*/
    
    void editTowerPos(Point pos);   /*编辑塔坐标*/
    PosBase *findExistTowerPos(Point pos);   /*根据坐标找到已经存在的塔坐标对象*/
    void createTowerPos(Point pos);   /*给定坐标,生成塔坐标对象*/
    void deleteTowerPos(PosBase *existPos);  /*给定塔坐标对象,删除塔坐标对象*/
    void deleteAllPos();     /*删除所有坐标对象*/
};

      看看init函数,代码如下:

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

    /*监听触摸事件*/
    auto listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan = [](Touch *touch, Event *event) {
        return true;
    };
    listener->onTouchEnded = [&](Touch *touch, Event *event) {
        Point pos = Director::getInstance()->convertToGL(touch->getLocationInView());
        editTowerPos(pos);
    };
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
    return true;
}


    

     看看editTowerPos函数:

void TowerPosEditorLayer::editTowerPos(Point pos) {
    /*如果单击了已经存在的塔坐标对象,则删除该坐标对象,否则创建新坐标对象*/
    PosBase *existPos = findExistTowerPos(pos);

    if (existPos != NULL) {
        deleteTowerPos(existPos);
    } else {
        createTowerPos(pos);
    }
}

PosBase *TowerPosEditorLayer::findExistTowerPos(Point pos) {
    for (auto basePos : m_towerPosList) {
        if (basePos->isClickMe(pos)) {
            return basePos;
        }
    }
    return NULL;
}

void TowerPosEditorLayer::createTowerPos(Point pos) {
    TowerPos *tPos = TowerPos::create(pos, true);
    this->addChild(tPos, 10);
    m_towerPosList.pushBack(tPos);
}

void TowerPosEditorLayer::deleteTowerPos(PosBase *existPos) {
    this->removeChild(existPos);
    m_towerPosList.eraseObject(existPos);
}

          TowerPosEditorLayer有一个Vector成员变量m_towerPosList,添加和删除TowerPos的操作实际上只是对m_towerPosList的增、删操作,查找TowerPos对象,就是遍历m_towerPosList,判断列表中的TowerPos的坐标是否和单机屏幕的坐标一致。

           值得注意的是,m_towerPosList存放的是PosBase对象,而不是TowerPos对象,这是多态的一种应用。我们仍然可以把TowerPos添加到m_towerPosList里,但从m_towerPosList中取出的对象类型默认是PosBase,在需要的时候我们可以强制转换为TowerPos(因为我们丢进去的就是TowerPos对象)。

           来看看剩下的函数,代码如下:

TowerPosEditorLayer::TowerPosEditorLayer() {
    m_iCurLevel = 1;
}
TowerPosEditorLayer::~TowerPosEditorLayer() {
}

void TowerPosEditorLayer::deleteAllPos() {
    this->removeAllChildrenWithCleanup(true);
    m_towerPosList.clear();
}

          将TowerPosEditorScene的scene函数改回原来的样子,代码如下:

Scene *TowerPosEditorScene::createScene() {
    auto scene = Scene::create();

    auto layer = TowerPosEditorLayer::create();
    scene->addChild(layer, 1);
    return scene;
}

         运行并输出:

      

     生成plist配置文件

       为TowerPosEditorLayer新增两个函数,代码如下:

void TowerPosEditorLayer::outputPosToPlistFile()
{
    ValueMap fileDataMap;

    /*各个属性*/
    int index = 1;
    for (auto posBase : posList) {
        ValueMap data;
        data["x"] = posBase->getPos().x;
        data["y"] = posBase->getPos().y;

        fileDataMap[StringUtils::toString(index)] = Value(data);
        
        index++;
    }
    FileUtils::getInstance()->writeToFile(fileDataMap, sFilePath);
}

       最终生成的文件类似这样:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC"-//Apple//DTD PLIST 1.0//EN" "http://www.apple/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>1</key>
        <dict>
            <key>x</key>
            <real>126.4497147</real>
            <key>y</key>
            <real>359.8025818</real>
        </dict>
        <key>2</key>
        <dict>
            <key>x</key>
            <real>356.071332</real>
            <key>y</key>
            <real>357.8025818</real>
        </dict>
        <key>3</key>
        <dict>
            <key>x</key>
            <real>631.6184692</real>
            <key>y</key>
            <real>359.8025818</real>
        </dict>
    </dict>
</plist>

       这是Cocos2d-x支持的一种文件格式,通过FileUtils工具类可以直接生成和读取这样的文件。

      一个炮台坐标对象对应一个dict标签,如下:

        <key>1</key>
        <dict>
            <key>x</key>
            <real>126</real>
            <key>y</key>
            <real>359</real>
        </dict>

        这样一个dict标签代表一个坐标为(126,359)的TowerPos对象数据,一个dict对应一个ValueMap结构,从中可以取出数据。

    操作层----TowerPosEditorOperateLayer

       TowerPosEditorOperateLayer.h文件:

class TowerPosEditorOperateLayer : public Layer {
public:
    TowerPosEditorOperateLayer();
    ~TowerPosEditorOperateLayer();

    static TowerPosEditorOperateLayer *create(TowerPosEditorLayer *layer);
    virtual bool init(TowerPosEditorLayer *layer);
private:
    /*编辑层*/
    TowerPosEditorLayer *m_editorLayer;
};

       TowerPosEditorOperateLayer.cpp文件:

TowerPosEditorOperateLayer::TowerPosEditorOperateLayer() {
}
TowerPosEditorOperateLayer::~TowerPosEditorOperateLayer() {
}
TowerPosEditorOperateLayer *TowerPosEditorOperateLayer::create(TowerPosEditorLayer *layer) {
    TowerPosEditorOperateLayer *oprLayer = new TowerPosEditorOperateLayer();
    if (oprLayer && oprLayer->init(layer)) {
        oprLayer->autorelase();
    } else {
        CC_DAFE_DELETE(oprLayer);
    }
    return oprLayer;
}
bool TowerPosEditorOperateLayer::init(TowerPosEditorLayer *layer)
{
    if (!Layer::init()) { return false; }
    
    this->m_editorLayer = layer;

    /*加载UI*/
    auto UI = cocostudio::GUIReader::getInstance()->    
                widgetFromJsonFile("EditorOperate/EditorOperate_1.ExportJson");
    this->addChild(UI);

    /*少了这句代码就不好玩了*/
    UI->setTouchEnabled(false);

    /*获取控件对象*/
    auto outputBtn = (Button *)Helper::seekWidgetByName(UI, "outputBtn");

    /*添加单机监听*/
    outputBtn->addTouchEventListener(this, toucheventselector(TowerPosEditorOperateLayer::outputBtnOnClick));

    return true;
}

void TowerPosEditorOperateLayer::outputBtnOnClick(Ref *, TouchEventType type) {
    if (type == TouchEventType::TOUCH_EVENT_ENDED) {
        m_editorLayer->outputPosToPlistFile();
    }
}

         当单击outputBtn按钮时,就会调用TowerPosEditorLayer的outputPosToPlistFile函数。

         UI->setTouchEnabled(false);这句代码一定要写上,否则,一个UI本身也是一个控件,这个控件默认是能接受触摸事件的。必须通过setTouchEnabled函数把UI的触摸响应关闭,否则,在UI下方的所有对象的触摸事件都会被吃掉,从而无法响应。

         最后把新加的层添加到场景里,修改TowerPosEditorScene的scene函数,代码如下:

Scene *TowerPosEditorScene::createScene() {
    auto scene = Scene::create();

    auto editorLayer = TowerPosEditorLayer::create();
    scene->addChild(editorLayer, 1);

    auto operLayer = TowerPosEditorOperateLayer::create(editorLayer);
    scene->addChild(operLayer, 2);
    return scene;
}

         效果:

          

        单机“输出文件”按钮,在Resources下的tollgate目录中多了一个towerPos_level_1.plist文件。

    读取plist配置文件,生成炮台坐标

      写一个工具类,用来读取配置文件,PosLoadUtil类:

      PosLoadUtil.h文件:

class PosLoadUtil : public CCNode {
public:
    static PosLoadUtil *getInstance();
    virtual bool init();

    /*根据坐标类型从plist配置文件中读取坐标对象
      sFilePath: 配置文件路径
      container: 存放坐标对象的容器
      iLevel: 如果存在container,该参数表示坐标对象在容器中的层次
      isDebug: 是否开启调试模式
    */
    Vector<PosBase *> loadPosWithFile (
        const char *sFilePath,
        Node *container,
        int iLevel,
        bool isDebug
    );
private:
    static PosLoadUtil *m_posLoadUtil;
};

       PosLoadUtil.cpp文件:

PosLoadUtil *PosLoadUtil::m_posLoadUtil = NULL;

PosLoadUtil *PosLoadUtil::sharedPosLoadUtil() {
    if (m_posLoadUtil == NULL) {
        m_posLoadUtil = new PosLoadUtil();
        if (m_posLoadUtil && m_posLoadUtil->init()) {
            m_posLoadUtil->autorelease();
        } else {
            CC_SAFE_DELETE(m_posLoadUtil);
        }
    }
    return m_posLoadUtil;
}

bool PosLoadUtil::init() {
    return true;
}

Vector<PosBase *>PosLoadUtil::loadPosWithFile(const char *sFilePath, Node *container, int iLevel, bool isDebug) {
    Vector<PosBase *> posList;

    ValueMap fileDataMap = FileUtils::getInstance()->getValueMapFromFile(sFilePath);

    int size = fileDataMap.size();
    for (int i=1; i<=size; i++) {
        Value value = fileDataMap.at(StringUtils::toString(i));
        ValueMap data = value.asValueMap();

        /*创建坐标对象*/
        PosBase *posBase = TowerPos::create(Point(data["x"].asInt(), data["y"].asInt()), isDebug);
        posList.pushBack(posBase);

        if (container != NULL) {
            container->addChild(posBase, iLevel);
        }
    }
    return posList;
}

        我们要给TowerPosEditorLayer新增一个函数,代码如下:

void TowerPosEditorLayer::loadConfigFile() {
    Size visibleSize = Director::getInstance()->getVisibleSize();

    /*添加地图背景*/
    std::string sBG = StringUtils::format("tollgate/level_%d.jpg", m_iCurLevel);
    Sprite *map = Sprite::create(sBG.c_str());
    map->setPosition(Point(visibleSize.width / 2, visibleSize.height / 2 ));
    this->addChild(map, 1);

    /*加载塔坐标对象*/
    std::string sTowerPosPath = StringUtils::format("tollgate/towerPos_level_%d.plist", m_iCurLevel);
    Vector<PosBase *> posList = PosLoadUtil::getInstance()->loadPosWithFile(sTowerPosPath.c_str(), this, 10, true);
    m_towerPosList.pushBack(posList);
}

        这个函数就只做了两件事件:

  •         根据当前关卡加载地图背景
  •         使用PosLoadUtil工具类加载炮台坐标配置文件,给m_towerPosList炮台坐标列表赋值。

       运行,效果:

         

   怪物坐标编辑器

enum EnumPosType {
    enTowerPos,   /*炮台坐标*/
    enMonsterPos,   /*怪物坐标*/
};

      MonsterPos.h文件:

class Monster;
class MonsterPos : public PosBase {
public:
    MonsterPos();
    ~MonsterPos();

    static MonsterPos *create(Point pos);
    static MonsterPos *create(Point pos, bool isDebug);
    bool init(Point pos);
    bool init(Point pos, bool isDebug);

    /*判断坐标是否进入范围*/
    virtual bool isClickMe(Point pos) override;

    void draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated);
private:
    void onDraw(const kmMat4 &transform, bool transformUpdated);
    CustomCommand _customCommand;

    Monster *m_monster;
};

     MonsterPos.cpp文件:

MonsterPos::MonsterPos() {
    m_pos = Point(0, 0);
    m_isDebug = false;
}
MonsterPos::~MonsterPos() {
}
MonsterPos *MonsterPos::create(Point pos) {
    MonsterPos *tPos = new MonsterPos;
    if (tPos && tPos->init(pos)) {
        tPos->autorelease();
    } else {
        CC_SAFE_DELETE(tPos);
    }
    return tPos;
}

MonsterPos *MonsterPos::create(Point pos, bool isDebug) 
{
    MonsterPos *tPos = new MonsterPos();
    if (tPos && tPos->init(pos, isDebug)) {
        tPos->autorelease();
    } else {
        CC_SAFE_DELETE(tPos);
    }
    return tPos;
}

bool MonsterPos::init(Point pos) {
    bool bRet = false;
    do {
        setPos(pos);
        bRet = true;
    } while(0);

    return bRet;
}

bool MonsterPos::init(Point pos, bool isDebug) {
    bool bRet = false;
    do {
        CC_BREAK_IF(! init(pos));

        m_isDebug = isDebug;
    
        bRet = true;
    } while(0);
    return bRet;
}

void MonsterPos::draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated) {
    if (m_isDebug) {
        _customCommand.init(_globalZOrder);
        _customCommand.func = CC_CALLBACK_0(MonsterPos::onDraw, this, transform, transformUpdated);
        renderer->addCommand(&_customCommand);
    }
}

void MonsterPos::onDraw(const kmMat4 &transform, bool transformUpdated) {
    kmGLPushMatrix();
    kmGLLoadMatrix(&transform);

    glLineWidth(4);
    /*绘制圆型*/
    DrawPrimitives::drawCircle(m_pos, MONSTER_RADIUS, 360, 20, false);
    glLineWidth(1);
    kmGLPopMatrix();
}

bool MonsterPos::isClickMe(Point pos) {
    Point srcPos = Point(m_pos.x - MONSTER_RADIUS, m_pos.y + MONSTER_RADIUS);
    Point destPos = Point(m_pos.x + MONSTER_RADIUS, m_pos.y - MONSTER_RADIUS);

    if (pos.x >= srcPos.x && pos.x <= destPos.x && pos.y <= srcPos.y && pos.y >= destPos.y) {
        return true;
    }
    return false;
}

    

     新增怪物坐标编辑器

       首先在TowerPosEditorLayer.h文件增加一些内容,代码如下:

EnumPosType m_enMode;               /*当前模式*/
void editMonsterPos(Point pos);      /*编辑怪物坐标*/
Vector<PosBase *> m_monsterPosList;   /*存放所有怪物的坐标对象*/
PosBase *findExistMonsterPos(Point pos);   /*根据坐标找到已经存在的怪物坐标对象*/
void createMonsterPos(Point pos);        /*给定坐标,生成怪物坐标对象*/
void deleteMonsterPos(PosBase *existPos);  /*给定怪物坐标对象,删除怪物坐标对象*/

       新增函数的实现:

void TowerPosEditorLayer::editMonsterPos(Point pos) {
    /*如果单击了已经存在的怪物坐标对象,则删除该坐标对象,否则创建新坐标对象*/
    PosBase *existPos = findExistMonsterPos(pos);

    if (existPos != NULL) {
        deleteMonsterPos(existPos);
    } else {
        createMonsterPos(pos);
    }
}

PosBase *TowerPosEditorLayer::findExistMonsterPos(Point pos) {
    for (auto basePos : m_monsterPosList) {
        if (basePos->isClickMe(pos)) {
            return basePos;
        }
    }
    return NULL;
}

void TowerPosEditorLayer::createMonsterPos(Point pos) {
    MonsterPos *mPos = MonsterPos::create(pos, true);
    this->addChild(mPos, 10);
    m_monsterPosList.pushBack(mPos);
}

void TowerPosEditorLayer::deleteMonsterPos(PosBase *existPos) {
    this->removeChild(existPos);
    m_monsterPosList.eraseObject(existPos);
}

       修改init函数:

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

    /*监听触摸事件*/
    auto listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan = [](Touch *touch, Event *event) {
        return true;
    };
    listener->onTouchEnded = [&](Touch *touch, Event *event) {
        Point pos = Director::getInstance()->convertToGL(touch->getLocationInView());

        /*判断当前编辑器的模式,进行不同的操作*/
        switch(m_enMode) {
        case enTowerPos:
            editTowerPos(pos);
            break;
        case enMonsterPos:
            editMonsterPos(pos);
            break;
        }
    };

    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);

    /*读取配置文件*/
    loadConfigFile();
    return true;
}

       效果:

       

  输出怪物坐标配置文件

     修改TowerPosEditorLayer的outputPosToPlistFile函数即可,代码如下:

void TowerPosEditorLayer::outputPosToPlistFile() {
    /*输出炮台坐标配置文件*/
    std::string sTowerPosPath = StringUtils::format("tollgate/towerPos_level_%d.plist", m_iCurLevel);
    outputPosToPlistFile(m_towerPosList, sTowerPosPath.c_str());

    /*输出怪物坐标配置文件*/
    std::string sMonsterPosPath = StringUtils::format("tollgate/monsterPos_level_%d.plist", m_iCurLevel);
    outputPosToPlistFile(m_monsterPosList, sMonsterPosPath.c_str());
}

     单击"输出文件"按钮,就能看到resources的tollgate目录下多了一个monsterPos_level_1.plist文件。

    修改loadConfigFile函数,代码如下:

void TowerPosEditorLayer::loadConfigFile(){
    /*这里省略了一些代码*/

    /*加载塔坐标对象*/
    std::string sTowerPosPath = StringUtils::format("tollgate/towerPos_level_%d.plist", m_iCurLevel);    
    Vecotr<PosBase *> posList = PosLoadUtil::getInstance()->loadPosWithFile(sTowerPosPath.c_str(), enTowerPos, this, 10, true);
    m_towerPosList.pushBack(poslist);

    /*加载怪物坐标对象*/
    std::string sMonsterPosPath = StringUtils::format("tollgate/monsterPos_level_%d.plist", m_iCurLevel);
    posList = PosLoadUtil::getInstance()->loadPosWithFile(
        sMonsterPosPath.c_str(), enMonsterPos, this, 10, true);
    m_monsterPosList.pushBack(posList);
}

        修改PosLoadUtil的loadPosWithFile函数,代码如下:

Vector<PosBase *> PosloadUtil::loadPosWithFile(const char *sFilePath, EnumPosType enPosType,
        Node *container, int iLevel, bool isDebug) {
    Vector<PosBase *> posList;

    ValueMap fileDataMap = FileUtils::getInstance()->getValueMapFromFile(sFilePath);

    int size = fileDataMap.size();
    for (int i=1; i<=size; i++) {
        Value value = fileDataMap.at(StringUtils::toString(i));
        ValueMap data = value.asValueMap();

        /*创建坐标对象*/
        PosBase *posBase = NULL;
        switch(enPosType) {
        case enTowerPos:
            posBase = TowerPos::create(Point(data["x"].asInt(), data["y"].asInt()), isDebug);
            break;    
        case enMonsterPos:
            posBase = MonsterPos::create(Point(data["x"].asInt(), data["y"].asInt()), isDebug);
            break;
        default:
            posBase = TowerPos::create(Point(data["x"].asInt(), data["y"].asInt()), isDebug);
            break;
        }
        
        posList.pushBack(posBase);

        if (container != NULL) {
            container->addChild(posBase, iLevel);
        }
    }
    
    return posList;
}

        运行项目,屏幕上出现了我们之前创建的怪物坐标对象,这就证明monsterPos_level_1.plist成功读取了。

   14.2 添加更多方便的操作

       (1) 新增切换坐标模式类型按钮

         如果每次编辑都要在代码里修改坐标类型,然后编译,运行,那得多麻烦。我们修改TowerPosEditorOperateLayer的init函数,代码如下:

boolTowerPosEditorOperateLayer::init(TowerPosEditorLayer *layer) {
    /*这里省略了很多代码*/
        
    /*获取控件对象*/
    auto changeModeBtn = (Button *)Helper::seekWidgetByName(UI, "changeModeBtn");
    
    /*添加点击监听*/
    changeModeBtn->addTouchEventListener(this, toucheventselector(TowerPosEditorOperateLayer::changeModeBtnOnClick));
}

     新增一个按钮,文本属性改为"切换模式",按钮的名字属性改为"changeModeBtn",然后导出文件,用新导出的文件替换旧的文件。

单击按钮时,执行changeModeBtnOnClick函数,代码如下:

void TowerPosEditorOperateLayer::changeModeBtnOnClick(Ref *, TouchEventType type) {
    if (type == TouchEventType::TOUCH_EVENT_ENDED) {
        m_editorLayer->changeMode();
    }
}

TowerPosEditorOperateLayer的changeModeBtnOnClick函数里又调用了TowerPosEditorLayer的changeMode函数,因为,要给TowerPosEditorLayer新增一个changeMode函数,代码如下:

void TowerPosEditorLayer::changeMode() {
    if(m_enMode == enMonsterPos) {
        m_enMode = enTowerPos;
    } else {
        m_enMode = enMonsterPos;
    }
}

  运行项目,单击change mode按钮之后,再单击屏幕的其他地方,会发现每次单击change mode按钮后,就会切换坐标类型。

    (2) 新增关卡切换按钮

       为TowerPosEditorLayer新增两个函数,代码如下:

int TowerPosEditorLayer::nextLvl() {
    deleteAllPos();    /*删除当前所有的坐标对象*/
    m_iCurLevel++;    /*关卡计数加1*/
    loadConfigFile();   /*重新读取配置文件*/

    return m_iCurLevel;
}

int TowerPosEditorLayer::preLvl() {
    deleteAllPos();   /*删除当前所有的坐标对象*/
    m_iCurLevel--;    /*关卡计数减1*/
    loadConfigFile();   /*重新读取配置文件*/

    return m_iCurLevel;
}

     切换关卡的逻辑很简单,删除现有的坐标对象,然后重新加载配置文件就可以了。

      我们需要有按钮来执行这个操作,修改TowerPosEditorOperateLayer的init函数,代码如下:

    

boolTowerPosEditorOperateLayer::init(TowerPosEditorLayer *layer) {
    /*省略了很多代码*/
        
    /*获取控件对象*/
    auto nextLevelBtn = (Button *)Helper::seekWidgetByName(UI, "nextLevelBtn");
    auto preLevelBtn = (Button *)Helper::seekWidgetByName(UI, "preLevelBtn");

    /*添加点击监听*/
    nextLevelBtn->addTouchEventListener(this, toucheventselector(TowerPosEditorOperateLayer::nextLevelBtnOnClick));
    preLevelBtn->addTouchEventListener(this, toucheventselector(TowerPosEditorOperateLayer::preLevelBtnOnClick));
}

void TowerPosEditorOperateLayer::nextLevelBtnOnClick(Ref *, TouchEventType type) {
    if (type == TouchEventType::TOUCH_EVENT_ENDED) {
        m_editorLayer->nextLvl();
    }
}

void TowerPosEditorOperateLayer::preLevelBtnOnClick(Ref *, TouchEventType type) {
    if (type == TouchEventType::TOUCH_EVENT_ENDED) {
        m_editorLayer->preLvl();
    }
}

   新增的按钮文本属性分别为"下一关","上一关",名字属性分别为"nextLevelBtn","preLevelBtn".然导出新的文件,替换旧的文件,效果如下:

    

 现在运行项目,单击"下一关"按钮,现有的坐标对象都消失了,编辑器清空了,其实我们已经到了第二关了。编辑好坐标后的,单击output按钮就能生成第二关的配置文件。

  

   (3) 绘制怪物行走路径

       让TowerPosEditorOperateLayer重写draw函数,首先重写draw函数,代码如下:

    TowerPosEditorOperateLayer.h文件:

class TowerPosEditorOperateLayer : public Layer {
public:
    /*省略了很多代码*/
    
    void draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated);

    private:
        void onDraw(const kmMat4 &transform, bool transformUpdated);
        CustomCommand _customCommand;
};

     TowerPosEditorOperateLayer.cpp文件:

void TowerPosEditorOperateLayer::draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated) {
    _customCommand.init(_globalZOrder);
    _customCommand.func = CC_CALLBACK_0(TowerPosEditorOperateLayer::onDraw, this,
                                transform, transformUpdated);
    renderer->addCommand(&_customCommand);
}

void TowerPosEditorOperateLayer::onDraw(const kmMat4 &transform, bool transformUpdated) {
    kmGLPushMatrix();
    kmGLLoadMatrix(&transform);
    Vector<PosBase *> m_monsterPosList = m_editorLayer->getMonsterPosList();
    for (auto posBase : m_monsterPosList) {
        if (prePos != NULL) {
            DrawPrimitives::drawLine(prePos->getPos(), posBase->getPos());
        }
        prePos = posBase;
    }

    kmGLPopMatrix();  //结束绘制
}

       怪物坐标对象必须从TowerPosEditorLayer里获取,所以,我们要给TowerPosEditorLayer新增一个函数,代码如下:

Vector<PosBase *> TowerPosEditorLayer::getMonsterPosList() {
    return m_monsterPosList;
}

     效果如下:

    

   (4) 整理代码结构:

      

  14.3 预备知识

      (1) 场景管理器

        SceneManager.h文件:

class SceneManager : public Ref {
public:
    /*场景枚举类*/
    enum EnumSceneType {
        en_TollgateScene,    /*关卡场景*/
        en_TollgateEditorScene,   /*关卡编辑器场景*/
        en_WinScene,      /*胜利场景*/
        en_GameOverScene,     /*游戏结束场景*/
    };
    
    /*获取场景管理器对象*/
    static SceneManager *getInstance();

    /*初始化*/
    virtual bool init();

    /*切换场景*/
    void changeScene(EnumSceneType enScenType);

private:
    /*场景管理器*/
    static SceneManager *mSceneManager;
};

       SceneManager.cpp文件:

SceneManager *SceneManager::mSceneManager = NULL;

SceneManager *SceneManger::getInstance() {
    if(mSceneManager == NULL) {
        mSceneManager = new SceneManager();
        if (mSceneManager && mSceneManager->init()) {
            mSceneManager->autorelease();
            mSceneManager->retain();
        } else {
            CC_SAFE_DELETE(mSceneManager);
            mSceneManager = NULL;
        }
    }

    return mSceneManager;
}

bool SceneManager::init() {
    return true;
}

void SceneManager::changeScene(EnumSceneType enScanType) {
    Scene *pScene = NULL;
    
    switch(enScenType) {
    case en_TollgateScene:   /*关卡场景*/
        break;
    case en_TollgateEditorScene: /*关卡编辑器场景*/
        pScene = TowerPosEditorScene::createScene();
        break;
    case en_WinScene:   /*胜利场景*/
        break;
    case en_GameOverScene:   /*游戏结束场景*/
        break;
    case en_GameOverScene:   /*游戏结束场景*/
        break;
    }

    if (pScene == NULL) {
        return;
    }

    Director *pDirector = Director::getInstance();
    Scene *curScene = pDirector->getRunningScene();
    if (curScene == NULL){
        pDirector->runWithScene(pScene);
    } else {
        pDirector->replaceScene(pScene);
    }
}

    试试场景管理器,修改AppDelegate的applicationDidFinishLaunching函数,代码如下:

bool AppDelegate::applicationDidFinishLaunching() {
    /*省略了很多代码*/

    /*调用场景管理器切换场景*/
    SceneManager::getInstance()->changeScene(SceneManager::en_TollgateEditorScene);

    //auto scene = TowerPosEditorScene::createScene();
    //director->runWithScene(scene);
    return true;
}

     运行项目,我们将看到熟悉的关卡编辑器界面。

    (2) 数据读取模块

      读取Csv文件需要用到三个类:StringUtil、CsvUtil、CsvData。代码结构如下:

     

    (3) 全局参数

      GlobalDefine.h文件:

#ifndef __GLOBAL_DEFINE_H__
#define __GLOBAL_DEFINE_H__

#include "cocos2d.h"

/*创建bool变量,包括get和set方法*/
#define CC_CC_SYNTHESIZE_BOOL(varName, funName) \
protected: bool varName; \
public: bool is##funName(void) const { return varName; }\
public: void set##funName(bool var) { varName = var; }

/*消息派发*/
#define NOTIFY cocos2d::NotificationCenter::getInstance()
#endif

      GlobalPath.h文件:(定义了一些游戏中常用的文件路径)

#ifndef _GlobalPath_H_
#define _GlobalPath_H_

#define PATH_CSV_HERO_CONF "csv/Hero.csv"    //英雄配置文件路径
#define PATH_CSV_MONSTER_CONF "csv/Monster.csv"  //怪物配置文件路径

#define PATH_BULLET_NOR "sprite/bullet/bulletNor.png"   //普通子弹文件路径

/* -----------语言文件 ---------------*/
#define PATH_II8N_PUBLIC "i18n/public.csv"   /*公用语言文件*/

/* ------------字体文件---------------*/
#define PATH_FONT_PUBLIC "Arial"
#endif

     GlobalParam.h文件:(定义了一些游戏中的整型变量)

#ifndef __GLOBAL_PARAM_H__
#define __GLOBAL_PARAM_H__

#include "cocos2d.h"
using namespace cocos2d;

class GlobalParam {
public:
    /*字体*/
    static const int PublicFontSize = 22;
    static const int PublicFontSizeLarge = 35;
    static const int PublicFontSizeLargest = 60;
};
#endif

     (4) I18N工具类

        I18N.h文件:

class I18N : public Ref {
public:
    static I18N *getInstance();
    virtual bool init();

    /*根据Key值获取字符串,返回const char *对象*/
    const char *getcString(EnumStrKey enStrKey);

    /*根据整形的Key值获取字符串,返回const char *对象*/
    const char *getcStringByKey(int iKey);

private:
    /*从配置文件中读取字符串,保存到字典里*/
    void loadStringsFromConf(const char *filePath);

    /*I18N对象*/
    static I18N *m_I18N;

    /*游戏中用到的字符串字典*/
    std::map<int, std::string> mStringMap;
};

       I18N主要有2个函数提供给外部调用:

  •      getcString函数: 根据EnumStrKey的类型返回char字符串;
  •      getcStringByKey: 根据字符串数字ID返回char字符串。

       I18N.cpp文件:

I18N *I18N::m_I18N = NULL;

I18N *I18N::getInstance() {
    if (m_I18N == NULL) {
        m_I18N = new I18N();
        if (m_I18N && m_I18N->init()) {
            m_I18N->autorelease();
            m_I18N->retain();
        } else {
            CC_SAFE_DELETE(m_I18N);
            m_I18N = NULL;
        }
    }
    return m_I18N;
}

bool I18N::init() {
    /*读取配置文件的字符串*/
    loadStringsFromConf("i18n/Public.csv");
    return true;
}

const char *I18N::getcString(EnumStrKey enStrKey) {
    return mStringMap.at(enStrKey).c_str();
}

const char *I18N::getcStringByKey(int iKey) {
    return mStringMap.at(iKey).c_str();
}

void I18N::loadStringsFromConf(const char *filePath) {
    CsvUtil::getInstance()->loadFile(filePath);

    Size size = CsvUtil::getInstance()->getFileRowColNum(filePath);

    int iRowNum = size.width;
    int iColNum = size.height;

    /*如果列数小于2,表示配置文件有问题*/
    if (iColNum < 2) {
        return;
    }

    /*将配置文件的所有字符串存放到字典中(忽略第一行)*/
    for (int i=1; i<iRowNum; i++) {
        int key = CsvUtil::getInstance()->getInt(i, 0, filePath);
        std::string value = CsvUtil::getInstance()->get(i, 1, filePath);

        mStringMap[key] = value;
    }
}

       修改TowerPosEditorScene的scene函数,代码如下:

Scene *TowerPosEditorScene::createScene() {
    auto scene = Scene::create();

    auto editorLayer = TowerPosEditorLayer::create();
    scene->addChild(editorLayer, 1);

    auto operLayer = TowerPosEditorOperateLayer::create(editorLayer);
    scene->addChild(operLayer, 2);

    /*在函数最后加上这句,测试I18N工具类*/
    log("I18N Test: %s", I18N::getInstance()->getcString(en_StrKey_Public_Confirm));
    return scene;
}

     运行项目输出:

I18N Test: 确定

    目前为止项目的目录结构,如下:

    

15.英雄诞生

   15.1 创建关卡场景

     TollgateScene.h文件如下:

#define TAG_MAP_LAYER 1 //关卡地图图层TAG
#define TAG_DATA_LAYER 2 //关卡数据图层TAG

class TollgateScene : public Layer {
public:
    static Scene *createScene();
    virtual bool init();
    CREATE_FUNC(TollgateScene);
};

       TollgateScene.cpp文件:

Scene *TollgateScene::createScene() {
    auto scene = Scene::create();

    TollgateScene *tgLayer = TollgateScene::create();

    TollgateMapLayer *mapLayer = TollgateMapLayer::create();

    scene->addChild(mapLayer, 1, TAG_MAP_LAYER);
    scene->addChild(tgLayer, 3);

    return scene;
}

bool TollgateScene::init() {
    if (!Layer::init()) {
        return false;
    }
    return true;
}

   15.2 地图层

     很明显TollgateMapLayer将会成为关卡的主要层,TollgateMapLayer.h文件:

class TollgateMapLayer : public Layer {
public:
    TollgateMapLayer();
    ~TollgateMapLayer();

    CREATE_FUNC(TollgateMapLayer);
    virtual bool init();
private:
    /*当前关卡*/
    int m_iCurLevel;

    /*读取关卡配置*/
    void loadConfig();
};

    TollgateMapLayer.cpp文件:

TollgateMapLayer::TollgateMapLayer() {
    m_iCurLevel = 1;
}

TollgateMapLayer::~TollgateMapLayer() {
}

bool TollgateMapLayer::init() {
    bool bRet = false;
    
    do {
        /*读取关卡配置*/
        loadConfig();

        bRet = true;
    } while (0);

    return true;
}

void TollgateMapLayer::loadConfig() {
    Size visibleSize = Director::getInstance()->getVisibleSize();

    /*添加背景音乐*/
    CocosDenshion::SimpleAudioEngine::getInstance()->playBackgroundMusic(
        StringUtils::format("music/tollgate_%d.mp3", m_iCurLevel).c_str(), true);

    /*添加地图背景*/
    std::string sBG = StringUtils::format("tollgate/level_%d.jpg", m_iCurLevel);
    Sprite *map = Sprite::create(sBG.c_str());
    map->setPosition(Point(visibleSize.width / 2, visibleSize.height / 2));
    this->addChild(map, 1);
}

      修改SceneManager的changeScene函数,代码如下:

void SceneManager::changeScene( EnumSceneType enScenType) {
    Scene *pScene = NULL;
    switch(enScenType) {
    case en_TollgateScene: /*关卡场景*/
        pScene = TollgateScene::createScene();
        break;
    case en_TollgateEditorScene:  /*关卡编辑器场景*/
        pScene = TowerPosEditorScene::createScene();
        break;
    case en_WinScene:   /*胜利场景*/
        break;
    case en_GameOverScene:    /*游戏结束场景*/
        break;
    
    /*这里省略了很多代码*/
}

     修改AppDelegate的applicationDidFinishLaunching函数,让默认启动场景变成TollgateScene,代码如下:

bool AppDelegate::applicationDidFinishLaunching() {
    /*这里省略了很多代码*/

    /*调用场景管理器切换场景*/
    SceneManager::getInstance()->changeScene(SceneManager::en_TollgateScene);
    return true;
}

     运行项目,效果如下,证明新增场景成功了.

       

   15.3 实体基类

      在我们创建英雄和怪物之前,必须要有一个实体基类,Entity.h文件:

class Entity : public Node {
public:
    Entity();
    ~Entity();

    void bindSprite(Sprite *sprite);
    Sprite *getSprite();

    void hurtMe(int iHurtValue);   /*被攻击*/
    bool isDead();                 /*是否死亡*/
protected:
    virtual void onDead();         /*实体死亡时调用*/
    virtual void onBindSprite();   /*绑定精灵对象时调用*/
    virtual void onHurt(int iHurtValue);   /*受伤害时调用*/
private:
    Sprite *m_sprite;

    CC_SYNTHESIZE(int, m_ID, ID);    //实体ID
    CC_SYNTHESIZE(int, m_iModelID, iModelID);    //模型ID(资源ID)
    CC_SYNTHESIZE(std::string, m_sName, sName);   //名字    
    CC_SYNTHESIZE(int, m_iHP, iHP);    //HP
    CC_SYNTHESIZE(int, m_iDefense, iDefense);   //防御
    CC_SYNTHESIZE(int, m_iSpeed, iSpeed);    //移动速度
    CC_SYNTHESIZE(int, m_iLevel, iLevel);   //等级
    bool m_isDead;    //标记是否死亡
};

    Entity.cpp文件:

Entity::Entity() {
    m_sprite = NULL;
    m_sName = "";
    m_iHP = 1;
    m_iDefence = 1;
    m_isDead = false;
    m_iSpeed = 1;
    m_iLevel =1;
}

Entity::~Entity() {
}

void Entity::bindSprite(Sprite *sprite) {
    if (this->m_sprite != NULL) {
        m_sprite->removeFromParentAndCleanup(true);
    }
    
    this->m_sprite = sprite;
    this->addChild(m_sprite);

    Size size = m_sprite->getContentSize();
    this->setContentSize(size);

    onBindSprite();
}

Sprite *Entity::getSprite() {
    return this->m_sprite;
}

void Entity::hurtMe(int iHurtValue) {
    if (m_isDead) {
        return;
    }

    /*最小伤害值为1*/
    if (iHurtValue <= getiDefence()) {
        iHurtValue = 1;
    }

    int iCurHP = getiHP();   /*当前HP*/    
    int iAfterHP = iCurHP - iHurtValue;   /*被攻击后的HP*/

    onHurt(iHurtValue);

    if (iAfterHP > 0) {
        setiHp(iAfterHP);
    } else {
        m_isDead = true;
        /*死亡*/
        onDead();
    }
}

bool Entity::isDead() {
    return this->m_isDead;
}

void Entity::onDead() {
}

void Entity::onBindSprite() {
}

void Entity::onHurt(int iHurtValue) {
}

     15.4 英雄管理器1---炮台对象

       在关卡中添加英雄的逻辑如下:

  •         根据炮台坐标配置文件创建所有炮台对象;
  •         单击炮台后创建英雄,前提是被单击的炮台是空炮台(没有放置英雄)。

         HeroManager.h文件:

#define TOWER_POS_LAYER_LVL 5    //塔坐标的层次
#define TOWER_BORDER_LAYER_LVL 8  //炮台的层次
#define TOWER_LAYER_LVL 10   //塔的层次
class HeroManager : public Node {
public:
    HeroManager();
    ~HeroManager();
    static HeroManager *createWithLevel(int iCurLevel);
    bool initWithLevel(int iCurLevel);
private:
    Vector<PosBase *> m_towerPosList;   /*存放所有塔的坐标对象*/
    Vector<TowerBorder *> m_towerBorderList;  /*存放所有炮台对象*/
    void createTowerBorder(int iCurLevel);   /*创建炮台*/
    void createTowerPos(Point pos);     /*给定坐标,生成塔坐标对象*/
};

      我们要从关卡编辑器生成的配置文件里读取炮台坐标对象(TowerPos),读取了所有的炮台坐标对象后,根据这些坐标生成炮台。

     HeroManager.cpp文件:

HeroManager::HeroManager() {
}
HeroManager::~HeroManager() {
}
HeroManager *HeroManager::createWithLevel(int iCurLevel) {
    HeroManager *heroMgr = new HeroManager();
    if (heroMgr && heroMgr->initWithLevel(iCurLevel)) {
        heroMgr->autorelease();
    } else {
        CC_SAFE_DELETE(heroMgr);
    }
    return heroMgr;
}

bool HeroManager::initWithLevel(int iCurLevel) {
    /*加载塔坐标对象*/
    std::string sTowerPosPath = StringUtils::format("tollgate/towerPos_level_%d.plist", iCurLevel);

    Vector<PosBase *> posList = PosLoadUtil::getInstance()->loadPosWithFile(
            sTowerPosPath.c_str(), enTowerPos, this, 10, false);
    m_towerPosList.pushBack(posList);

    /*创建炮台*/
    createTowerBorder(iCurLevel);

    return true;
}

void HeroManager::createTowerBorder(int iCurLevel) {
    for (auto tPos : m_towerPosList) {
        TowerBorder *border = TowerBorder::create();
        border->upgrade();
        border->upgrade();
        border->setPosition(tPos->getPos());

        this->addChild(border);
        m_towerBorderList.pushBack(border);
    }
}

void HeroManager::createTowerPos(Point pos) {
    TowerPos *tPos = TowerPos::create(pos, true);
    this->addChild(tPos, TOWER_POS_LAYER_LVL);
    m_towerPosList.pushBack(true);
}

            

      TowerBorder是一个很简单的类:

       TowerBorder.h文件:

class TowerBorder : public Entity {
public:
    TowerBorder();
    ~TowerBorder();
    
    CREATE_FUNC(TowerBorder);
    virtual bool init();

    void upgrade();   /*升级*/
};

      TowerBorder.cpp文件:

TowerBorder::TowerBorder() {
    m_iLevel = 1;
}

TowerBorder::~TowerBorder() {
}

bool TowerBorder::init() {
    return true;
}

void TowerBorder::upgrade() {
    if (getSprite() != NULL) {
        getSprite()->stopAllActions();
    }

    std::string sFilePath = StringUtils::format("sprite/hero/border_%d.png", m_iLevel);
    Sprite *sprite = Sprite::create(sFilePath.c_str());

    bindSprite(sprite);

    m_iLevel++;

    if (m_iLevel == 2) {
        auto rotateBy = RotateBy:create(25.0f, 360, 360);
        auto repeat = RepeatForever::create(rotateBy);

        sFilePath = StringUtils::format("sprite/hero/magic_border_%d.png", m_iLevel);
        sprite = Sprite::create(sFilePath.c_str());
        sprite->setOpacity(80);
        sprite->runAction(repeat);
        this->addChild(sprite, 10);
    }
}

          TowerBorder利用Action来实现动画效果。

       修改一下TollgateMapLayer的init函数,让英雄管理器生效,代码如下:

//在TollgateMapLayer.cpp里加上下面的宏定义:
#define HOME_LAYER_LVL 3    //堡垒的层次
#define HERO_LAYER_LVL 10    //英雄的层次
#define MONSTER_LAYER_LVL 15    //怪物的层次
#define BULLET_LAYER_LVL 20    //子弹的层次

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

    /*读取关卡配置*/
    loadConfig();

    /*创建英雄管理器*/
    m_heroMgr = HeroManager::createWithLevel(m_iCurLevel);
    this->addChild(m_heroMgr, HERO_LAYER_LVL);

    return true;
}

      运行查看效果:

    

  15.5 英雄管理器2--英雄对象

      Hero.h文件:

class Hero : public Entity {
public:
    Hero();
    ~Hero();

    static Hero* create(Sprite *sprite);
    bool init(Sprite *sprite);

    /*给定英雄ID,从配置文件中读取英雄数据*/
    static Hero *createFromCsvFileByID(int iHeroID);
    bool initFromCsvFileByID(int iHeroID);

    CC_SYNTHESIZE(int, m_iBaseAtk, iBaseAtk);    //基础攻击力
    CC_SYNTHESIZE(int, m_iCurAtk, iCurAtk);   //当前攻击力
    CC_SYNTHESIZE(int, m_iAtkSpeed, iAtkSpeed);    //攻击间隔(单位:毫秒)
    CC_SYNTHESIZE(int, m_iAtkRange, iAtkRange);    //攻击范围(半径)
    CC_SYNTHESIZE(int, m_iUpgradeCostBase, iUpgradeCostBase);  //升级消耗基础值
    CC_SYNTHESIZE(float, m_fUpgradeAtkBase, fUpgradeAtkBase); //升级攻击加成系数

    /*升级英雄*/
    void upgrade();
};

     Hero.cpp文件:

Hero::Hero() {
}

Hero::~Hero() {
}

Hero *Hero::create(Sprite *sprite) {
    Hero *hero = new Hero();

    if (hero && hero->init(sprite)) {
        hero->autorelease();
    } else {
        CC_SAFE_DELETE(hero);
    }
    return hero;
}

bool Hero::init(Sprite *sprite) {
    bool bRet = false;
    do {
        CC_BREAK_IF(!sprite);
        bindSprite(sprite);
        bRet = true;
    } while(0);

    return bRet;
}

Hero *Hero::createFromCsvFileByID(int iHeroID) 
{    
    Hero *hero = new Hero();
    if (hero && hero->initFromCsvFileByID(iHeroID)) {
        hero->autorelease();
    } else {
        CC_SAFE_DELETE(hero);
    }
    return hero;
}

bool Hero::initFromCsvFileByID(int iHeroID) {
    bool bRet = false;
    
    do {
        CsvUtil *csvUtil = CsvUtil::getInstance();
        
        std::string sHeroID = StringUtils::toString(iHeroID);

        /*寻找ID所在的行*/
        int iLine = csvUtil->findValueInWithLine(sHeroID.c_str(), enHeroPropConf_ID, PATH_CSV_HERO_CONF);

        CC_BREAK_IF(iLine < 0);
        
        setID(iHeroID);
        setiModelID(csvUtil->getInt(iLine, enHeroPropConf_BaseAtk, PATH_CSV_HERO_CONF));

        setiBaseAtk(csvUtil->getInt(iLine, enHeroPropConf_BaseAtk, PATH_CSV_HERO_CONF));
        setiCurAtk(getiBaseAtk());
        setiAtkSpeed(csvUtil->getInt(iLine, enHeroPropConf_AtkSpeed, PATH_CSV_HERO_CONF));
        setiAtkRange(csvUtil->getInt(iLine, enHeroPropConf_AtkRange, PATH_CSV_HERO_CONF));
        setiUpgradeCostBase(csvUtil->getInt(iLine, enHeroPropConf_UpgradeCostBase, PATH_CSV_HERO_CONF));
        setfUpgradeAtkBase(csvUtil->getFloat(iLine, enHeroPropConf_UpgradeAtkBase, PATH_CSV_HERO_CONF));
        Sprite *sprite = Sprite::create(StringUtils::format("sprite/hero/hero_%d.png", iHeroID).c_str());
        CC_BREAK_IF(!sprite);
        Cc_BREAK_IF(!init(sprite));
        bRet = true;
    } while(0);

    return bRet;
}

     设置英雄的速度属性,代码所示:

      setiAtkSpeed(csvUtil->getInt(iLine, enHeroPropConf_AtkSpeed, PATH_CSV_HERO_CONF));

     enHeroPropConf_AtkSpeed是枚举值,包含在EnumHeroPropConfType.h文件里,代码如下:

enum EnumHeroPropConfType {
    enHeroPropConf_ID,                //英雄ID
    enHeroPropConf_Name,              //英雄名字
    enHeroPropConf_Type,              //英雄类型
    enHeroPropConf_ModelID,           //英雄模型ID
    enHeroPropConf_BaseAtk,           //基础攻击力
    enHeroPropConf_AtkSpeed,          //攻击间隔(单位:秒)
    enHeroPropConf_AtkRange,          //攻击范围(半径)
    enHeroPropConf_UpgradeAtkBase,    //升级攻击加成系数
    enHeroPropConf_UpgradeCostBase,   //升级消耗基础值
};

     英雄属性的配置文件我们放在resources的csv目录下,命名为Hero.csv,如下图:

     

15.6 英雄管理器3--炮台和英雄的关系

   我们给TowerBorder新增一个成员变量和一些函数,代码如下:

public:
    bool isClickMe(Point pos);    /*判断坐标是否进入范围*/
    void bindHero(Hero *hero);    /*绑定英雄对象*/
    Hero *getHero();              /*获取英雄对象*/
    void deleteHero();            /*删除英雄对象*/
private:
    Hero *m_hero;                 /*绑定的塔*/

    TowerBorder赋予了绑定、删除、获取英雄对象的功能,并且新增了一个isClickMe函数用于判断是否单击了炮台对象。

    代码如下:

TowerBorder::TowerBorder() {
    m_iLevel = 1;
    m_hero = NULL;
}

bool TowerBorder::isClickMe(Point pos) {
    Size size = getSprite()->getContentSize();
    Point borderPos = getPosition();

    Point srcPos = Point(borderPos.x - size.width/2, borderPos.y + size.height) / 2;
    Point destPos = Point(borderPos.x + size.width/2, borderPos.y - size.height/2);

    if (pos.x >= srcPos.x && pos.x <= destPos.x && pos.y <= srcPos.y && pos.y >= destPos.y) {
        return true;
    }

    return false;
}

void TowerBorder::bindHero(Hero *hero) {
    m_hero = hero;
}

Hero *TowerBorder::getHero() {
    return m_hero;
}

void TowerBorder::deleteHero() {
    if (m_hero != NULL) {
        m_hero->removeFromParent();
    }
}

15.7 英雄管理器4--加入创建英雄的功能

     为HeroManager新增一个函数,代码如下:

private:
    /*找到被单击的炮台对象*/
    TowerBorder *findChickTowerBorder(Point pos);
};

 

TowerBorder *HeroManager::findClickTowerBorder(Point pos) {
    for (auto tBorder : m_towerBorderList) {
        if (tBorder->isClickMe(pos)) {
            return tBorder;
        }
    }
    return NULL;
}

bool HeroManager::initWithLevel(int iCurLevel) {
    /*加载塔坐标对象(之前写的代码,省略了)*/
    /*创建炮台(之前写的代码,省略了)*/

    /*添加触摸监听*/
    auto listener = EventListenerTouchOneByOne::create();

    listener->onTouchBegan = [](Touch *touch, Event *event) {
        return true;
    };
    listener->onTouchEnded = [&](Touch *touch, Event *event) {
        Point pos = Director::getInstance()->convertToGL(touch->getLocationInView());

        /*获取被单击的塔坐标*/
        TowerBorder *clickBorder = findClickTowerBorder(pos);

        if (clickBorder == NULL) {
            return;
        }

        /*当前塔坐标没有英雄对象,则添加英雄*/
        if (clickBorder->getHero() == NULL) {
            Hero *hero = Hero::createFromCsvFileByID(1);
            hero->setPosition(clickBorder->getPosition());
            this->addChild(hero, TOWER_LAYER_LVL);
            
            /*绑定英雄对象到炮台*/
            clickBorder->bindHero(hero);
        }
    };
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
    return true;
}

    

     onTouchEnded函数逻辑如下:

  •       获取当前被单击的炮台对象;
  •       判断炮台对象是否已经绑定了英雄;
  •       如果炮台对象没有绑定英雄,则创建一个新的英雄,添加到英雄管理器里,并且绑定到炮台对象。目前英雄都只读取ID为的配置。

    运行游戏,在炮台上单击一下,效果如下:

     

    15.8 怪物管理器

       MonsterManager.h文件:

class MonsterPos;
class Monster;
class PosBase;
class MonsterManager : public Node {
public:
    MonsterManager();
    ~MonsterManager();
    static MonsterManager *createWithLevel(int iCurLevel);
    bool initWithLevel(int iCurLevel);

    void createMonsters(int iCurLevel);    /*读取配置文件创建怪物*/
    int getNotShowMonsterCount();        /*获取还没有出场的怪物数量*/
    MonsterPos *getMonsterStartPos();    /*获取怪物出场坐标*/
    MonsterPos *getMonsterEndPos();      /*获取怪物终点坐标*/
    Vector<Monster *> getMonsterList();   /*获取怪物列表*/
private:
    Vector<Monster *> m_monsterList;    /*怪物列表*/
    Vector<Monster *> m_notShowMonsterList;   /*未出场的怪物列表*/
    Vector<MonsterPos *> m_monsterPosList;   /*存放所有怪物的坐标对象*/
    float m_fShowTimeCount;     /*用于计算怪物出场时间*/
    void showMonster(float dt);   /*检查是否有新怪物出场*/
};

      看一下MonsterManager比较简单的一些函数,代码如下:

MonsterManager::MonsterManager() {
    m_fShowTimeCount = 0;
}

MonsterManager::~MonsterManager() {
}

MonsterManager *MonsterManager::createWithLevel(int iCurLevel) {
    MonsterManager *monsterMsg = new MonsterManager();

    if (monsterMgr && monsterMgr->initWithLevel(iCurLevel)) {
        monsterMgr->autorelease();
    } else {
        CC_SAFE_DELETE(monsterMgr);
    }

    return monsterMgr;
}

bool MonsterManager::initWithLevel(int iCurLevel) {
    /*创建怪物*/
    createMonsters(iCurLevel);

    return true;
}

int MonsterManager::getNotShowMonsterCount() {
    return m_notShowMonsterList.size();
}

Vector<Monster *> MonsterManager::getMonsterList() {
    return m_monsterList;
}

MonsterPos *MonsterManager::getMonsterStartPos() {
    return (MonsterPos *) m_monsterPosList.at(0);
}

MonsterPos *MonsterManager::getMonsterEndPos() {
    return (MonsterPos *)m_monsterPosList.at(m_monsterPosList.size() - 1);
}

     在initWithLevel函数里调用了一个createMonsters函数,代码如下:

void MonsterManager::createMonsters(int iCurLevel) {
    /*加载怪物坐标对象*/
    std::string sMonsterPosPath = StringUtils::format("tollgate/monsterPos_level_%d.plist", iCurLevel);
    auto posList = PosLoadUtil::getInstance()->loadPosWithFile(sMonsterPosPath.c_str(), enMonsterPos, this, 10, false);
    m_monsterPosList.pushBack(posList);
    

    /*读取配置文件*/
    std::string sMonsterConfPath = StringUtils::format("tollgate/monster_level_%d.plist", iCurLevel);

    ValueMap fileDataMap = FileUtils::getInstance()->getValueMapFromFile(sMonsterConfPath.c_str());

    int size = fileDataMap.size();
    for (int i=1; i <= size; i++) {
        Value value = fileDataMap.at(StringUtils::toString(i));
        ValueMap data = value.asValueMap();

        /*从怪物出场配置文件读取怪物ID和出场时间,保存所有怪物到未出场列表*/
        int id = data["id"].asInt();
        float fShowTime = data["showTime"].asFloat();

        auto monster = Monster::createFromCsvFileByID(id);
        monster->setfShowTime(fShowTime);
        monster->setVisible(false);

        /*设置怪物精灵*/
        std::string spPath = StringUtils::format("sprite/monster/monster_%d.png", monster->getiModelID());
        monster->bindSprite(Sprite::create(spPath.c_str()));

        /*保存怪物对象到怪物列表*/
        m_monsterList.pushBack(monster);

        /*保存怪物对象到未出场怪物列表*/
        m_notShowMonsterList.pushBack(monster);

        this->addChild(monster);
    }
    
    /*开始检查是否有新怪物出场*/
    this->schedule(schedule_selector(MonsterManager::showMonster));
}


   

       函数的逻辑如下:

       (1) 读取怪物坐标配置文件,保存到列表里,当怪物出场时需要这些坐标列表,怪物将按照坐标列表的位置行走。

       (2) 读取怪物配置文件,和之前的炮台坐标配置文件、怪物坐标配置文件是一样的规则,通过FileUtils的getValueMapFromFile就可以加载,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple/DTDs/PropertyList-1.0.dtd"/>
    
<plist version="1.0">
    <dict>
        <key>1</key>
        <dict>
            <key>id</key>
            <integer>1001</integer>
            <key>showTime</key>
            <integer>3</integer>
        </dict>
        <key>2</key>
        <dict>
            <key>id</key>
            <integer>1004</integer>
            <key>showTime</key>
            <integer>5</integer>
        </dict>
    </dict>
</plist>

      (3) 在怪物配置文件里配置怪物的ID和出场时间(单位是秒),根据怪物ID创建Monster对象,为Monster对象的出场时间属性赋值。

      (4) 所有创建的Monster对象都添加到怪物列表和未出场怪物列表,未出场怪物列表里记录了所有还未出现的怪物。

      (5) 如果某个怪物到达了出场时间就让怪物出现并开始按照怪物坐标列表行走,同时未出场怪物列表汇总删除该怪物。

      (6) 利用schedule每一帧都调用showMonster函数,该函数用来控制怪物是否出场。

      showMonster函数,代码如下:

void MonsterManager::showMonster(float dt) {
int iNotShowMonsterCount = m_notShowMonsterList.size();

    if (iNotShowMonsterCount > 0) {
        m_fShowTimeCount += dt;
    }
    /*获取怪物的第一个坐标点*/
    auto monsterFirstPos = getMonsterStartPos();
    
    Vector<Monster *> monsterWantToDelete;
    for (auto monster : m_notShowMonsterList) {
        /*时间达到怪物的出场时间,让怪物出场*/
        if (m_fShowTimeCount >= monster->getfShowTime()) {
            /*添加怪物到删除列表,出场后的怪物要从未出场列表中删除*/
            monsterWantToDelete.pushBack(monster);

            monster->setPosition(monsterFirstPos->getPos());
            monster->setVisible(true);
        
            /*让怪物按照指定坐标行走*/    
            monster->moveByPosList(m_monsterPosList);
        }
    }

    /*删除已经出场的怪物*/
    for (auto monster : monsterWantToDelete) {
        m_notShowMonsterList.eraseObject(monster);
    }
}

    

          然后就是遍历未出场怪物列表,判断m_fShowTimeCount是否达到或超过了某个怪物的出场时间,如果是,则让怪物出场。

   15.9 移动控制器1---控制器基类

          创建一个控制器基类,命名为ControllerBase,代码如下:

class ControllerBase : public Node {
};

   15.10 移动控制器2---移动控制器基类

       ControllerMoveBase基类,头文件如下:

/*检查移动的间隔时间,时间越短,移动越短,也越平滑,时间太长,移动会卡顿*/
#define CHECK_MOVE_SPEED_LVL1 0.1f
#define CHECK_MOVE_SPEED_LVL2 0.04f
#define CHECK_MOVE_SPEED_LVL3 0.03f

#define SPEED 1

class ControllerMoveBase : public ControllerBase {
public:
    ControllerMoveBase();
    ~ControllerMoveBase();

    CC_SYNTHESIZE(int, m_iSpeed, iSpeed); //移动速度
protected:
    Entity *m_entity;   //实体对象
    bool m_isMoving;    //是否正在移动
    bool m_isXLeft;    //标记X方向是否往左移
    bool m_isYUp;     //标记Y方向是否往上移
    int m_iCheckMoveSpeed;   //检查移动的间隔时间,时间越短,移动越快。

    /*给定当前坐标和目标坐标,计算出下一次要设置的坐标*/
    Point getNextPos(Point curPos, Point destPos);
};

         看看ControllMoveBase.cpp文件,代码如下:

ControllerMoveBase::ControllerMoveBase() {
    m_isMoving = false;
    m_isXLeft = false;
    m_isYUp = false;
    m_iSpeed = SPEED;
    m_iCheckMoveSpeed = CHECK_MOVE_SPEED_LVL2;
    m_entity = NULL;
}

ControllerMoveBase::~ControllerMoveBase() {
}

Point ControllerMoveBase::getNextPos(Point curPos, Point destPos) {
    /*判断移动方向*/
    if (curPos.x > destPos.x) {
        m_isXLeft = true;
    }
    else {
        m_isXLeft = false;
    }
    if (curPos.y < destPos.y) {
        m_isYUp = true;
    }
    else {
        m_isYUp = false;
    }

    /*根据移动方向和速度值改变当前坐标*/
    if (curPos.x < destPos.x && m_isXLeft == false) {
        curPos.x += m_iSpeed;
        if (curPos.x > destPos.x) {
            curPos.x = destPos.x;
        }
    } 
    else if (curPos.x > destPos.x && m_isXLeft == true) {
        curPos.x -= m_iSpeed;
        if (curPos.x < destPos.x) {
            curPos.x = destPos.x;
        }
    }

    if (curPos.y < destPos.y && m_isYUp == true) {
        curPos.y += m_iSpeed;
        if (curPos.y > destPos.y) {
            curPos.y = destPos.y;
        }
    }
    else if (curPos.y > destPos.y && m_isYUp == false) {
        curPos.y -= m_iSpeed;
        if (curPos.y < destPos.y) {
            curPos.y = destPos.y;
        }
    }
    return curPos;
}


        

     getNextPos函数根据将要移动的方向设置下一步的坐标。

   15.11 移动控制器3---按指定坐标列表移动

     ControllerSimpleMove头文件,代码如下:

class ControllerSimpleMove : public ControllerMoveBase {
public:
    ControllerSimpleMove();
    ~ControllerSimpleMove();

    static ControllerSimpleMove *create(Entity *entity);
    bool init(Entity *entity);

    void checkMoveUpdate(float delta);

    /*按照给定的坐标点移动*/
    void moveByPosList(Vector<PosBase *> posList);

    /*按照给定的坐标点和移动速度*/
    void moveByPosList(Vector<PosBase *> posList, int iSpeed);

    /*根据给定的坐标点、速度、移动间隔时间移动*/
    void moveByPosList(Vector<PosBase *> posList, int iSpeed, int iSpanTime);

    /*根据给定坐标移动*/
    void moveByPos(PosBase *pos);
private:
    void moveOneStep();  /*移动一步*/
    void nextMovePos();  /*设置下一个移动目的点*/
private:
    Vector<PosBase *> m_movePosList;   //移动目的列表
    PosBase *m_curDestPos;          //当前移动目的地
    float m_MoveSpan;               //移动间隔时间
    float m_fMoveTimeCount;         //计时器
};

       看看nextMovePos函数代码如下:

void ControllerSimpleMove::nextMovePos() {
    if (m_movePosList.size() < 1) {
        return;
    }

    m_curDestPos = m_movePosList.at(0);

    m_movePosList.erase(0);
}

       nextMovePos函数的逻辑如下:

  •     判断移动列表中是否已经没有了坐标对象;
  •     取得移动列表中的第一个坐标对象;
  •     删除坐标列表中已经取出来的对象。

       moveByPosLis函数,代码如下:

void ControllerSimpleMove::moveByPosList(Vector<PosBase *> posList) {
    if (posList.size() < 1) {
        return;
    }

    this->m_movePosList.clear();
    this->m_movePosList.pushBack(posList);

    nextMovePos();
    this->m_isMoving = true;
}

       看剩余的两个moveByPosList函数,代码如下:

void ControllerSimpleMove::moveByPosList(Vector<PosBase *> posList, int iSpeed) {
    this->m_iSpeed = iSpeed;
    moveByPosList(posList);
}

void ControllerSimpleMove::moveByPosList(Vector<PosBase *>posList, int iSpeed, int iSpanTime) {
    m_MoveSpan = iSpanTime;
    moveByPosList(posList, iSpeed);
}

      再看看moveByPos函数,代码如下:

void ControllerSimpleMove::moveByPos(PosBase *pos) {
    if (m_isMoving == false && pos != NULL) {
        Vector<PosBase *> posList;
        posList.pushBack(pos);
        moveByPosList(posList);
    }
}

    15.12 怪物来了

        先看头文件,代码如下:

class Monster : public Entity {
public:
    Monster();
    ~Monster();
    CREATE_FUNC(Monster);
    virtual bool init();

    /*给定怪物ID,从配置文件中读取怪物数据*/
    static Monster *createFromCsvFileByID(int iMonsterID);
    bool initFromCsvFileByID(int iMonsterID);
public:
    /*按照给定的坐标点移动*/
    void moveByPosList(Vector<PosBase *> posList;
private:
    ControllerSimpleMove *m_moveController;      //移动控制器
    CC_SYNTHESIZE(int, m_iLevel, iLevel);        //等级
    CC_SYNTHESIZE(float, m_fShowTime, fShowTime);  //出场间隔:秒
};

       Monster.cpp文件,代码如下:

Monster::Monster() {
    m_moveController = NULL;
    m_iSpeed = MONSTER_SPEED_INIT;
}

Monster::~Monster() {
}

bool Monster::init() {
    m_moveController = ControllerSimpleMove::create(this);
    this->addChild(m_moveController);
    return true;
}

Monster *Monster::createFromCsvFileByID(int iMonsterID)) {
    Monster *monster = new Monster();

    if (monster && monster->initFromCsvFileByID(iMonsterID)) {
        monster->autorelease();
    }
    else {
        CC_SAFE_DELETE(monster);
    }
    return monster;
}

bool Monster::initFromCsvFileByID(int iMonsterID) {
    bool bRet = false;

    do {
        CsvUtil *csvUtil = CsvUtil::getInstance();

        std::string sMonsterID = StringUtils::format("%d", iMonsterID);
        
        /*寻找ID所在的行*/
        int iLine = csvUtil->findValueInWithLine(sMonsterID.c_str(), enMonsterPropConf_ID, PATH_CSV_MONSTER_CONF);

        CC_BREAK_IF(iLine < 0);
        
        setID(iMonsterID);
        setiLevel(csvUtil->getInt(iLine, enMonsterPropConf_Level, PATH_CSV_MONSTER_CONF));
        setiModelID(csvUtil->getInt(iLine, enMonsterPropConf_ModelID, PATH_CSV_MONSTER_CONF));
        setiDefense(csvUtil->getInt(iLine, enMonsterPropConf_Defense, PATH_CSV_MONSTER_CONF));
        setiHP(csvUtil->getInt(iLine, enMonsterPropConf_Hp, PATH_CSV_MONSTER_CONF));
        setiSpeed(csvUtil->getInt(iLine, enMonsterPropConf_Speed, PATH_CSV_MONSTER_CONF));

        CC_BREAK_IF(!init());

        bRet = true;
    } while(0);

    return bRet;
}

void Monster::moveByPosList(Vector<PosBase *> posList) {
    if (posList.size() < 1) {
        return;
    }
    m_moveController->moveByPosList(posList, 2, getiSpeed());
}


    }
    

     

    新增一个怪物属性的枚举类,EnumMonsterPropConfType.h,代码如下:

enum EnumMonsterPropConfType {
    enMOnsterPropConf_ID,       //怪物ID
    enMonsterPropConf_Name,     //怪物名字
    enMonsterPropConf_Level,    //怪物等级
    enMonsterPropConf_Type,     //怪物类型
    enMonsterPropConf_ModelID,  //怪物模型ID
    enMonsterPropConf_Defence,   //防御力
    enMonsterPropConf_Hp,        //血量
    enMonsterPropConf_Speed,     //移动速度
};

 

//TollgateMapLayer.h里新增成员变量:
/*怪物管理器*/
MonsterManager *m_monsterMgr;

//TollgateMapLayer的init函数中添加以下代码:
/*创建怪物管理器*/
m_monsterMgr = MonsterManager::createWithLevel(m_iCurLevel);
this->addChild(m_monsterMgr, MONSTER_LAYER_LVL);

      运行游戏,效果如下:

   

    15.13 英雄攻击

       我们为Hero新增一个函数:checkAtkMonster,代码如下:

void Hero::checkAtkMonster(float dt, Vector<Monster *> monsterList) {
    if (m_atkMonster != NULL) {
        /*怪物已死亡*/
        if (m_atkMonster->isDead()) {
            /*从怪物列表中删除怪物*/
            monsterList.eraseObject(m_atkMonster);

            /*清除锁定的怪物引用*/
            m_atkMonster = NULL;
            return;
        }

        /*攻击冷却结束,可以攻击*/
        if (m_isAtkCoolDown == false) {
            atk();    
        }

        /*判断怪物是否离开攻击范围*/
        checkAimIsOutOfRange(monsterList);
    } else {
        /*选择一个进入攻击范围的怪物*/
        chooseAim(monsterList);
    }
}

         checkAtkMonster函数的逻辑如下:

        (1) 首先想办法获取关卡地图层(TollgateMapLayer)里的所有出场怪物。

        (2) m_atkMonster记录了英雄当前锁定的攻击目标;

        (3) 如果攻击目标为空,则调用chooseAim函数,从怪物列表汇总查找进入了攻击范围的英雄,设为攻击目标。

        (4) 如果攻击目标不为空,判断目标是否死亡,如果目标已死亡,则从怪物列表中删除这个怪物,并且将英雄攻击目标置为空,本次攻击结束。

        (5) 如果攻击目标不为空,且英雄的攻击冷却结束,则调用atk函数进行攻击。

        (6) 判断攻击目标是否离开了英雄的攻击范围,调用checkAimIsOutOfRange函数进行判断,并且进行一些处理。

          我们至少完成以上6件事情,英雄才能攻击怪物。

   15.14 在Hero里获取怪物列表

       给TollgateMapLayer添加一个update函数,代码如下:

bool TollgateMapLayer::init() {
    /*这里省略了很多代码*/

    this->schedule(schedule_selector(TollgateMapLayer::logic));
    return true;
}

void TollgateMapLayer::logic(float dt) {
    m_heroMgr->logic(dt, m_monsterMgr->getMonsterList());
}

         给HeroManager也添加一个logic函数,代码如下:

void HeroManager::logic(float dt, Vector<Monster *> monsterList) {
    for (auto tBorder : m_towerBorderList) {
        if (tBorder->getHero() != NULL) {
            tBorder->getHero()->checkAtkMonster(dt, monsterList);
        }
    }
}

       经过TollgateMapLayer调用HeroManager的logic函数,再有HeroManager的logic函数调用Hero的checkAtkMonster,这就把怪物列表传递给了Hero。

       整个场景只调用一次schedule(schedule_selector(Layer::logic))操作,然后通过一个logic函数去调用其他Layer的logic函数,而不是每个Layer都分别注册一次schedule。

   15.15 查找并锁定攻击目标

      为Hero新增一个函数:chooseAim,该函数用于查找并锁定攻击目标,代码如下:

void Hero::chooseAim(Vector<Monster *> monsterList) {
    for (auto monster : monsterList) {
        if (monster->isVisible() && isInAtkRange(monster->getPosition())) {
            chooseAtkMonster(monster);
            log("InAtkRange!!!");
            break;
        }
    }
}

       chooseAim函数会遍历怪物列表,判断每一个怪物是否进入了英雄的攻击范围(isInAtkRange函数),如果是,则锁定攻击目标(chooseAtkMonster)。代码如下:

bool Hero::isInAtkRange(Point pos) {
    int iDoubleAtkRange = getiAtkRange();   //攻击范围
    Point heroPos = getPosition();

    float length = pos.getDistanceSq(heroPos);
    if (length <= iDoubleAtkRange * iDoubleAtkRange) {
        return true;
    }
    return false;
}

void Hero::chooseAtkMonster(Monster *monster) {
    m_atkMonster = monster;
}

       然后,我们要用一个m_atkMonster记录攻击目标,所以要在Hero.h文件新增一个成员变量,代码如下:

/*攻击是否冷却*/
bool m_isAtkCoolDown;

/*当前锁定的怪物*/
Monster *m_atkMonster;

/*检测并选择进入攻击范围的怪物,或者攻击已进入范围的怪物*/
void checkAtkMonster(float ft);

/*判断坐标是否在攻击范围内*/
bool isInAtkRange(Point pos);

/*锁定要攻击的怪物*/
void chooseAtkMonster(Monster *monster);

    15.16 英雄的攻击

      完善Hero的checkAtkMonster函数,代码如下:

void Hero::checkAtkMonster(float ft, Vector<Monster *> monsterList) {
    if (m_atkMonster != NULL) {
        /*怪物死亡*/
        if (m_atkMonster->isDead()) {
            /*从怪物列表中删除怪物(省略代码)*/
            /*清除锁定的怪物引用(省略代码)*/
            return;
        }
        /*攻击冷却结束,可以攻击*/
        if (m_isAtkCoolDown == false) {
            atk();
        }

        /*判断怪物是否离开攻击范围*/
        checkAimIsOutOfRange(monsterList);
    }
    else {
        /*选择一个进入攻击范围的怪物(省略代码)*/
    }
}

       来看看atk函数,代码如下:

void Hero::atk() {
    log("Atk!!!");
    
    /*标记攻击冷却*/
    m_isAtkCoolDown = true;

    /*英雄攻击有间隔时间,指定若干秒后恢复攻击*/
    this->scheduleOnce(schedule_selector(Hero::atkCollDownEnd), getiAtkSpeed()/1000.0f);
}

void Hero::atkCollDownEnd(float dt) {
    m_isAtkCoolDown = false;
}

         再看一下checkAimIsOutOfRange函数,代码如下:

void Hero::checkAimIsOutOfRange(Vector<Monster *> monsterList) {
    if (m_atkMonster != NULL) {
        if (isInAtkRange(m_atkMonster->getPosition()) == false) {
            missAtkMonser();
        }
    }
}

void Hero::missAtkMonster() {
    log("Out Of Range!!!");
    m_atkMonster = NULL;
}

         最后在Hero的构造函数里做一些初始化操作,代码如下:

Hero::Hero() {
    m_atkMonster = NULL;
    m_isAtkCoolDown = false;
}

       调试结果如下:

    

     15.17 子弹管理器1---子弹类

        BulletBase.h文件:

class BulletBase : public Entity {
public:
    BulletBase();
    ~BulletBase();

    void lockAim(Entity *entity);   /*锁定攻击目标*/
    Entity *getAim();               /*获取攻击目标*/
    bool isArrive();                /*是否到达目标*/

    /*是否正在使用*/
    void setUsed(bool isUsed);
    bool isUsed();
protected:
    /*锁定攻击目标时调用,留给子类做处理*/
    virtual void onLockAim(Entity *aim) = 0;

    bool m_isArrive;   //是否达到了攻击目标(是否攻击了目标)
private:
    bool m_isUsed;     //标记是否已经在使用中
    Entity *m_aim;     //攻击目标

    CC_SYNTHESIZE(int, m_iAtkValue, iAtkValue);   //攻击力
    CC_SYNTHESIZE(int, m_iSpeed, iSpeed);         //速度
};

    
    

         子弹主要有攻击力和速度两个属性,提供给外部调用的功能也有两个,一个是锁定攻击目标,一个是设置子弹是否在使用中。

        BulletBase.cpp文件,代码如下:

BulletBase::BulletBase() {
    m_isUsed = false;
    m_aim = NULL;
    m_iSpeed = SPEED_DEFAULT;
    m_iAtkValue = 1;
}
BulletBase::~BulletBase() {
}

void BulletBase::setUsed(bool isUsed) {
    this->m_isUsed = isUsed;
    
    setVisible(isUsed);
}

bool BulletBase::isUsed() {
    return this->m_isUsed;
}
void BulletBase::lockAim(Entity *entity) {
    if (entity != NULL) {
        m_aim = entity;
        onLockAim(m_aim);
    }
}

Entity *BulletBase::getAim() {
    return this->m_aim;
}

bool BulletBase::isArrive() {
    return m_isArrive;
}

        开始创建第一种类型的子弹---普通子弹。NormalBullet,头文件如下:

class BulletNormal : public BulletBase {
public:
    BulletNormal();
    ~BulletNormal();

    CREATE_FUNC(BulletNormal);
    virtual bool init();

    static BulletNormal *create(Sprite *sprite);
    bool init(Sprite *sprite);
protected:
    virtual void onLockAim(Entity *aim);
private:
    void normal();
};

       NormalBullet.cpp文件:

BulletNormal::BulletNormal() {
    m_iSpeed = SPEED_NORMAL;
}

BulletNormal::~BulletNormal() {
}

BulletNormal *BulletNormal::create(Sprite *sprite) {
    BulletNormal *bulletNor = new BulletNormal();
    if (bulletNor && bulletNor->init(sprite)) {
        bulletNor->autorelease();
    } else {
        CC_SAFE_DELETE(bulletNor);
    }
    return bulletNor;
}

bool BulletNormal::init(Sprite *sprite) {
    bindSprite(sprite);
    return true;
}

bool BulletNormal::init() {
    bool bRet = false;
    do {
        Sprite *sprite = Sprite::create(PATH_BULLET_NOR);
        CC_BREAK_IF(!sprite);
        CC_BREAK_IF(!init(sprite));
        bRet = true;
    } while(0);
    return bRet;
}

void BulletNormal::onLockAim(Entity *aim) {
    m_isArrive = false;
    Point aimWorldPos = aim->getParent()->convertToWorldSpace(aiim->getPosition());
    Point dstPos = this->getParent()->convertToNodeSpace(aimWorldPos);
    
    auto moveTo = MoveTo::create(0.5f, dstPos);
    auto callFunc = CallFunc::create([&](){
        moveEnd();
    });

    auto actions = Sequence::create(moveTo, callFunc, NULL);
    this->runAction(actions);
}

void BulletNormal::moveEnd() {
    m_isArrive = true;
}

     15.18 子弹管理器2---子弹管理器

        BulletManager.h文件:

#define BULLET_MAX_CACHE_NUM 10     //子弹缓存最大数量

class BulletBase;
class BulletManager : public Node {
public:
    BulletManager();
    ~BulletManager();
    static BulletManager *create();
    bool init();

    /*从缓存中获取一个未被使用的子弹*/
    BulletBase *getAnyUnUsedBullet();
private:
    Vector<BulletBase *> m_bulletList;    /*子弹列表*/
    void createBullets(Node *parent);     /*创建缓存子弹*/
    void bulletLogicCheck(float dt);      /*子弹逻辑*/
};

      看一下BulletManager比较简单的几个函数,代码如下;

BulletManager::BulletManager() {
}

BulletManager::~BulletManager() {
}

BulletManager *BulletManager::create() {
    BulletManager *bulletMgr = new BulletManager();
    if (bulletMgr && bulletMgr->init()) {
        bulletMgr->autorelease();
    }
    else {
        CC_SAFE_DELETE(bulletMgr);
    }
    return bulletMgr;
}

bool BulletManager::init() {
    /*创建子弹对象*/
    createBullets(parent);
    
    /*循环检测子弹逻辑*/
    this->schedule(schedule_selector(BulletManager::bulletLogicCheck), BULLET_LOGIC_CHECK_INTERVAL);

    return true;
}

        来看createBullets函数,代码如下:

void BulletManager::createBullets(Node *parent) {
    BulletBase *bullet = NULL;
    for (int i=0; i<BULLET_MAX_CACHE_NUM; i++) {
        bullet = BulletNormal::create();

        bullet->setUsed(false);
        m_bulletList.pushBack(bullet);

        this->addChild(bullet);
    }
}

        看看bulletLogicCheck函数,这个函数用于控制所有子弹的行为,代码如下:

void BulletManager::bulletLogicCheck(float dt) {
    for (auto bullet : m_bulletList) {
        if (bullet->isUsed()) {
            auto aim = bullet->getAim();
            
            if (aim != NULL) {
                /*判断子弹是否到达了目标处,如是的,则伤害目标*/
                if(bullet->isArrive()) {
                    aim->hurtMe(bullet->getiAtkValue());
    
                    /*子弹攻击目标后,重置为未使用状态*/
                    bullet->setUsed(false);
                }
            }
        }
    }
}

       一个很重要的函数---getAnyUnUsedBullet函数,从子弹列表中查找状态为未使用的子弹,用于英雄攻击时从子弹管理器中获取子弹对象,代码如下:

BulletBase *BulletManager::getAnyUnUsedBullet() {
    for (auto bullet : m_bulletList) {
        if (bullet->isUsed() == false) {
            bullet->setUsed(true);    
            return bullet;
        }
    }

    return NULL;
}

       15.19 子弹管理器3---英雄开始发射子弹

         为了Hero添加一个成员变量,代码如下:

/*子弹管理类*/
BulletManager *m_bulletMgr;

       修改Hero的init函数,代码如下:

bool Hero::init(Sprite *sprite) {
    bool bRet = false;
    
    do {
        CC_BREAK_IF(!sprite);

        bindSprite(sprite);

        /*创建子弹管理器*/
        m_bulletMgr = BulletManager::create();
        this->addChild(m_bulletMgr);
        bRet = true;
    } while(0);

    return bRet;
}

       最后,修改Hero的atk函数,代码如下:

void Hero::atk() {
    /*从子弹管理器中取出一个未被使用的子弹对象*/
    BulletBase *bullet = m_bulletMgr->getAnyUnUsedBullet();
    if (bullet != NULL) {
        /*根据英雄情况设置子弹属性,锁定攻击目标*/
        Point heroWorldPos = this->getParent()->convertToWorldSpace(getPosition());
        bullet->setPosition(bullet->getParent()->convertToNodeSpace(heroWorldPos));
        bullet->setiAtkValue(getiCurAtk());
        bullet->lockAim(m_atkMonster);

        /*标记攻击冷却*/
        m_isAtkCoolDown = true;
        
        /*英雄攻击有间隔时间,指定若干秒后恢复攻击*/
        this->scheduleOnce(schedule_selector(Hero::atkCollDownEnd), getiAtkSpeed()/1000.0f);
    }
}

        我们把打印日志的地方修改为从子弹管理器中获取一个子弹对象,然后赋予子弹属性,锁定攻击目标,于是,子弹就开始飞向怪物了。

        运行项目,将看到如下效果:

      

    15.20 怪物血量条

        给Monster新增一些东西,代码如下:

#include "editor-support/cocostudio/CCSGUIReader.h"
#include "cocos-ext.h"
#include "ui/CocosGUI.h"
using namespace cocos2d::ui;
using namespace cocostudio;
USING_NS_CC_EXT;

class Monster : public Entity {
    /*省略了很多代码*/

protected:
    virtual void onDead() override;
    virtual void onBindSprite() override;
    virtual void onHurt(int iHurtValue) override;
private:
    LoadingBar *m_hpBar;
    int m_iMaxHP;
};

      新增函数代码如下:

void Monster::onDead() {
    this->removeFromParent();
}
void Monster::onBindSprite() {
    /*创建血量条控件*/
    auto UI = cocostudio::GUIReader::getInstance()->widgetFromJsonFile("HpBar/HpBar_1.ExportJson");
    this->addChild(UI);

    /*设置坐标*/
    Size size = this->getContentSize();
    UI->setPosition(Point(size.width * 0.5f, size.height));

    /*设置坐标*/
    Size size = this->getContentSize();

    UI->setPosition(Point(size.width * 0.5f, size.height));

    /*获取进度条控件*/
    m_hpBar = (LoadingBar *)Helper::seekWidgetByName(UI, "hpbar");
    m_hpBar->setPercent(100);

    /*记录初始血量*/
    m_iMaxHP = getiHP();
}

void Monster::onHurt(int iHurtValue) {
    m_hpBar->setPercent(getiHP() / (float)m_iMaxHP * 100);
}

     onDead函数在怪物死亡时被调用,主动将自己从游戏里删除。

     运行游戏,看到血量条出现了:

    

    15.21 炮台操作按钮----英雄华丽升级

       首先,给TowerBorder新增两个函数,用于控制操作按钮的出现和隐藏,代码如下:

class TowerBorder : public Entity {
public:
    /*省略了很多代码*/
    
    void showTowerOprBtns();    /*显示操作按钮*/
    void deleteOprBtns();       /*删除操作按钮*/
};

      看看showTowerOprBtns函数,代码如下:

void TowerBorder::showTowerOprBtns() {
    if (m_isOprBtnsShow == true) {
        /*已经是显示状态*/
        return;
    }

    if (m_cancelBtn == NULL) {
        /*手动创建一个按钮*/
        auto heroOprBtn = Button::create();
        heroOprBtn->loadTextureNormal("button2.png");

        /*用clone函数复制三个按钮对象*/
        m_cancelBtn = (Button *)heroOprBtn->clone();
        m_deleteBtn = (Button *)heroOprBtn->clone();
        m_upgradeBtn = (Button *)heroOprBtn->clone();

        /*初始化按钮位置*/
        resetOprBtns();

        m_cancelBtn->setTitleText("cancel");
        m_deleteBtn->setTitleText("delete");
        m_upgradeBtn->setTitleText("upgrade");

        m_cancelBtn->addTouchEventListener(this, toucheventselector(TowerBorder::cancelClick));

        m_deleteBtn->addTouchEventListener(this, toucheventselector(TowerBorder::deleteClick));
        m_upgradeBtn->addTouchEventListener(this, toucheventselector(TowerBorder::upgradeClick));

        this->getParent()->addChild(m_cancelBtn, 999);
        this->getParent()->addChild(m_deleteBtn, 999);
        this->getParent()->addChild(m_upgradeBtn, 999);
    }
    m_cancelBtn->setEnabled(true);
    m_deleteBtn->setEnabled(true);
    m_upgradeBtn->setEnabled(true);

    /*按钮出场特效*/
    m_cancelBtn->runAction(EaseBounceOut::create(MoveBy::create(0.5f, Point(0, 100))));
    m_deleteBtn->runAction(EaseBounceOut::create(MoveBy::create(0.5f, Point(-100, 0))));
    m_upgradeBtn->runAction(EaseBounceBtn::create(MoveBy::create(0.5f, Point(100, 0))));

    m_isOprBtnShow = true;
}


        

       步骤解释:

       (1)首先判断操作按钮是否已经在显示,如是,就不做任何操作。

       (2)我们有3个操作按钮,分别是取消、删除和升级。第一次显示操作按钮时,需要先创建这三个按钮控件,并做初始化。创建按钮控件时,笔者使用了clone函数,这个函数可以直接从现有的对象复制一份出来,当成新的对象来使用。这样可以避免重新创建,在一定程度上可以提高效率。

        (3) 创建完按钮,分别监听按钮的单击事件。

        (4) 给按钮执行动作,让按钮出厂时没那么生硬。EaseBounceOut是一个预定的Action,它给让其他动作赋予回弹的效果。

           三个操作按钮并不是添加到炮台(TowerBorder)身上,而是添加到跑跳的父节点上,这样可以避免操作按钮被其他对象遮挡(比如英雄),体验好一些。

        showTowerOprBtns函数里涉及很多新的成员变量和函数,代码如下:

private:
    /*操作按钮UI*/
    Button *m_cancelBtn;
    Button *m_deleteBtn;
    Button *m_upgradeBtn;

    bool m_isOprBtnsShow;

    /*恢复操作按钮的默认位置*/
    void resetOprBtns();

    void cancelClick(Ref *target, TouchEventType type);
    void deleteClick(Ref *target, TouchEventType type);
    void upgradeClick(Ref *target, TouchEventType type);

         resetOprBtns比较简单,代码如下;

void TowerBorder::resetOprBtns() {
    /*让按钮恢复到中点的位置*/
    Point pos = this->getPosition();
    m_cancelBtn->setPosition(pos);
    m_deleteBtn->setPosition(pos);
    m_upgradeBtn->setPosition(pos);
}

        resetOprBtns会在关闭操作按钮时被调用,也就是在deleteOprBtns函数中调用,代码如下:

void TowerBorder::deleteOprBtns() {
    if (m_cancelBtn != NULL) {
        m_cancelBtn->setEnabled(false);
        m_deleteBtn->setEnabled(false);
        m_upgradeBtn->setEnabled(false);

        resetOprBtns();
    }

    m_isOprBtnsShow = false;
}

      同时,m_isOprBtnsShow用于标记操作按钮的显示状态。

      最后,就剩下三个按钮单击时所触发的函数了,代码如下:

void TowerBorder::cancelClick(Ref *target, TouchEventType type) {
    if (type == TouchEventType::TOUCH_EVENT_ENDED) {
        deleteOprBtns();
    }
}

void TowerBorder::deleteClick(Ref *target, TouchEventType type) {
    if (type == TouchEventType::TOUCH_EVENT_ENDED) {
        deleteHero();
        m_hero = NULL;

        deleteOprBtns();
    }
}

void TowerBorder::upgradeClick(Ref *target, TouchEventType type) {
    if (type == TouchEventType::TOUCH_EVENT_ENDED) {
        m_hero->upgrade();
        deleteOprBtns();
    }
}

        cancelClick函数就是关闭操作按钮,直接调用deleteOprBtns函数即可。

        deleteClick函数是删除炮台上的英雄,先调用deleteHero函数解决炮台对Hero的绑定,然后关闭操作按钮。

        upgradeClick函数用于升级英雄,直接调用Hero的upgrade函数,然后关闭操作按钮。

        TowerBorder还有最后一个地方要修改,在构造函数里对成员变量做一些初始化,代码所示:

TowerBorder::TowerBorder() {
    m_iLevel = 1;
    m_hero = NULL;
    m_cancelBtn = NULL;
    m_deleteBtn = NULL;
    m_upgradeBtn = NULL;
    m_isOprBtnsShow = false;
}

 

bool HeroManager::initWithLevel(int iCurLevel) {
    /*加载塔坐标对象(省略)*/
    /*创建炮台(省略)*/
    /*添加触摸监听(省略)*/

    listener->onTouchEnded = [&](Touch *touch, Event *event) {
        /*省略了一些代码*/
        /*当前塔坐标没有英雄对象,则添加英雄*/
        if (clickBorder->getHero() == NULL) {}
            /*绑定英雄对象到炮台(省略)*/
        }
        else {
            clickBorder->showTowerOprBtns();   /*显示炮台操作按钮*/
        }
    };
    return true;
}

       给英雄增加upgrade函数:

void Hero::upgrade() {
    Sprite *sprite = getSprite();
    if (sprite == NULL || m_iLevel >= 4) {
        return;
    }
    m_iLevel++;   /*增加等级*/

    /*英雄等级特效*/
    if (m_iLevel == 2) {
        Sprite *heroTop1 = Sprite::create("sprite/hero/hero_top_1.png");
        this->addChild(heroTop1);
    }
    if (m_iLevel == 3) {
        Sprite *heroTop2 = Sprite::create("sprite/hero/hero_top_2.png");
        this->addChild(heroTop2);
        
        auto rotateBy = RotateBy::create(25.0f, 360, 360);
        auto repeat = RepeatForever::create(rotateBy);
        heroTop2->runAction(repeat);
    }
    if (m_iLevel == 4) {
        Sprite *heroTop3 = Sprite::create("sprite/hero/hero_top_3.png");
        this->addChild(heroTop3);

        auto rotateBy = RotateBy::create(10.0f, 360, 360);
        auto repeat = RepeatForever::create(rotateBy);
        heroTop3->runAction(repeat);
    }

    /*增加英雄攻击力*/
    setiBaseAtk(getiBaseAtk() * m_fUpgradeAtkBase);
    setiCurAtk(getiBaseAtk());
}


    

        运行游戏,单击英雄,然后进行各种操作,如下:

     

   15.22 怪物起点和终点魔法台

void TollgateMapLayer::createEndPoint() {
    MonsterPos *pos = m_monsterMgr->getMonsterEndPos();
    
    Sprite *home = Sprite::create("sprite/end.png");
    home->setPosition(pos->getPos());

    auto rotateBy = RotateBy::create(15.0f, 360, 360);
    auto repeat = RepeatForever::create(rotateBy);
    home->runAction(repeat);

    this->addChild(home, HOME_LAYER_LVL);
}

void TollgateMapLayer::createStartPoint() {
    MonsterPos *pos = m_monsterMgr->getMonsterStartPos();

    Sprite *startSp = CCSprite::create("sprite/start.png");
    startSp->setPosition(pos->getPos());

    auto *rotateBy = RotateBy::create(15.0f, 360, 360);
    auto repeat = RepeatForever::create(rotateBy);
    startSp->runAction(repeat);
    
    this->addChild(startSp, HOME_LAYER_LVL);
}

      createEndPoint函数创建的就是怪物的终点,怪物的终点也就是玩家的堡垒,createStartPoint函数负责创建怪物的起点。怪物的起点和终点其实就是获取怪物坐标列表的第一个和最后一个坐标,然后创建一个精灵,赋予一些Action动作让它看起来更华丽。

      最后,在TollgateMapLayer的init函数的最后添加两行代码,代码如下:

/*创建起始点*/
createStartPoint();

/*创建终点*/
createEndPoint();

    效果如下:

    

   

16.1 关卡信息UI

     新建一个Layer,用来处理关卡信息的逻辑,命名为TollgateDataLayer,头文件如下:

#include "editor-support/cocostudio/CCSGUIReader.h"
#include "ui/CocosGUI.h"
using namespace cocos2d::ui;
using namespace cocostudio;
class TollgateDataLayer : public Layer {
public:
    TollgateDataLayer();
    ~TollgateDataLayer();

    CREATE_FUNC(TollgateDataLayer);
    virtual bool init();
};

     看看TollgateDataLayer.cpp文件,代码如下:

TollgateDataLayer::TollgateDataLayer() {
}
TollgateDataLayer::~TollgateDataLayer() {
}
bool TollgateDataLayer::init() {
    if (!Layer::init()) {
        return false;
    }
    /*加载UI*/
    auto UI = cocostudio::GUIReader::getInstance()->widgetFromJsonFile("TollgateUI/TollgateUI_1.ExportJson");
    this->addChild(UI);
    
    UI->setTouchEnabled(false);
    return false;
}

     我们把TollgateDataLayer添加到场景里,修改TollgateScene的createScene函数,代码如下:

Scene *TollgateScene::createScene() {
    auto scene = Scene::create();

    TollgateScene *tgLayer = TollgateScene::create();
    TollgateDataLayer *dataLayer = TollgateDataLayer::create();
    TollgateMapLayer *mapLayer = TollgateMapLayer::create();

    Scene->addChild(mapLayer, 1, TAG_MAP_LAYER);
    scene->addChild(tgLayer, 3);
    scene->addChild(dataLayer, 5, TAG_DATA_LAYER);

    return scene;
}

      效果如下:

    

16.2 关卡信息数据刷新---NotificationCenter的应用

    我们先为TollgateDataLayer.h添加三个函数,代码如下:

void recvRefreshTowerSoulNum(Ref *pData);
void recvRefreshMonsterNum(Ref *pData);
void recvRefreshMagicNum(Ref *pData);

int m_iTowerSoulNum;    /*塔魂数量*/
int m_iMonsterNum;      /*怪物数量*/
int m_iMagicNum;        /*魔力数量*/

Text *m_towerSoulLab;      /*塔魂标签*/
Text *m_monsterLab;        /*怪物标签*/
Text *m_magicLab;          /*魔力标签*/

      我们要在特定的事件发生时修改这三种数据,所以我们要订阅三种消息,分别对应这三种数据的刷新,而刷新数据就是修改三个信息的标签值。

       修改一下TollgateDataLayer的init函数,代码如下:

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

    /*加载UI*/
    auto UI = cocostudio::GUIReader::getInstance()->widgetFromJsonFile("TollgateUI/TollgateUI_1.ExportJson");
    this->addChild(UI);

    UI->setTouchEnabled(false);

    /*塔魂标签*/
    m_towerSoulLab = (Text *)Helper::seekWidgetByName(UI, "towerSoulLab");
    
    /*怪物标签*/
    m_monsterLab = (Text *)Helper::seekWidgetByName(UI, "monsterNumLab");

    /*魔力标签*/
    m_magicLab = (Text *)Helper::seekWidgetByName(UI, "magicLab");

    /*订阅消息*/
    NOTIFY->addObserver(this, callfuncO_selector(TollgateDataLayer::recvRefreshTowerSoulNum),
        "TowerSoulChange",
        NULL);

    NOTIFY->addObserver(this, 
        callfuncO_selector(TollgateDataLayer::recvRefreshMonsterNum),
        "MonsterNumChange",
        NULL);

    NOTIFY->addObserver(this,
        callfuncO_selector(TollgateDataLayer::recvRefreshMagicNum),
        "MagicChange",
        NULL);
    return true;
}

      看看这三个消息发生时的函数,代码如下:

void TollgateDataLayer::recvRefreshTowerSoulNum(Ref *pData) {
    int iAltValue = (int)pData;
    m_iTowerSoulNum += iAltValue;
    m_towerSoulLab->setText(StringUtils::toString(m_iTowerSoulNum));
}

void TollgateDataLayer::recvRefreshMonsterNum(Ref *pData) {
    int iAltValue = (int)pData;
    m_iMonsterNum += iAltValue;
    m_monsterLab->setText(StringUtils::toString(m_iMonsterNum));
}

void TollgateDataLayer::recvRefreshMagicNum(Ref *pData) {
    int iAltValue = (int)pData;
    m_iMagicNum += iAltValue;
    m_magicLab->setText(StringUtils::toString(m_iMagicNum));
}

     最后,我们还要做一下初始化和释放的工作,代码如下:

TollgateDataLayer::TollgateDataLayer() {
    m_iTowerSoulNum = 0;    /*塔魂数量*/
    m_iMonsterNum = 0;     /*怪物数量*/
    m_iMagicNum = 0;       /*魔力数量*/
}
TollgateDataLayer::~TollgateDataLayer() {
    NOTIFY->removeAllObservers(this);
}

     我们要修改一下TollgataMapLayer,给它添加一个initData函数,代码如下:

void TollgateMapLayer::initData() {
    /*初始化塔魂、怪物和魔力数量*/
    int iTowerSoulNum = 5;
    int iMonsterNum = m_monsterMgr->getNotShowMonsterCount();    /*怪物数量*/
    int iMagicNum = 100;                      /*魔力数量*/

    NOTIFY->postNotification("TowerSoulChange", (Ref *)iTowerSoulNum);
    NOTIFY->postNotification("MonsterNumChange", (Ref *)iMonsterNum);
    NOTIFY->postNotification("MagicChange", (Ref *)iMagicNum);
}

     我们在TollgateMapLayer层里发送信息,改变关卡信息UI的值。

      修改一下TollgateScene的createScene函数,代码如下:

Scene *TollgateScene::createScene() {
    auto scene = Scene::create();
    
    TollgateScene *tgLayer = TollgateScene::create();
    TollgateMapLayer *mapLayer = TollgateMapLayer::create();
    TollgateDataLayer *dataLayer = TollgateMapLayer::create();

    scene->addChild(mapLayer, 1, TAG_MAP_LAYER);
    scene->addChild(tgLayer, 3);
    scene->addChild(dataLayer, 5, TAG_DATA_LAYER);

    mapLayer->initData();
    return scene;
}

16.3 怪物数量刷新

     在MonsterManager.cpp的showMonster函数的最后加上几句代码,代码如下:

/*发布怪物数量改变消息*/
int iMonsterNumChange = -monsterWantToDelete.size();
NOTIFY->postNotification("MonsterNumChange", (Ref *)iMonsterNumChange);

     运行游戏,就能看到没出现一只怪物,关卡信息UI上的怪物数量就会减1,如下:

    

16.4 怪物安息---怪物死亡后塔魂数量刷新

     我们来刷新塔魂数量,塔魂也就是我们的金钱,以后英雄升级需要花费塔魂。

     怪物死亡时会调用onDead函数。代码如下:

void Manager::onDead() {
    this->removeFrom();

    /*发布塔魂增加消息*/
    int iTowerSoulNumChange = 3 * getiLevel();
    NOTIFY->postNotification("TowerSoulChange", (Ref *)iTowerSoulNumChange);
}

     运行游戏,尽量打死一只怪物试试,效果如下:

    
   

16.5 堡垒安息---怪物到达堡垒后扣除魔力值

    ControllerMoveBase.h文件,代码如下:

class ControllerMoveBase : public ControllerBase {
public:
    void bindMoveEndFunc(std::function<void()> moveEndFunc);
protected:
    /*用于移动结束时的回调*/
    std::function<void()> m_moveEndFunc;
};

 

void ControllerMoveBase::bindMoveEndFunc(std::function<void()> moveEndFunc) {
    m_moveEndFunc = moveEndFunc;
}

   看看ControllerSimpleMove的moveOneStep函数,代码如下:

void ControllerSimpleMove::moveOneStep() {
    /*省略了很多代码*/
    
    /*到达当前目的地,开始下一个目的地*/
    if (entityPos.x == curDestPos.x && entityPos.y == curDestPos.y) {
        if (m_movePosList.size() > 0) {
            nextMovePos();
        } 
        else {
            /*移动结束*/
            if (m_moveEndFunc) {
                m_moveEndFunc();
                m_moveEndFunc = nullptr;
            }
        }
    }
}

     最后,我们还要修改Monster的init函数,代码如下:

16.6 打怪升级---英雄升级扣除塔魂

      我们在英雄升级的时候发送消息,改变塔魂数量即可,修改一下Hero的upgrade函数,代码如下:

void Hero::upgrade() {
    Sprite *sprite = getSprite();
    if (sprite == NULL || m_iLevel >= 4) {
        return;
    }

    /*判断塔魂是否足够*/
    auto dataLayer = (TollgateDataLayer *)Director::getInstance()->getRunningScene()->getChildByTag(TAG_DATA_LAYER);
    int iCurMagicNum = dataLayer->getiTowerSoulNum();

    int iCostTowerSoul = m_iUpgradeCostBase * m_iLevel;
    if (iCurMagicNum < iCostTowerSoul) {
        return;
    }

    /*发布消息,扣除塔魂*/
    NOTIFY->postNotification("TowerSoulChange", (Ref *)iCostTowerSoul);

    /*省略了很多代码*/
}

      给TollgateDataLayer新增一个getiTowerSoulNum函数,以便获取塔魂数量。getiTowerSoulNum函数代码如下:

int TollgateDataLayer::getiMagicNum() {
    return m_iMagicNum;
}
int TollgateDataLayer::getiTowerSoulNum() {
    return m_iTowerSoulNum;
}

     运行游戏,然后升级英雄,我们就能看到塔魂的数量减少了。

16.7 关卡选择---根据关卡树加载游戏

    新建一个关卡选择场景,头文件代码如下:

class TollgateSelectScene : public CCLayer {
public:
    static Scene *createScene();    
    virtual bool init();
    CREATE_FUNC(TollgateSelectScene);
private:
    void level_1(Ref *target, TouchEventType type);
    void level_2(Ref *target, TouchEventType type);
    void level_3(Ref *target, TouchEventType type);
};

     创建的界面比较简陋,只有三个按钮,来看看TollgateSelectScene.cpp文件:

bool TollgateSelectScene::init() {
    bool bRet = false;
    do {
        CC_BREAK_IF(! Layer::init());

        /*加载UI*/
        auto UI = cocostudio::GUIReader::getInstance()->widgetFromJsonFile("TgSelectUI/TgSelectUI_1.ExportJson");
        this->addChild(UI);

        Button *tgSelect1Btn = (Button *)Helper::seekWidgetByName(UI, "tgSelect1Btn");
        Button *tgSelect2Btn = (Button *)Helper::seekWidgetByName(UI, "tgSelect2Btn");
        Button *tgSelect3Btn = (Button *)Helper::seekWidgetByName(UI, "tgSelect3Btn");

        tgSelect1Btn->addTouchEventListener(this, toucheventselector(TollgateSelectScene::level_1));
        tgSelect2Btn->addTouchEventListener(this, toucheventselector(TollgateSelectScene::level_2));
        tgSelect3Btn->addTouchEventListener(this, toucheventselector(TollgateSelectScene::level_3));
        bRet = true;
    } while(0);

    return bRet;
}

void TollgateSelectScene::level_1(Ref *target, TouchEventType type) {
    if (type != TouchEventType::TOUCH_EVENT_ENDED) {
        return;    
    }
    GlobalClient::getInstance()->setiCurTollgateLevel(1);
    SceneManager::getInstance()->changeScene(SceneManager::en_TollgateScene);
}

void TollgateSelectScene::level_2(Ref *target, TouchEventType type) {
    if (type != TouchEventType::TOUCH_EVENT_ENDED) {
        return;    
    }

    GlobalClient::getInstance()->setiCurTollgateLevel(2);
    SceneManager::getInstance()->changeScene(SceneManager::en_TollgateScene);
}

void TollgateSelectScene::level_3(Ref *target, TouchEventType type) {
    if (type != TouchEventType::TOUCH_EVENT_ENDED) {
        return;
    }
    GlobalClient::getInstance()->setiCurTollgateLevel(3);
    SceneManager::getInstance()->changeScene(SceneManager::en_TollgateScene);
}

     关卡的加载是在TollgateMapLayer层进行的,我们在TollgateMapLayer的构造函数里初始化m_iCurLevel的值即可,代码如下:

TollgateMapLayer::TollgateMapLayer() {
    m_iCurLevel = GlobalClient::getInstance()->getiCurTollgateLevel();
}

     然后,我们新增了TollgateSelectScene场景,修改SceneManager的changeScene函数,添加一个switch分支,代码如下:

case en_TollgateSelectScene: /*关卡选择场景*/
    pScene = TollgateSelectScene::scene();
    break;

    GlobalClient是一个单例类,存放我们的一些全局数据,方便各个场景或者层之间传递数据,代码如下:

GlobalClient.h文件:
class GlobalClient : public Ref {
public:
    static GlobalClient *getInstance();
    CREATE_FUNC(GlobalClient);
    virtual bool init();
private:
    static GlobalClient *m_GlobalClient;

    CC_SYNTHESIZE(int, m_iCurTollgateLevel, iCurTollgateLevel);
};

GlobalClient.cpp文件:
GlobalClient *GlobalClient::m_GlobalClient = NULL;
GlobalClient *GlobalClient::getInstance() {
    if (m_GlobalClient == NULL) {
        m_GlobalClient = new GlobalClient();
        if (m_GlobalClient && m_GlobalClient->init()) {
            m_GlobalClient->autorelease();
            m_GlobalClient->retain();
        }
        else {
            CC_SAFE_DELETE(m_GlobalClient);
            m_GlobalClient->retain();
        }
    }
    
    return m_GlobalClient;
}

bool GlobalClient::init() {
    return true;
}

      运行游戏,效果如下:

    

16.8 胜利条件判断

      我们修改一下Monster的init函数,代码如下:

CC_SYNTHESIZE_BOOL(m_isMoveEnd, MoveEnd);    //是否达到目的地

     然后,我们修改一下Monster的init函数,代码如下:

bool Monster::init() {
    m_moveController = ControllerSimpleMove::create(this);
    this->addChild(m_moveController);

    /*绑定移动结束回调函数*/
    m_moveController->bindMoveEndFunc([&](){
        /*发布魔力值改变消息*/
        int iMagicChange = -getiLevel() * 10;
        NOTIFY->postNotification("MagicChange", (Ref *)iMagicChange);

        m_isMoveEnd = true;
    });
    return true;
}

        

     当怪物移动结束时,将m_isMoveEnd标记为true,代表怪物达到目的地。

     我们为MonsterManager新增一个函数,代码如下:

void MonsterManager::logic(float dt) {
    Vector<Monster *> monsterWantToDelete;
        for (auto monster : m_monsterList) {
            /*从列表中删除已经到达目的地的怪物,先记录,后删除*/
            if (monster->isMoveEnd() == true) {
                monsterWantToDelete.pushBack(monster);
            }
            /*从列表汇总删除已经死亡的怪物,先记录,后删除*/
            else if (monster->isDead() == true) {
                monsterWantToDelete.pushBack(monster);
            }
        }

        /*正式删除的怪物*/
        for (auto monster : monsterWantToDelete) {
            monster->removeFromParent();
            m_monsterList.eraseObject(monster);
        }
        if (m_monsterList.size() == 0) {
            /*怪物数量为0,发布怪物全灭消息*/
            NOTIFY->postNotification("AllMonsterDead");
        }
}

       最后,判断怪物列表中是否还有怪物,如果怪物数量为0,则发布一条"AllMonsterDead"消息。

     在MonsterManager的createMonsters函数最后添加一句代码,代码如下:

this->schedule(schedule_selector(MonsterManager::showMonster));
this->schedule(schedule_selector(MonsterManager::logic));

      然后,我们要在TollgateDataLayer层处理"AllmonsterDead"消息,为TollgateDataLayer新增一个函数,代码如下:

void TollgateDataLayer::recvAllMonsterDead(Ref *pData) {
    if (m_iMagicNum > 0) {
        SceneManager::getInstance()->changeScene(SceneManager::en_WinScene);
    }
}

      要接收"AllMonsterDead"消息,就要订阅消息,我们在TollgateDataLayer的init函数的最后再订阅一条消息,代码如下:

NOTIFY->addObserver(this, callfuncO_selector(TollgateDataLayer::recvAllMonsterDead),
                    "AllMonsterDead", NULL);

      WinScene场景只是一个很简单的场景,关键的两个函数,代码如下:

bool WinScene::init() {
    if (!Layer::init()) {
        return false;
    }
    Size visibleSize = Director::getInstance()->getVisibleSize();

    /*添加一个标签*/
    Label *winLab = Label::create("You Win!", PATH_FONT_PUBLIC, GlobalParam::PublicFontSizeLarge);
    winLab->setPosition(ccp(visibleSize.width / 2, visibleSize.height / 2));
    this->addChild(winLab);

    /*3秒后返回关卡选择场景*/
    this->schedule(schedule_selector(WinScene::backToTollgateSelectScene), 3.0f);

    return true;
}
void WinScene::backToTollgateSelectScene(float dt) {
    sceneManager::getInstance()->changeScene(SceneManager::en_TollgateSelectScene);
}

16.9 失败条件判断

     直接在接收到“MagicChange”消息时做判断,修改TollgateDataLayer的recvRefreshMagicNum,在最后添加几句代码,代码如下:

void TollgateDataLayer::recvRefreshMagicNum(Ref *pData) {
    /*省略了很多代码*/

    /*魔力值小于等于0,游戏失败*/
    if (m_iMagicNum <= 0) {
        SceneManager::getInstance()->changeScene(SceneManager::en_GameOverScene);
    }
}

   

 

 

 

资料链接:https://pan.baidu/s/106JS0kNH_J6pT2Ns5hW3Sg 密码:ixl3

更多推荐

《Cocos2d-x3.x游戏开发之旅》学习

本文发布于:2023-04-14 23:26:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/a443880125e145022008b571ee9a36f5.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:之旅   游戏开发   Cocos2d

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 77181文章数
  • 14阅读数
  • 0评论数