在游戏中使用“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,它继承于PropertySet和EventSet。从这里开始,需要说明一个CEGUI中常见的概念:在CEGUI中,如果存在某对象为xxx,通常会有一个xxxSet与之对应,而xxxSet的任务是对其进行管理或是分发的工作。因此,对于PropertySet而言,同时存在有Property,而Property的概念是:构建一个物件所必须的属性或组件。

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

Window拥有了PropertySet和EventSet的特征后,在初始化的时候,它自己便会往里面“填入”许多的属性和事件,丰富一番后,它也会定义一些接口,供子类继承或是供外部操作使用,像是会有接口virtual   void    drawSelf(float z)  = 0;(供子类实现绘制),当然也会有一些公共的操作接口,如voidsetYPosition(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, const String& value) = 0;

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

String AbsoluteHeight::get(const PropertyReceiver* receiver) const

{

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

}

void AbsoluteHeight::set(PropertyReceiver* receiver, const String& value)

{

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

}

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

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

String与float的转换:

float PropertyHelper::stringToFloat(const String& str)

{

usingnamespace std;

float val = 0;

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

return val;

}

String与Image的转换:

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

{

usingnamespace std;

char imageSet[128] = {0};

char imageName[128] = {0};

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

const Image* image;

try

{

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

}

catch (UnknownObjectException)

{

image = NULL;

}

return image;

}

Event

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

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

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

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

class CEGUIEXPORT MouseEventArgs : public WindowEventArgs

{

public:

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

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

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

MouseButton button;         //!< one of the MouseButton enumerated values describing 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 events currently counted in a multi-click sequence (for button inputs only).

};

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

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

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

小结

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

渲染器

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

Texture

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

virtualushort  getWidth(void) const = 0;

virtualushort  getHeight(void) const = 0;

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

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

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

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

Renderer

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

virtualvoid    addQuad(const Rect& dest_rect, float z, const Texture* tex, const Rect& texture_rect, const ColourRect& 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的渲染分为“立即模式”和“缓冲模式”,这里是两种模式的切换开关

virtualTexture*    createTexture(void) = 0;

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

virtualTexture*    createTexture(const String& filename, const String& resourceGroup) = 0;

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

virtualTexture*    createTexture(float size) = 0;

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

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的屏幕范围

virtualuint    getMaxTextureSize(void) const   = 0;

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

virtualuint    getHorzScreenDPI(void) const    = 0;

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

virtualuint    getVertScreenDPI(void) const    = 0;

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

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

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

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

struct QuadVertex

{

f32     uv[2];

u32     color;

f32     vertex[3];

};

接着定义Quad:

struct QuadInfo

{

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.

return z > other.z;

}

};

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

typedef std::vector<QuadInfo> quad_container;

quad_container              d_quadlist;     //!< quads

typedef std::vector<QuadVertex> vertex_container;

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

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

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

if (!d_queueing)

{

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

}

else

{

QuadInfo quad;

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<const tl_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

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

一图胜过千言万语。如上图所示,整张位图便是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

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

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

typedefstd::map<String, Image> ImageRegistry;

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

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

const Image&    getImage(const String& name) const;

或是自行定义Image

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

void        defineImage(const String& name, const Rect& image_rect, const Point& render_offset);

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

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

To be continue…

在游戏中使用“CEGUI”相关推荐

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

    在游戏中使用"CEGUI" - 第一章(底层) 日期:2006/4/13 – 2006/10/10 本文首次刊登于<游戏创造>,现开放与大家共享,转载请注明出处.   ...

  2. 全文:在游戏中使用CEGUI —— 第一章(底层)

    本文首次刊登于<游戏创造>,现开放与大家共享,转载请注明出处.   作者介绍 唐亮(千里马肝),四年游戏从业经验,曾任职于大宇软星科技(上海)有限公司任程序技术指导,现在ATI任Engin ...

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

    简介 CEGUI(Crazy Eddie's GUI http://www.cegui.org.uk)是一个自由免费的GUI库,基于LGPL协议,使用C++实现,完全面向对象设计.CEGUI开发者的目 ...

  4. rust熔炉怎么带走_Rust游戏中12个实用小技巧,包含无伤下坠、直梯爬楼

    Rust是一款第一人称生存网络游戏,有点像野外求生,但这款游戏内容则更加丰富.刺激.血腥. 在这款游戏中玩家的第一任务就是活下来,而想要活下来你将要接受饥饿.干渴.寒冷等.游戏中玩家需要建造自己的庇护 ...

  5. 在线试玩,在体感游戏中打败泰森,这位小哥破解了任天堂「拳无虚发」

    视学算法报道 编辑:张倩.陈萍 加入体感控制,这位小哥破解了原版任天堂的拳击格斗游戏「拳无虚发」. 在经典红白机上玩拳击游戏是很多人的童年回忆,其中就包括任天堂 1987 年发布的拳击格斗游戏--「P ...

  6. 用 Redis 搞定游戏中的实时排行榜,附源码!

    原文:segmentfault.com/a/1190000019139010 1. 前言 前段时间刚为项目(手游)实现了一个实时排行榜功能, 主要特性: 实时全服排名 可查询单个玩家排名 支持双维排序 ...

  7. C++模拟游戏中鼠标点击和键盘按键

    游戏中模拟键盘输入,有时回被系统屏蔽,Java等语言都试过很多方法,好像都没用,所以下面给出一种C++实现方法 #include <iostream> #include <windo ...

  8. 机器学习如何彻底改变游戏中的物理模拟

    来源:AI科技评论本文约2600字,建议阅读10分钟 神经网络模拟物理比物理解算器快5000倍. 量子力学奠基者之一.英国理论物理学家保罗·狄拉克(Paul Dirac)在1929年说过:" ...

  9. 由游戏中加入“人脸识别”而引发的思考

    近期某国产热门"吃鸡"游戏中引入了"人脸识别系统",引发了不少争议.很多玩家认为只有在认证健康系统防沉迷的时候才会触发.实际上,通过多次版本更新之后 ,现如今& ...

最新文章

  1. Linux 环境变量配置
  2. JS-匀速运动-运动停止
  3. [UGUI]圆形Image
  4. SAP Hybris Commerce里类似ABAP里的透明表设计
  5. unity ppr_智能自动PPR更改事件策略
  6. sml完整形式_教资会的完整形式是什么?
  7. 关于停止发表“每周新闻回顾”的通知
  8. php只显示指定文件类型_PHP 上传时的文件类型
  9. webbench接口并发测试
  10. 数据库设计的三大范式[学习笔记]
  11. HDU1214 圆桌会议【数学】
  12. Agenda for Mac(多功能日历管理工具)中文版
  13. 80%的程序员不了解的微服务内幕
  14. 下载并安装WIN7 SP2的官方补丁包
  15. java常识-java怎么换行
  16. 常识性知识,高速快捷知识
  17. docker 自动签到模板制作
  18. springBoot 报警告 Could not find acceptable represent
  19. win10 系统亮度无法调节
  20. 【DP练习】月饼盒(提高版)(vijos1255)

热门文章

  1. GC垃圾回收算法三种方式
  2. 开发者选项 Disable HW overlays
  3. URP渲染管线初步解析
  4. 程序员到30岁之后应该何去何从?治恐慌
  5. php使用redis的批量发送短信
  6. 爱情发生器:36个问题+4分钟对视=告别单身
  7. 物联网空开价格_智慧物联网远程空开断路器
  8. [shell]星号按99乘法表排列
  9. SQL servere 范式、事务
  10. 微信小程序 选择器 地区选择器