See what's going on with flipcode!




 

Creating a Scalable Console System with STL - Part 1
by (08 October 2004)



Return to The Archives
Introduction


Hello! My name is Facundo M. Carreiro and this is my first attempt in writing an article. I am currently starting my first year in the "UBA" university in Buenos Aires, Argentina, I'm going for the "Computer Science" MBA. I am currently developing a free Multiplayer Massive Online Role Playing Game in my free time, its fully in 3D using OpenGL and its portable to Windows and Linux. I'm learning a lot while I do it and I wish to share what possible with the community I learned from. I hope this article is helpful to you and I would really appreciate some feedback.

Assumptions
To successfully read this article I suggest 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 if you have any doubts 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


    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 con 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
    Rendering 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<std::string> m_commandBuffer;

    std::list<console_item_t> m_itemList;

    console_function defaultCommand;

    ...

    protected: std::list<std::string> m_textBuffer;

    std::string m_commandLine;

    ... };


    This sample class contains the most important parts of the console class, always check the attached souce code to see the complete and working code. I will now start to explain the different parts of the class:

    
    console();
    virtual ~console();
     


    This 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 (more info here, and here).

    
    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 char
        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 straightfoward, but I can do some explaining about the union :). A union is used when you want more than one variable to share the same space of memory (more info here). 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 (more info here), I will now explain it.

    typedef void (*console_function)(const std::vector<std::string> &); 


    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 you haven't lost yourself.

    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<std::string> & args)
    {
        console->print(args[0]);
        console->print(" is not a recognized command.\n");
    }

    void initialize() { ... console->setDefaultCommand(default); ... }


    That example function would print "< command name > 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, preety straightfoward.

    
    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 instanciable 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<std::string> m_commandBuffer;

    std::list<console_item_t> m_itemList;


    These two lists (more info about std::list click here) are the responsible for holding the command line buffer, that is composed of several strings (more info about std::string click here) and 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<std::string> 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<std::string> arguments;
        std::list<console_item_t>::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, isnt 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 dont pass a copy of he vector, we pass it by reference!, be sure to check this out because its a very useful technique. You can save time, memory and much more! (optional site 2).


    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<std::string> & args)
    {
        for(int i = 0; i < args.size(); ++i)
        {
            console->print(args[i]);
            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 acces the arguments vector (more info here).


    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 hobbie, or for money... You have the power, use it wisely...

    Facundo Matías Carreiro



    Reference
    If you had a hard time reading this article then I recommend you read a good C/C++ book and some articles/tutorials on the topics discussed in this article. I will now provide you 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


    Article Series:
  • Creating a Scalable Console System with STL - Part 1
  •  

    Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
    Please read our Terms, Conditions, and Privacy information.