Table of contents
Our simulator is no different than most games in that it has two main "states": "{LEX()}GUI{LEX}" and "Game". However, one of the most confusing topics in the history of Ogre and {LEX()}CEGUI{LEX} that I have run across is in fact getting the two to work together. It actually is fairly straightforward, but there are so many incomplete examples and out-of-date articles and uneducated forum posts on it that it took me 5 times as long as it should have to get it working.
First off, the CEGUI example in this Wiki: forget it. Or at least the part that deals with {LEX()}RTT{LEX} (Render To Texture). It's unused in the example and serves only to confuse the issue hopelessly. In fact, there are only about four lines of code needed to load a GUI into an Ogre window.
But before you do that, you need to create the GUI first. If you are not picky, you can easily use the TaharezLook widget set and not worry about it. It's complete, fairly sharp and it works. For the time being, while our team is working on our project's look and feel and layouts, I am using the TaharezLook set for placeholder development of the two-state execution mentioned above, and I will gladly share with you what knowledge I painfully learned during the past few days.
First, a note about our simulator design. The first thing that happens on startup is the GUI is displayed, at the "Welcome" screen. Ours for now just has a few buttons which lead to other pages (Options) or perform command actions ("Instant Action", Quit). The Instant Action button takes the user to a page deeper in the navigational map (you do have one for your game, correct?) where he or she can select a mission or whatever to join or play. How you obtain the list of levels or missions available for your game is entirely up to you. We currently do it by reading each file in the "resource/missions" folder and obtaining the header data to populate a dropdown list of missions. You might simply use a manifest file that contains the mission names and associated filename, or you may have a completely different solution. It's up to you.
Later you will see that we use multiple scene managers. Why? Each mission will be built to use a particular scene manager, depending on whether the user is in the great outdoors, inside a cave or building, or on an aerospace or outerspace mission. The reason that the scene manager plugin system is present in Ogre is that no one single scene manager implementation is the best for all types of scenes, and as we will see, switching between them at runtime is fairly simple.
CEGUI
CEGUI is the "official" Ogre GUI library. While Ogre has 2D functionality, it quickly became apparent that going much further than simple overlays was "out of scope" and CE's GUI system was adopted. If you have not already, you should get CEGUI from the CEGUI website.
Find the "datafiles" directory in either the source tree or wherever in the binary distribution (or in the root directory of the CELayoutEditor download install), and look for the layouts/ directory. Each file you find there is a descriptions of the layout for an entire GUI page (sheet). While you certainly can create a GUI layout in code (and there are plenty of reasons why you might want to), you don't have to. 99% of CEGUI users can happily create only layout files and never create a single window in code. After all, as you'll see, the layout simply loads into a window like any other CEGUI window and can be treated no differently than any other.
The scheme referenced in each widget name (i.e. "TaharezLook/EditBox" is an EditBox in the TaharezLook scheme) can be found in the schemes/ directory, which in turn references an imageset in the imagesets/ directory, which in the end is nothing more than a glorified HTML imagemap concept: each widget (and widget state) is mapped from the TGA image for that scheme using a four-corner rectangular mapping notation. Review the files and you'll see how it all works together. I have to confess I've not delved too deeply into the looknfeel/ directory stuff, but it appears simply to address, not surprisingly, the look and feel and behavior of each widget in the scheme.
While you are there, as discussed before, feel free to remove the relative pathnames wherever you find them (i.e. "../datafiles/..."); since we provided the location of all of our resources to the ResourceManager during initialization, we don't need to keep track of what lives where — ResourceManager will find them for us.
If you want, feel free to load up a layout in the CELayoutEditor (separate {LEX()}CVS{LEX} module, separate project, available for download from CEGUI site). Play around with it, create a new one, whatever you want. You won't play with it long due to its clumsiness, but it is instructive to see how it all goes together, especially if you build one from scratch.
Keep in mind that each .layout file is the definition for a single GUI sheet/page. You will need one for each page in your GUI.
Finally, note that the extension convention for CEGUI files (.scheme, .layout, etc.) really is just that: convention. CEGUI does not look for specific extensions; in fact, you have to provide CEGUI with the whole filename when loading them, so if you don't like .layout for some reason, feel free to use whatever you like.
Handling GUI Events
If you are at all familiar with MFC programming, you are most of the way there to understanding CEGUI event handling. The major difference is that your application subscribes to GUI events by providing a handler method for CEGUI to call when an event happens on a widget. CEGUI then calls your method with an EventArgs object so that you can retrieve a pointer to the window that sourced the message (i.e. in the case of a button click handler, the args will allow you to access the window that is the button). You should understand that in CEGUI (much as in any windowing system), EVERYTHING is a window.
The oddest thing about CEGUI, but something that makes great sense once you understand it, is that CEGUI does NOT deal with input AT ALL. It does not handle any input devices, not the keyboard, not the mouse, not anything. So how does it get input from these devices?
You have to capture the input yourself (using an input {LEX()}API{LEX} like the aforementioned {LEX()}OIS{LEX}) and provide CEGUI with notification that a button was clicked or a key was pressed. Same with mouse movement. Using the injectMouseMove(), injectKeyDown(), injectKeyUp() etc. methods, you tell CEGUI that something happened and it will tell you in return if something was click or changed or whatever. Really elegant, once you understand the processing flow, and completely removes the need for CEGUI to try to horn in on the increasing number of things interested in HID input in your application. ๐
The .scheme file for the menu
So, enough already! I already have Ogre fired up and I am tired of looking at my blank rendering window, I want to see a GUI there! For purposes of demonstration, let's use the layout I created for our project:
<?xml version="1.0" ?> <GUILayout> <Window Type="DefaultWindow" Name="DemoLayout"> <Window Type="TaharezLook/FrameWindow" Name="Main"> <Property Name="RelativeMinSize" Value="w:0.2 h:0.2" /> <Property Name="RelativeMaxSize" Value="w:1.0 h:1.0" /> <Property Name="Position" Value="x:0.0 y:0.0" /> <Property Name="Size" Value="w:1.0 h:1.0" /> <Property Name="Text" Value="MyProject" /> <Property Name="CloseButtonEnabled" Value="False" /> <Window Type="TaharezLook/Button" Name="cmdQuit"> <Property Name="Text" Value="Quit" /> <Property Name="Position" Value="x:0.4 y:0.7" /> <Property Name="Size" Value="w:0.2 h:0.07" /> </Window> <Window Type="TaharezLook/Button" Name="cmdOptions"> <Property Name="Position" Value="x:0.4 y:0.6" /> <Property Name="Size" Value="w:0.2 h:0.07" /> <Property Name="Text" Value="Options" /> </Window> <Window Type="TaharezLook/Button" Name="cmdInstantAction"> <Property Name="Position" Value="x:0.4 y:0.5" /> <Property Name="Size" Value="w:0.2 h:0.07" /> <Property Name="Text" Value="InstantAction" /> </Window> </Window> </Window> </GUILayout>
[IMPORTANT NOTE: For users of CEGUI 0.4.x and earlier, the above layout will work fine. For users of 0.5.x and later, which at this time is CVS HEAD, you MUST move to the Unified Dimension (UDim) manner of specifying window coordinates. CVS HEAD currently has support for Position and Size and the like ENTIRELY REMOVED. You have been warned. ๐ If you are not sure what version you are running, CEGUI.log will happily tell you. Find it sitting next to Ogre.log during and after your game's execution.]
This is the entire contents of the layout file for the main screen that shows up when you start our project. Notice that CEGUI likes the UVW style of "normalized" coordinates; this allows you to free yourself from worrying about different resolutions your user might choose. I have the main window set at 1.0 for width and height; this will make it take up the whole rendering window, which is what we want. The TaharezLook scheme and look/feel will tile a particular subimage from its imageset across the background of the window, so it looks good. In fact, you can cut/paste this layout into a file and load it up in CELayoutEditor and see how it looks; it looks the same in-game.
Code to use the .scheme file with CEGUI
Here is the code to load this layout into the rendering window we've created earlier, and display it:
// setup -GUI system mGUIRenderer = new CEGUI::OgreCEGUIRenderer(window, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, guiSceneMgr); mGUISystem = new CEGUI::System(mGUIRenderer); CEGUI::Logger::getSingleton().setLoggingLevel(CEGUI::Informative); CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme"); mGUISystem->setDefaultMouseCursor((CEGUI::utf8*)"TaharezLook", (CEGUI::utf8*)"MouseArrow"); CEGUI::FontManager::getSingleton().createFont("bluehighway.font"); mGUISystem->setDefaultFont((CEGUI::utf8*)"BlueHighway-12"); // set the mouse cursor initially in the middle of the screen mGUISystem->injectMousePosition((float)window->getWidth() / 2.0f, (float)window->getHeight() / 2.0f);
[NOTE: The above code will not compile if you are using CEGUI 0.7.1. As this portion of the tutorial is for academics, the principles described still hold. The 0.7.1 functional code is included in the part of the tutorial that covers actual implementation of CEGUI.]
Some notes on the above: first, it's basically the code from the CEGUI tutorial without the distracting and unnecessary {LEX()}RTT{LEX} stuff. Then, I chose Tahoma-12 because, well, just because. I like it. You may have to "borrow" the TTFs from the Windows or Linux fonts directories. (Alternately, you can simply reference the C:\Windows\Fonts directory as another ResourceGroup location. Neat, huh?) Third, notice the complete lack of any directory information at all with the filenames; this is taken care of by the ResourceManager, which we are leveraging by using the OgreCEGUIRenderer.
Remember the guiSceneMgr we obtained earlier (see here)? This is where it is used again to set up the GUI. That c-tor call on the OgreCEGUIRenderer is basically "boilerplate" and you'll use it similarly in your project.
The last thing to notice is the injectMousePosition() call; we do this to place the mouse in the center of the screen. Oddly enough, it takes absolute coordinates instead of the scaled 0.0-1.0 method.
- (It may take absolute coordinates over the scaled method because the user's mouse may be at any location on the screen - and if I understand correctly, the scaled method would be scaling relative to its 'parent' window which may only go as high as the application's window and not the entire desktop real estate. In fullscreen this would work, but in windowed mode would limit you in possible use of the injection. Wow that sounds long winded, it's early, but figured it may help someone out there's understanding of it all.)
Subscribing to CEGUI events
Subscribing to CEGUI events is straightforward; I will not show you how our subscriptions are done because it's more complex than is necessary for purposes of demonstration. Suffice to say that your subscription code will look a lot like this:
CEGUI::Window *win; m_win = CEGUI::WindowManager::getSingleton().getWindow(sheetName); win = m_win->getChild("cmdQuit"); win->subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(Quit_OnClick, this));
getWindow() in CEGUI will get a pointer to the named window, wherever it may be. Above, we are getting a pointer to the root window of the sheet we loaded.
Handling CEGUI events
You would call this from within a class implemented to handle GUI events; in our case, there is one class per "dialog" or "sheet" or "page" (whatever you want to call it). The handler method for this event, in the handler class for the "Main" GUI sheet (that we cleverly called "GuiEventHandler_Main") is implemented as:
bool GuiEventHandler_Main::Quit_OnClick(const CEGUI::EventArgs &args) { // initiate system shutdown (we do this by requesting a shutdown, // which eventually will stop the main loop) m_stateManager.requestStateChange(SHUTDOWN); return true; }
Translating OGRE events to CEGUI events
The last essential thing you'll need to work with CEGUI and Ogre is translating input system events into something that CEGUI can consume. Our input system, not surprisingly, is based on DirectInput on Win32. It uses a {LEX()}MFC{LEX}-like "On*()" naming scheme for dispatching HID events, such as OnKeyDown() and OnMouseMove(). In the GUI state, these events have to be handled and then sent off to CEGUI, which is not as straightforward as it might seem. Mouse input is fine and intuitive, but for keyboard input, not only do you have to send the keyboard scancode, you have to send it the character it represents as well.
If you are reading this in a country that uses dead-key combinations, I wish you the best of luck if you are trying to roll your own input on Win32: the ToUnicode()/ToUnicodeEx() API on Win32 are so horribly done that MS devs themselves can't deal with it without complaining. The rest of us that use the Latin character set are fine, but if you plan on international support for your game then you have your work cut out for you; I suggest a long hard look at the keyboard code in GTK's Win32 port for how to handle all of this. OIS handles some of this, but as of this writing, Unicode is not yet supported.
But I digress. You want to see some code on injecting input into CEGUI, not a rant on stupid Microsoft implementations (which could keep us here 'til Christmas). So, here is some:
// event data container typedef struct { int x, y, z; int button; } MouseData; typedef struct { unsigned char keyCode; // hardware keyboard scan code unsigned long mbcc; // multi-byte character code (UNICODE) bool alt; bool ctrl; bool shift; } KeyData; void InputProcessor::OnMouseMove(Input::MouseData &evt) { int x = evt.x; int y = evt.y; if (m_stateManager.getCurrentState() == GUI) m_video.guiMouseEvent(x, y, 0, 0); } void InputProcessor::OnMouseMoveZ(Input::MouseData &evt) { int z = evt.z; if (m_stateManager.getCurrentState() == GUI) m_video.guiMouseEvent(0, 0, z, 0); } void InputProcessor::OnMouseButtonDown(Input::MouseData &evt) { int button = evt.button; if (m_stateManager.getCurrentState() == GUI) m_video.guiMouseEvent(evt.x, evt.y, 0, -button); } void InputProcessor::OnMouseButtonUp(Input::MouseData &evt) { int button = evt.button; if (m_stateManager.getCurrentState() == GUI) m_video.guiMouseEvent(evt.x, evt.y, 0, button); } void InputProcessor::OnKeyDown(Input::KeyData &evt) { if (m_stateManager.getCurrentState() == GUI) m_video.guiKeyboardEvent((long)evt.keyCode, evt.mbcc, true, evt.alt, evt.ctrl, evt.shift); } void InputProcessor::OnKeyUp(Input::KeyData &evt) { if (m_stateManager.getCurrentState() == GUI) m_video.guiKeyboardEvent((long)evt.keyCode, evt.mbcc, false, evt.alt, evt.ctrl, evt.shift); }
Note that our video subsystem handles GUI and game input command differently, and therefore different methods need to be called (also note that there are no "game" input calls here yet). On the receiving end of that call, you'll find:
void video::guiKeyboardEvent(long keycode, long ch, bool down, bool alt, bool ctrl, bool shift) { if (down) { mGUISystem->injectKeyDown(keycode); mGUISystem->injectChar(ch); } else mGUISystem->injectKeyUp(keycode); } static CEGUI::MouseButton guiButton[3] = { CEGUI::LeftButton, CEGUI::MiddleButton, CEGUI::RightButton }; // x, y are 0 to N-1 where N=screen dimensions // z is any integral number // button indicates which button was pressed/released; negative // numbers are "down" events, and zero indicates no button event void video::guiMouseEvent(int x, int y, int z, int button) { if (button == 0) mGUISystem->injectMouseMove(x, y); else { if (button > 0) mGUISystem->injectMouseButtonUp(guiButton[button-1]); else mGUISystem->injectMouseButtonDown(guiButton[(-button)-1]); } }
It's that simple, really. On Win32, ToUnicodeEx() usually will give you the Unicode character that corresponds to the keystroke indicated by the keycode, as it is laid out on the current keyboard you have set in Windows (not sure how locales are handled on Linux), and you pass that into CEGUI so it can display the character you typed.
Switching From -GUI To Game And Vice Versa
Remember the showGui() method mentioned in the Initialization article? Here's its definition:
if (window) window->removeAllViewports(); if (guiSceneMgr) guiSceneMgr->removeAllCameras(); else guiSceneMgr = ogre->getSceneManager(ST_GENERIC); if (sceneMgr) sceneMgr->removeAllCameras(); camera = guiSceneMgr->createCamera("Main"); camera->setPosition(0, 0, 300); camera->lookAt(0, 0, 0); window->addViewport(camera);
This is how you switch scene managers in Ogre. First, you need to remove all of a window's viewports, then you need to remove all cameras a scene manager may be using, then you can start operating on the alternate scene manager and create cameras from it (and add viewports using those cameras). The code above is for the general case, and will deal with switching out of the "Game" state as well as starting up from scratch. For the sake of completeness, the showScene() method is
window->removeAllViewports(); if (guiSceneMgr) guiSceneMgr->removeAllCameras();
The conventional usage of these two methods is such that each will be followed up by a call to load content into the scene; for the GUI, it will be the layout loading code above, and for the scene, it will be whatever code you have to load a world and geometry into that scene (we'll visit this part later). For now, it's enough to know that showScene() is called from the code that handles our app's "Launch" GUI button press event.
And what of those events? Not much, really. If you've ever done MFC programming or VB forms programming, you know how little there can be to do in response to some events. In fact, the entire code we have in response to the Launch button press is
Mission::MissionFile mf; std::ifstream mfile("demo.xxx"); mf.deserialize(mfile, 0); mfile.close(); m_stateManager.requestStateChange(NORMAL); m_video.showScene(); m_video.loadWorld(mf);
This code loads our binary mission file format, deserializes it into something that can be used later, requests a state change from the state manager, calls the showScene() to switch scene managers, and loads the world and geometry into the scene, and then off it goes. At some point there will be a loader progress bar updated while the scene and geometry all load, and that will complete the appearance of a "professional" game. ๐
State Management
State Manager? What's that? Does that come with Ogre?
Nope, it's just a small "gatekeeper" class that allows us to synchronize the input dispatching loop with game states (GUI input is handled much differently and by a different class than game input is handled; we'll show that later). You'll be perfectly able to write your own state manager, since every game has its own personal states; what works for one might not work for another. At this stage, ours has STARTUP, SHUTDOWN, GUI and NORMAL; others might be implemented at a later date depending on what the needs are.
Prev <<< Practical Application - Initialization | Practical Application - Let's Get Started >>> Next |