Introduction
Assumptions
To successfully read this article I suppose that you have a good knowledge of the C++ programming language, virtual and pure virtual inheritance, and the STL library. Knowing a bit about function pointers, unions and structs would also be great in order to fully understand the article. The code will be written in the C++ language with an Object-Oriented Architecture. I will not explain the different parts of the STL library but I will provide links to reference pages of the ones I will use (i.e. std::vector, std::list, std::string) so don't panic if you've never heard of them. Please read all the reference links that are between parenthesis if you don't fully understand a specific topic and always feel free to send me an e-mail to [email="fcarreiro@fibertel.com.ar"]fcarreiro@fibertel.com.ar[/email] if you have any doubt or wish to communicate something. Thank you.
Who is this article for?
This article will explain how to create an abstract console interface that can be used in lots of contexts such as a "Quake Style" console or just a simple text-based console.
What we WILL do:
- We will focus on the inner operations and design of the console system so it will fit for many future purposes.
- We will create a console where you can input and output variables and execute commands.
- We will provide examples of where and how to use the system. What we will NOT do:
- We will NOT explain how to render a fancy-looking console.
- We will NOT create a game with a console So, if you are looking for an article on how to create a complex and extensible console system then this is for you; if you want to know how to make it look good then wait for the second part of the article ;)
If you are still here then come on and follow to the next section where we will discuss the different parts that we need for the console system to work perfectly...
Parts of the console
Parts of the console
We will divide the console into four main parts: the input, the parsing-related functions, the text-buffers, and the rendering output. This diagram also follows the data flow circuit. Each part will have associated variables and classes to interact with the rest of the system, have a little patience and you'll see them in action...
INPUT
Key Input: The keys the user presses have to be passed to the console system so it can process them and add the characters to the command line buffer. PARSING-RELATED
Item List: This is the list of available commands and their associated functions, we also include here the list of variables and its type.
Command Parser: After entering a command line we need something to analyze it and do what has to be done. This is the job of the Command Parser. TEXT-BUFFERS
Command Line Buffer: It is the actual command line being written by the user, after hitting "enter" it passes to the command line parser and most probably triggers a command.
Output History Buffer: The text that the console outputs after executing a command or parsing a line is stored in an History Buffer of n lines.
Command History Buffer: When you press enter to execute a command it is stored in the Command History Buffer so if you want you can see or re-execute previous commands easily. OUTPUT
Rending Output: There has to be some way to render the console data to the screen, should it be text or graphics.
Data Flow Chart
The data goes IN the class by using a function that receives the keys then, if needed, it calls the command parser who executes the command, changes the variable and saves the output in the buffers. The rendering function can be overloaded by the derived class and it has access to the text buffers so it can present the data in the screen. In the next section we will explain how to design the class in order to follow this design.
Planning the Console
Designing the class
When we write the base console class we are looking forward for it to be extensible because it isn't planned to be used alone but to be used as a base class to a more complex console class. I will explain this later when we get to the usage section (virtual functions info here).
class console { public: console(); virtual ~console(); public: void addItem(const std::string & strName, void *pointer, console_item_type_t type); void removeItem(const std::string & strName); void setDefaultCommand(console_function func); ... void print(const std::string & strText); ... void passKey(char key); void passBackspace(); void passIntro(); ... public: virtual void render() = 0; private: bool parseCommandLine(); private: std::vector m_commandBuffer; std::list m_itemList; console_function defaultCommand; ... protected: std::list m_textBuffer; std::string m_commandLine; ... }; This sample class contains the most important parts of the console class, always check the attached source code to see the complete and working code. I will now start to explain the different parts of the class:
console(); virtual ~console(); These two functions are the constructor and destructor of the class. In the constructor we initialize all the variables and the destructor is used to free all the lists and items. The destructor *HAS TO BE* virtual because we are going to use this as a base class and in order to properly call the derived class' destructor we make the base class' destructor virtual.
void addItem(const std::string & strName, void *pointer, console_item_type_t type); This function is used to add an item to the console, an item can either be a command or a variable. For example if you write "/quit" and hit enter the console may quit but if you write "color red" then the console will assign "red" to the string variable "color". You may also want the console to report the content of "color" if you write the name of the variable and hit enter. To be able to do this we have to create a "console_item_t" struct, and a "console_item_type_t" enumeration, one will store the item and the other will identify the item type:
enum console_item_type_t { CTYPE_UCHAR, // variable: unsigned char CTYPE_CHAR, // variable: char CTYPE_UINT, // variable: unsigned int CTYPE_INT, // variable: int CTYPE_FLOAT, // variable: float CTYPE_STRING, // variable: std::string CTYPE_FUNCTION // function }; The "console_item_type_t" enumeration will identify the item, as you can see the item can be of different variable types or it can be a function. You can easily add more variable types by adding some names to the enumeration and just a few lines of code in other function, you'll see.
typedef struct { std::string name; console_item_type_t type; union { void *variable_pointer; console_function function; }; } console_item_t; The first two variables are straightforward, but I should do some explaining about the union. A union is used when you want more than one variable to share the same space of memory. The first item inside the union is a pointer, THE pointer to the variable when the item type is some variable type. The second variable is a function pointer, I will now explain it.
typedef void (*console_function)(const std::vector &); Here we define the "console_function" type, this line of code means: "All the function command handles should be of type void and have a parameter where it will be passed the list of arguments for that command".
Inside the union both the function pointer and the variable pointer are of type "void *" and only one will be used at the same time, that's why we can use a union to save some space in memory (we save one void pointer for each item in the list).
We will now go back to the main console class, I hope I haven't lost you.
void setDefaultCommand(console_function func); When the console system can't find a suitable command that matches the user's commandline it executes the default command. This function MUST BE called before running the system. If you don't want or need a special default function you can make one that prints out an error message:
void default(const std::vector & args) { console->print(args[0]); console->print(" is not a recognized command.\n"); } void initialize() { ... console->setDefaultCommand(default); ... } That example function would print " is not a recognized command.".
void print(const std::string & strText); The "print" function just adds text to the output history buffer.
void removeItem(const std::string & strName); This function is used to remove an item from the list by providing its name, pretty straightforward.
void passKey(char key); void passBackSpace(); void passIntro(); These three functions are used to control keyboard input: The first one is used to send the characters to the console ,i.e. passkey('c'); would write a "c" in the console. The second function is used to delete the last character from the console (when backspace is pressed). And the last one is used to execute the command line.
virtual void render() = 0; This is our virtual rendering interface, it will be used in the derived class to present the content of the console to the screen. By making it pure virtual we ensure that this class is not instantiable so it can not be used alone.
void parseCommandLine(); The parseCommandLine function will be explained later, it has a whole section for its own.
private: std::list m_commandBuffer; std::list m_itemList; These two lists are the responsible for holding the command line buffer, that is composed of several strings the item list that has already been discussed before. I made these variables private because the derived class will have no need to access them directly.
std::list m_textBuffer; Here we have another list with the history of all the console output, when initializing the console we choose how many lines to store. If the buffer passed the maximum number of lines then the oldest line is erased and the new one is added. Exactly the same happens with the command line buffer.
Console Core
Parsing the Command Line
Now we have to make a function that looks in the list of items and executes it if it's a command or otherwise changes the variable. It all starts in the "passIntro" function.
void console::passIntro() { if(m_commandLine.length() > 0) parseCommandLine(); } ...and continues in "parseCommandLine"...
bool console::parseCommandLine() { std::ostringstream out; // more info here std::string::size_type index = 0; std::vector arguments; std::list::const_iterator iter; // add to text buffer if(command_echo_enabled) { print(m_commandLine); } // add to commandline buffer m_commandBuffer.push_back(m_commandLine); if(m_commandBuffer.size() > max_commands) m_commandBuffer.erase(m_commandBuffer.begin()); // tokenize while(index != std::string::npos) { // push word std::string::size_type next_space = m_commandLine.find(' '); arguments.push_back(m_commandLine.substr(index, next_space)); // increment index if(next_space != std::string::npos) index = next_space + 1; else break; } // execute (look for the command or variable) for(iter = m_itemsList.begin(); iter != m_ itemsList.end(); ++iter) { if(iter->name == arguments[0]) { switch(iter->type) { ... case CTYPE_UINT: if(arguments.size() > 2)return false; else if(arguments.size() == 1) { out.str(""); // clear stringstream out << (*iter).name << " = " << *((unsigned int *)(*iter).variable_pointer); print(out.str()); return true; } else if(arguments.size() == 2) { *((unsigned int *)(*iter).variable_pointer) = (unsigned int) atoi(arguments[1].c_str()); return true; } break; ... case CTYPE_FUNCTION: (*iter).function(arguments); return true; break; ... default: m_defaultCommand(arguments); return false; break; } } } } Nice function, isn't it? It is very easy to understand though, but I will explain the most difficult parts anyway.
The first part of the function adds the commandline to the output text buffer, this works as a command echo, you can enable it or disable it. It's just an extra feature, if you want erase everything related with it and the console will just continue to work perfectly.
The second part adds the commandline to the command history buffer, we've talked about this before.
The third part tokenizes (divides) the commandline into a vector of strings where the first element (element zero) is the actual name of the command and all the other elements are arguments.
The last and more complex part starts by looking one by one all the commands and variables in the list and then compares the name provided in the command line with the name stored in the item information, if we have a match then we go on, if we don't we execute the default command.
If we find that the commandline first argument is a variable and that we have not provided any argument (we just wrote the variable name) then its a query command and we simply format the string and print out the variable content. If we have provided one argument then we convert the argument string to the item type format and we set it to memory (remember arguments size is 2 because the first element is the command or variable name itself!).
We may also come across the execution of a command which its a lot easier, in this case we just execute the associated function passing the vector with the arguments to it. Note that we don't pass a copy of the vector, we pass it by reference!
Usage
Overloading the class
This system is only useful if extended, it is only a base and it must be used as a new class, it must be completed with new functions and a new context. Now I will briefly explain how to do this but we'll focus on this topic in the next part of this article so check this site periodically to see if its online ;)
class text_console : public console { text_console(); ~text_console(); virtual void render(); }; void text_console::render() { ... // use the text-buffers to render or print some text to the screen print(m_textBuffer); ... }
Passing keys
When you detect a keypress by using any means that you want (could be DirectInput, SDL or whatever) you have to pass it to the console for it to act properly, here's a pseudo-code:
char c = get_keypress(); switch(C) { case BACKSPACE: console->passBackspace(); break; case INTRO: console->passIntro(); break; default: console->passKey(C); break; } This is just an example of how to switch the key input and send it to the console.
Adding Variables
If you want the user to be able to change or query a memory variable by writing its name in the console then you can add it to the list in the following way:
static std::string user_name; console->addItem("user_name", &user_name, CTYPE_STRING); That's all ;)
Adding Commands
One of the strong points of a console is that it lets the user execute commands, by adding them to the list you can easily make the console pass a list of arguments to the hook function.
void print_args(const std::vector & args) { for(int i = 0; i < args.size(); ++i) { console->print(args); console->print(", "); } console->print("\n"); } void initialize() { ... console->addItem("/print_args", print_args, CTYPE_FUNCTION); ... } After adding the command when the user types "/print_args 1 2 hello" the console would output "1, 2, hello". This is just a simple example of how to access the arguments vector.
Conclusion
Well well, what have we learned?
Now you can design, code and use an extensible and complex console system that uses STL containers for efficiency and stability. In this part of the article we created the base class for the console system and in further articles we will discuss how to create a *REAL* text-console system and compile it. We'll also probably create the typical "Quake" style console that we all love... and want. The uses of this systems are infinite, the only limit is your imagination (*wink*).
You can check the attached code here to help you understand the system we tried to design. NEVER copy-paste this code or any code because it will be no good for you, the best you can do is to understand it, understand how and why it works and rewrite it or copy the example and adjust it to your needs.
Thank you very much for reading this article and I hope it is helpful to you and you use your new knowledge to make amazing new games to have fun, for hobby, or for money... You have the power, use it wisely...
Facundo Matias Carreiro
[email="fcarreiro@fibertel.com.ar"]fcarreiro@fibertel.com.ar[/email]
Reference
If you had a hard time reading this article then I recommend you to read a good C/C++ book and some articles/tutorials on the topics discussed in this article. I will now provide you of some links, they may not be the best way to learn this but they are free and they are online. I strongly recommend buying some books if you can afford them, for those who can't (like me) here are the links...
Thinking in C++ (e-book)
http://www.planetpdf.com/mainpage.asp?WebPageID=315
C++ Reference
http://www.cppreference.com
C++ Polymorphism
http://cplus.about.com/library/weekly/aa120602b.htm
C++ Virtual Functions
http://www.glenmccl.com/virt_cmp.htm
Virtual Destructors
http://cpptips.hyperformix.com/cpptips/why_virt_dtor2
http://cpptips.hyperformix.com/Ctordtor.html
Unions and Data Types
http://www.cplusplus.com/doc/tutorial/tut3-6.html
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vccelng/htm/class_29.asp
Function Pointers
http://www.function-pointer.org/
Standard Template Library (please buy a book for this!)
http://www.cs.brown.edu/people/jak/proglang/cpp/stltut/tut.html
http://www.xraylith.wisc.edu/~khan/software/stl/STL.newbie.html
http://www.yrl.co.uk/~phil/stl/stl.htmlx
Passing by Reference
http://www.hermetic.ch/cfunlib/ast_amp.htm
http://www.cs.iastate.edu/~leavens/larchc++manual/lcpp_64.html#SEC64
Constructor Initializer Lists
http://www.blueturnip.com/projects/edu/cs/cpp/initializer-lists.html