Ogre中级教程(九): 深入CEGUI

时间:2021-11-13 14:57:55

中级教程九
  深入CEGUI
出自Ogre3D开放资源地带
跳转到: 导航, 搜索
我想,迟到总比没到好。(译者:作者这句话意思大约是说,这篇文章虽然发布的晚了些,但是总比完全没有CEGUI这方面的教程好。)
      目录
      [隐藏]
        1 CEGUI
          1.1 UI的渲染
          1.2 输入系统和CEGUI
          1.3 CEGUI数据文件
        2 编码
          2.1 数据
          2.2 接下来,看编码
            2.2.1 初始化CEGUI
            2.2.2 CEGUI的输入支持
        3 结束语
        4 下载这些代码


CEGUI
在本文中我们将准备把CEGUI整合到实际开发项目中。
这个版本的代码将告别VC71项目,并采用VC8(Visual
C++2005)项目。实际上,自我工作以来,一直到最近我都一直使用VC71。那些使用VC71的朋友应该有足够的经验把VC71的项目转移到VC8上----这是非常简单直接的事情。

这个例子的代码是基于Ogre1.4.x版本的----另外,如果你使用Ogre1.2.x版本,你可能会发现下面的例子是需要修改一些问题才能顺利运行的。这是因为这1.4.x版本的Ogre消息泵(message
pump)有一些变动,另外OIS库本身也有些更变了。总之,下面的例子代码最好能够使用OgreSDK1.4.1进行编译测试。
CEGUI是一个2D的UI渲染库。它支持大部分渲染引擎,不仅支持Ogre,也同样的支持OpenGL和Irrlicht引擎.
它同时又是基于XML脚本文件格式应用的一个重量级库。当然,你可以在你的代码中定义所有的UI信息,但如果你在使用XML文件进行UI文件的定义也会非常简单,CEGUI也能够在运行时对这些XML文件进行读取和分析。CEGUI本身有一些例子和支持这些例子的资源。你可以看一看这些例子中是如何使用XML脚本文件,如何进行UI单元的定义的。也可以去看看例子中的
.layout 文件和
.xsd文件,这些会对你学习CEGUI有所帮助。当然,你可能需要在自己的程序中自定义一些UI组件。但是,很遗憾,自定义组件已经出了本文的讲解范围。请到CEGUI官方获得更多信息。

UI的渲染
CEGUI在一些四方形网格上渲染它的UI单元,并将它们渲染到一个“屏幕空间”中。那么,将意味着这些承载着UI单元的四方形网格的整个绘制过程是完全不接受任何视角变换和投影变换影响的。这一条是任何2D
UI系统运行的基准方法。
CEGUI通过“OgreGUIRenderer”类与Ogre进行结合。这个类所在文件存放在Ogre的“Samples”文件夹下,然而却定义在“CEGUI”命名空间内。OgreGUIRenderer简单的取得了CEGUI中的一些绘制命令,并将这些命令转译成Ogre中的网格和材质数据,并将其放置到"层渲染队列(Overlay
render queue)"中,通过这种手段Ogre就可以对其进行绘制了.同时,这样可以使UI在我们的渲染场景中永远处于近屏顶端。
在我们的程序代码中,我们除了提供一个OgreGUIRenderer类的实体对象给CEGUI,其他的渲染相关部分我们不用操心。
输入系统和CEGUI
CEGUI本身没有任何的输入处理,如果我们程序中需要对不同的输入数据进行处理,那么我们必须为它提供整套的输入事件处理系统。
这样做有一些好处。其中之一就是你不需要担心UI库会和你的应用程序争夺输入数据的处理权。此外,你可以*的完全的把CEGUI从“输入循环”中移除出来,并不给它任何输入数据信息。你同样也不需要依赖于任何的第三方IO库和API,你可以*的选择输入设备:OIS,SDL,DirectInput,Win32,XWindows作为输入库都可以,完全的取决于你。

在本文中我们将使用OIS做为CEGUI的输入处理系统。
CEGUI数据文件
说起来,你可能能够在代码中完美的进行你的UI层的定义。但是用以下几种XML文件做为CEGUI的配置方法将会更加简单和高效。这些文件包括”scheme”,
”looknfeel”, ”layout”, ”imagesets”和CEGUI显示使用的文字”font”。
Scheme 定义着在同一个配置下不同的有效UI元件。例如. 我们程序中需要使用的一些按钮Buttons,下拉选菜单ListBoxes等等UI元件都能够在 .scheme 格式的文件中找到。
Look-And-Feel 定义着每个需要显示的UI元件的各种属性,包括了它的纹理,颜色,状态属性等。我们可以查看 .looknfeel文件。
Layout 定义了UI元件的父层级关系,位置,大小等与其他UI元件可能联系到的属性。我们可以查看 .layout 文件。
Imageset 定义了一种配置下的纹理,UV值等信息,我们可以查看 .imageset 文件。
font 显而易见。CEGUI需要知道我们程序中使用的文字纹理类型,纹理清晰度等信息这些都记录在 . font 文件中。
在本文的附件中有个gui.zip文件,你能够在里面找到上述所有文件的一些例子。认真查看每个类型的文件,虽然它们都是XML格式的文件,注意它们各自的格式以及包含了UI的什么信息。如果你依旧看不明白,可以去CEGUI的官方站点看看对它们功能的描述。

本文将使用”TcharazLookSkin”这套配置。(Tcharaz是创建这个配置的一个人名,他创建了这个配置的.scheme,
.look-and-feel, .texture和.imageset)
编码
好了,介绍的足够多了,现在我们看看实际编码。如果你坚持看了Ogre的这系列教程例子,你应该很熟悉如何创建一个按下Esc就退出的Ogre渲染窗口的框架了。在这个例子中,我们用一种主菜单的面版来替代以前的空窗口框架。这个主菜单包含三个按钮,但是我只打算让Quit这个按钮进行事件响应处理。我们除了使用ESC键之外,我们还可以通过Quit按钮退出本程序。不知道通过之前的讲解你是否能明白,CEGUI通过别的输入系统获得用户输入事件后能够将其事件都反映出来,就如本例,我们可以在自己的代码中对鼠标点击事件做出响应。一会你从代码中就能轻易的明白如何在自己的代码中Hook到CEGUI的事件行为。注意:我们下面的代码操作都是基于C++的,如果你对希望在脚本(例如Lua或其他脚本引擎)中Hook到CEGUI的事件行为,希望你能到CEGUI官方去看看如何做到。

数据
首先,我们来看一下CEGUI中至关重要的数据文件---Layout文件。你可以通过CEGUI的Layout编辑器来生成和编辑该文件,但是在本例中下面这个XML格式的
.layout
文件完全是我手动编写出来的,实际上很简单。在Ogre的Debug文件夹下resource目录下有个压缩包是gui.zip,你在里面会发现一个文件叫”Katana.layout”,下面是该文件的全部内容:

<?xml version="1.0" ?>
<GUILayout>
   <Window Type="TaharezLook/FrameWindow" Name="Main">
       <Property Name="UnifiedAreaRect" Value="{{0.0,0},{0.0,0},{1.0,0},{1.0,0}}" />
       <Property Name="FrameEnabled" Value="false" />
       <Property Name="TitlebarEnabled" Value="false" />
       <Property Name="CloseButtonEnabled" Value="False" />
       <Window Type="TaharezLook/Button" Name="cmdQuit">
          <Property Name="Text" Value="Quit" />
          <Property Name="UnifiedAreaRect" Value="{{0.4,0},{0.7,0},{0.6,0},{0.77,0}}" />
       </Window>
       <Window Type="TaharezLook/Button" Name="cmdOptions">
          <Property Name="UnifiedAreaRect" Value="{{0.4,0},{0.6,0},{0.6,0},{0.67,0}}" />
          <Property Name="Text" Value="Options" />
       </Window>
       <Window Type="TaharezLook/Button" Name="cmdInstantAction">
          <Property Name="UnifiedAreaRect" Value="{{0.4,0},{0.5,0},{0.6,0},{0.57,0}}" />
          <Property Name="Text" Value="Instant Action" />
       </Window>
   </Window>
</GUILayout>
你在浏览这些代码的时候,首先可能发现里面那些神秘的数字。在CEGUI
0.5.x版本中,UI元件需要定义的是UI元件的绝对尺寸大小。而在这个layout文件中,我们使用的是统一的相对尺寸范围。Value =””
中定义了元件的左,右,上,下点的位置。但这个位置我们既可以定义这些点的绝对位置,也可以是定义这些点针对其父容器UI元件的相对位置。例如,下面这个UI元件的区域定义:

{{0.4,0},{0.5,0},{0.6,0},{0.57,0}}
这些数值告诉了CEGUI,这个UI元件在其上一层元件容器中的相对位置和大小。
如本例,这个元件左上角将距离其上一层元件容器的左上角的右偏移距离为 (0.4 *
上一层元件容器宽度),这个元件左上角将距离其上一层元件容器的左上角的下偏移距离为 (0.5 * 上一层元件容器高度)。该UI元件宽度将为((0.6 –
0.4)* 上一层元件宽度 )大小,高度将为( (0.57 – 0.5) * 上一层元件告诉 )大小。
那么,那些参数中的”0”是什么意义呢?如果你想同时指定UI元件的绝对象素位置的话,可以把这些”0”替换为该元件的象素位置。所以这就意味着,你能够根据自己喜好,为UI元件设置相对坐标或者象素的绝对坐标。我想不起来指定象素绝对坐标时这些数值的意义,如果谁看到本例并且知道这些绝对坐标数值意义的话,请帮助补充到本文中。:)不过,如果谁是曾经做过网页的话,将会非常熟悉这些绝对坐标的意义和顺序,因为这俩完全是一个东西。

(补充:译者推荐在游戏制作中使用相对坐标,这样的话在对窗口缩放或对单一UI窗口缩放时会容易的避免一些问题。)
在上面的这个Layout文件中,第一行告诉CEGUI,我们想创建一个顶层的窗口”Main”,而且这个窗口大小是占满整个程序显示区----它的相对宽高是(1.0
- 0.0
)。我们不希望看到它的标题栏,也不希望看到这个顶层窗口右上边的关闭按钮和其边框。于是,下面我们设置他们的Value为false。(这三项是FrameWindow框架窗口默认存在的捆绑控件)。

接下来,我们在屏幕中放置三个按钮----这样的话,在我们运行程序的时候将会从上到下见到“Instant
Action”“Options”“Quit”三个按钮。按钮控件的名称属性是非常重要的,这是我们在代码中获取该控件行为的唯一标识,这点我们一会就会见到。
接下来,看编码
我将要重点说明在代码中整合CEGUI时有什么更变。首先,在main.cpp中,需要额外加一些头文件。
// 需要能够创建CEGUI渲染接口
#include "OgreCEGUIRenderer.h"
// CEGUI 的头文件
#include "CEGUISystem.h"
#include "CEGUIInputEvent.h"
#include "CEGUIWindow.h"
#include "CEGUIWindowManager.h"
#include "CEGUISchemeManager.h"
#include "CEGUIFontManager.h"
#include "elements/CEGUIFrameWindow.h"
我们将创建GUI渲染器的一个实例,所以我们需要包含OgreCEGUIRenderer.h这个头文件。你可以在OgreSDK或Ogre源代码的Samples/include文件夹下找到该文件。

而上面代码中包含的CEGUI头文件都能够在OgreSDK的include/CEGUI文件夹下找到。(在安装OgreSDK时这些通常被自动设置为默认包含头文件目录了)。

如果你在编译时候这里出错,请保证同时添加了包含了这两个目录”include/CEGUI”和”include”。
你也可以不包含”
elements/CEGUIFrameWindow.h”,它仅仅是老版本的CEGUI中残留的一个习惯,新版本的CEGUI中已经可以不包含它了。不过包含的话也是没什么坏处的。

初始化CEGUI
Main.cpp
// 在有一个有效的场景管理器和Windows窗口后,我们可以创建一个GUI渲染器
CEGUI::OgreCEGUIRenderer* pGUIRenderer = new CEGUI::OgreCEGUIRenderer
{
    Window,     // 这个渲染目标窗口应该在之前创建好,CEGUI将会在这个窗口上进行渲染。
     Ogre::RENDER_QUEUE_OVERLAY,      // CEGUI将会在这个渲染队列中进行渲染工作
     False;     // 渲染队列的排列方式。False代表将一切放置在渲染队列最前,而非最后。
     3000,      // 这个参数在现在的CEGUI中已经是不必须的了—--它指定UI的最大范围。
     sceneMgr,  // 用这个场景管理器对UI进行管理
};
// 创建一个根CEGUI类
CEGUI::System* pSystem = new CEGUI::System(pGUIRenderer);
// 设置Log层级,Log中能够提供我们一些Ogre运行的信息,CEGUI.log文件在工作目录下
CEGUI::Logger::getSingleton().setLoggingLevel(CEGUI::Informative);
// 使用指定的CEGUI配置(关于配置更多的信息可以查看CEGUI的文档)
CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme", (CEGUI::utf8*)"GUI");
// 使用指定的CEGUI的鼠标图标 (鼠标信息被定义在look-n-feel中)
pSystem->setDefaultMouseCursor((CEGUI::utf8*)"TaharezLook", (CEGUI::utf8*)"MouseArrow");
// 指定在UI控件中使用的文字字体
CEGUI::FontManager::getSingleton().createFont("Tahoma-8.font", (CEGUI::utf8*)"GUI");
pSystem->setDefaultFont((CEGUI::utf8*)"Tahoma-8");
// 从 .layout脚本文件读取一个UI布局设计,并将其放置到GUI资源组中。(本例中的.layout文件您可以从 resource/gui.zip 中找到。)
CEGUI::Window* pLayout = CEGUI::WindowManager::getSingleton().loadWindowLayout("katana.layout", "", "GUI");
// 接下来我们告诉CEGUI显示哪份UI布局。当然我们可以随时更换显示的UI布局。
pSystem->setGUISheet(pLayout);
上面的代码中有大量的注释了,所以这里我不再重复太多。值得注意的是,我们在创建CEGUI渲染器时需要使用”Ogre渲染窗口对象”
和一个”场景管理器”,所以我们之前必须创建好这两个对象。
CEGUI的输入支持
我们若是需要修改InputHandle类中的一些input消息来源,则需要一些额外的参数:一个指向我们创建的CEGUI::System实例的一个指针。因为我们需要通过InputHandler将一些外界的消息压入CEGUI消息队列中。
Input.cpp
// 鼠标监听
bool InputHandler::mouseMoved(const OIS::MouseEvent &evt)
{
     m_pSystem->injectMouseWheelChange(evt.state.Z.rel);
     return m_pSystem->injectMouseMove(evt.state.X.rel, evt.state.Y.rel);
}
bool InputHandler::mousePressed(const OIS::MouseEvent &evt, OIS::MouseButtonID btn)
{
     CEGUI::MouseButton button = CEGUI::NoButton;
     if (btn == OIS::MB_Left)
           button = CEGUI::LeftButton;
     if (btn == OIS::MB_Middle)
           button = CEGUI::MiddleButton;
     if (btn == OIS::MB_Right)
           button = CEGUI::RightButton;
     return m_pSystem->injectMouseButtonDown(button);
}
bool InputHandler::mouseReleased(const OIS::MouseEvent &evt, OIS::MouseButtonID btn)
{
     CEGUI::MouseButton button = CEGUI::NoButton;
     if (btn == OIS::MB_Left)
           button = CEGUI::LeftButton;
     if (btn == OIS::MB_Middle)
           button = CEGUI::MiddleButton;
     if (btn == OIS::MB_Right)
           button = CEGUI::RightButton;
     return m_pSystem->injectMouseButtonUp(button);
}
// 键盘监听
bool InputHandler::keyPressed(const OIS::KeyEvent &evt)
{
     unsigned int ch = evt.text;
     m_pSystem->injectKeyDown(evt.key);
     return m_pSystem->injectChar(ch);
}
bool InputHandler::keyReleased(const OIS::KeyEvent &evt)
{
     if (evt.key == OIS::KC_ESCAPE)
            m_simulation->requestStateChange(SHUTDOWN);
     return m_pSystem->injectKeyUp(evt.key);
}
注意:如果用户输入过快,那么injectChar或injectKeyUp时可能会出现字符消息丢失的可能,此时上列函数可能会返回false值。所以,若我们希望上面的函数长期返回true值,尽量使用按键消息缓冲。

代码中很清楚明确的说明了如何把input事件通知给CEGUI ----
就是那几个”inject***”的函数调用而已。(这些函数定义才CEGUI::System中)。你同样可以在别的地方进行这几个函数调用。我把它们放在input
handler类中是因为这样很方便,并且完全不用担心Input程序是否在休息,也无需去知道它低层实现的细节。----
总之,它将数据通过某种处理后通知我们的UI使其处理某种行为。 Main.cpp
// InputHandler处理CEGUI的input事件后,我们需要获得它的一个指针,并将给CEGUI::System的一个实例使用。
InputHandler *handler = new InputHandler(pSystem, sim, hWnd);
// put us into our "main menu" state
sim->requestStateChange(GUI);
我们在上面代码中同时把”SIMULATION”状态替换成了”GUI”状态。一般情况下,我们程序中应当在直接进入游戏前提供一个主菜单,通过这个主菜单我们进行游戏状态的变换。所以,我们需要写一个主菜单类专门进行游戏状态的变换,它需要获取UI的信息并进行相应的处理。在本例中,我仅简单的提供一个框架说明这个流程,并为真正的对游戏状态进行变化,所以直接写在main.cpp中了。

// 创建一个主菜单实例
MainMenuDlg* pDlg = new MainMenuDlg(pSystem, pLayout, sim);
在本例中,这个MainMenuDlg的行为处理类在下面的MainMenuDlg.h / .cpp中有声明和定义。
MainMenuDlg.h
#pragma once
#include "CEGUIWindow.h"
namespace CEGUI
{
    class System;
    class Window;
}
class Simulation;
class MainMenuDlg
{
public:
      MainMenuDlg(CEGUI::System* pSystem, CEGUI::Window* pSheet, Simulation* pSimulation);
      ~MainMenuDlg();
      // CEGUI事件处理函数说明:函数的命名随便,但是你必须这样声明bool <method name>(const CEGUI::EventArgs &args)
      bool Quit_OnClick(const CEGUI::EventArgs &args);
      bool Options_OnClick(const CEGUI::EventArgs &args);
      bool Launch_OnClick(const CEGUI::EventArgs &args);
private:
      CEGUI::System* m_pSystem;    // 一个CEGUI::System实例的指针
       CEGUI::Window* m_pWindow;    // 指向一个版面的窗口指针
       Simulation* m_pSimulation;   // 指向一个仿真管理器的指针
};
MainMenuDlg.cpp
#include "MainMenuDlg.h"
#include "Simulation.h"
#include "CEGUISystem.h"
#include "CEGUIWindow.h"
#include "CEGUIWindowManager.h"
#include "elements/CEGUIPushButton.h"
MainMenuDlg::MainMenuDlg(CEGUI::System *pSystem, CEGUI::Window *pSheet, Simulation *pSimulation)
{
      m_pSystem = pSystem;
      m_pWindow = pSheet;
      m_pSimulation = pSimulation;
      // 钩住窗口元件的事件处理函数
       CEGUI::PushButton* pQuitButton = (CEGUI::PushButton *)CEGUI::WindowManager::getSingleton().getWindow("cmdQuit");
       pQuitButton->subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(&MainMenuDlg::Quit_OnClick, this));
       CEGUI::PushButton* pOptionsButton = (CEGUI::PushButton *)CEGUI::WindowManager::getSingleton().getWindow("cmdOptions");
       pOptionsButton->subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(&MainMenuDlg::Options_OnClick, this));
       CEGUI::PushButton* pLaunchButton = (CEGUI::PushButton *)CEGUI::WindowManager::getSingleton().getWindow("cmdInstantAction");
       pLaunchButton->subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(&MainMenuDlg::Launch_OnClick, this));
}
MainMenuDlg::~MainMenuDlg()
{
}
bool MainMenuDlg::Quit_OnClick(const CEGUI::EventArgs &args)
{
       m_pSimulation->requestStateChange(SHUTDOWN);
       return true;
}
bool MainMenuDlg::Launch_OnClick(const CEGUI::EventArgs &args)
{
       return true;
}
bool MainMenuDlg::Options_OnClick(const CEGUI::EventArgs &args)
{
       return true;
}
这里有两个重要的事情需要说明一下:(a)如何让事件处理函数钩取到CEGUI的事件。(b)在例子中,我们处理了”Quit”按钮按下的消息(象按下ESC键一样处理,让程序关闭)。如果你有兴趣,可以在上面的代码里对Launch_OnClick和Options_OnClick函数补充使其进行实际的消息处理,也可以为更多的GUI元件事件消息进行处理。

结束语
实际上,也没有更多的事情可做了。----
何去创建你的UI事件处理,如何将Input事件压入CEGUI都已经告诉你了。你可以用这里的代码,也可以自己去写,但是基础的这些东西是没有变化的。关于CEGUI和Ogre确实也没有更多的可说的了
---- 你只需要在自己的代码中对其他的一些UI元件事件进行hook后处理就可以了。
下载这些代码
我使用Ogre1.4.x和VC8把这些代码做了整理和测试。你可以在这里下载这些代码。请注意,我将编译后的可执行文件输出到
$(OGRE_HOME)/bin/Debug
文件夹下了,你可以自行修改其目录。同时,你要保证编译出的可执行文件目录下有个”resource”目录,你还需要将gui.zip拷贝到这个目录下。当然,在DEBUG模式下,你可以设置工作目录。另外注意的是,我仅对项目的Debug模式做了设置,所以,如果你想在Release模式下运行,你还需要自己配置一下编译环境。好运。