CEGUI In Practice - A Game Console
Written for CEGUI 0.7
Works with versions 0.7.x (obsolete)
Written for CEGUI 0.8
Works with versions 0.8.x (stable)
Works with latest CEGUI stable!
CEGUI In Practice series
Contents
CEGUI InPractice 6
Welcome back! This tutorial we will put together the knowledge from the past few chapters to create a functioning Console window. The last few tutorials have given us the basics. Now its time to put that knowledge to use and make something we can use in our game.
Game Plan
Okay, now lets look at what we need to do. We will want to be able to type in commands, press the send button, and have them output into the text box. We will also probably want to acknowledge when enter is pressed as thats the 'normal' feel of typing into a chat.
We discussed earlier how to send input to CEGUI (CEGUI In Practice - Managing input), so we will have to assume that you have that working (and probably built the application structure behind it). We will also assume you used the previous tutorial's layout file and naming for the console window ( CEGUI In Practice - Using .layout files).
Due to the fact that we are getting to a slightly more advanced implementation of CEGUI here, and because we may want the ConsoleWindow to do other more useful things (like parsing strings for /say commands) I'm going to create a class to encompass the CEGUI ConsoleWindow.
#include <CEGUI/CEGUI.h> class GameConsoleWindow { public: GameConsoleWindow(); // Constructor void setVisible(bool visible); // Hide or show the console bool isVisible(); // return true if console is visible, false if is hidden private: void CreateCEGUIWindow(); // The function which will load in the CEGUI Window and register event handlers void RegisterHandlers(); // Register our handler functions bool Handle_TextSubmitted(const CEGUI::EventArgs &e); // Handle when we press Enter after typing bool Handle_SendButtonPressed(const CEGUI::EventArgs &e); // Handle when we press the Send button void ParseText(CEGUI::String inMsg); // Parse the text the user submitted. void OutputText(CEGUI::String inMsg, // Post the message to the ChatHistory listbox. CEGUI::colour colour = CEGUI::colour( 0xFFFFFFFF)); // with a white color default CEGUI::Window *m_ConsoleWindow; // This will be a pointer to the ConsoleRoot window. CEGUI::String sNamePrefix; // This will be the prefix name we give the layout static int iInstanceNumber; // This will be the instance number for this class. bool m_bConsole; };
Alright, so that will be our class. It might look daunting, but don't worry it will all make sense in the end.
The Constructor
I'm going to use the constructor as the main point of automation here. And while I believe it is normally bad practice to call functions which could potentially fail within a constructor, I'm going to anyway for ease of this tutorial. We will have it Initialize some variables, and then call the CreateCEGUIWindow function.
int GameConsoleWindow::iInstanceNumber; // Don't forget this declaration GameConsoleWindow::GameConsoleWindow() { m_ConsoleWindow = NULL; // Always good practice to initialize a pointer to a NULL value, helps when switching to Release Builds. iInstanceNumber = 0; sNamePrefix = ""; CreateCEGUIWindow(); setVisible(false); m_bConsole = false; }
Setting up the Window
Now we need to setup the window. We have done this before in the .layout tutorial so we will do it again here. However, we mentioned before that you could use a prefix when loading a .layout to avoid name conflicts if you load multiple layouts. So we need to account for that. Will we need multiple consoles? Who knows, might as well build it in while we're designing it right?
Lets write the CreateCEGUIWindow function:
void GameConsoleWindow::CreateCEGUIWindow() { // Get a local pointer to the CEGUI Window Manager, Purely for convenience to reduce typing CEGUI::WindowManager *pWindowManager = CEGUI::WindowManager::getSingletonPtr(); // Now before we load anything, lets increase our instance number to ensure no conflicts. // I like the format #_ConsoleRoot so thats how i'm gonna make the prefix. This simply // Increments the iInstanceNumber then puts it + a _ into the sNamePrefix string. sNamePrefix = ++iInstanceNumber + "_"; // Now that we can ensure that we have a safe prefix, and won't have any naming conflicts lets create the window // and assign it to our member window pointer m_ConsoleWindow // inLayoutName is the name of your layout file (for example "console.layout"), don't forget to rename inLayoutName by our layout file // Note : for CEGUI 0.7 m_ConsoleWindow = pWindowManager->loadWindowLayout(inLayoutName,sNamePrefix); // Note : for CEGUI 0.8 m_ConsoleWindow = pWindowManager->loadLayoutFromFile(inLayoutName); // Being a good programmer, its a good idea to ensure that we got a valid window back. if (m_ConsoleWindow) { // Lets add our new window to the Root GUI Window CEGUI::System::getSingleton().getGUISheet()->addChildWindow(m_ConsoleWindow); // Now register the handlers for the events (Clicking, typing, etc) (this)->RegisterHandlers(); } else { // Something bad happened and we didn't successfully create the window lets output the information CEGUI::Logger::getSingleton().logEvent("Error: Unable to load the ConsoleWindow from .layout"); } }
Alright so that created the window. And after the window was created, we Registered its handlers. Lets look at how we go about registering those handlers. Below is the RegisterHandlers function, it will probably look familiar if you have been reading along:
void GameConsoleWindow::RegisterHandlers() { // Alright now we need to register the handlers. We mentioned above we want to acknowledge when the user presses Enter, and // when they click the 'Send' button. So we need to register each of those events // First lets register the Send button. Our buttons name is "ConsoleRoot/SendButton", but don't forget we prepended a name to // all the windows which were loaded. So we need to take that into account here. m_ConsoleWindow->getChild(sNamePrefix + "ConsoleRoot/SendButton")->subscribeEvent( CEGUI::PushButton::EventClicked, // If we recall our button was of type CEGUI::PushButton in the .scheme // and we want to acknowledge the EventClicked action. CEGUI::Event::Subscriber( // What function to call when this is clicked. Remember, all functions // are contained within (this) class. &GameConsoleWindow::Handle_SendButtonPressed, // Call Handle_SendButtonPressed member of GameConsoleWindow this)); // Using (this) instance we're in right now // Now for the TextSubmitted, we will be registering the event on the edit box, which is where the users cursor will be when //they press Enter. I'm not going to break this down as much, because I believe that is very ugly to read, but was a good //way of expressing it. Here is the function call. m_ConsoleWindow->getChild(sNamePrefix + "ConsoleRoot/EditBox")->subscribeEvent(CEGUI::Editbox::EventTextAccepted, CEGUI::Event::Subscriber(&GameConsoleWindow::Handle_TextSubmitted,this)); }
That last line looks pretty ugly, but remember if you include namespace CEGUI; you won't have all those Ugly CEGUI:: prefixing all the code. As you can see, once you get the hang of registering events its pretty easy. One thing to note, is there are more than one way to account for certain actions. On the button, you could also have registered for:
CEGUI::PushButton::EventMouseClick CEGUI::PushButton::EventMouseButtonDown CEGUI::PushButton::EventMouseButtonUp
Depending on what result you were looking for, and where you wanted the action to take place. For example, windows interfaces usually react on the MouseButtonUp, allowing the user to click it, and then slide off the mouse if it wasn't what they really wanted to press.
The Handlers
Now that we have registered the events, we need to actually write the functions which will handle those events. We need to handle both the Text Submitted, and the SendButton's event.
bool GameConsoleWindow::Handle_TextSubmitted(const CEGUI::EventArgs &e) { // The following line of code is not really needed in this particular example, but is good to show. The EventArgs by itself // only has limited uses. You will find it more useful to cast this to another type of Event. In this case WindowEventArgs // could be much more useful as we are dealing with a CEGUI::Window. Notably, this will allow you access to the .window // member of the argument, which will have a pointer to the window which called the event. You can imagine that would be // useful! const CEGUI::WindowEventArgs* args = static_cast<const CEGUI::WindowEventArgs*>(&e); // Now we need to get the text that is in the edit box right now. CEGUI::String Msg = m_ConsoleWindow->getChild(sNamePrefix + "ConsoleRoot/EditBox")->getText(); // Since we have that string, lets send it to the TextParser which will handle it from here (this)->ParseText(Msg); // Now that we've finished with the text, we need to ensure that we clear out the EditBox. This is what we would expect // To happen after we press enter m_ConsoleWindow->getChild(sNamePrefix + "ConsoleRoot/EditBox")->setText(""); return true; }
Now you might be wondering why we didn't just Parse the text in here? Well If the user presses Enter, or presses the Send button the text will either way have to be parsed, and it will be parsed in the same manner. So why write the same code twice?
Below is the code for handling the SendButton being pressed. I know I just harped on rewriting code, and this will ironically look alot like the Code for when text has been submitted. But thats because we don't really need the button to do much. We probably could have just had the ButtonPress trigger the Handle_TextSubmitted above when we registered the handlers, but what if we wanted a clicking noise to be played when the player pressed the button? Then we would need a seperate handler for the Send button. Anyway 'nuff said, lets show the code. Note : For CEGUI 0.8 the root node for layout (ConsoleRoot in this case) is not necessary so you must use this : "EditBox" and not "ConsoleRoot/EditBox" (it's the same thing for Button and other)
bool GameConsoleWindow::Handle_SendButtonPressed(const CEGUI::EventArgs &e) { CEGUI::String Msg = m_ConsoleWindow->getChild(sNamePrefix + "ConsoleRoot/EditBox")->getText(); (this)->ParseText(Msg); m_ConsoleWindow->getChild(sNamePrefix + "ConsoleRoot/EditBox")->setText(""); return true; }
Parsing that Text
This portion is not going to be so much CEGUI Related as it will be string manipulation.
void GameConsoleWindow::ParseText(CEGUI::String inMsg) { // I personally like working with std::string. So i'm going to convert it here. std::string inString = inMsg.c_str(); if (inString.length() >= 1) // Be sure we got a string longer than 0 { if (inString.at(0) == '/') // Check if the first letter is a 'command' { std::string::size_type commandEnd = inString.find(" ", 1); std::string command = inString.substr(1, commandEnd - 1); std::string commandArgs = inString.substr(commandEnd + 1, inString.length() - (commandEnd + 1)); //convert command to lower case for(std::string::size_type i=0; i < command.length(); i++) { command[i] = tolower(command[i]); } // Begin processing if (command == "say") { std::string outString = "You:" + inString; // Append our 'name' to the message we'll display in the list OutputText(outString); } else if (command == "quit") { // do a /quit } else if (command == "help") { // do a /help } else { std::string outString = "<" + inString + "> is an invalid command."; (this)->OutputText(outString,CEGUI::colour(1.0f,0.0f,0.0f)); // With red ANGRY colors! } } // End if / else { (this)->OutputText(inString); // no commands, just output what they wrote } } }
Phew, thats kinda an annoying function isn't it? Well, the point to note is that it checks for a forward slash, if it finds one then it enters the if/else conditions to see if it found an applicable command. If it does it does something. In this example it handles the actions right there, if you were to write it a little more advanced, it could goto a (this)->ShowHelp() or (this)->QuitGame().
The point to note is the OutputText. That will add the items to the ListBox of Chat history. We will work on that function now.
OutputText
A point to note here. A CEGUI::Listbox window doesn't naturally word wrap. And wordwrapping is a very nice thing to have in a chat window, as everyone has different resolutions on their screens, and some people REALLY like to ramble! (But not this magnificant author of course!). So we will use a Custom created widget which is available on the forums [1]. you could use a normal listbox, but you wouldn't get word-wrapping.
void GameConsoleWindow::OutputText(CEGUI::String inMsg, CEGUI::colour colour) { // Get a pointer to the ChatBox so we don't have to use this ugly getChild function everytime. CEGUI::Listbox *outputWindow = static_cast<CEGUI::Listbox*>(m_ConsoleWindow->getChild(sNamePrefix + "ConsoleRoot/ChatBox")); CEGUI::FormattedListboxTextItem *newItem=0; // This will hold the actual text and will be the listbox segment / item newItem = new CEGUI::FormattedListboxTextItem(inMsg,CEGUI::HTF_WORDWRAP_LEFT_ALIGNED); // Instance the Item with Left // wordwrapped alignment newItem->setTextColours(colour); // Set the text color outputWindow->addItem(newItem); // Add the new ListBoxTextItem to the ListBox }
Now we add function setVisible and isVisible to toggle the visibility of the console :
void GameConsoleWindow::setVisible(bool visible) { m_ConsoleWindow->setVisible(visible); m_bConsole = visible; CEGUI::EditBox* editBox = m_ConsoleWindow->getChild(sNamePrefix + "ConsoleRoot/EditBox"); if(visible) editBox->activate(); else editBox->deactivate(); } bool GameConsoleWindow::isVisible() { return m_ConsoleWindow->isVisible(); }
Here is the console.layout file (only for CEGUI 0.7)
<?xml version="1.0" encoding="UTF-8"?> <GUILayout> <Window Type="TaharezLook/FrameWindow" Name="ConsoleRoot" > <Property Name="UnifiedAreaRect" Value="{{0,0},{0,0},{0,848},{0,226}}" /> <Property Name="Text" Value="Console" /> <Property Name="RollUpEnabled" Value="False" /> <Property Name="SizingEnabled" Value="False" /> <Property Name="DragMovingEnabled" Value="False" /> <AutoWindow NameSuffix="__auto_titlebar__" > <Property Name="DraggingEnabled" Value="False" /> </AutoWindow> <Window Type="TaharezLook/Listbox" Name="ConsoleRoot/ChatBox" > <Property Name="UnifiedAreaRect" Value="{{0,0},{0,0},{0,812},{0,160}}" /> </Window> <Window Type="TaharezLook/Editbox" Name="ConsoleRoot/EditBox" > <Property Name="UnifiedAreaRect" Value="{{0,0},{0,166},{0,649},{0,197}}" /> </Window> <Window Type="TaharezLook/Button" Name="ConsoleRoot/SendButton" > <Property Name="UnifiedAreaRect" Value="{{0,672},{0,164},{0,771},{0,196}}" /> <Property Name="Text" Value="Send" /> </Window> </Window> </GUILayout>
and a bonus : The console.layout file (only for CEGUI 0.8). But you can make your own with CEED.
<?xml version="1.0" encoding="UTF-8"?> <GUILayout version="4" > <Window Type="Vanilla/FrameWindow" Name="ConsoleRoot" > <Property Name="Area" Value="{{0,0},{0,0},{0,848},{0,226}}" /> <Property Name="Text" Value="Console" /> <Property Name="RollUpEnabled" Value="False" /> <Property Name="SizingEnabled" Value="False" /> <Property Name="DragMovingEnabled" Value="False" /> <AutoWindow NamePath="__auto_titlebar__" > <Property Name="DraggingEnabled" Value="False" /> </AutoWindow> <Window Type="Vanilla/Listbox" Name="ChatBox" > <Property Name="Area" Value="{{0,0},{0,0},{0,812},{0,160}}" /> </Window> <Window Type="Vanilla/Editbox" Name="EditBox" > <Property Name="Area" Value="{{0,0},{0,166},{0,649},{0,197}}" /> </Window> <Window Type="Vanilla/Button" Name="SendButton" > <Property Name="Area" Value="{{0,672},{0,164},{0,771},{0,196}}" /> <Property Name="Text" Value="Send" /> </Window> </Window> </GUILayout>
Conclusion
And that ladies and gentleman, is our Console box. Not too terrible was it? Now of course, remember there are many ways to skin the proverbial cat. This is just an example of ways to do things. If you would like some homework, you could investigate why the SendButton does weird things when you drag and resize the window. I'll give you a hint, look at the way we're defining how big the ConsoleRoot is and where its child windows are placed... and what implications we might have by using relative scales for things.. Okay, i guess that was a big hint wasn't it!
Till next time...