• 在游戏中使用“CEGUI”


    在游戏中使用“CEGUI” —第一章(底层)

    日期:2006/4/13 – 2006/10/10

     

    本文首次刊登于《游戏创造》,现开放与大家共享,转载请注明出处。

     

    作者介绍

           唐亮(千里马肝),四年游戏从业经验,曾任职于大宇软星科技(上海)有限公司任程序技术指导,现在ATI任Engineer,主要负责XP/Vista下的Display Driver。迄今为止主要个人作品为《阿猫阿狗2》,参与开发《汉朝与罗马》、《阿猫阿狗大作战OLG》和《仙剑奇侠传4》,主要研究方向为C++、图形渲染技术和系统架构。

    blog地址:http://oiramario.cnblogs.com

     

     

    简介

           CEGUI(Crazy Eddie’s GUI http://www.cegui.org.uk)是一个自由免费的GUI库,基于LGPL协议,使用C++实现,完全面向对象设计。CEGUI开发者的目的是希望能够让游戏开发人员从繁琐的GUI实现细节中抽身出来,以便有更多的开发时间可以放在游戏性上。

    CEGUI的渲染需要3D图形API的支持,如OpenGL或Direct3D。另外,使用更高级的图形库也是可以的,像是OGRE、Irrlicht和RenderWare,关键需求可以简化为二点:

    1.        纹理(Texture)的支持

    2.        直接写屏(RHW的顶点格式、正交投影、或者使用shader实现)

    本文截止日时,CEGUI的最新版本是0.4.1(本文的讨论也是基于此版本),提供了SDK和全部源码的下载,同时为了适应不同的使用需求,还根据STL的使用区分为Native(VC自带的P.J. 版STL)和STLport(基于SGI STL实现的跨编译器版本,详细见http://www.stlport.org),以及VC6.0、VC7.0、VC7.1和VC8.0几种。

    除此之外,CEGUI还同步提供了官方界面编辑器LayoutEditor,以方便UI的制作,下载地址:http://www.2dgame-tutorial.com/downloads/CELayoutEditorSetup_0.4.1.exe。作为界面编辑器,它需要系统级界面以提供编辑器操作,在此之前的0.3.0版是基于MFC实现的;而在0.4.1版本中,改为基于wxWidgets(跨平台的本地UI框架,这里的UI指Window操作系统底层,如:Windows、Unix和Mac,详见http://www.wxwidgets.org)实现。

    OGRE作为目前最活跃的开源3D引擎,许多公司开始使用它进行游戏开发,原因也是其功能非常得全面和强大。在最初,OGRE曾经实现过一版UI,但是最后却放弃自己的实现而选择了CEGUI。

    Why

    很多人可能会觉得UI这种东西很简单,自己写就好了。我想这首先要看标准是什么了,如果只是简单的按钮、图片什么的控件,那当然不必要去负担如此大的一个库。但是,如果是以Windows 9x这样为标准,那么就不是一般得复杂了,M$也不是白混的,还要继续坳的话,那么就请自己试实现一次吧,就会发现其实事情不像是看上去那么容易。

    另外,CEGUI也是由人设计出来的,我坚信会有其他的大牛可以做得到。但是,这样做真的有必要吗,有可能你在De一个别人2年前早已修掉的Bug,而别人这时正在做下一代框架,干麻不花这个时间一起去完善它呢?

    最后,我想就是开源的力量。凡事不去尝试,是不会了解到其真象的。为什么会有所谓头脑风暴,这就是集体的力量,广大人民群众齐心协力,会让人感到个人力量的有限。

    那么,让我们放下成见,卸掉包袱,开始这一次CEGUI之旅。

    设计思想

    WidgetSets

    CEGUI的设计思想是以窗口为单位的WidgetSets,它称作这些WidgetSets为xxxLook,例如自带的两个TaharezLook和WindowsLook,也就是说在同一个Look里,所有的同类型的控件都长一个模样(这个可能无法满足我们通常游戏中的需要,所以要对其进行一些改造),感觉上比较像Windows98的Theme(主题),只不是Theme的概念更大,包括了桌面、音效和鼠标等。

    TaharezLookWindowsLook

     

           如上左右二图,可以看到,所有该Look所支持的Control类型所需要的图素都被一张图片所包含,假设需要更改样式和外观,可以设计多张拥有同样结构和相同元素的图片,然后换图即可。

    体系结构

           CEGUI的窗口体系结构,跟以往我们所了解的一样,它底层的基类是Window,如下图:

           以上便是CEGUI提供给我们的控件集合,其他不在此范畴内的复合控件,也可以使用这些基本控件组合而成。

    Window

    可以看到,中间黑块中的Window,它继承于PropertySetEventSet。从这里开始,需要说明一个CEGUI中常见的概念:在CEGUI中,如果存在某对象为xxx,通常会有一个xxxSet与之对应,而xxxSet的任务是对其进行管理或是分发的工作。因此,对于PropertySet而言,同时存在有Property,而Property的概念是:构建一个物件所必须的属性或组件。

    举例来说,WindowProperties::AbsoluteHeight是一个在namespace WindowProperties中的一个Window属性AbsoluteHeight,用作描述Window的高度。同理,EventSet是全部Window事件的集合,其中就有像EventSized用作描述Window大小改变的事件(理解同“消息”)。

           Window拥有了PropertySetEventSet的特征后,在初始化的时候,它自己便会往里面“填入”许多的属性和事件,丰富一番后,它也会定义一些接口,供子类继承或是供外部操作使用,像是会有接口virtual   void    drawSelf(floatz)  = 0;(供子类实现绘制),当然也会有一些公共的操作接口,如void setYPosition(float y);(设置坐标)。

           在上图的右边,有一长串由Window派生出来的子控件,也就是由这些控件构成了整个CEGUI,其中包括有基本的控件:按钮、文字、图片、编辑框等;也有较复杂的复合控件:列表框、表格、多行编辑框等,它们由多个基本控件组合而成。另外,作为一种附属窗体Tooltip,它就是当鼠标在某控件上悬停一会儿后出现的说明框。

    下图中,描述了整个Window所拥有的信息,所有的事件响应,所有的基本属性:

           显而易见,这的确十分庞大,以致于我无法在不浪费页面的情况下,同时让这个体系图能够清晰得显示。

    Property

           作为“属性”的描述,需要注意的是,所有的Property都是一个独立的class,哪怕只是一个简单的AbsoluteHeight,那为什么要把一个int变量搞得如此神秘和复杂呢?

    原因有二个:

    1.        操作接口化,使用Interface来隔离各模块,当功能发生变动,只需要修改实现,而接口不变

    2.        序列化,便于Window在从文件中读取时存取和初始化各属性

    而实现一个Property,基本上简单到只需要实现两个接口:

    virtual String  get(const PropertyReceiver* receiver) const= 0;

    virtualvoid    set(PropertyReceiver* receiver, constString& value) = 0;

     

    相同之处在于参数PropertyReceiver* receiver,其中receiver在不同控件中的Property代表着不同的含义,对于WindowProperties::AbsoluteHeight而言,receiver就等同于Window的实例,所以我们可以直接static_cast<Window*>(receiver)。因为每个Property都代表了不同的属性含义,在存取时也就需要不同的处理方式,所以传入一个宿主实例的指针,由Property自己决定应该做的事情。下面以WindowProperties::AbsoluteHeight的实现为例,相信只要看完之后,就会非常清楚Property的工作原理了。

    String AbsoluteHeight::get(constPropertyReceiver* receiver) const

    {

        returnPropertyHelper::floatToString(static_cast<const Window*>(receiver)->getAbsoluteHeight());

    }

     

    voidAbsoluteHeight::set(PropertyReceiver* receiver, constString& value)

    {

        static_cast<Window*>(receiver)->setHeight(Absolute,PropertyHelper::stringToFloat(value));

    }

       

        对,它仅仅只是再次调用了Window的接口去设置了一下,这也就是封装的概念和意义。

           出现了一个新面孔PropertyHelper,为了方便属性的存取,它提供了一些类似std::itoa和std::atoi这样的函数来简化字符串操作;对于复杂的PropertyPropertyHelper通过定义一些规范的格式来操作,像是

    Stringfloat的转换:

    floatPropertyHelper::stringToFloat(const String& str)

    {

        usingnamespacestd;

       

        floatval = 0;

        sscanf(str.c_str()," %f", &val);

       

        returnval;

    }

    StringImage的转换:

    constImage* PropertyHelper::stringToImage(const String& str)

    {

        usingnamespacestd;

       

        char imageSet[128] = {0};

        charimageName[128] = {0};

       

        sscanf(str.c_str()," set:%127s image:%127s", imageSet, imageName);

       

        constImage* image;

       

        try

        {

            image =&ImagesetManager::getSingleton().getImageset((utf8*)imageSet)->getImage((utf8*)imageName);

        }

        catch(UnknownObjectException)

        {

            image =NULL;

        }

       

        returnimage;

    }

     

     

     

     

    Event

    作为“事件”的描述,与Property不同的是,Event是以String实现的,它只是一段文字描述,当不同的事件发生时,CEGUI便会发送对应的Event来通知窗口。

    一个Window会有很多像是EventMouseMoveEventKeyDownEventSized等等这样的事件。从名字上,就可以很容易得区分它们各自所代表的意义,以EventMouseMove为例,它的真身是const String Window::EventMouseMove( (utf8*)"MouseMove" );,是的,它就只是一个字符串而已。以EventMouseMove为例,当CEGUI底层在处理消息时,会判断鼠标是否在该窗体的区域范围中移动时,如果是,则通过接口

    virtualvoid    fireEvent(constString& name, EventArgs& args, constString& eventNamespace = "");

    来发送事件给该窗口。其中,name是消息字符串名称,args中存放着该消息对应的一些信息以供函数处理,像是EventMouseMove就对应MouseEventArgs来传递数据,以下是实现:

          

    classCEGUIEXPORT MouseEventArgs : public WindowEventArgs

    {

    public:

        MouseEventArgs(Window*wnd) : WindowEventArgs(wnd) {}

       

        Point           position;           //!< holds current mouse position.

        Vector2     moveDelta;      //!< holds variation of mouse position from lastmouse input

        MouseButton button;         //!< one of the MouseButton enumerated valuesdescribing the mouse button causing the event (for button inputs only)

        uint            sysKeys;            //!< current state of the system keys and mouse buttons.

        float           wheelChange;        //!<Holds the amount the scroll wheel has changed.

        uint            clickCount;     //!< Holds number of mouse button down eventscurrently counted in a multi-click sequence (for button inputs only).

    };

          

    因为WindowEventArgs是从EventArgs派生过来的,那么Window就可以通过成员函数

    virtualvoid    onMouseMove(MouseEventArgs&e);来响应该事件了。

                          

    哦,我不会忘记这里还有一个参数eventNamespace,还是举例说明一下吧,在Window中,它就是constString Window::EventNamespace("Window");,用来区分在不同控件中可能会出现的同名事件。

    小结

    上面只是简单扼要得介绍了一些CEGUI的基础概念,对于一个熟悉Window的人而言,可能会觉得“不过如此”,但是,事情往往说起来容易做起来难。从整个设计体系来看,固然一个Window like的系统怎么也逃不出这些个概念,然而在控件的细节实现上,还是有很多复杂繁琐的东西需要去实现。

    渲染器

    前面说了那么多逻辑层的底层机制,接下来想要将CEGUI的界面显示出来,则必须要实现两个类:TextureRenderer。它们算作是“渲染底层”;而CEGUI会在此基础上再完成一些“中间层”(像是Image之类);最上面才是控件类,共三层构成了整个CEGUI。

    Texture

           实现Texture需要重载几个接口,依次是:

          

    virtual ushort  getWidth(void) const= 0;

    virtual ushort  getHeight(void) const= 0;

    virtualvoid    loadFromFile(constString& filename, const String& resourceGroup) = 0;

    virtualvoid    loadFromMemory(constvoid* buffPtr,uint buffWidth, uint buffHeight) = 0;

       

        CEGUI需要通过这些接口操作纹理对象:得到纹理的宽度和高度、二种不同的载入方式。这里唯一需要解释的部分就是constString& resourceGroup,通过使用不同的“组” 前缀名,以区分可能相同名称的资源名,保证资源唯一ID的存取。

           Texture虽然很简单,但它却是Renderer实现所必须的一个重要组成部件。

    Renderer

           实现Renderer需要重载更多的接口,因为数量比较多,且不像Texture的接口那么容易从字面上理解,所以我在下面会分别作解释:

    virtualvoid    addQuad(constRect& dest_rect, float z, const Texture* tex,const Rect& texture_rect, constColourRect& colours, QuadSplitMode quad_split_mode) = 0;

    增加一个Quad到渲染缓冲中。因为对象是Quad,所以一些参数都是以Rect(4个顶点)为单位在描述,这可能会和以往的了解有些许不同:

    dest_rect,,              目标位置

    z,                       前后层次关系

    tex,                   纹理指针

    texture_rect,,         纹理坐标

    colours,                   顶点颜色

    quad_split_mode,     4个顶点的顺序(顺时针、逆时针)

     

    virtualvoid    doRender(void) = 0;

    渲染全部UI(整个Quad缓冲)

     

    virtualvoid    clearRenderList(void) = 0;

    清空全部渲染缓冲

     

    virtualvoid    setQueueingEnabled(bool setting)= 0;

    对于Quad的渲染分为“立即模式”和“缓冲模式”,这里是两种模式的切换开关

     

    virtual Texture*    createTexture(void) = 0;

    描述Renderer如何创建一个Texture,通常就是new一个Texture后返回指针

     

    virtual Texture*    createTexture(constString& filename, const String& resourceGroup) = 0;

    描述Renderer如何从文件中创建一个Texture,通常是调用上面的函数后得到新建的Texture,然后调用TextureloadFromFile

     

    virtual Texture*    createTexture(floatsize) = 0;

    描述Renderer如何根据指定的大小来创建一个Texture,通常是调用上面的函数后得到新建的Texture,然后根据size创建一块临时的内存,最后调用TextureloadFromMemory

     

    virtualvoid    destroyTexture(Texture*texture) = 0;

    销毁指定的Texture,通常Renderer都会保存一份Texture的列表便于管理,这里除了会delete传入的指针外,还会从管理列表中删除它

     

    virtualvoid    destroyAllTextures(void) = 0;

    销毁纹理列表中的全部纹理

     

    virtualbool    isQueueingEnabled(void) const= 0;

    查询缓冲渲染模式是否打开

     

    virtualfloat   getWidth(void) const    = 0;

    得到渲染设备的宽度,通常就是Viewport的宽度

     

    virtualfloat   getHeight(void) const   = 0;

    得到渲染设备的高度,通常就是Viewport的高度

     

    virtual Size    getSize(void) const     = 0;

    得到渲染设备的大小,通常就是Viewport的宽高

     

    virtual Rect    getRect(void) const     = 0;

    得到渲染设备的区域,通常就是Viewport的屏幕范围

     

    virtual uint    getMaxTextureSize(void) const   = 0;

    得到渲染设备支持可创建的最大纹理的尺寸:D3D通过查询Caps得到,OpenGL通过调用glGetIntegerv(GL_MAX_TEXTURE_SIZE,&s_max_size);得到

     

    virtual uint    getHorzScreenDPI(void) const    = 0;

    得到屏幕的水平DPI(Dot Per Inch),通常等于96

     

    virtual uint    getVertScreenDPI(void) const    = 0;

    得到屏幕的垂直DPI(Dot Per Inch),通常等于96

    当然,以上给出的只是virtual =0; 这样的pure virtual的部分,除此之外,Renderer还有提供一些其他的接口供使用,具体可以自行去看.h中的接口部分。

    介绍完接口实现之后,接下来是Renderer的渲染工作原理:

           首先定义一个Vertex的概念,它应该是满足3DAPI的渲染需要,通常会是纹理坐标、顶点颜色和顶点位置的一个结构体:

    structQuadVertex

     {

        f32     uv[2];

        u32     color;

        f32     vertex[3];

     };

     

           接着定义Quad:

          

    structQuadInfo

    {

        GLuint      texid;              //!< 纹理ID

        Rect            position;               //!< 区域

        f32         z;                  //!< z

        Rect            texPosition;            //!<纹理区域

        u32         topLeftCol;         //!< 左上顶点的颜色

        u32         topRightCol;            //!< 右上顶点的颜色

        u32         bottomLeftCol;          //!< 左下顶点的颜色

        u32         bottomRightCol;     //!< 右下顶点的颜色

       QuadSplitMode splitMode;            //!< 拼接的模式

        // 排序用

        booloperator< (const QuadInfo& other)const

        {

            // this is intentionally reversed.

            returnz > other.z;

        }

    };

     

           然后Renderer会把这些Vertex和Quad管理起来

          

    typedefstd::vector<QuadInfo> quad_container;

    quad_container              d_quadlist;     //!<quads

    typedefstd::vector<QuadVertex> vertex_container;

    vertex_container            d_vertexes;     //!<vertex buffer(system memory)

     

    还记得CEGUI中有一个Image吧(因为这里是讨论Renderer的实现,所以暂且简单得说明一下),所有控件的绘制都是通过Image实现的,而它实际上是调用了RendereraddQuad方法,下面是实现代码

    // 非队列渲染的quad直接绘制

    if(!d_queueing)

    {

        renderQuadDirect(dest_rect,z, tex,texture_rect, colours, quad_split_mode);

    }

    else

    {

        QuadInfoquad;

        quad.position           = dest_rect;

        quad.position.d_bottom  = d_display_area.d_bottom -dest_rect.d_bottom;

        quad.position.d_top     = d_display_area.d_bottom -dest_rect.d_top;

        quad.z              = z;

        quad.texid              = static_cast<consttl_ceguiTexture *>(tex)->getOGLTexid();

        quad.texPosition        = texture_rect;

        quad.topLeftCol         = colourToOGL(colours.d_top_left);

        quad.topRightCol        = colourToOGL(colours.d_top_right);

        quad.bottomLeftCol      = colourToOGL(colours.d_bottom_left);

        quad.bottomRightCol     = colourToOGL(colours.d_bottom_right);

       quad.splitMode      = quad_split_mode;

        d_quadlist.push_back(quad);

    }

     

    如源码所示,根据开关,Renderer决定传入的Quad是立即渲染还是放入Quad缓冲中,而缓冲中的Quad会在doRender时一起绘制。

    所谓的立即渲染,以下是伪代码描述:

     

    传入一个Quad

    准备拥有6个顶点的顶点数组(2个三角形)

    将Quad中的顶点信息逐个填入顶点数组

    然后调用渲染API绘制2个三角形(D3D中是DrawPrimitive,OpenGL中是glDrawElements)

     

    同样,对于缓冲模式,唯一不同的是需要遍历Quad缓冲中所有的Quad,将顶点信息都填入Vertex缓冲中,一次性提交尽可能多的顶点数目。为什么说是“尽量”呢?因为不同的Quad可能拥有着不同的贴图或是一些渲染状态需要改变,那么这样就无法批量提交了。虽然UI是2D的图片集合,但是也存在有前后关系,所以Quad提供了排序的操作,而doRender会在绘制前对Quad缓冲进行排序,这样可以保证绝对正确的前后关系。

    有意思的是,因为CEGUI本身会按照UI的前后顺序来调用addQuad,只要我们在WidgetSets(即那些xxxLook)中,能够以正确的顺序来绘制Image的话,那么Quad缓冲中的Quad便已经是有序的,再次手动排序就没必要了,这对帧数的提高有很大的影响。

    Image

          

           上面这张是ImageImageset两者的关系图,但是如何去理解它们倒底是什么东西呢?以至于我不得不自己手动去画一张示意图了……

           一图胜过千言万语。如上图所示,整张位图便是Imageset,其中的A和B两个矩形部分就是Image。通过这样描述了拼图的概念,放到3D环境里,Imageset即是从图片中创建出的一个Texture,而这张图片中可能包括有多张小图,那么也是指这个Imageset存在有多个Image

           回到第一张关系图,可以看到Image通过d_offset记录了所在图片的偏移量,d_area记录了区域范围,还有d_owner记录了所属Imageset的指针,通过这些信息,足够可以计算纹理UV了,所以从本质上来说,Image就是用来记录一张图片所在纹理中的区域纹理坐标而已。

           每个Image还有一个名字用来唯一标识,通过这个名字我们可以在Imageset中对Image进行存取。另外,Image提供了众多的绘制函数供外部使用,具体请见对应的.h文件。

    Imageset

          

           前面已经介绍了ImagesetImage的关系,这里再来看一下Imageset

           如图所示,Imageset通过d_texture来操作纹理图片,那么它是如何管理Image的呢,请看下面的定义:

          

           typedef std::map<String,Image> ImageRegistry;

    ImageRegistry   d_images;   //!< Registry of Image objects for the imagesdefined for this Imageset

          

           Imageset通过使用std::map,将StringImage一一对应,然后我们就可以通过Image的名称来进行查询

          

           const Image&    getImage(constString& name) const;

          

           或是自行定义Image

           void        defineImage(constString& name, const Point& position, const Size&size, const Point& render_offset)

        void        defineImage(constString& name, const Rect& image_rect, constPoint& render_offset);

          

           如果我们拥有所有的Image信息,就可以将Imageset保存到xml文件,然后下次直接从文件载入就好了,不必每次都去重新定义

          

    void        load(constString& filename, const String& resourceGroup);

          

    To be continue…

  • 相关阅读:
    CMS网站 中最好用的!
    成为优秀设计师的十大条件
    网站变色(黑白)!
    设计师必知的18种服装风格
    HDFS核心类FileSystem的使用
    Hadoop的伪分布式安装和部署的流程
    初学MapReduce离线计算(eclipse实现)
    hdfs的客户端读写流程以及namenode,secondarynamenode,checkpoint原理
    hadoop常用的操作指令
    TableLayoutPanel&SplitContainer 布局
  • 原文地址:https://www.cnblogs.com/zengqh/p/2477384.html
Copyright © 2020-2023  润新知