【cocos2d-x 3.x 学习与应用总结】2: 在cocos2d-x中使用ccbi

前言

本文以3.9版本的cocos2d-x为例,总结了如何在代码中解析、加载ccbi文件。给出一个最简单的使用ccbi实现的helloworld的例子、一个加强版的HelloWorld示例、以及一个最贴近实际使用情况的ccbi使用示例, 并结合示例分析ccbi的解析过程。

官方示例程序

ccbi功能支持的源代码

cocos对ccbi的支持是在extensions这个模块里面,以v3.9为例,解析ccbi的代码的物理路径是放在cocos2d/cocos/editor-support/cocosbuilde这个路径下面,在vs的解决方案管理器中如下图所示:

【cocos2d-x 3.x 学习与应用总结】2: 在cocos2d-x中使用ccbi_第1张图片

官方示例源代码

cocos官方的ccbi使用示例代码是在(vs中):TestCpp/ExtensionsTest/CocosBuilderTest这个分组下面,
其中CocosBuilderTest.cpp的实现如下,init方法是解析加载ccbi的关键代码:

#include "CocosBuilderTest.h"
#include "../../testResource.h"
#include "HelloCocosBuilder/HelloCocosBuilderLayerLoader.h"

USING_NS_CC;
USING_NS_CC_EXT;
using namespace cocosbuilder;

CocosBuilderTests::CocosBuilderTests()
{
    ADD_TEST_CASE(CocosBuilderTestScene);
}

bool CocosBuilderTestScene::init() {
    if (TestCase::init())
    {
        /* Create an autorelease NodeLoaderLibrary. */
        auto nodeLoaderLibrary = NodeLoaderLibrary::newDefaultNodeLoaderLibrary();

        nodeLoaderLibrary->registerNodeLoader("HelloCocosBuilderLayer", HelloCocosBuilderLayerLoader::loader());

        /* Create an autorelease CCBReader. */
        cocosbuilder::CCBReader * ccbReader = new cocosbuilder::CCBReader(nodeLoaderLibrary);

        /* Read a ccbi file. */
        auto node = ccbReader->readNodeGraphFromFile("ccb/HelloCocosBuilder.ccbi", this);

        ccbReader->release();

        if (node != nullptr) {
            this->addChild(node);
        }

        return true;
    }

    return false;
}

示例代码中包括了动画、UI按钮、菜单、标签、粒子、Scrollview等相关示例,这里不贴代码了,需要的话去cpptests工程里看就行了。

下面以我的实际使用经验总结一下在实际项目中该如何使用ccbi,先从一个HelloWorld说起。

使用ccbi的HelloWorld

使用cocosbuilder创建一个简单的ccbi

在cocosbuilder中新建一个hello.ccb, 注意:不要勾选js controll.(在Document菜单项下最后一个选项)

添加一个Label,名字叫mLabel:

【cocos2d-x 3.x 学习与应用总结】2: 在cocos2d-x中使用ccbi_第2张图片

添加一个MenuItemImage名字叫mBtn, selector名字叫onClick.

保存,发布为hello.ccbi.

创建一个最简单的class,来使用创建出来的ccbi文件:

CcbiHelloWorld.h: 定义了一个class继承自Node,用来挂载ccbi

#ifndef PLAYING_WITH_COCOS3D_PAGE_CCB_HELLOWORLD_H
#define PLAYING_WITH_COCOS3D_PAGE_CCB_HELLOWORLD_H

#include "cocos2d.h"
#include "cocosbuilder/CocosBuilder.h"

class CcbiHelloWorld : public cocos2d::Node
{
public:
    CREATE_FUNC(CcbiHelloWorld);
    bool init() override
    {
        using cocosbuilder::NodeLoaderLibrary;
        using cocosbuilder::CCBReader;

        // 第一步: 创建一个NodeLoaderLibrary
        auto loaderLib = NodeLoaderLibrary::newDefaultNodeLoaderLibrary();

        // 第二步: 创建CCBReader
        auto ccbReader = new CCBReader(loaderLib);

        // 第三步: 调用CCBReader的readNodeGraphFromFile的方法,传入ccbi名字
        auto node = ccbReader->readNodeGraphFromFile("ccbi/hello.ccbi", this);
        ccbReader->release();

        // 解析完毕,可以使用Node了。
        if ( node )
        {
            addChild(node);
        }
        return true;
    }
};

#endif //PLAYING_WITH_COCOS3D_PAGE_CCB_HELLOWORLD_H

在AppDelegate的applicationDidFinishLaunching()方法结尾处,添加启动代码:

auto node = CcbiHelloWorld::create();
auto scene = Scene::create();
scene->addChild(node);
director->runWithScene(scene);

编译运行,一个最简单的使用ccbi的例子就跑起来了,如下图所示:

【cocos2d-x 3.x 学习与应用总结】2: 在cocos2d-x中使用ccbi_第3张图片

从上面编写HelloWorld的过程可以看出,要想把ccbi当做节点添加到界面是很简单的事情,从CcbiHelloWorld类的init方法中看到, 几句关键代码加起来不超过10行。

不过,这个例子仅仅是把cocosbuilder当做了画板来用,所有的东西加到界面上都是死的, 点按钮也没有反应,我不能获取某个节点,做一些动作和属性修改的操作。有点像静态网站和动态网站的区别,游戏又不是美术作品,肯定是要动起来的。因此我需要能够获取ccbi界面上的元素,并且对它做一些操作。下面的第二个例子是展示了如何绑定ccbi上的变量和menu回调。

绑定ccbi上的变量和回调, 加强版HelloWorld

在上一个例子的基础上,来实现加强版HelloWorld, CcbiHelloWorldEnhanced

CcbiHelloWorldEnhanced.h:

#ifndef PLAYING_WITH_COCOS3D_PAGE_CCB_HELLOWORLD_ENHANCED_H
#define PLAYING_WITH_COCOS3D_PAGE_CCB_HELLOWORLD_ENHANCED_H

#include "cocos2d.h"
#include "cocosbuilder/CocosBuilder.h"
#include "cocosbuilder/CCBMemberVariableAssigner.h"
#include "cocosbuilder/CCBSelectorResolver.h"

class CcbiHelloWorldEnhanced 
    : public cocos2d::Node
    , public cocosbuilder::CCBMemberVariableAssigner    // 绑定ccbi上的变量
    , public cocosbuilder::CCBSelectorResolver          // 绑定ccbi上的回调
{
public:
    // ------------------ create方法和init方法相对于第一个例子都没有修改 -------------------
    CREATE_FUNC(CcbiHelloWorldEnhanced);

    bool init() override
    {
        using cocosbuilder::NodeLoaderLibrary;
        using cocosbuilder::CCBReader;

        // 第一步: 创建一个NodeLoaderLibrary
        auto loaderLib = NodeLoaderLibrary::newDefaultNodeLoaderLibrary();

        // 第二步: 创建CCBReader
        auto ccbReader = new CCBReader(loaderLib);

        // 第三步: 调用CCBReader的readNodeGraphFromFile的方法,传入ccbi名字
        auto node = ccbReader->readNodeGraphFromFile("ccbi/hello.ccbi", this);
        ccbReader->release();

        // 解析完毕,可以使用Node了。
        if (node)
        {
            addChild(node);
        }
        return true;
    }

    // ============ 重写 CCBMemberVariableAssigner 的方法,绑定ccbi上的变量 ==========
    // 这个函数是在CCBReader在解析过程中,遇到有名字的变量就会回调过来
    // 在这里来把我在ccbi上定义的两个变量"mLabel"和"mBtn"两个控件绑定到两个成员变量label_和menuItemImage_上.
    bool onAssignCCBMemberVariable( cocos2d::Ref* target,
                                    const char* memberVariableName,
                                    cocos2d::Node* node) override
    {
        // ccbi上的变量名
        std::string name(memberVariableName);
        if (name == "mLabel")                               // 是那个"mLabel"Label吗?
        {
            node->retain();
            label_ = dynamic_cast<cocos2d::Label*>(node);
            if (!label_) 
            {
                node->release();                            // 类型不匹配的话,就release掉,避免内存泄露
            }
            return true;
        }
        else if (name == "mBtn")                            // 是那个"mBtn"MenuItemImage吗?
        {
            node->retain();
            menuItemImage_ = dynamic_cast<cocos2d::MenuItemImage*>(node);
            if (!menuItemImage_)
            {
                node->release();                            // 类型不匹配的话,就release掉,避免内存泄露
            }
            return true;
        }

        return false;
    }

    // ========== 重写 CCBSelectorResolver 的方法实现回调绑定 ================================
    cocos2d::SEL_MenuHandler onResolveCCBCCMenuItemSelector(
        cocos2d::Ref * pTarget, const char* pSelectorName) override
    {
        if (std::string(pSelectorName) == "onClick")        // 是mBtn声明的那个onClick回调吗?
        {
            return CC_MENU_SELECTOR(CcbiHelloWorldEnhanced::onClicked);     // 返回一个Ref::(*)(Ref*)类型的成员函数指针
        }
        return nullptr;
    }


    // 忽略 CCBSelectorResolver 的control回调绑定 
    cocos2d::extension::Control::Handler onResolveCCBCCControlSelector(
        cocos2d::Ref * pTarget, const char* pSelectorName) override
    {
        return nullptr;
    }

    // 被绑定到"onClick"的回调函数,类型是Ref::(*)(Ref*), 其中sender参数就是ccbi上的那个MenuItemImage的指针
    void onClicked(cocos2d::Ref *sender)
    {
        // sender 就是从ccbi上解析出来的那个MenuItemImage
        CCAssert(menuItemImage_ == sender, "sender should be the item binded");

        if (label_) 
        {
            label_->runAction(cocos2d::RotateBy::create(0.5, 360));
        }
    }

    // 构造,析构,注意初始化和release.
    CcbiHelloWorldEnhanced() : label_(nullptr), menuItemImage_(nullptr) {}
    ~CcbiHelloWorldEnhanced() 
    {
        CC_SAFE_RELEASE(label_);
        CC_SAFE_RELEASE(menuItemImage_);
    }

    cocos2d::Label          *label_;
    cocos2d::MenuItemImage  *menuItemImage_;
};

#endif //PLAYING_WITH_COCOS3D_PAGE_CCB_HELLOWORLD_ENHANCED_H

修改AppDelegate的applicationDidFinishLaunching方法:

auto node = CcbiHelloWorldEnhanced::create();
auto scene = Scene::create();
scene->addChild(node);
director->runWithScene(scene);

编译运行,可以看到如下效果, 并且CCAssert(menuItemImage_ == sender)也测试通过。

这个例子已经让ccbi的使用更充分了,游戏动了起来,而且也能绑定到ccbi上的变量了。但是有两个问题还很明显:

问题1:在绑定控件变量的时候onAssignCCBMemberVariable中,if, else的过度使用。

在这个ccbi上,只有两个控件,因此勉强可以接受上面例子那样挨个判断if来比较变量名字,而实际的开发中,ccbi不可能这么简单的,上百个控件都是有可能的, 难道要写100个if else? 另外,如果没个变量都想绑定,那么我要同时定义100个成员变量?它们还可能是各种不同类型。

问题2:同理,回调的绑定onResolveCCBCCMenuItemSelector也是一样,如果有10个MenuItemImage, 每个都有自己的回调名字,难道我要定义10个类似onClicked(Ref*)这样的回调函数吗, 在每个if else分支中返回一个?

下面着手解决这两个问题,不需要定义那么多成员变量,也不需要定义那么多回调函数,让ccbi的使用更简单、自然。

一个更加实用的ccbi使用方法, 避免频繁的if else.

解决问题1

使用关联容器保存【变量名】–【节点】的映射,用的时候拿变量名当做key来取

示意代码如下:

// 绑定的【变量名】---- 【节点】的映射, 用来获取ccbi上的控件
typedef std::unordered_map<std::string, cocos2d::Node*> NodeMap;
NodeMap     nodes_;

// 解析的时候
bool CcbiHelloWorldEnhanced::onAssignCCBMemberVariable(cocos2d::Ref* target, const char* memberVariableName, cocos2d::Node* node)
{
    std::string name(memberVariableName);

    node->retain();                         // strong reference to node, 强引用
    auto iter = nodes_.find(name);          // 是否已有同名变量
    if (iter == nodes_.end()) 
    {
        nodes_.insert({name, node});        // 放入哈希表
    }
    else
    {
        iter->second->release();            // 覆盖掉同名变量
        iter->second = node;
    }

    return true;                            // return true将告诉ccbi解析模块,我已处理完绑定操作,不需要再找别的解析器(绑定)了。
}

// 用的时候
void CcbiHelloWorldEnhanced::onClicked(Ref* sender)
{
    auto label = dynamic_cast<Label*>(nodes_.find("mLabel")->second);
}

这就是第一个问题的解决方案,后面会有完整的代码实例。

解决问题2

所有的回调都用同一个回调函数onMenuItemSelected(name, sender), 在回调函数onMenuItemSelected内部通过name来判断是哪个回调被调用。这个问题的关键在于,怎样使用关联容器保存【节点(MenuItemImage)】–【回调名】的映射

模仿第一个问题的解决方案,看是否可以在解析时绑定MenuItemImage和回调名. 对比一下CCBMemberVariableAssignerCCBSelectorResolver里的两个接口,就会发现,后者并没有提供一个回调接口传回正在解析的node:

class CC_DLL CCBMemberVariableAssigner {
public:
        // 三个参数:
        // target :是启动解析的那个类,比如HelloWorldEnhanced;
        // memberVariableName: 节点的名字
        // node: 要绑定的节点,会被从CCBReader回传回来
        virtual bool onAssignCCBMemberVariable(cocos2d::Ref* target, const char* memberVariableName, cocos2d::Node* node) = 0;

        // 不用看下面的暂时没用
        virtual bool onAssignCCBCustomProperty(cocos2d::Ref* target, const char* memberVariableName, const cocos2d::Value& value) { return false; };
};
class CC_DLL CCBSelectorResolver {
public:
    // 只有两个参数:
    // pTarget :是启动解析的那个类,比如HelloWorldEnhanced;
    // pSelectorName: 回调的名字
    // 没有第三个参数node!!!
    virtual cocos2d::SEL_MenuHandler onResolveCCBCCMenuItemSelector(cocos2d::Ref * pTarget, const char* pSelectorName) = 0;

    // 不用看下面的暂时没用
    virtual cocos2d::SEL_CallFuncN onResolveCCBCCCallFuncSelector(cocos2d::Ref * pTarget, const char* pSelectorName) { return NULL; };
    virtual cocos2d::extension::Control::Handler onResolveCCBCCControlSelector(cocos2d::Ref * pTarget, const char* pSelectorName) = 0;
};

这就使得第二个问题要比第一个问题要复杂一些,因为CCBReader在解析回调函数时,并没有把MenuItemImage传回CCBSelectorResolver的回调函数onResolveCCBCCMenuItemSelector中,这就导致我在绑定回调的时候,无法知道当前绑定的回调名称是跟哪个MenuItemImage关联的。因此在解析时,不能把【MenuItemImage】–【回调名】这种关联建立并保存起来;当MenuItemImage被点击的时候,才会把自己作为sender传递给回调函数中,此时回调函数虽然拿到了sender,可是并不知道这个sender对应着哪个回调名称, 也就是说无法做到使用一个回调函数处理来区别处理所有的回调事件。

因此,要解决第二个问题,就要像绑定变量名那样,让回调函数把MenuItemImage给传回来。于是我要在CCBSelectorResolver中添加一个接口,如下:

class CC_DLL CCBSelectorResolver {
public:
    virtual cocos2d::SEL_MenuHandler onResolveCCBCCMenuItemSelector(cocos2d::Ref * pTarget, const char* pSelectorName) = 0;
    // 新添加的回调
    virtual cocos2d::SEL_MenuHandler onResolveCCBCCMenuItemSelectorPassSender(cocos2d::Ref * pTarget, const char* pSelectorName, cocos2d::Ref* sender) 
    {return nullptr; }

    // 不用看下面的暂时没用
    virtual cocos2d::SEL_CallFuncN onResolveCCBCCCallFuncSelector(cocos2d::Ref * pTarget, const char* pSelectorName) { return NULL; };
    virtual cocos2d::extension::Control::Handler onResolveCCBCCControlSelector(cocos2d::Ref * pTarget, const char* pSelectorName) = 0;
};

新加的函数onResolveCCBCCMenuItemSelectorPassSender, 在onResolveCCBCCControlSelector的基础上添加了第三个参数sender,即MenuItemImage.

接下来要修改解析回调函数的地方:NodeLoader::parsePropTypeBlock

// 省略无关代码...

SEL_MenuHandler selMenuHandler = 0;

CCBSelectorResolver * targetAsCCBSelectorResolver = dynamic_cast<CCBSelectorResolver *>(target);

// 原来的回调解析, 不回传pNode, pNode即当前的MenuItemImage.
if(targetAsCCBSelectorResolver != nullptr)
{
    selMenuHandler = targetAsCCBSelectorResolver->onResolveCCBCCMenuItemSelector(target, selectorName.c_str());
}

// 新添加的回调,回传pNode. 如果第一个回调中返回了0 (nullptr), 那么就会从这里解析回调。
if (0 == selMenuHandler) 
{
    selMenuHandler = targetAsCCBSelectorResolver->onResolveCCBCCMenuItemSelectorPassSender(target, selectorName.c_str(), pNode);
}

上面这样修改之后,【MenuItemImage】—【回调名】的映射问题就解决了,下面是解析回调的示意代码:

// CCBSelectorResolver
// 让原来的回调解析返回nullptr, 从而让解析流程走到下面的`onResolveCCBCCMenuItemSelectorPassSender`
cocos2d::SEL_MenuHandler CcbiHelloWorldEnhanced::onResolveCCBCCMenuItemSelector(
        cocos2d::Ref * pTarget, const char* pSelectorName) override
{
    return nullptr;
}

cocos2d::SEL_MenuHandler CcbiHelloWorldEnhanced::onResolveCCBCCMenuItemSelectorPassSender(
        cocos2d::Ref * pTarget, 
        const char* pSelectorName, 
        cocos2d::Ref* sender) override
 {
     std::string actionName(pSelectorName);
     auto iter = actions_.find(sender);
     if (iter == actions_.end())
     {
         actions_.insert({ sender, actionName });           // 保存【MenuItemImage】---【回调名】映射
     }
     else
     {
         iter->second = actionName;                         // 同名的被覆盖
     }

     return CC_MENU_SELECTOR(CcbiHelloWorldEnhanced::onMenuItemEvent);     // 使用一个统一的回调函数,代替了N个回调函数。
 }

// 在统一的回调函数中,根据被点击按钮,查询到映射关系,得知是哪个回调名称对应的按钮被点击从而做出操作。
void CcbiHelloWorldEnhanced::onMenuItemEvent(cocos2d::Ref* target)
{
    auto iter = actions_.find(target);
    if (iter != actions_.end()) 
    {
        onMenuItemSelected(iter->second, target);
    }
}

void CcbiHelloWorldEnhanced::onMenuItemSelected(const std::string &actionName, cocos2d::Ref *target)
{
    if (actionName == "a")
    {
        // rotate.
    }
    else if (actionName == "b")
    {
        // jump.
    }
    else if (actionName == "....")
    {
        // ......
    }
}

这样就解决了第2个问题,使得ccbi的使用变得统一,简单。

两个问题解决后,最终的示例

思路:把解析ccbi的操作提取出来,封装成一个页面基类CCBPage,具体的页面继承这个基类就具有了ccbi解析的功能。同时在CCBPage中保存ccbi上的变量名和节点之间的映射,以及菜单按钮消息的绑定等映射,以便实现获取和控制ccbi界面元素, 监听按钮回调等功能。

下面是CCBPage的实现:

CCBPage.h : 解析ccbi的页面基类,其它逻辑界面继承CCBPage即拥有解析ccbi的功能。

#ifndef PLAYING_WITH_COCOS3D_PAGE_CCBPAGE_H
#define PLAYING_WITH_COCOS3D_PAGE_CCBPAGE_H

#include "pages/SuperPage.h" // 暂时忽略,是作者的小游戏框架里的东西
#include "util/StateMachine.h" // 暂时忽略,是作者的小游戏框架里的东西
#include "LogicDirector.h" // 暂时忽略,是作者的小游戏框架里的东西

#include "cocos2d.h"
#include "cocosbuilder/CCBMemberVariableAssigner.h"
#include "cocosbuilder/CCBSelectorResolver.h"
#include <unordered_map>
#include <string>

class PageManager;                                      // 忽略

class CCBPage 
    : public SuperPage                                  // 忽略
    , public cocosbuilder::CCBMemberVariableAssigner    // 用来绑定从ccbi解析出来的 【变量名】---- 【节点】之间的映射
    , public cocosbuilder::CCBSelectorResolver          // 用来绑定ccbi中菜单和UI控件按钮的【节点】----【回调名称】之间的映射
{
    // 与ccbi解析无关,忽略
    friend PageManager;

public:

    // 与ccbi解析无关,忽略
    void loadUI() override {};
    void unloadUI() override {};

    // 页面基类定义的解析ccbi操作
    void loadFromCcbi(const std::string &ccbi);

    // 获取ccbi上节点的模板函数, 比如ccbi上节点叫: "mLabel", 类型是Label, 那么
    // 使用 auto label = getCcbiChild<Label>("mLabel")就会获取到ccbi上的label.
    template <typename ChildType = cocos2d::Node>
    ChildType* getCcbiChild(const std::string &name);

    // CCBMemberVariableAssigner定义的回调函数,在ccbi解析过程中,由ccbi解析模块里的代码回调到此处,重写此方法,以实现自己的绑定逻辑.
    bool onAssignCCBMemberVariable(cocos2d::Ref* target, const char* memberVariableName, cocos2d::Node* node) override;

    // CCBSelectorResolver定义的回调函数,用来绑定Menu和Control上的回调函数,由ccbi解析模块里的代码回调到此处,重写此方法,以实现自己的绑定逻辑.
    // 自己添加的回调解析函数,回传被解析的MenuItemImage: sender.
    cocos2d::SEL_MenuHandler onResolveCCBCCMenuItemSelectorPassSender(
            cocos2d::Ref * pTarget, 
            const char* pSelectorName, 
            cocos2d::Ref* sender) override;

    // 让原来的回调返回nullptr, 以使解析逻辑走到onResolveCCBCCMenuItemSelectorPassSender.
    cocos2d::SEL_MenuHandler onResolveCCBCCMenuItemSelector(cocos2d::Ref * pTarget, const char* pSelectorName) override
    {
        return nullptr;
    }
    cocos2d::extension::Control::Handler onResolveCCBCCControlSelector(cocos2d::Ref * pTarget, const char* pSelectorName) override
    {
        return nullptr;
    }

    // ccbi上按钮的回调函数统一路由到此处,CCBPage的子类重写此函数,并根据actionName来判断区分按钮点击事件
    virtual void onMenuItemSelected(const std::string &actionName, cocos2d::Ref *target);

    // 清空已经绑定的【变量名】---- 【节点】, 【节点】----【回调名称】的映射。
    void clear();

protected:
    // 绑定到ccbi上菜单上的回调函数
    void onMenuItemEvent(cocos2d::Ref* target);

    CCBPage();
    virtual ~CCBPage();

private:

    // 绑定的【变量名】---- 【节点】的映射, 用来获取ccbi上的控件
    typedef std::unordered_map<std::string, cocos2d::Node*> NodeMap;
    NodeMap     nodes_;

    // 绑定的【节点】----【回调名称】的映射, 用来实现菜单和Control按钮的回调
    typedef std::unordered_map<cocos2d::Ref*, std::string> ActionMap;
    ActionMap   actions_;
};

// 获取ccbi上节点的模板函数的实现
template <typename ChildType>
ChildType* CCBPage::getCcbiChild(const std::string &name)
{
    auto iter = nodes_.find(name);
    if (iter != nodes_.end()) 
    {
        return dynamic_cast<ChildType*>(iter->second);
    }
    return nullptr;
}

#endif //PLAYING_WITH_COCOS3D_PAGE_CCBPAGE_H

CCBPage.cpp: 解析ccbi的页面基类实现

#include "pages/CCBPage.h"
#include "cocosbuilder/CocosBuilder.h"
#include "cocosbuilder/CCBReader.h"

USING_NS_CC;
USING_NS_CC_EXT;
using namespace cocosbuilder;

CCBPage::CCBPage() { }

CCBPage::~CCBPage()
{
    clear();
}

// 解析ccbi操作的封装, 过程同HelloWorld中示例一样
void CCBPage::loadFromCcbi(const std::string &ccbi)
{
    // 第一步: 创建一个NodeLoaderLibrary
    auto loaderLib = NodeLoaderLibrary::newDefaultNodeLoaderLibrary();

    // 第二步: 创建CCBReader
    auto ccbReader = new CCBReader(loaderLib, this);

    // 第三步: 调用CCBReader的readNodeGraphFromFile的方法,传入ccbi名字
    auto node = ccbReader->readNodeGraphFromFile(ccbi.c_str(), this);
    ccbReader->release();

    // 解析完毕,可以使用Node了。
    if (node != NULL)
    {
        addChildRaw(node);      // addChildRaw是作者的小游戏框架里定义的操作,功能同addChil()
    }
}

// 绑定ccbi上的【变量名】--- 【节点】间的映射。
// 参数说明:
// target: this
// memberVariableName: ccbi上分配给该node的名字
// node: ccbi上的node。
bool CCBPage::onAssignCCBMemberVariable(cocos2d::Ref* target, const char* memberVariableName, cocos2d::Node* node)
{
    if (target != this) 
    {
        return false;
    }

    std::string name(memberVariableName);

    node->retain();                         // strong reference to node, 强引用
    auto iter = nodes_.find(name);          // 是否已有同名变量
    if (iter == nodes_.end()) 
    {
        nodes_.insert({name, node});        // 放入哈希表
    }
    else
    {
        iter->second->release();            // 覆盖掉同名变量
        iter->second = node;
    }

    return true;                            // return true将告诉ccbi解析模块,我已处理完绑定操作,不需要再找别的解析器(绑定)了。
}

// 绑定ccbi上的菜单按钮回调, 保存【节点】---【回调名称】之间的映射
// 参数说明:
// pTarget : this
// pSelectorName: 回调名称
// sender: 正在被解析到的节点,MenuItemImage
cocos2d::SEL_MenuHandler CCBPage::onResolveCCBCCMenuItemSelectorPassSender(
    cocos2d::Ref * pTarget, const char* pSelectorName, cocos2d::Ref* sender)
{
    if (pTarget != this)
    {
        return false;
    }

    std::string actionName(pSelectorName);
    auto iter = actions_.find(sender);
    if (iter == actions_.end())
    {
        actions_.insert({ sender, actionName });        // 保存【MenuItemImage】---【回调名】到哈希表
    }
    else
    {
        iter->second = actionName;                      // 覆盖MenuItemImage相同的映射
    }

    return CC_MENU_SELECTOR(CCBPage::onMenuItemEvent);  // 使用一个统一的回调函数,代替了N个回调函数。
}

// 被绑定函数的回调函数
void CCBPage::onMenuItemEvent(cocos2d::Ref* target)
{
    auto iter = actions_.find(target);
    if (iter != actions_.end()) 
    {
        onMenuItemSelected(iter->second, target);       //分发给onMenuItemSelected处理
    }
}

// 真正的按钮回调函数, 子类重写这个函数,以实现响应回调事件处理
void CCBPage::onMenuItemSelected(const std::string &actionName, cocos2d::Ref *target)
{
    CCLOG("warning: default onMenuItemSelected called, actionName: %s\n", actionName.c_str());
}

// 清空绑定的变量映射
void CCBPage::clear()
{
    actions_.clear();
    for (auto &item : nodes_) 
    {
        item.second->release();
    }
    nodes_.clear();
}

定义完毕,下面使用CCBPage来创建一个页面,来测试一下它的ccbi解析、变量绑定、按钮回调绑定的效果:

CCBTestPage.h : 模拟具体的逻辑页面,继承CCBPage, 以实现ccbi解析等功能。

#ifndef PLAYING_WITH_COCOS3D_CCBPAGE_TEST_H
#define PLAYING_WITH_COCOS3D_CCBPAGE_TEST_H

// 忽略掉无关包含文件...., 仅需关心CCBPage
#include "pages/CCBPage.h"

class RootPage;

class CCBTestPage 
    : public CCBPage            // 继承CCBPage
    , public State<RootPage>
{

public:
    // create 方法
    CREATE_FUNC(CCBTestPage);

    // 重写CCBPage的方法,实现按钮消息回调
    void onMenuItemSelected(const std::string &actionName, cocos2d::Ref *target)
        override;

    // 忽略
    void loadUI() override;
    void unloadUI() override;

    // 忽略
    void onEnterState() override;
    void onExecuteState() override {};
    void onExitState() override;

protected:
    CCBTestPage();
    ~CCBTestPage();
};

#endif //PLAYING_WITH_COCOS3D_CCBPAGE_TEST_H

CCBTestPage.cpp:具体逻辑页面的实现, 模拟具体的逻辑页面,继承CCBPage, 以实现ccbi解析等功能。

#include "pages/CCBTestPage.h"
#include "PageManager.h"
USING_NS_CC;

// 注册游戏界面,忽略
bool ccb_test_page_created = PageManager::getInstance()->registerPage(
    "CCBTestPage", CCBTestPage::create());

CCBTestPage::CCBTestPage() { }

CCBTestPage::~CCBTestPage() { }

// 测试跑起来,先调用这个方法
void CCBTestPage::onEnterState()
{
    loadUI();                       // 加载界面
}

// 页面退出,会调用这个方法
void CCBTestPage::onExitState()
{
    unloadUI();                     // 卸载界面
}

void CCBTestPage::loadUI()
{
    loadFromCcbi("ccbi/hello.ccbi");       // 调用父类CCBPage的ccbi解析操作
}

void CCBTestPage::unloadUI()
{
    removeAllChildren();
}

// 重写的父类方法,监听ccbi上按钮的回调
void CCBTestPage::onMenuItemSelected(const std::string &actionName, cocos2d::Ref *target)
{
    if ("onClick" == actionName)    // 回调名称 "onClick"就是ccbi上指定的selector,
    {
        auto label = getChild<Label>("mLabel");     // 使用父类CCBPage的getChild方法,获取ccbi上的变量节点
        if (label) 
        {
            label->runAction(RotateBy::create(0.5, 360));   // 控制该节点
        }
    }
}

运行效果如图所示:

从最后这个CCBTestPage的例子可以看出,在封装了CCBPage之后,ccbi的使用就很简单了:

1. 继承CCBPage, 在初始化的时候,调用父类接口loadFromCcbi(ccbiName)来加载界面

2. 如果对按钮回调感兴趣,就重写onMenuItemSelected来处理回调,通过参数actionName来区分哪个点击事件。

3. 如果要控制界面上某个元素,通过父类接口getCcbiChild<Type*>(name)来获取。

不需要自己定义成员变量,也不需要定义回调函数(重写父类的onMenuItemSelected).

ccbi解析过程分析

最后简要描述一下ccbi的解析过程,结合这源码来看不是很难理解。

几个关键的类

  1. NodeLoaderLibraryNodeLoader(及其子类)

  2. CCBReader

  3. CCBMemberVariableAssignerCCBSelectorResolver

它们的协作关系是,

  1. CCBReader控制整个解析的过程,包括读ccbi头、读ccbi版本号、提取ccbi中的字符串变量、读ccbi序列帧,以及最关键的,(递归)读取ccbi中的节点结构树。

  2. NodeLoaderLibrary维护了一系列的NodeLoader及其子类,用来创建各种NodeLoader,根据ccbi上的节点类型,实现类似反射的效果,比如ccbi上有个CCLabelTTF(cocosbuilder很久就不更新了,因此其中的节点类型还是带CC前缀的),那么NodeLoaderLibrary就会从其内部的library中找到LabelTTFLoader来实现解析操作;ccbi上碰到了CCMenuItemImage, 那么NodeLoaderLibrary就根据“CCMenuItemImage”找到MenuItemImageLoader来解析节点。。。; NodeLoader是在读取节点结构树的时候被CCBReader用来创建具体类型的节点,同时用来解析、设置该节点的一些属性。CCBReader会根据节点的类型名称到NodeLoaderLibrary找到具体的CCNodeLoader(子类)来完成实际的解析工作。

  3. CCBMemberVariableAssignerCCBSelectorResolver: CCBMemberVariableAssigner作用是在读取节点结构树的时候被CCBReader用来接收CCBReader解析出来的节点名字和节点对象指针。如上例中的“mLable”。要保存mLabel—>Label 这对映射的话,需要在CCBReaderTestScene的onAssignCCBMemberVariable方法里做相应操作,比如放入一个哈希表; 其解析过程在CCBReader::readNodeGraph函数里可以看到。CCBSelectorResolver的作用与此类似,是被用来绑定回调, 其解析过程在NodeLoader::parsePropTypeBlock方法里。由于CCBPage继承了CCBMemberVariableAssigner和CCBSelectorResolver, 所以它能被CCBReader用来解析绑定节点和回调。具体代码在CCBReader.cpp里可以看到。

下图给出了几个相关类的协作关系:

【cocos2d-x 3.x 学习与应用总结】2: 在cocos2d-x中使用ccbi_第4张图片

本文中的cocosbuilder下载地址

  • cocosbuilder v3.0

源码

  • CcbiHelloWorld.h

  • CcbiHelloWorldEnhanced.h

  • CCBPage.h

  • CCBPage.cpp

  • CCBTestPage.h

  • CCBTestPage.cpp

  • 修改后的CCBSelectorResolver

  • 修改后的CCNodeLoader

  • Resources

作者水平有限,对相关知识的理解和总结难免有错误,还望给予指正,非常感谢!

欢迎访问github博客,与本站同步更新

你可能感兴趣的:(cocos2d-x,ccbi)