您的位置:首页 > 移动开发 > Cocos引擎

Cocos2d-x 之 CCGUI设计与实现

2016-05-28 09:22 232 查看


序言

cocos2d-x使用有半年多时间,想来去年这时候还在写游戏服务器程序,不得不感慨变化很大。这半年时间对2d游戏客户端开发也越来越熟悉,视野也变得更加开阔,就拿游戏的UI开发来说,最早学习cocos2d-x的时候,是在代码里面写UI,后来写了一个简单的UI编辑器,用在一个音乐游戏中,效果还可以;再后来接触cocosbuilder,效率就更高了。

过些天去新公司,项目是做一个ARPG游戏,对UI的要求更高,cocosbuilder也显得力不从心,端游里面,用CEGUI和mygui的很多,而且这两者都非常强大,不过它们和ogre联系太紧密,如果想在cocos2d-x里面使用此二者,要做的工作量会比较大,我目前已经将CEGUI的源代码移植到cocos2d-x的ios环境中,编译链接没问题,然而却需要写一个ogre的渲染接口,由于对glsles语言不是很熟,一时就放下了。

然而也因此产生了一个想法,那就是采用cocos2d-x作为底层实现,提供一套GUI接口以及与之兼容的cocosbuilder、cegui、mygui的layout解析接口扩展。

想到了就能做到,我花了大概一天的时间写了一个简单而自认为比较好的雏形,暂时命名为CCGUI,这个雏形的标准如下:
采用树状结构,所有GUI控件继承自Widget类,该Widget类派生自TouchNode类,TouchNode派生自CCNode和CCTargetedTouchDelegate,TouchNode的作用是注册和销毁touch事件。

Widget是一个具有touch功能的矩形区域,虽然派生自CCNode,但是Widget系统内部有自己的树结构,这样没有依赖CCNode的一些树结构属性,而是自成一家。
解决触摸优先级问题,所见即所得,显示在上层的Widget优先级更高,用户不用自己设置优先级。
解决触摸后窗口前置问题,Widget在接受touch事件后处于激活状态,可以将窗口前置到最上层,并提供窗口层次关系调整的接口。

“2)”和“3)”结合在一起能够完成cegui这种GUI所能完成的最重要的窗口与窗口之前的层次问题,这也是本雏形核心部分之一。
touch事件在底层设计上脱离CCTargetedTouchDelegate的控制,而完全是另外一种机制,这种机制是这样的:根节点root由WidgetManager创建时产生,设置root的触摸优先级为CCGUI的最低优先级,普通GUI控件的优先级均为Normal级别,这样root会在CCGUI的节点里面最后响应touch事件,所有具有touchable属性的GUI节点都会响应ccTouchBegin事件,在响应时会向WidgetManager注册自己,在root节点响应时,计算当前注册的哪个GUI节点获得焦点,此时才真正调用该GUI节点的touch事件,这里不妨说成是二次响应。
整个设计干净利落,没有多余的东西,GUI层的节点非常易于控制与touch回调,Widget类进一步组合与派生,非常容易支持TextBox、ScrollView、ItemBox等控件。
采用string类型name为唯一id,虽然是字符串,但是优化下效率应该不会成为瓶颈,好处是非常便于开发者调用。

目前已经完成了几个简单的控件TextBox、ScrollView、ListBox等,支持CocosBuilder的ccb文件解析,明天再花一天时间应该能够将之前的cocosbuilder UI结构全部替换成这套新的雏形结构。

现在我将这部分代码开源出来(https://github.com/dansen/ccgui),希望对cocos2d-x GUI系统实现感兴趣的同学有所帮助。

CCGUI有志于成为cocos2d-x的cegui、mygui的解析框架,这样不用花精力去写编辑器,嘎嘎!!

这里说要支持cegui、mygui自然有些自夸,仅仅算一个愿景吧^_^。

最后吐槽下cocosbuilder的几个问题:

(1)通过tag得到节点,是一个比较头疼得问题,如果是多层树结构,必须记住每个层次的tag,可读性很差,用起来比较麻烦。

(2)一直觉得CCMenu是个傻乎乎的设计,最蛋疼的是CCMenuItemImage不能添加子节点(尼玛我要在这上面加点文字、图片啥的折腾死)。

(3)支持控件少,适合小游戏开发,给了一个ScrollView,没有列表滚动条等控件,不过话说回来,国外大部分游戏的控件并不复杂,很多情况下都是我们自己弄复杂了。

(4)编辑器本身不容易与脚本结合,C++里面加一个touch回调不容易,在lua里面注册回调也不容易。


RichTextBox的实现(支持多颜色、表情、超链接)

cocos2d-x提供了基本CCLabelTTF用于文本的绘制,文本底层的实现流程是这样的,字符串string先用本地API转化成CCImage,这一步依赖平台接口,然后再由CCImage转化成CCTexture2D纹理,进而用于文本的渲染。

那么如何通过CCLabelTTF等字体类实现一个RichTextBox,支持多颜色、换行、表情(图片)、超链接呢?

我们不打算从OpenGL底层来做这个工作,因为那样工作量非常大,不现实,因此考虑在已有的cocos2d-x接口上进行处理,来组合出我们需要富文本。

由于3年前用过windows GDI底层接口做过一个彩色文本编辑器,对于拼接问题也是手到擒来,甚至感觉这个相对还要简单很多。

先分解工作及流程:

(1)      多颜色:多个不同颜色的CCLabelTTF拼在一起就是一个多颜色文本了。

(2)      换行:这个相对棘手,换行需要考虑当前RichTextBox的高度和宽度,如果当前行的字符数已满,需要被动换行,另外如果有\n换行符,需要主动换行。

(3)      表情:表情即图片或动画,实现起来比文字要简单。

(4)      超链接:由于采用CCGUI系统里面的TextBox作为基本拼接元,本身就具有接受点击的潜质,只需要设置是否可触摸即可。

(5)      文本里面每个字符的宽度都需要精确计算,而计算的代价比较大(PC上一个字符计算的时间代价大概为1ms),但由于字符数一定,常用中英文字符也就几千个而已,因此可以在游戏运行时做缓存,这样能够极大提高效率。缓存算法不是一次性计算所有字符,而是每帧计算若干个字符,这样能够保证游戏的流畅性,加载可以放在游戏运行时进行。

这里详细说明一下换行的实现思路,因为这是这个控件的比较麻烦的地方,弄清楚后其他的都好说。

对于新加(append)进RichTextBox的文本,首先计算该文本的像素宽度(width),宽度计算出来之后,逻辑伪代码如下:
If 插入行剩余宽度 >= width then
         直接插入文本
更新插入位置(x,y)
Else
         计算插入行能插入多少字符
         填满插入行
         计算剩余宽度
         While(有剩余)
                  If 剩余宽度 > RichTextBox.widththen
                            填满本行
                            计算剩余宽度
                   Else
                            直接插入文本
                            更新插入位置
                   End
End
End


效果图如下:

[caption id="attachment_4440" align="alignnone" width="500"]

 效果图(1)[/caption]

[caption id="attachment_4441" align="alignnone" width="300"]

 效果图(2)[/caption]

看起来不是很复杂,但是注意的细节还是比较多的,首先是文本必须是utf8格式,windows默认是gbk,需要转换(采用cocos2d-x的附带的第三方iconv库),转换之后需要计算该文本里面一个utf8字符占了多少char字符,通常一个utf8格式的汉字占用3个char字符;另外一个是计算每个utf8字符的像素宽度,这个我是直接用CCLabelTTF来计算,如果自己写计算宽度的API,也是需要依赖平台的,如windows平台可能要用到GetCharWidth32等     API,再换到IOS,又是另外的计算方法,这个工作量也比较大,希望后面能有人整理出来,不过现在我们就偷偷懒,而且基于上面第(5)点所说的优化方案,其实没有可担忧的。

来新公司后,用了这套GUI设计方案,由于不再属于个人作品,而是严肃的商业代码,希望后面能够开源。

目前模仿CEGUI写完了所有的基本控件(一个星期时间),在实现上也参考了很多CEGUI的代码,这样能够在很大程度上与方家的API保持一致。

博客里面会大致讲解比较麻烦的控件实现方式,如模仿IOS系统GUI的NumberPicker,以及有翻页功能的ScrollView,各位看官,博客比代码慢很多,还请见谅。


循环列表框的实现(数字选择器)

我们知道,cocos2d-x扩展里面提供了一个CCScrollView的实现,这东西写的还不错,大致模拟了系统的那种滑动效果,并有裁剪的效果。对于数字选择器(NumberPicker)控件,我们首先需要在CCSrollView的基础上实现一个列表框(ListBox),列表框的实现不会太难,博文里面就不详细讲了。

ListBox提供几个简单的接口,addItem,removeItem等,那么想想该如何让这个ListBox在拖动到最顶或者最底时自动循环呢?

如果你的思路局限在如何往ListBox添加需要的元素以接着让ListBox能够继续滚动的话,那估计就比较难实现一个高效的循环列表了。

在实现循环列表之前,我首先想到的实现一个平衡列表——列表框的中心部分作为平衡锚点,如果列表上移或者下移,导致列表不再平衡,则调整列表元素位置,使之保持平衡状态。

对于循环列表,如果往下移,每下移一格作为非平衡的判定条件,此时将顶部元素移动到最下部,并重置ScrollView的offset。

数字选择器的设计的伪代码大致如下:
初始化:
blanceListBox:addItems(0,…,9)
为每个元素设置位置;

当移动一格时:
If direction == UP then
将起始元素移到最后一个元素的位置;
将后面的元素依次往上移动一个位置;
Else
将最后一个元素移到起始元素的位置;
将前面的元素依次往下移动一个位置;
end
重置scroll view的offset为起始状态;


由于这种设计不涉及元素(精灵、纹理)的增删操作,只是在某一帧设置元素的位置,效率是非常高的,最初在设计时有些担心重置元素时存在抖动问题,但最终显示效果很平滑,so,that’s great!

效果图如下:





分页列表框的实现(仿IOS图标分页列表)

本篇主要介绍IOS应用图标桌面的分页效果设计,这种控件主要用于RPG等游戏的背包部分,算是很常用的了。

分页列表框暂且命名为GridBox,继承自ScrollView,拥有滚动属性和分页属性,这里简单讨论后者的实现思路。

cocos2d-x扩展提供的CCScrollView支持滑动,滑动的具体实现是这样的:

(1)  touch begin时初始化滑动的距离为0;

(2)  touch move时改变content的offset,使之随手指移动;

(3)  touch end时计算手指的滑动距离,并以此距离作为起始速度,启动schedule开始做减速运动;

(4)  schedule在速度降为SCROLL_DEACCEL_DIST以下时停止运动;

由上可知,CCScrollView并没有直接计算content的偏移量,而是根据起始速度做减速运动,而如果要让CCScrollView支持分页,输入必须是页的宽度,每次偏移为一个页宽度,所以需要提前计算content的偏移量。

由速度可以计算出偏移量,由偏移量可以计算出速度,让我们为CCScrollView添加两个函数,一个是根据距离计算速度的函数,一个是根据速度计算距离的函数。
//通过距离计算速度  
    virtual cocos2d::CCPoint calculateSpeed(cocos2d::CCSize distance);  
    //通过速度计算距离  
    virtual cocos2d::CCSize calculateDistance(cocos2d::CCPoint speed);


有了这两个函数(事实上我们只需要一个),我们就可以轻松的实现分页了,哦,忘了还有个比较重大的事情,CCScrollView的回调需要大改!
class CCGUIScrollViewDelegate  
{  
public:  
    virtual ~CCGUIScrollViewDelegate() {}  
    //CCMove动作停止  
    virtual void scrollViewDidAnimateStop(CCGUIScrollView* view) {};  
    //滚动结束  
    virtual void scrollViewDidScrollStop(CCGUIScrollView* view) {};  
    //CCMove动作开始  
    virtual void scrollViewDidAnimateStart(CCGUIScrollView* view) {};  
    //滚动开始  
    virtual void scrollViewDidScrollStart(CCGUIScrollView* view) {};  
    //重置  
    virtual void scrollViewDidReset(CCGUIScrollView* view) {};  
    //滚动中  
    virtual void scrollViewDidScroll(CCGUIScrollView* view) {};  
    //缩放操作  
    virtual void scrollViewDidZoom(CCGUIScrollView* view) {};  
    //是否移动了给定的一个偏移值  
    virtual void scrollViewDidMoveSegment(CCGUIScrollView* view, bool up) {};  
};


如上,重点添加了滚动开始结束回调和Animate的开始结束回调,在滚动开始时计算速度,然后覆盖之前手指产生的速度,这样不管手指怎么滑动,最多也就滑动一页。

这里有两个小地方需要注意,首先是scroll view的运动行为,scroll view首先通过schdule来进行减速运动,运动结束之后,可能会执行CCMoveTo这个acon,也就是说,可能产生Animate的回调。

其次在手指touch end时,会有一段偏移,这个偏移需要考虑在内,以便进一步计算scroll view接下来需要移动的差值,另外为了不让scoll view在翻页的中间过程中发生回弹,可能需要做进一步的差值处理。

这里不详细描述算法过程,具体实现还是需要注意很多细节的呵。

参考:

(1)cocosd-x的CCScrollView源代码;


树形控件的实现

树形控件是GUI系统中较为复杂的一种控件,用途非常广泛,本篇不对该控件作详细描述,仅针对游戏中应用的树形作一个分析,cocos2d-x本身不对该控件提供任何支持,这里也不可能写一个类似于操作系统层面的复杂控件。

控件需求:

(1)支持折叠,点击父节点,子节点可以折叠或展开

(2)支持滑动,树形控件的背景层是一个scroll view,根据树形控件的大小自动调整大小。

当然,基础组件还是本系列的Widget和ScrollView,在写到本篇的时候,Widget和ScrollView相较于最初的雏形,已经更加成熟和稳定,因为大部分控件已经大量用于复杂的游戏系统之中。

实现思路:
数据层和显示层分离,数据层是一个树形的数据结构,这是计算机课程里面的基本结构,这里就不作任何描述了;
每次添加新的节点,数据层先更新,然后更新显示层,也即数据层是显示层的输入;
显示层相对麻烦,较为困难的是正确计算节点的位置,本篇重点讨论这个问题。

控件图例:




抽象图如下:




为了更加清晰的计算所有节点的位置,这里定义几个符号,用于后面的描述。

节点i本身的高度为,节点i包括其孩子的总高度为,节点i所在位置(从上往下)定义为,如上抽象图,从上往下依次为节点1,2,3,4,5,6,那么有:





依此类推,




A的计算比较简单,依次将字节几点的A相加,最后加上自己的高度即可。注意如果节点折叠的话,则有A=H。

现在我们根据节点高度H和总高度A来计算其位置P;

我们需要通过一些基本的数据,来推导一个数学式子,使得对于任意的节点,可以轻松的计算其位置。

假定节点1位于位置0,2、3、4、5、6依次往下递增,我们有:





OK,注意等式的右边涉及到了基值0、符号H、符号A、符号P,感觉有些乱,然而仔细分析可以发现,对于任意一个节点,如果该节点的前面有兄弟节点,则依赖其兄弟节点即可计算自己的位置,算式为 

 ,如果该节点前面是父亲节点,则需要依赖父节点计算位置,算式为 

 ,总结如下:




注意mn代表的不一定是两个数,也可能是

(其中m=12232131,n=2)这样复杂的符号。

有了这几个式子,写代码应该不成问题了吧,树形控件的位置问题解决以后,其他的都是码砖添瓦的事情,这里就不一一介绍了。

CEGUI里面实现了一个树形控件,然而注释里面明确指出由于设计和实现的质量较差,未能达到其他控件的同一水准,不推荐使用,让人着实蛋疼,不知道是否该参考它的接口与实现。

关于本篇文章,如果有任何疑问,恳请指教。

另外说明一下,最近在看高老爷子的《具体数学》,写的非常不错,老爷子写文章用词既严谨又不失诙谐,阅读起来也是一件乐事,作为一名计算机科学专业毕业的学生,也希望自己后面的文章能够做到严谨哈。

参考:

CEGUI源代码Tree部分;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: