OGRE/CEGUI i Ogre
Informacje wstępne
[edytuj]Zakładam, że masz znajomości programowania C + + i jesteś w stanie skonfigurować i opracować aplikacje w Ogre. Ten samouczek bazuje na poprzednim i zakłada, że już pracowałeś z nim.
Wprowadzenie
[edytuj]W tej instrukcji zbadamy sposób korzystania z CEGUI (wbudowany system GUI) z Ogre. Ten podręcznik pokaże jak dodać w CEGUI podstawowe elementy funkcjonalności aplikacji. UWAGA: Ten podręcznik nie nauczy Cię, jak używać CEGUI. Ten przewodnik pomoże Ci zacząć. Wszelkie dalsze pytania i pomoc w CEGUI należy kierować do ich strony głównej.
W module kod jest dostępny kompletny kod źródłowy. Czytając ten podręcznik należy powoli dodawać kod do własnego projektu i oglądać jego wyniki.
Rozpoczęcie
[edytuj]Początkowy kod
[edytuj]W tym samouczku, będziemy używać wstępny kod źródłowy jako punkt wyjścia. Jeśli pracowałeś w poprzednich samouczkach, powinno być to znane do Ciebie. Utwórz projekt w kompilatorze wybranym dla tego projektu i dodaj plik źródłowy, który zawiera ten kod:
#include "ExampleApplication.h" #include <CEGUI/CEGUI.h> #include <OIS/OIS.h> #include <OgreCEGUIRenderer.h> class TutorialListener : public ExampleFrameListener, public OIS::MouseListener, public OIS::KeyListener { public: TutorialListener(RenderWindow* win, Camera* cam) : ExampleFrameListener(win, cam, true, true) { mContinue=true; mMouse->setEventCallback(this); mKeyboard->setEventCallback(this); } // CEGUIDemoListener bool frameStarted(const FrameEvent &evt) { mKeyboard->capture(); mMouse->capture(); return mContinue && !mKeyboard->isKeyDown(OIS::KC_ESCAPE); } bool quit(const CEGUI::EventArgs &e) { mContinue = false; return true; } // MouseListener bool mouseMoved(const OIS::MouseEvent &arg) { return true; } bool mousePressed(const OIS::MouseEvent &arg, OIS::MouseButtonID id) { return true; } bool mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id) { return true; } // KeyListener bool keyPressed(const OIS::KeyEvent &arg) { return true; } bool keyReleased(const OIS::KeyEvent &arg) { return true; } private: bool mContinue; }; class CEGUIDemoApplication : public ExampleApplication { public: CEGUIDemoApplication() : mSystem(0), mRenderer(0) { } ~CEGUIDemoApplication() { if (mSystem) delete mSystem; if (mRenderer) delete mRenderer; } protected: CEGUI::System *mSystem; CEGUI::OgreCEGUIRenderer *mRenderer; void createScene(void) { } void createFrameListener(void) { mFrameListener= new TutorialListener(mWindow, mCamera); mFrameListener->showDebugOverlay(true); mRoot->addFrameListener(mFrameListener); } }; #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 #define WIN32_LEAN_AND_MEAN #include "windows.h" INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT) #else int main(int argc, char **argv) #endif { // Create application object CEGUIDemoApplication app; try { app.go(); } catch(Exception& e) { #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 MessageBoxA(NULL, e.getFullDescription().c_str(), "An exception has occurred!", MB_OK | MB_ICONERROR | MB_TASKMODAL); #else fprintf(stderr, "An exception has occurred: %s\n", e.getFullDescription().c_str()); #endif } return 0; }
Kompilacja
[edytuj]Teraz musisz sprawdzić, czy skompilujesz i uruchomisz ten kod. Wynik powinien wyświetlić pusty ekran (naciśnij ESC aby wyjść). Jeśli masz błędy linkera podczas próby kompilacji, należy się upewnić, że CEGUIBase_d.lib i OgreGUIRenderer_d.lib linker dodaje do wejścia (w trybie debugowania, po dopuszczeniu trybu usunąć _d z nich).
A Brief Introduction
[edytuj]CEGUI is a fully featured GUI library that can be embedded in 3D applications such as Ogre (it also supports pure DirectX and OpenGL as well). Much in the same way that Ogre is only a graphics library (and doesn't do other things such as sound, physics, etc), CEGUI is only a GUI library, meaning it does not do its own rendering nor does it hook into any mouse or keyboard events. In fact, in order for CEGUI to render at all, you have to provide a renderer for it (which is the OgreGUIRenderer library included in the SDK), and in order for it to even understand mouse and keyboard events you have to manually inject them into the system. This may seem like a pain at first, but in reality very little code is required to make this happen. It also allows you to have full control over the rendering and the input, CEGUI will never get in the way.
There are many aspects to CEGUI and many quirks that will be unfamiliar to you (even if you have used GUI systems before). I will try to slowly introduce them to you as we go along.
Integracja z OGRE
[edytuj]Inicjalizacja CEGUI
[edytuj]Poznaliśmy trochę sposób włączania CEGUI w poprzednim akapicie. Na razie wiedzy tej nie będziemy pogłębiać.
Znajdź funkcję createScene i dodaj do niej następujący kod:
mRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, mSceneMgr); mSystem = new CEGUI::System(mRenderer);
Teraz, kiedy włączyliśmy CEGUI, musimy wybrać szate graficzną, jakiej będziemy używać. Wygląd CEGUI można dowolnie zmieniać. Więcej informacji na ten temat znajdziesz na stronie CEGUI.
Następująca linijka kodu definiuje skórkę:
CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme");
Domyślnie Ogre nie zawiera innych skórek CEGUI, ale jeżeli pobierzesz CEGUI z ich strony, znajdziesz więcej skórek (możesz też zrobić swoją własną). Następną rzeczą którą musimy zrobić jest zaimplementowanie kursora myszy oraz domyślnej czcionki:
mSystem->setDefaultMouseCursor((CEGUI::utf8*)"TaharezLook", (CEGUI::utf8*)"MouseArrow"); mSystem->setDefaultFont((CEGUI::utf8*)"BlueHighway-12");
Throughout this tutorial series we will be using CEGUI to display the mouse cursor, even when we have no other use for the GUI library. It is possible to use another GUI library to render the mouse, or to simply create your own mouse cursor using Ogre directly (though this latter option can be a bit involved). If you are only using CEGUI for the mouse cursor and are concerned about memory usage or the disk space that your game takes up, you can look into one of these options to replace CEGUI.
Lastly note that in that last code snippet we have set the default mouse cursor, but we did not set the mouse cursor directly using the MouseCursor::setImage function as we will in later tutorials. This is because in this tutorial we will always be over some kind of CEGUI window (though it may be invisible), so setting the default cursor will, in effect, make the mouse cursor be the image we selected. If we set the mouse cursor directly and did not set the default, the mouse cursor would be invisible every time it passed over a CEGUI window (which, in this tutorial, will be all the time). On the other hand, setting the default mouse image does nothing if you do not have any CEGUI windows displayed, as will be the case in later tutorials. In that situation, calling MouseCursor::setImage will display the cursor for the application. Example:
CEGUI::MouseCursor::getSingleton().setImage(CEGUI::System::getSingleton().getDefaultMouseCursor());
Injecting Key Events
[edytuj]CEGUI does not handle input in any way. It does not read mouse movements or keyboard input. Instead it relies on the user to inject key and mouse events into the system. The next thing we will need to do is to handle the key events. If you are working with CEGUI, you will need to have the mouse and keyboard in buffered mode so you can receive the events directly and inject them as they happen. Find the keyPressed function and add the following code to it:
CEGUI::System *sys = CEGUI::System::getSingletonPtr(); sys->injectKeyDown(arg.key); sys->injectChar(arg.text);
After getting the system object, we need to do two things. The first is to inject the key down event into CEGUI. The second is to inject the actual character that was pressed. It is very important to inject the character properly since injecting the key down will not always bring about the desired result when using a non-English keyboard. The injectChar was designed with Unicode support in mind.
Now we need to inject the key up event into the system. Find the keyReleased function and add the following code:
CEGUI::System::getSingleton().injectKeyUp(arg.key);
Note that we do not need to inject a character up event, only the key up event is required.
Converting and Injecting Mouse Events
[edytuj]Now that we have finished dealing with keyboard input, we will now need to take care of mouse input. We have a small issue that we will need to address however. When we injected the key up and down events into CEGUI we never had to convert the key. Both OIS and CEGUI use the same key codes for keyboard input. The same is not true for mouse buttons. Before we can inject mouse button presses into CEGUI, we will need to write a function which converts OIS button IDs into CEGUI button IDs. Add the following function near the top of your source code, just before the TutorialListener class:
CEGUI::MouseButton convertButton(OIS::MouseButtonID buttonID) { switch (buttonID) { case OIS::MB_Left: return CEGUI::LeftButton; case OIS::MB_Right: return CEGUI::RightButton; case OIS::MB_Middle: return CEGUI::MiddleButton; default: return CEGUI::LeftButton; } }
Now we are ready to inject mouse events. Find the mousePressed function and add the following code:
CEGUI::System::getSingleton().injectMouseButtonDown(convertButton(id));
This should be roughly self-explanatory. We convert the button ID which was passed in, and pass the result to CEGUI. Find the mouseReleased function and add this line of code:
CEGUI::System::getSingleton().injectMouseButtonUp(convertButton(id));
Lastly, we need to inject mouse motion into CEGUI. The CEGUI::System object has an injectMouseMove function which expects relative mouse movements. The OIS::mouseMoved handler gives us those relative movements in the state.X.rel variable and the state.Y.rel variables. Find the mouseMoved function and add the following code:
CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);
That's it. Now CEGUI is fully setup and receiving mouse and keyboard events.
Windows, Sheets, and Widgets
[edytuj]Introduction
[edytuj]CEGUI is very different from most GUI systems. In CEGUI, everything that is displayed is a subclass of the CEGUI::Window class, and a window can have any number of children windows. This means that when you create a frame to contain multiple buttons, that frame is a Window. This also leads to some strange things to happen. You can place a button inside another button, though that would really never happen in practice. The reason that I mention all of this is when you are looking for a particular widget that you have placed in the application, they are all called Windows, and are accessed by functions which call them as such.
In most practical uses of CEGUI, you will not create each individual object through code. Instead you create a GUI layout for your application in an editor such as the CEGUI Layout Editor. After placing all of your windows, buttons, and other widgets onto the screen as you like them, the editor saves the layout into a text file. You may later load this layout into what CEGUI calls a GUI sheet (which is also a subclass of CEGUI::Window).
Lastly, know that CEGUI contains a large number of widgets that you can use in your application. We will not cover them in this tutorial, so if you decide to use CEGUI, be sure to take a good look at their website for more information.
Loading a Sheet
[edytuj]In CEGUI loading a sheet is very easy to do. The WindowManager class provides a "loadWindowLayout" function which loads the sheet and puts it into a CEGUI::Window object. Then you call CEGUI::System::setGUISheet to display it. We will not be using this in this tutorial, but I would feel remiss if I did not at least show you an example of its use. Do not add this to the tutorial (or if you do, remove it after you have seen the results):
// Do not add this to the program CEGUI::Window* sheet = CEGUI::WindowManager::getSingleton().loadWindowLayout((CEGUI::utf8*)"ogregui.layout"); mSystem->setGUISheet(sheet);
This sets the sheet currently being displayed. You can later retrieve this sheet by calling System::getGUISheet. You can also swap the GUI sheet seamlessly by calling setGUISheet with whatever sheet you want to swap to (though be sure to hold onto a pointer to the current sheet if you wish to swap it back).
Manually Creating an Object
[edytuj]As I said before, most of the time you use CEGUI, you will be using GUI sheets that you create using an editor. Occasionally, however, you will need to manually create a widget to put on the screen. In this example, we will be adding a Quit button which we will later add functionality to. Since we will be adding more than just the Quit button to the screen by the time the tutorial is over, we need to first create a default CEGUI::Window which will contain all of the widgets we will be creating. Add this to the end of the createScene function:
CEGUI::WindowManager *win = CEGUI::WindowManager::getSingletonPtr(); CEGUI::Window *sheet = win->createWindow("DefaultGUISheet", "CEGUIDemo/Sheet");
This uses the WindowManager to create a "DefaultGUISheet" called "CEGUIDemo/Sheet". While we could name the sheet anything we like, it's very common (and encouraged) to name the widget in a hierarchical manner such as "SomeApp/MainMenu/Submenu3/CancelButton". The next thing we need to do is to create the Quit button and set its size:
CEGUI::Window *quit = win->createWindow("TaharezLook/Button", "CEGUIDemo/QuitButton"); quit->setText("Quit"); quit->setSize(CEGUI::UVector2(CEGUI::UDim(0.15, 0), CEGUI::UDim(0.05, 0)));
This is very close to being cryptic. CEGUI uses a "unified dimension" system for its sizes and positions. When setting the size you must create a UDim object to tell it what size it should be. The first parameter is the relative size of the object in relation to its parent. The second parameter is the absolute size of the object (in pixels). The important thing to realize is that you are only supposed to set one of the two parameters to UDim. The other parameter must be 0. So in this case we have made a button which is 15% as wide as its parent and 5% as tall. If we wanted to specify that it should be 20 pixels by 5 pixels, we would do that by setting the second parameter in both of the UDim calls to be 20 and 5 respectively.
The last thing we have to do is attach the Quit button to the sheet we have created, and then set the current GUI sheet for the system to be that sheet:
sheet->addChildWindow(quit); mSystem->setGUISheet(sheet);
Now if you compile and run your application you will see a Quit button in the top left hand corner of the screen, but it does not yet do anything when you click on it.
Events
[edytuj]Events in CEGUI are very flexible. Instead of using an interface that you implement to receive events, it uses a callback mechanism which binds any public function (with the appropriate method signature) to be the event handler. Unfortunately this also means that registering events is a bit more complicated than it is for Ogre. We will now register to handle the Quit button's click event to exit the program when it is pressed. To do that, we will first need a pointer to the Quit button we created in the previous section. Find the TutorialListener's constructor and add the following code:
CEGUI::WindowManager *wmgr = CEGUI::WindowManager::getSingletonPtr(); CEGUI::Window *quit = wmgr->getWindow((CEGUI::utf8*)"CEGUIDemo/QuitButton");
Now that we have a pointer to the button, we now will subscribe to the clicked event. Every widget in CEGUI has a set of events that it supports, and they all begin with "Event". Here is the code to subscribe to the event:
quit->subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(&TutorialListener::quit, this));
The first parameter to subscribeEvent is the event itself. The second parameter is an Event::Subscriber object. When creating a Subcriber object, the first thing we pass in is a pointer to the function that will handle the event (note the & symbol which gives us the pointer to the function). The second thing we pass to subscriber is the TutorialListener object which will handle the event (which is the "this" object). That's it! Our TutorialListener::quit function (which has already been defined) will handle the mouse click and terminate the program.
Compile and run your application to test this out.
One thing to note is that we can create any number of functions to handle events for CEGUI. The only restriction on them is that they must return a bool and they must take in a single parameter of type "const CEGUI::EventArgs &". For more information about events (and how to unsubscribe from them), be sure to read more on the CEGUI website.
Render to Texture
[edytuj]One of the more interesting things we can do with CEGUI is to create a render to texture window (full RTT Tutorial). This allows us to create a second Viewport which can be rendered directly into a CEGUI widget. To do this, we first need to setup a scene for us to look at. Add the following code to the bottom of the createScene function:
mSceneMgr->setAmbientLight(ColourValue(1, 1, 1)); mSceneMgr->setSkyDome(true, "Examples/CloudySky", 5, 8); Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh"); SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(Vector3(0, 0, -300)); headNode->attachObject(ogreHead);
Now we must create the RenderTexture. The RenderSystem object provides the functionality to render to a texture. To do this we create a texture with the TextureManager::createManual function. For this program we will create a 512 x 512 texture:
RenderTexture *tex= mRoot->getTextureManager()->createManual ("R2TTex", "Default", TEX_TYPE_2D, 512, 512, 0, PF_R8G8B8, TU_RENDERTARGET) ->getBuffer()->getRenderTarget();
See the API reference for more information on this function. Next we need to create a Camera and a Viewport to look at the scene we have created. Note that we have changed a couple of Viewport options, including turning off Overlays...which is very important to do or you will get CEGUI and Ogre overlays within our mini-window.
Camera *cam = mSceneMgr->createCamera("R2TCam"); cam->setPosition(100, -100, -400); cam->lookAt(0, 0, -300); Viewport *v = tex->addViewport(cam); v->setOverlaysEnabled(false); v->setClearEveryFrame(true); v->setBackgroundColour(ColourValue::Black);
Note that we have added the Viewport to the texture itself (as opposed to the RenderWindow, which is where we usually add Viewports). Now that we have created our scene and our texture, we need to embed it within CEGUI. You can create a CEGUI::Texture from any Ogre texture by calling the OgreCEGUIRenderer::createTexture function:
CEGUI::Texture *cTex = mRenderer->createTexture((CEGUI::utf8*)"R2TTex");
Unfortunately, this is where things get complicated. In CEGUI you never just deal with a single Texture or a single image. CEGUI works with image sets instead of individual images. It is very useful to work with entire grids of images when you are trying to define the look and feel of a skin you are creating (for example, take a look at TaharezLook.tga in the media folder of the SDK to see what an image set looks like). However, even when you are only trying to define a single image, you must create an entire image set for it. This is what we will be doing:
CEGUI::Imageset *imageSet = CEGUI::ImagesetManager::getSingleton().createImageset((CEGUI::utf8*)"R2TImageset", cTex); imageSet->defineImage((CEGUI::utf8*)"R2TImage", CEGUI::Point(0.0f, 0.0f), CEGUI::Size(cTex->getWidth(), cTex->getHeight()), CEGUI::Point(0.0f,0.0f));
The first line creates the image set (called "R2TImageset") from the texture that we have provided it. The next line (which calls defineImage), specifies that the first and only image is called "R2TImage" and it is as large as the entire cTex texture we have provided. Finally we need to create the StaticImage widget which will house the render texture. The first part is no different from creating any other window:
CEGUI::Window *si = win->createWindow((CEGUI::utf8*)"TaharezLook/StaticImage", "R2TWindow"); si->setSize(CEGUI::UVector2(CEGUI::UDim(0.5f, 0), CEGUI::UDim(0.4f, 0))); si->setPosition(CEGUI::UVector2(CEGUI::UDim(0.5f, 0), CEGUI::UDim(0, 0)));
Now we need to specify which image this StaticImage widget will display. Once again, since CEGUI always deals with image sets and not individual images, we must now retrieve the exact image name from the image set, and to display it:
si->setProperty("Image", CEGUI::PropertyHelper::imageToString(&imageSet->getImage((CEGUI::utf8*)"R2TImage")));
If it seems like we have packed a texture into an image set only to unpack it again, it's because that's exactly what we have done. Manipulating images in CEGUI is not one of the easiest or most straightforward things in the library. The last thing we need to do is to add the StaticImage widget to the GUI sheet we created earlier:
sheet->addChildWindow(si);
Now we are finished. Compile and run the application.