版权声明:本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名elloop(包含链接)
#前言
本文以3.9版本的cocos2d-x为例,总结了如何在代码中解析、加载ccbi文件。给出一个最简单的使用ccbi实现的helloworld的例子、一个加强版的HelloWorld示例、以及一个最贴近实际使用情况的ccbi使用示例, 并结合示例分析ccbi的解析过程。
官方示例程序
ccbi功能支持的源代码
cocos对ccbi的支持是在extensions这个模块里面,以v3.9为例,解析ccbi的代码的物理路径是放在cocos2d/cocos/editor-support/cocosbuilde这个路径下面,在vs的解决方案管理器中如下图所示:
官方示例源代码
cocos官方的ccbi使用示例代码是在(vs中):TestCpp/ExtensionsTest/CocosBuilderTest这个分组下面,
其中CocosBuilderTest.cpp的实现如下,init方法是解析加载ccbi的关键代码:
示例代码中包括了动画、UI按钮、菜单、标签、粒子、Scrollview等相关示例,这里不贴代码了,需要的话去cpptests工程里看就行了。
下面以我的实际使用经验总结一下在实际项目中该如何使用ccbi,先从一个HelloWorld说起。
使用ccbi的HelloWorld
使用cocosbuilder创建一个简单的ccbi
在cocosbuilder中新建一个hello.ccb, 注意:不要勾选js controll.(在Document菜单项下最后一个选项)
添加一个Label,名字叫mLabel:
添加一个MenuItemImage名字叫mBtn, selector名字叫onClick.:
保存,发布为hello.ccbi.
创建一个最简单的class,来使用创建出来的ccbi文件:
CcbiHelloWorld.h: 定义了一个class继承自Node,用来挂载ccbi
在AppDelegate的applicationDidFinishLaunching()方法结尾处,添加启动代码:
编译运行,一个最简单的使用ccbi的例子就跑起来了,如下图所示:
从上面编写HelloWorld的过程可以看出,要想把ccbi当做节点添加到界面是很简单的事情,从CcbiHelloWorld类的init方法中看到, 几句关键代码加起来不超过10行。
不过,这个例子仅仅是把cocosbuilder当做了画板来用,所有的东西加到界面上都是死的, 点按钮也没有反应,我不能获取某个节点,做一些动作和属性修改的操作。有点像静态网站和动态网站的区别,游戏又不是美术作品,肯定是要动起来的。因此我需要能够获取ccbi界面上的元素,并且对它做一些操作。下面的第二个例子是展示了如何绑定ccbi上的变量和menu回调。
绑定ccbi上的变量和回调, 加强版HelloWorld
在上一个例子的基础上,来实现加强版HelloWorld, CcbiHelloWorldEnhanced
CcbiHelloWorldEnhanced.h:
修改AppDelegate的applicationDidFinishLaunching方法:
编译运行,可以看到如下效果, 并且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来取
示意代码如下:
这就是第一个问题的解决方案,后面会有完整的代码实例。
解决问题2
所有的回调都用同一个回调函数onMenuItemSelected(name, sender), 在回调函数onMenuItemSelected内部通过name来判断是哪个回调被调用。这个问题的关键在于,怎样使用关联容器保存【节点(MenuItemImage)】--【回调名】的映射
模仿第一个问题的解决方案,看是否可以在解析时绑定MenuItemImage和回调名. 对比一下CCBMemberVariableAssigner
和CCBSelectorResolver
里的两个接口,就会发现,后者并没有提供一个回调接口传回正在解析的node:
这就使得第二个问题要比第一个问题要复杂一些,因为CCBReader在解析回调函数时,并没有把MenuItemImage传回CCBSelectorResolver的回调函数onResolveCCBCCMenuItemSelector
中,这就导致我在绑定回调的时候,无法知道当前绑定的回调名称是跟哪个MenuItemImage关联的。因此在解析时,不能把【MenuItemImage】–【回调名】这种关联建立并保存起来;当MenuItemImage被点击的时候,才会把自己作为sender传递给回调函数中,此时回调函数虽然拿到了sender,可是并不知道这个sender对应着哪个回调名称, 也就是说无法做到使用一个回调函数处理来区别处理所有的回调事件。
因此,要解决第二个问题,就要像绑定变量名那样,让回调函数把MenuItemImage给传回来。于是我要在CCBSelectorResolver
中添加一个接口,如下:
新加的函数onResolveCCBCCMenuItemSelectorPassSender
, 在onResolveCCBCCControlSelector
的基础上添加了第三个参数sender,即MenuItemImage.
接下来要修改解析回调函数的地方:NodeLoader::parsePropTypeBlock
上面这样修改之后,【MenuItemImage】—【回调名】的映射问题就解决了,下面是解析回调的示意代码:
这样就解决了第2个问题,使得ccbi的使用变得统一,简单。
两个问题解决后,最终的示例
思路:把解析ccbi的操作提取出来,封装成一个页面基类`CCBPage`,具体的页面继承这个基类就具有了ccbi解析的功能。同时在`CCBPage`中保存ccbi上的变量名和节点之间的映射,以及菜单按钮消息的绑定等映射,以便实现获取和控制ccbi界面元素, 监听按钮回调等功能。
下面是CCBPage
的实现:
CCBPage.h : 解析ccbi的页面基类,其它逻辑界面继承CCBPage
即拥有解析ccbi的功能。
CCBPage.cpp: 解析ccbi的页面基类实现
定义完毕,下面使用CCBPage来创建一个页面,来测试一下它的ccbi解析、变量绑定、按钮回调绑定的效果:
CCBTestPage.h : 模拟具体的逻辑页面,继承CCBPage, 以实现ccbi解析等功能。
CCBTestPage.cpp:具体逻辑页面的实现, 模拟具体的逻辑页面,继承CCBPage, 以实现ccbi解析等功能。
运行效果如图所示:
从最后这个CCBTestPage的例子可以看出,在封装了CCBPage之后,ccbi的使用就很简单了:
1. 继承CCBPage, 在初始化的时候,调用父类接口loadFromCcbi(ccbiName)来加载界面
2. 如果对按钮回调感兴趣,就重写onMenuItemSelected
来处理回调,通过参数actionName来区分哪个点击事件。
3. 如果要控制界面上某个元素,通过父类接口getCcbiChild<Type*>(name)
来获取。
不需要自己定义成员变量,也不需要定义回调函数(重写父类的onMenuItemSelected).
#ccbi解析过程分析
最后简要描述一下ccbi的解析过程,结合这源码来看不是很难理解。
几个关键的类
-
NodeLoaderLibrary和NodeLoader(及其子类)
-
CCBReader
-
CCBMemberVariableAssigner和CCBSelectorResolver
它们的协作关系是,
-
CCBReader控制整个解析的过程,包括读ccbi头、读ccbi版本号、提取ccbi中的字符串变量、读ccbi序列帧,以及最关键的,(递归)读取ccbi中的节点结构树。
-
NodeLoaderLibrary维护了一系列的NodeLoader及其子类,用来创建各种NodeLoader,根据ccbi上的节点类型,实现类似反射的效果,比如ccbi上有个CCLabelTTF(cocosbuilder很久就不更新了,因此其中的节点类型还是带CC前缀的),那么NodeLoaderLibrary就会从其内部的library中找到LabelTTFLoader来实现解析操作;ccbi上碰到了CCMenuItemImage, 那么NodeLoaderLibrary就根据“CCMenuItemImage”找到MenuItemImageLoader来解析节点。。。; NodeLoader是在读取节点结构树的时候被CCBReader用来创建具体类型的节点,同时用来解析、设置该节点的一些属性。CCBReader会根据节点的类型名称到NodeLoaderLibrary找到具体的CCNodeLoader(子类)来完成实际的解析工作。
-
CCBMemberVariableAssigner和CCBSelectorResolver: CCBMemberVariableAssigner作用是在读取节点结构树的时候被CCBReader用来接收CCBReader解析出来的节点名字和节点对象指针。如上例中的“mLable”。要保存mLabel—>Label 这对映射的话,需要在CCBReaderTestScene的onAssignCCBMemberVariable方法里做相应操作,比如放入一个哈希表; 其解析过程在CCBReader::readNodeGraph
函数里可以看到。CCBSelectorResolver的作用与此类似,是被用来绑定回调, 其解析过程在NodeLoader::parsePropTypeBlock
方法里。由于CCBPage继承了CCBMemberVariableAssigner和CCBSelectorResolver, 所以它能被CCBReader用来解析绑定节点和回调。具体代码在CCBReader.cpp里可以看到。
下图给出了几个相关类的协作关系:
本文中的cocosbuilder下载地址
源码
欢迎访问github博客,与本站同步更新