Crazy Eddies GUI System:Tutorials:Creating a scriptable interface using CEGUI
Contents |
Introduction to interfaces and CEGUI
Introduction
The user interface is an important part of the game(play). There isn't a computer game that does not have some kind of an interface. Whether it's a text interface only like commonly found in Multi User Dungeons or a complex interface which allows us to build units (RTS), choose a character action (Adventures), or give us important information regarding our altitude (Flight-sims).
The "Graphical User Interface” or "GUI" from now on, is very important for the player. This tutorial will not deal with the actual design issues regarding interfaces. An interface can be too complex, too simple, wrong color scheme, confusing... This is something you should deal by yourself. An easy way to determine if your interface is to let some people work with it who are not involved in the game's development at all.
This tutorial will focus on the development of the interface, using an open-source library called "Crazy Eddie's GUI" (CEGUI). After this introduction a simple analysis will follow of why choosing an open-source library might be good for you (or not). Next to this there will be more in-depth information regarding CEGUI's background, license information and informative links.
For easy reference, a word reference list can be found at the end of this tutorial.
Why a third-party library?
Some of the readers might have experience with the creation of their own GUI. They will probably agree on the fact that the development of a decent and clean GUI system is time consuming, and challenges a lot of programming problems. Events, Z-ordering, mouse and keyboard input, creating controls and still keeping the GUI interfaces clean. When you try to do things ad hoc ('on the fly'), things will get quite nasty in the end.
Most game programmers that visit this wiki are independent developers. They are the only programmer, or member of a small programming team (if you are lucky). Since the development of a (semi complex) game takes a lot of time, shortcuts are something which you should consider. Why reinvent the wheel when there are decent libraries available? The same applies to the open-source physics engines and image libraries.
Pros and cons. Everything has its good and its bad things. The following points try to give you an idea about the pros and cons regarding the use of a third party library:
Pros:
- You can save a lot of time not reinventing the wheel, time which can be spend somewhere else
- Often a community available which provides valuable information
- It's likely to be better than what you were going to make
Cons:
- Losing control, you might get a lot of extra dependencies which you do not want
- Often the libraries are projects that work with a versioning system, which makes files more complex
- Most of the time you need to recompile the project yourself... sometimes without make- or solution files
- The license might be incompatible
In the general situation when there are only a few programmers working on a game, (open-source) third party libraries should be seriously considered.
Crazy Eddie's Graphical User Interface (or: CEGUI)
After the introduction, the time has to come to explain more about this GUI library. The library can be downloaded at the CEGUI site. At the time of writing a new stable version (0.4.1?) has just been released. I recommend this version. The versions available from the CVS repository are not always the easiest to compile, since a lot of things change regularly! The stable version comes with solution / makefiles, making it easy to recompile.
There is a small community at the CEGUI site: A wiki and the forum. The wiki contains some articles about the usage of CEGUI. The forums are reasonably active. The programmers that work on CEGUI often reply within a day regarding any issue, problem or comment.
When you download the stable release, you will also find out that there are plenty of samples that demonstrate a lot of the basic things which you can use later in your own project.
One strength of the CEGUI library is the option to choose a renderer. Right now you can choose if you want to use DirectX 8, DirectX9, or OpenGL as renderer. One of it's other awesome features is the Lua scripting module. This scripting module seamlessly integrates with CEGUI (using tolua++). You can handle events in Lua code as well, keeping the game logic and the interface code quite separated from each other.
CEGUI has the ability to load a complete 'window' (which contains all other controls) from a file. This is really great, especially because events can be be added as properties immediately. No need to take care of the event binding in code, it works like a HTML Javascript property (which I will address in the chapter "Lua and CEGUI" of course). Next to the layout files, there are files that describe a font, and the associated font bitmap. Or an imageset (tileset) which is an image containing more images, like various buttons. I have to give some criticism on the amount of files that are being used to create an interface, though. The system works well but here is a short list of files required to get a simple interface working:
Fonts: A ".font" file + the TrueType font belonging to it. Images: Imageset file (.imageset), and also the image which belongs to it Layout: *.xml or *.layout Schemes: *.scheme files
Of course the fonts and images are eventually standardized during development, so there won't be a lot of fonts needed. But the amount of files is still reasonable.
A separate application which you can download is called CELayoutEditor. At the time of writing, the current stable version is 0.4.1, and it can be downloaded here. This is a nice application which allows you to draw an interface in a method similar to Visual Basic or other RAD environments. First draw a form, then start drawing the controls, labels, picture boxes, lists, combo's, etcetera. CEGUI comes packed with default controls. And if you cannot find something you need, you can simply create your own control and recompile CEGUI.
Skins. CEGUI has full skinning support and comes with two or three standard designs which you can modify at will, by replacing the default images and altering the imageset files. The only thing you need is a text editor and a program to open bitmaps. Recently a Skin repository has opened where everyone can upload and download custom skins. All files in CEGUI are based on .XML (with exception of binary data for example, like bitmaps)
Like any open-source project, there is a license. This license tells you what you can do with CEGUI and what not. The license of CEGUI is quite simple: You can use it in commercial and non-commercial projects at will, but you will have to link to the library dynamically. This means that CEGUI will be 'attached' to your project as DLL (Dynamic Link Library) files. This means that you will have about four or five DLL files: The renderer, the CEGUI system, the Lua scripting module and any additional controlsets which you use.
A nice extra is the string functions which CEGUI provides. Easily add float and integers to a string, and display it without hassles.
This chapters ends with a list of the positive and negative things about CEGUI. It might help making a choice whether to use CEGUI, or not.
Positive things
- Feature rich, a lot of controls which share a lot of properties and events
- Multiple renderers available: Use it with OGRE, IrrLicht, SDL, DirectX 8 or 9, OpenGL.
- Uses .XML which is easy to edit
- Decent layout editor available
- Awesome Lua scripting support using toLua++
- Forums are reasonably well maintained
- A lot of examples to work with
Negative things
- Everything is dynamically linked. Expect some DLL files to be shipped with the final game
- The combination of font files, image sets, skins, schemes are overwhelming!
- The documentation is not always very clear
How interfaces work
Interfaces often work with events. Events are triggered by certain actions: Key presses, mouse clicks, mouse moving, the scroll wheel, and a lot more. Windows uses events as well, the entire OS uses events. When resizing a window, repainting it, key presses and mouse actions. If you have Spy++ installed on your computer (which should come with nearly every Visual Studio version) you can actually watch the windows events happen when you move around.
CEGUI uses events as well. A certain event type (e.g. a mouse click) of a control (eg: button) can be registered to a certain function. When the player presses this button the function that was specified during the registration will be called, and the necessary arguments will be passed along. (Which control, which mouse button or key, etc.)
The events in CEGUI are "bubbling". This means that the events go up like a bubble in the water. An example should explain "bubbling" quite well: Imagine a window. This window has an imagebox and on top of this imagebox is a button. *Only* the window has an event handler, the other controls ignore everything that happens. When the player clicks on the button, nothing happens. The event "bubbles" up one level and arrives at the imagebox. The imagebox does not want to trigger any actions as well, so the event bubbles up again. The window uses the event and calls our function. The event will simply stop now from bubbling up (although the window was already the end of the hierarchy). Even registered events can tell the event continuing to bubble up by returning 'false' in the event handler (even if it actually did something)
This behavior is similar to common Chain of Responsibility design pattern (see Design Patterns by Gamma et al.). The motivation is decoupling the event producer from the actual event handler.
Like in Windows, everything in CEGUI is based on a root object. This root object is a "window" object. This window object receives the basic events and performs various functions to trigger events, and has standard properties like size, position and colors. A control inherits (use an object 'foundation' for itself) the window and builds on top of this. The click animation for example, or a disabled state. Using inheritance a lot of redundant code can be kept away and makes it a lot easier to build new controls. If someone wanted to make a listbox with images in every listitem, the listbox control can simply be inherited and a little bit modified to create such a control.
CEGUI needs mouse and keyboard input of course. This is done by just calling a few functions. No need to throw away your current DirectInput code! Simply add a few lines of code in the polling functions or event handlers and CEGUI uses the inputs that you acquire by other methods, seamless.
The hierarchy of CEGUI is also quite similar to the way the GUI works of Windows (or other OS which have a GUI). There is one single root window. Everything else will be placed on this window. In CEGUI there is either "relative" or "absolute" positioning mode. In the "relative" positioning mode, the properties that specify the location of the control are offsets added to the parent element. The position of the object is relative to the parent.
This also means that moving the parents will move the child objects! In absolute mode the coordinates given are based on the top-left coordinate (0,0) of the root window. Creating a child in a frame, and then moving the frame makes the object stay at it's original position. CEGUI has the option to allow scaling of controls and fonts when resizing the window. This can be convenient for various types of windows, and can also be turned off if desired.
Getting started
Now that the basics of CEGUI and interfaces have been discusses, the time has arrived to start on using CEGUI. As mentioned before, CEGUI is an open-source project. Therefore you often have to collect some files together to make it work. This chapter is about just that: Getting the necessary files and adding CEGUI to your project. This tutorial is using Visual Studio 2003 as development environment (IDE). If you are using a different IDE and are interested in expanding this tutorial to add information about setting up a project in a different IDE (Dev-C++, Codeblocks, etc.) then by all means go ahead.
Files to download
Please proceed to the CEGUI download page. First we want to download the source code, so we proceed to the source code section. The "stable" releases should work just fine, and recommended. The list of downloads might be somewhat confusing. Since this tutorial is about Visual Studio, our choice goes to "Crazy Eddie's GUI System Mk-2 Source Code (zip archive)". (version 0.4.1 at the moment) Please check the list if there are other stable releases which are newer.
You will also need to download an additional zip file which contains all the dependencies that CEGUI uses. For example "FreeType" is used, which is an open-source font library. To download the dependencies download "Common dependencies for all MSVC++ versions" which you will find here. The zip file contains two directories which you need to extract manually to the in the CEGUI directory. To do this please proceed as following:
- Extract the zip file directories
- You will see two directories: dependencies\lib, and dependencies\include
- Move "\lib" (without 'dependencies'!) to the CEGUI \lib directory
- Move "\include" to the CEGUI \include directory
- \dependencies should be empty now, so you can delete this directory.
Now proceed to the directory "cegui_mk2\makefiles". And choose the proper version for your operating system (or compiler), and open the CEGUI project. We will need to change one configuration item in the file "CEGUIConfig.h". Comment the line:
#define CEGUI_WITH_XERCES
Which will turn off a separate XML library (which is another separate dependency). Now CEGUI will use "TinyXML" internally, and save ourselves an extra dependency.
You should be able to successfully compile CEGUI at this moment!
Setting up Visual Studio
Since we have all files ready, we will need to add various references in our Visual Studio project. The following is therefore project specific and might not work immediately in your project.
First right click on the name of your project, and choose "properties". Then proceed to the menu "Linker->General", and click on the "..." in the "additional libraries" field. Then add the following names:
- CEGUIBase_d.lib
- CEGUILua_d.lib
- lua_and_tolua++_d.lib
In the same screen an option can be found called "Additional library directories", click on the "..." again and add the path to \CEGUI\Bin directory.
In the treeview on the left, click on "C++->General" and add the "CEGUI\Include" directory to the includes list (“Additional include directories”). Now the compiler can find the include files used by CEGUI.
Repeat the above as well for other build modes (eg: Release/Debug mode), except the library list is different (no CEGUI debug DLL's, notice the lack of _d at the end of the filenames):
- CEGUIBase.lib
- CEGUILua.lib
- lua_and_tolua++.lib
And click on "OK".
But, the include files still need to be referenced somewhere. I assume that your Visual Studio solution has a include file containing a lot of #include's (like precompiled headers). Open this file and add:
//tolua++: #include "tolua++.h" #include "LuaInterface.h" //CEGUI: #include "CEGui.h" #include "CEGUILua.h" #include "CEGUIPropertyHelper.h" #include "renderers/directx9GUIRenderer/d3d9renderer.h" using namespace CEGUI; #ifdef _MSC_VER # if defined(DEBUG) || defined (_DEBUG) # pragma comment (lib, "DirectX9GUIRenderer_d.lib") # else # pragma comment (lib, "DirectX9GUIRenderer.lib") # endif #endif
Try to compile your project now (F7). Please note that, if any errors should occur now you will need to try to solve them yourself. The most likely problem is a wrongly set path or a missing include file.
Does it compile? Good! Then the project is ready to use CEGUI and also calls for the second part of the tutorial: Working with CEGUI.
Working with CEGUI
Now that the project compiles successfully with the CEGUI include files and libraries taken into account, the moment has arrived to initialize the library and create ourselves an interface. The source code of the sample application can be downloaded at the "Source code" chapter.
First we initialize the CEGUI system. We will tell here what kind of renderer to use, the fact that we want to use the UA scripting mod, etc. Notice that the arguments are a little bit different between various CEGUI versions.
The arguments of the renderers can be different as well. This tutorial assumes DirectX9, so we create a DirectX9 renderer:
We are creating an instance of the LuaScriptingMod. If there is a Lua state already somewhere, it's possible to use this state as well (as an argument of the constructor). This tutorial assumes that there is not such a state present already, and therefore creates a new state.
First we create a renderer object, which is like a 'device' on its own. This renderer makes CEGUI work under various graphic libraries (DirectX, OpenGL)
//Create our DirectX9 renderer pRenderer = new CEGUI::DirectX9Renderer(App->mp_d3d_device, 512); For DirectX8 it might be for example:
pRenderer = new CEGUI::DirectX81Renderer(App->mp_d3d_device, 512);
The 512 is the number of quads (rectangles) that you expect to use. I just put it on 512, since it just worked well. Haven't had any problems with it yet. Then, we create the LuaScriptingMod object:
scriptmod = new LuaScriptModule(); setLuaAppPath(); state = scriptmod->getLuaState(); tolua_LuaInterface_open(state);
The setLuaAppPath is a function that will set the necessary Lua variables so that we can easily use "App.path" to load textures for example. Please check the code for more details.
To use our Lua interfaces made by toLua++ (more will follow on that later) we need to call "tolua_LuaInterface_open" once. From that point the Lua scripts can access anything we have exposed using toLua++.
Now that all the necessary objects are setup, the only thing that needs to be done is a location to the "startup config". This is a special configuration file which will tell CEGUI to use a script to start the CEGUI interface (it's still completely possible to do everything from your C++ code as well).
The contents of the config file are completely self explaining:
<?xml version="1.0" ?> <CEGUIConfig InitScript="datafiles\scripts\demo8.lua" />
CEGUI will use the above LUA script when the System object will be created.
char* tmp = (App->GetApplicationPath() + "datafiles\\configs\\demo8.config").CharString(); new System(pRenderer, NULL,NULL,scriptmod, (utf8*)(tmp));
The above code will tell CEGUI where the configuration file can be found, and will create a new system object (notice there isn't anything at the left side of 'new'). When this call succeeds, CEGUI is completely loaded. If anything goes wrong in the Lua script, CEGUI will give you very nice information in a file called "CEGUI.log".
Well, this was all of it. CEGUI is up and running! But now the actual fun starts. From this point a interface can be made. Before we actually get there, a few other objects will be explained first: The manager objects. The objects take care of fonts, images and windows. All these objects are singleton (once instance available) and can also be accessed from Lua.
It's possible to use WindowManager::getSingleton().functionname() everywhere. But it's quite a lot easier to make use of a variable whenever you need to access a manager:
WindowManager& winMgr = WindowManager::getSingleton(); ImagesetManager& imgMgr = ImagesetManager::getSingleton(); SchemeManager& scmMgr = SchemeManager::getSingleton();
Now "winMgr.getWindow()" can be used to accessed a window instead of "WindowManager::getSingleton().getWindow()".
To get full documentation about these managers, I will refer to the class list manual of CEGUI. This tutorial will now proceed in the creation of an interface using C++ code. Creating an interface using Lua will follow next.
Rendering the interface
To render our interface, we just need to add one single call inside our render loop:
System::getSingleton().renderGUI();
Nothing else is needed!
Mouse movement, time, and keypresses
CEGUI needs user input, or it won't be much of an user interface! That's why we need to "inject" certain events in CEGUI. This is not a hard thing to do.
Every frame (in an update function) a timepulse needs to be injected. This is the time that passed between this frame, and the previous frame:
System::getSingleton().injectTimePulse(Timing->FrameDuration()); //Frame pulses are frame durations
That will make timed function work (think about tool tips disappearing and appearing)
For Mouse input we can either use the Windows events, DirectInput, or any other custom library which you might use. This is an example using the Window events (which you might have already in your application's WindowProc handler):
case WM_LBUTTONDOWN: CEGUI::System::getSingleton().injectMouseButtonDown(CEGUI::LeftButton); break; case WM_LBUTTONUP: CEGUI::System::getSingleton().injectMouseButtonUp(CEGUI::LeftButton); break; case WM_RBUTTONDOWN: CEGUI::System::getSingleton().injectMouseButtonDown(CEGUI::RightButton); break; case WM_RBUTTONUP: CEGUI::System::getSingleton().injectMouseButtonUp(CEGUI::RightButton); break; case WM_MBUTTONDOWN: CEGUI::System::getSingleton().injectMouseButtonDown(CEGUI::MiddleButton); break; case WM_MBUTTONUP: CEGUI::System::getSingleton().injectMouseButtonUp(CEGUI::MiddleButton); break;
Keyboard events can also be handled here:
case WM_KEYDOWN: CEGUI::System::getSingleton().injectKeyDown((CEGUI::utf32)pWParam); break; case WM_KEYUP: CEGUI::System::getSingleton().injectKeyUp((CEGUI::utf32)pWParam); break; case WM_CHAR: CEGUI::System::getSingleton().injectChar((CEGUI::utf32)pWParam); break;
Those are all the inputs that CEGUI should receive to work properly. Timing, Mouse and keyboard input
Creating an interface using C++
The interface needs a 'root' element. This is the element that contains all other controls and windows.
DefaultWindow* root = (DefaultWindow*)winMgr.createWindow("DefaultWindow", "Root");
PushButton* btn = static_cast<PushButton*>(winMgr.createWindow("TaharezLook/Button", "Root/mybutton"));
root->addChildWindow(btn);
The first line of the above code creates the root window. This is really an empty window, since it doesn't even have a "look". Now, the second line is basically the default way of adding things: Create a window, and cast it to a different type. Before we proceed, first something about the arguments:
- The first argument specifies the look of the object (like a 'skin'), and behind the slash the object type is given. Another default skin is called "Falagard". The button could also have been created using "Falagard/Button".
- The second argument is the "path". In the introduction the "hierarchy" of interfaces was explained. This is where it comes back. The second argument is the name: It will tell CEGUI where it is located. Therefore you can have multiple objects with the same name, but not with the same path. Think of it as a directory structure. It is quite the same. An window needs to be retrieved using the same path. So "mybutton" won't work. You will need "Root/mybutton".
And finally the third line of code will add the button (as a child) to the root window. This is how it works for nearly every object. Objects have properties as well. These properties determine the position, size, text, etc. This sets the size and caption of the button:
btn->setSize(Size(0.25f, 0.1f));
btn->setText("Exit Demo");
Very straightforward! For a complete list of properties please proceed to the CEGUI class list.
How about adding a label (Static text) as well:
StaticText* stxt = static_cast<StaticText*>(winMgr.createWindow("TaharezLook/StaticText", "Root/mybutton"));
root->addChildWindow(stxt);
Completely the same as the previous code: Create a window, specify the scheme/objecttype as first argument, the path in the second argument, cast it to the type you wanted, and then add it to the root object.
Events
Right now we can press as long as we want on the button, but nothing will happen. There aren't any event handlers 'tied' to the button yet. Creating an event handler does not take too much effort either:
WindowManager::getSingleton().getWindow("Root/mybutton")->
subscribeEvent(PushButton::EventClicked, Event::Subscriber(&Myclass::handleButton, this));
We can rewrite this in a more readable way (demonstrating the singleton variables once more):
WindowManager& winMgr = WindowManager::getSingleton();
Window* window = winMgr.getWindow("Root/mybutton");
window->subscribeEvent(PushButton::EventClicked, Event::Subscriber(&Myclass::handleButton, this));
The code tries to get the specified window (notice the path!) and subscribes to an event. The first argument of the subScribeEvent specifies the type of event that we want to subscribe to. "PushButton::EventClicked" means that we subscribe to the click event of the pushbutton (which is the object we use). The GUI objects do not share all the same properties. A slider object has "Slider::EventValueChanged" for example, while the Pushbutton does not have such an event. For a complete list of properties check this page. For a complete list of all CEGUI objects, please proceed here
The second argument gets a handle to a C++ function. The code is creating a pointer to the "ClassName:functionname" function, and 'this' is required to specify the current object. Which means I assume that the function is in the same class as the above code. When a different class is needed, you will need to give a pointer to the other object(class)
The "handleButton" function is a normal function:
bool CEGUISample::handleButton(const CEGUI::EventArgs& e)
{
//Do something
// event was handled
return true;
}
The function doesn't do much right now, but let me discuss the specifics. As you might see the function has an argument "EventArgs". This argument will be filled with event information. A few pieces of information you can get: Mouse position, which mouse button, which object triggered the event, etc. Also notice the 'bool' being returned. Remember when I talked about event bubbling? When you return 'true' the event won't bubble up. It will stop at this event. If you returned false, it will continue traversing the hierarchy. For a mouse move event this might be trivial. But for our click event it isn't really: Only the button will receive the event anyway.
Event argument
As you might have seen in the handleButton() code, an argument is given to the function. This argument gives you information about the event. There are various argument types available depending on the event that occurred. For example a mouse event returns "MouseEventArgs", which contains the clicked button, position, movement delta's. A keyboard event returns "KeyEventArgs" which will tell you which key is pressed and which system keys (control, shift, alt, ...). A Drag and drop event tells you what dropped. There are quite some events available, but they all share one single property: The "window" object. This object tells you either what the source was of the event, or the destination (the object that received the event)
One important point regarding the events is casting. Some code is easier than a thousands words :).
//Handle mousemovement
bool CEGUISample::handleMouseMove(const CEGUI::EventArgs& e)
{
//Upcast to mouse event args:
const MouseEventArgs evMouse = static_cast<const MouseEventArgs&>(e);
return true;
}
The "handleMouseMove" event is subscribed to receive MouseMove events. But! It takes an "EventArgs" argument. This argument does not contain anything about the mouse, so we can cast it to the MouseEventArgs using the above code. The same applies to all other events. With above code you could use "evMouse.position" to determine where the mouse is located.
Unsubscribing an event
You might want to remove events. Removing an event involves two steps: Get the window with the event you subscribed to, and then remove it.
Window* window = winMgr.getWindow("Root/mybutton");
window->removeEvent(PushButton::EventClicked);
This should cover the events. We have covered the initializing (subscribing), using (handlers) and the unsubscribing of events. The next chapter will be about the usage of toLua++ to create a Lua interface for your project. This will allow Lua to communicate directly with your game code.
Using toLua++
toLua++ is a free tool which you can download here. toLua++ enables Lua to communicate with your C++ code by creating an "interface layer" between the C++ code and Lua. This chapter deals with the generation of this interface. Although the manual of toLua covers quite a lot (and it isn't that difficult), this chapter shouldn't be left out of the tutorial. Also note that you may find references to "toLua" (without ++). This is the original version meant for C. the toLua++ contains extra extensions to work better with C++.
The nice thing about toLua++ is that virtually all your objects are usable. It's possible to use the LPDIRECT3DTEXTURE8, std::string, or your GameObject, WeaponObject, or whatever you desire!
toLua++ uses 'package files' (*.pkg). These files are copies of the header files (*.h), but modified for toLua++. You might want to add extra methods to the Lua interface, or remove function. For example: Should it be possible to add objects by script or not? If not, the AddObject() function can be removed. CEGUI comes with quite some package files already. These files are used to make the CEGUI library available from Lua. Although in the earlier chapters this was addressed before, I do want to say (again) that the options are endless: Script event handlers, create windows or buttons. Hide them, show them change colors, handle the event so that a C++ function is called... Be creative!
Here is a quick example from my own game. This is the class that arranges the Models:
class CEModelManager
{
public:
//Get the singleton.
static CEModelManager& getSingleton();
void SetDevice(LPDIRECT3DDEVICE9 Dev);
void LoadModel(CEString strFileName, const char* strName);
void LoadErrorModel();
CED3DSkinnedMesh* GetModel(CEString strFileName);
};
Obviously we do not need everything in there.
Be sure to remove any "private" items, or toLua++ will complain about it!
class CEModelManager
{
public:
//Get the singleton.
static CEModelManager& getSingleton();
void LoadModel(CEString strFileName, const char* strName);
};
With this reduced version we are quite close to what I wanted. To be able to load 3D models using Lua, so there is no need to write extra loading code for a list of filenames, and the greatest benefit: No need to recompile when adding a simple model during the first initial 'hard coding' phases of a project (if you share my (bad) habit).
To load a model the Lua code needed would be:
local modelman = CEModelManager:getSingleton() modelman:LoadModel(CEString:new(App.data.."objects\\barrel.ms3d"), "barrel.ms3d");
Now this is quite neat!
Before we continue, first some more about the toLua++ process. Basically there are three steps:
- Make package file(s)
- Compile package file(s)
- Include the single generated file in your application.
I've already addressed the first part, regarding the creation of the package file. On to the second part. I'm using a simple batch file to compile all my package files. This batch file compiles one single package file. This single package file actually includes all the other files, and therefore a complete file is generated. To create a batch file, simply create a text file in your favorite text editor, put the code below in it, and save it with an extension of ".bat":
REM Create lua interface tolua++ -H LuaInterface.h -o LUAInterface.cpp LuaInterface.pkg
Assuming toLua++ is in the same directory (or in the PATH variable). This batch file will generate an header file "LuaInterface.h", and a c++ file "LuaInterface.cpp" (which contains all the interfaces) using the packagefile "LuaInterface.pkg".
The contents of "LuaInterface.pkg" is only include commands for toLua++:
$pfile "LUATextureManager.pkg" $pfile "LUAModelManager.pkg" $pfile "LUAShaderManager.pkg"
If something goes wrong during the compilation, toLua++ will tell you why and where. Compilation only takes a few seconds. If you double click on the “batch file” the window will disappear immediately. To solve this, do the following:
Go to the start menu, and choose “Run…”. Type “cmd” (without quotes) and press Enter. Now you should have a dosbox open. Use standard DOS commands to navigate to the directory of the batch file, and execute it by simply typing it’s name (compiler.bat for example). Anything that goes wrong will be shown, and the window will stay open.
To use the generated files, simply drag and drop (or use Add file...) the header and code file in your project. Assuming that you set the project up to use CEGUI (see chapter XX), it should compile right away.
If you change one of the names, or arguments of one of the functions you referenced in the package files, (and included the generated files in your project) you will receive errors during compilation. You will need to apply (the same) modifications to the package file, and recompile the toLua++ files again (using the batch file).
For further in-depth information regarding the toLua++ compiler, I will have to point you to the manual, more in-depth information is out of the scope of this tutorial.
Lua and CEGUI
This might be the chapter that you have been waiting for. In this chapter I will explain a lot regarding the usage of Lua and CEGUI.
Working source code can be downloaded here. Please follow the "source code" instructions at the end of this tutorial (or read the included readme) for proper installation of the files.
In Chapter two Lua was already initialized, using the "boot strap" script which tells CEGUI to use a Lua file to initialize (additionally) further. This chapter continues from that point.
First, we need to consider what we want to do in this Lua script. In my own project this Lua script is calling about ten other Lua files together (I prefer things separated from each other) and is loading 3D Models, Textures, Configuration of units - and of course the interface!
Note: You might see pieces of code like "App.scripts..", and wonder where this comes from. It is =not= a Lua constant. Remember the setLuaPath function? Check out the sample source code, and you will find the function. It sets some predefined paths to make file handling a lot easier. App.scripts points (for me) to a \scripts directory. Additional filenames are appended using "..".
Including files
Although this tutorial will not go into detail about the Lua language, knowing how to include Lua files might be a good thing to get used to immediately:
dofile(App.scripts.."tl_Main.lua");
"dofile" will execute a script from a file. It's similar to a #include in C++. So, if you have multiple files which you want to combine in one file you could simply do:
dofile(App.scripts.."tl_1.lua"); dofile(App.scripts.."tl_2.lua"); dofile(App.scripts.."tl_3.lua");
(the tl_ is my own prefix for toLua++)
Lua/CEGUI essentials
Before we get to the real juicy part of this chapter, there are some extra details that need to be explained.
CEGUI Singletons
The first thing which you really can't live without are the singletons. The CEGUI singletons are essential to the Lua script. Fortunately, accessing these are maybe even easier than in our C++ code.
Here are some common singletons:
local winMgr = CEGUI.WindowManager:getSingleton() local logger = CEGUI.Logger:getSingleton() local imageman = CEGUI.ImagesetManager:getSingleton() local schememan = CEGUI.SchemeManager:getSingleton() local fontman = CEGUI.FontManager:getSingleton() local system = CEGUI.System:getSingleton()
Actually, I think that covers nearly all singletons which we will ever use. The names look familiar (they should!) like "WindowManager" (stores all the windows/buttons/etc.), "ImagesetManager" (stores all the images), "FontManager" (Loads and keeps references to fonts) and quite some more.
The above code will simply create a (local) variable to make it easier for you to access the managers. Using the manager is very much like the C++ code we have written earlier:
winMgr:getWindow("WindowName/Window"):subscribeEvent("MouseEnter", "luafunctionname")
That single line of code shows three things:
- How to use functions. Instead of a "->" or a "." we need to use ":" between function names
- The getWindow() call is completely the same as in C++ (and so does the rest of all functions)
- And we see how to subscribe to an event. This event will call a Lua function, instead of a C++ function though.
That should get you nail biting of excitement already! One important thing left to know before we start on more serious code:
Casting to CEGUI objects
You might remember that we had to cast quite a lot in C++. From a Window to a Button, or from EventArgs to MouseEventArgs. This needs to be done in Lua as well (when you need it, of course)
The creators of CEGUI have thought about that as well, fortunately. There are a number of special 'tool' functions which you can use to cast an object to a different type. Here are a few, to give you an impression:
ceguiLua_toMouseCursorEventArgs ceguiLua_toWindowEventArgs ceguiLua_toMouseEventArgs ceguiLua_toCheckbox ceguiLua_toEditbox ceguiLua_toPushButton
The functions work easy: Throw the object you want to 'convert' to a different type in the function, and the function returns the new object. Like this:
btnExit = ceguiLua_toPushButton(winMgr:getWindow("WindowName/exit"))
Various samples of what you can do
All the important things have been covered now. Time to do some simple demonstrations. With comments, of course!
Simple button click
function cmdButton_click(args) --GUI manager local guiman = CEGUIManager:getSingleton() -- Perform the build item click method. guiman:BuildItemClick(args); return true; end
This function is called after the "Build" button was clicked. The script will create a variable to my CEGUIManager class (which is a C++ singleton class) and execute the "BuildClick(args)", which is a C++ function. This function will take care of the actual building part for example (creating a progress bar, taking resources, etc.).
Creating an interface from scratch
The following method is an easy method to make multiple "sheets" in your game, while retaining flexibility.
function CreateInterfaceRoot()
-- Get GUI singleton
local guiman = CEGUIManager:getSingleton()
local winMgr = CEGUI.WindowManager:getSingleton()
local logger = CEGUI.Logger:getSingleton()
local system = CEGUI.System:getSingleton()
local rootWindow = winMgr:getWindow("root")
w = winMgr:loadWindowLayout("datafiles/layouts/wgSelectSide.xml")
w:setVisible(true)
rootWindow:addChildWindow(w)
w = winMgr:loadWindowLayout("datafiles/layouts/wgOptions.xml")
w:setVisible(false)
rootWindow:addChildWindow(w)
end
The code is fairly simple, and offers some great flexibility. The code simply gets a variable to the root window element (everything will become a child of this 'parent'). Then, we use the loadWindowLayout to load a complete interface with one single call! setVisible makes some windows hidden, and only one visible.
The XML file will also contain a default "root" element. This root element will be included, so therefore the name will be: "root/rootofXMLfile". Make sure the names inside match the final name! For example, a button with a name "cmdClick" would be "root/rootofXMLFile/cmdClick" inside the XML file, already containing the root of the window which is not in the XML file.
If you add events to your XML file, those will be instantly created as well, and will trigger the given Lua functions when they are fired! With a simple button it would also be possible to hide windows:
Switching windows (sheets)
function cmdMainMenuOptions_click(args)
local winMgr = CEGUI.WindowManager:getSingleton()
local rootMenu = winMgr:getWindow("root/MainMenu")
local rootOptions = winMgr:getWindow("root/Options")
-- Hide main menu.
rootMenu:setVisible(false)
-- Show the "Options" menu
rootOptions:setVisible(true)
end
When the above function is triggered (by a click on a button in the main menu, for example) the function will hide the root of the MainMenu, and show the root of the Options menu, effectively hiding and showing all the controls of those 'windows' respectively.
Loading extra data
It's possible to load extra imagesets, fonts and other resources on the fly. This way you do not have to add additional configuration parsers, or hard coding in your game (loose snippets):
Loading Fonts
-- load and set default font
local fontman = CEGUI.FontManager:getSingleton()
local font = fontman:createFont("datafiles/fonts/Commonwealth-10.font" )
system:setDefaultFont( font )
Loading Imagesets
local imageman = CEGUI.ImagesetManager:getSingleton()
imageman:createImageset("datafiles/imagesets/set1.imageset");
imageman:createImageset("datafiles/imagesets/set2.imageset");
Loading extra scripts
dofile(App.scripts.."interface\\script1.lua") dofile(App.scripts.."interface\\script2.lua")
Logging
CEGUI has quite a decent logging functionality. It will create a (new) log file whenever CEGUI is initialized. In Lua, you can write to this log:
local logger = CEGUI.Logger:getSingleton() logger:logEvent( "[LUA]: Loading imagesets..." ) logger:logEvent( "[LUA]: Player clicked on a button..." )
CEGUI has various error levels (priorities): warnings, errors, notices, etc. You can make use of this if you want. Normally CEGUI shows everything in the logfile. If you want, you can change this by altering the logging level *TODO HOW*
Adding a list item
//Add a listbox:
Listbox* lbox = static_cast<Listbox*>(winMgr.createWindow("TaharezLook/Listbox", "Root/Listbox1"));
rootWindow->addChildWindow(lbox);
lbox->setMaximumSize(Size(1.0f, 1.0f));
lbox->setPosition(Point(0.04f, 0.25f));
lbox->setSize(Size(0.42f, 0.3f));
//Add the top 5 of the GPWiki forums:
lbox->addItem(new MyListItem("Ryan"));
lbox->addItem(new MyListItem("Almar"));
lbox->addItem(new MyListItem("Snoolas"));
lbox->addItem(new MyListItem("Machaira"));
lbox->addItem(new MyListItem("Codehead"));
Well, this should give you a quick launch on interfaces using Lua and CEGUI. Creativity is the limit! Background colors can be altered, images, size, and position: Nearly everything you could do with CEGUI in C++, is possible with Lua.
A Listbox in Taharezlook.
CELayoutEditor: WYSIWYG for CEGUI
This is a very useful tool that can be downloaded here.
It allows you to create an interface using methods similar to the visual development environments. Although this tutorial won't go very in-depth regarding this tool, here are a few interesting things:
- Windows and objects can be placed in a hierarchy (objects inside an object)
- Both absolute and relative positioning mode
- WYSIWYG (What You See Is What You Get)
- Saves the layout to a XML file, ready to use in CEGUI.
The program is nice, my only comments (which are due to be fixed in the next release) are:
- No indentation in the created XML. This makes things harder to read
- Events aren't saved in the XML
- Sometimes it's hard to select and resize an object, because the program thinks you want to click on a parent window.
The CELayoutEditor.
Source code
This tutorial comes with some sample code to experiment with, and shows various of the techniques demonstrated in this tutorial. The GUI inside ain't very pretty (see the above screenshot). The functionality counts though :-). The example code demonstrates the usage of various objects (slider, button, textbox and more) and uses Lua code and C++ code.
The example code contains all the files that are needed to run in the same directory. Most of these files (images, fonts) are 100% the same as the files that come with CEGUI. But, extracting the files to the right places is a tedious job (about 6-7 different directories) and therefore I've included them in the same directory as the tutorial.
The code builds on top of the CEGUI sample helper, (which is not included, but is simply in all CEGUI source code releases) so there is no extra initialization code for windows and event handlers. Note that the code is written in Visual Studio 2003 and DirectX9. It should simply run in other renderers as well, because of the cross-render nature of CEGUI.
Usage
To use the example code, unzip all files (with directories) in 'cegui_mk2\Samples\', which should result eventually in 'cegui_mk2\Samples\GPWikiDemo'.
You can download the example code here: GPWikiDemo.zip.
Conclusion
The tutorial started with an introduction about CEGUI, and why you should use a library. Then we went low-level regarding the inner workings of user interfaces. After the theory, the practical side of the tutorial started: First setting up CEGUI inside your project, and then creating the necessary code to initialize everything. After CEGUI was implemented in our project the tutorial went into a crash-course about CEGUI and C++... and how to create an interface with code. From there the tutorial went into the subscribing, using and unsubscribing of events. Events that will make your interface come to life. After all this coding we went external and started using Lua to create an interface.
Because you got here, I assume you finished reading through the tutorial. I really hope that this tutorial covered everything you wanted to know about the subject "Creating a scriptable interface using CEGUI". Feel free to modify this tutorial as needed. You might encounter typo's, which you want to correct. Or add some extra information somewhere in the tutorial. Do not hesitate!
References
A list of references used in this tutorial, or additional links which might be an interesting read.
CEGUI
Lua
toLua/toLua++
Word reference
A short summary of words and their meaning (in relation to CEGUI)
CELayoutEditor: A tool for CEGUI which allows developers to create a GUI using a IDE. Supports all the controls provided by CEGUI and also the absolute and relative positioning systems.
Font: Fonts are used to give text a certain look. In CEGUI its possible to use custom fonts, CEGUI comes with a few fonts which can be found in "cegui_mk2\Samples\datafiles\fonts".
GUI: Graphical User Interface. A graphical user interface which displays information to the user (player) and accepts input from the user. It is an interface between human and computer.
Imageset: An imageset is made out of two files: An image file and a *.imageset (or *.xml) file. The imageset file tells CEGUI which section of the image has what name. Therefore it's possible to assign multiple tiles from one bitmap. For an example checkout the *.imageset files in the "cegui_mk2\Samples\datafiles\imagesets" directory.
ImagesetManager: The singleton manager that keeps track of all the imagesets you have loaded. (Imagesets will also be loaded through this class)
Layout: The controls being placed in a window. CEGUI works with XML based layout files which containts a root window, and child windows. When loaded the XML will be transformed in a working interface. Sample layouts can be found in "cegui_mk2\Samples\datafiles\layouts".
Lua: An open-source scripting language, which receives more and more popularity. Games like Grim Fandango, Baldur's Gate, Far Cry and a Monkey Island remake have been using these successfully. There are way more games using this scripting language. visit Lua.org for more information.
Scheme: Schemes are like "skins". They define a complete set of GUI controls. Changing scheme is similar to, for example switching the skin of Windows to a higher contrast one. CEGUI comes with two schemes: TaharezLook, WindowsLook. These can be found in "cegui_mk2\Samples\datafiles\schemes". It's quite possible to make your own schemes or skins, the CEGUI site has a repository for them.
SchemeManager: A singleton class that keeps track of all loaded schemes.
Sheet: A sheet is like a separate "page" or "dia" in CEGUI. It allows you to quickly hide and show controls at once. For example you might have a sheet "Main menu", "Options" "Save game", and change between these with button presses, so the player gets a feeling of navigating through a menu structure.
Singleton: A (code) class that can be instanced only once. CEGUI makes quite some use of Singletons in the manager objects (WindowManager, ImagesetManager, FontManager, etc.)
Subscribing: Registering to (receive) events from certain controls is called "Subscribing" in CEGUI.
toLua(++): An open-source tool which turns C(or C++) header files in interfacing code. There are two versions, toLua and toLua++. The latter has support for C++ specific features (classes, std::string, etc.).
Unsubscribing: Unregistering events is called Unsubscribing.
Window 1: Everything in CEGUI comes eventually down to a window. The "Window" object (or class) is the root of all controls. Whether it's a button or a slider, they all inherit the members of a window object.
Window 2: The "DefaultWindow" object is the root window of (nearly) every layout file and interface objects. At least one root object is required for CEGUI to work. Roto objects often do not contain graphical items, they are "transparant containers".
WindowManager: Singleton class that keeps track of all Windows which you have made in CEGUI (thus, also all controls).
WYSIWYG: What You See Is What You Get: What is shown in a preview window, is also the "final" result. Often used in graphical editors (FrontPage, Dreamweaver) but also in CEGUI's own Layout editor: CELayoutEditor.
XML: A text-based file format similar to HTML, but instead of text mark-up language, it is a data mark-up language.





