Creating a Command Console

So, on the second try, I managed to implement a command console to use in my project framework. When I say command console I mean the text input box many games have which allow you to alter variables and dispatch commands to the game in real time, as introduced (I think) in the Quake engine. I'd been after a feature like this for a while as it is very useful for developing game play where a lot of variable tweaking is required, which can become tedious very quickly if it means recompiling from source every time a small change is made. So how does it work? Well the basic idea is this:
    The console class stores a map of strings to command objects. The strings represent the command as it is input to the console window, and the command is the data to be acted upon, be it a variable or a function. When the console class is requested to execute a command, it looks up the command string in the map and, if it is found, executes the command, passing any other text entered on the command line as arguments to the command function. My main reference was this article, which inspired my class, although it appears a little outdated (it was written around 10 years ago). In it is explained a command format which is derived from the source of the Quake games, which are C based. Briefly it looks like:

typedef void(*CommandFunc)(vector<string>&);

enum DataType
{
    INT,
    FLOAT,
    //others
    FUNCTION
};

struct Command
{
    string name;
    DataType type;
    union
    {
        void* var;
        CommandFunc func;
    }
};

It seems a bit intimidating, but it's not all that complicated. First there is a type definition for a function pointer which declares the prototype for a command function that takes a vector of strings as a parameter. It is this vector which holds any arguments being passed from the command line. Using a function pointer means a command can be told to point to any suitable function on the fly, enabling quick rebinding of commands to functions. The DataType enumeration is used by the command to store what kind of data the command is pointed at. The Command struct contains a union of two pointer types - one a function pointer, the other of type void which can be pointed at any variable. Storing the data type as a separate member means that the void pointer can be cast to the correct type when necessary, by first checking the data type value. The two pointers are stored in a union which means they share the same memory space. A command will only ever point to one or the other type and, as both members are pointers (not the data that they are pointing at), will only ever require the same amount of memory.
    While this works, and is efficient, it is also error prone. For example a command may be pointed at something else during its lifetime - but if the type member isn't correctly updated then trying to read the data pointed at can cause an error. If a command is pointed at a float variable but the type member is left stating that the data is of unsigned char, the data format will be both misinterpreted and only partially read (one byte of uchar for a four byte float). A compiler may or may not catch this error, but it will cause all kinds of undefined behaviour.
   I decided that a more modern approach could be taken using C++11 features, specifically std::function and lambda expressions. std::function is a class for creating objects which fulfill the requirements of a function pointer. You can bind existing functions to the object, or create them from lambda expressions. Lambda expressions allow you to declare a function inline with the code, and without any forward declaration. They also allow you to capture local variables. This is useful because it does away with the need to point to any variable data in the Command struct, you need only a single function which can then act upon that variable. My command struct is, in fact, not a struct at all:

typedef std::function<std::string(std::vector<std::string>&)> Command;

The template declares a function prototype similar to that of the function pointer in the previous example - with the exception of the std::string return value (I'll come back to that later). Now creating a command is safer and easier, and can be done anywhere in code which is accessible to the console class.

Command c = [&myVar](std::vector<std::string> args)->std::string
{
    myVar = args[0];
    return "variable changed to: " + args[0];
};

This is a little generalised of course, a proper function would include code for validating argument values etc. Note how the lambda is able to capture the local variable myVar by reference and update it. There is a possibility, however, that myVar will go out of scope before the command is called, so you must be wary of its lifetime.

So having settled on a command format, the Console class begins to take shape. The class needs to perform a few things:

  • Map commands to command line strings
  • Provide ability to add and remove commands
  • Provide a visible interface for the user to input command strings
  • Parse inputted command strings and forward them to the correct commands.

The first two points are pretty straight forward. A private member

std::map<std::string, Command> m_commands;

is all that's needed to store available commands. Then two simple public functions can add and remove commands from the map:

void Console::Add(const std::string name&, command cmd)
{
    assert(m_commands.find(name) == m_commands.end());
    m_commands.insert(std::make_pair(name, cmd));
}

void Console::Remove(const std::string& name)
{
    const auto c = m_commands.find(name);
    if(c != m_commands.end())
        m_commands.erase(c);
}

By asserting that the command doesn't already exist in the Add function we make sure that it's not possible to accidentally assign the same command name to a different function during development. This could otherwise lead to some serious head scratching...
    The second two points on the list are a bit more involved. So far what has been covered is fairly generic and will work with any framework you may be using, but the graphical side of the interface is very much dependent on which library you use to draw on screen. In my case I'm using SFML, so from here I'll probably be covering a lot of SFML specific stuff. Actually parsing the command string is fairly generic and portable but, in my example, is tied to the SFML code.
    Moving the string data around requires a couple of buffers for the input and output. The input buffer can be a single string member

std::string m_inputBuffer;

For the output, however, its nice to be able to view a bit of history, so I use a list

std::list<std::string> m_outputBuffer;

Whenever a message is printed to the console it gets put in the output buffer, by pushing it onto the back. The reason I use a list is so that once it reaches a predefined length (usually the max number of lines visible on the screen) then I pop the front member. This could also be done with a vector by using

vector.erase(vector.begin());

There are most likely pros and cons of each method - if you feel it is better to do it the other way feel free to comment as to why, I'm always looking to better improve my code :)

To draw text on screen the most straight forward way is to make the Console class inherit from sf::Drawable and implement the draw() function. In my specific case I then used an instance of sf::RectangleShape for the background, and two instances of sf::text to display the input and output buffers. These are all private members of the Console class. The draw function then overlays all of these onto the screen when the console is set to visible. If you want to get a bit more creative it wouldn't be a stretch to also inherit sf::Transformable and create a floating window for the console, which can be dragged around the screen. I'll not cover that here though. I also added a small private function which then updates the display

void Console::m_UpdateText()
{
    m_inputText.setString(m_inputBuffer);

    std::string output;
    for(const auto& s : m_outputBuffer)
        output += s + "\n";

    m_outputText.setString(output);
}

where m_inputText and m_outputText are the sf::Text instances. Handling the text input is a bit more involved. To get the keystrokes the Console class needs to handle the SFML TextEntered event. This can be done via a HandleEvent function, which needs to be called from the event polling loop of the main application. This way the console can hook any events and deal with them appropriately. Later on this function will also prove useful for another reason:

void Console::HandleEvent(const sf::Event& event)
{
    if(m_show)
    {
        if(event.type == sf::Event::TextEntered)
        {
            if (event.text.unicode > 31
                && event.text.unicode < 127
                && m_inputBuffer.size() < maxBufferLength)
            {
                m_inputBuffer += static_cast<char>(event.text.unicode);
                m_UpdateText();
            }

        }

        else if(event.type == sf::Event::KeyPressed)
        {
            switch(event.key.code)
            {
            case sf::Keyboard::BackSpace:
                if(!m_inputBuffer.empty())m_inputBuffer.pop_back();
                m_UpdateText();
                break;
            }
        }
    }
}

First I should note that m_show is a boolean set depending on whether or not the console is visible on screen. It's used in several places; here to prevent text being entered when the console is not visible. If an event is of type sf::Event::TextEntered it is then hooked, checked and appended to m_inputBuffer. The range check between 31 and 127 makes sure that the text is in the ASCII range of characters, and that Delete and Backspace strokes are ignored. The ASCII range it not so important - for other locales you may want to update this, but for my purposes it was enough to keep it simple. Once the character has been added to the line m_UpdateText() is called so that the changes are visible on screen.
    If the event is of type sf::Event::KeyPressed then any Backspace strokes are used to remove the last character from the input buffer (after checking that it has any characters to remove) then the display updated once again.

Hopefully at this point you are still with me. The console should now be drawable on screen, and allow the user to enter commands as text. The final bullet point is to be able to parse the commands entered and use them to execute any stored Command functions if they are found. This is done with

void Console::m_ParseCommand()
{
    if(!m_inputBuffer.empty())
    {
        std::vector<std::string> commands = tokenize(m_inputBuffer, ' ');
        const auto c = m_commands.find(commands[0]);
        if(c != m_commands.end())
        {
            m_commands.erase(m_commands.begin());
            std::string result = c->second(commands);
            if(!result.empty()) Print(result);
        }
        else
        {
            Print(commands[0] + ": command not found");
        }

        m_inputBuffer.clear();
        m_UpdateText();
    }
}

I've removed some of the string formatting code from the function for brevity - normally I would do a bit more validation on the input string - but the code will still parse a command which is properly formed. First the tokenize function splits the command input string at each space it finds. Each sub-string is then placed in the commands vector as a new entry. The definition of the tokenize function is omitted here; you can either use boost or roll your own. The first entry in the command vector is expected to be the command itself, it's used to search the map of commands for a match. If the command is found the first entry is then erased from the command list as it is no longer needed, and any remaining lines are passed as parameters to the function of the found command

std::string result = c->second(commands);

Notice that the return value from the function is stored in result. When creating the typedef for the Command function I stated that the return value is of type string. I added this so that when a function is called it can return a message that can be printed to the console to indicate either success or some sort of error message. result is then checked to see if it is empty, and printed to the console if not. Print is a public member function of console:

void Console::Print(const std::string& str)
{
    m_outputBuffer.push_back(str);
    if(m_outputBuffer.size() > maxBufferLines)
        m_outputBuffer.pop_front();

    if(m_show)
        m_UpdateText();
}

It simply pushes the supplied string onto the output buffer and removes the front entry if the size exceeds a predefined limit. Then, if the console is visible, the sf::Text instances are updated with the new text.
Returning to m_ParseCommand, if the command is not found an error is printed to the console. Finally the input buffer is cleared, and the on screen text is updated.
    To bring it all together m_ParseCommand has to be called when the user presses Return. In the HandleEvent function, below where the Backspace key press event is handled, an extra case is added:

case sf::Keyboard::Return:
    m_ParseCommand();
    break;

which completes the basis for a fully functioning console. As a test I created a command in the Console constructor:

Command c = [this](std::vector<std::string> l) -> std::string
{
    for(const auto& i : m_commands)
        Print(i.first);

    return std::to_string(m_commands.size()) + " commands found.";

}
Add("list_commands", c);

which simply prints a list of all available commands to the console when you type list_commands.



If you made it this far down the page, well done (and thanks!) this post has turned out to be a tad longer than I anticipated - hopefully a fact mitigated by providing at least some useful information. I had planned originally to go into the key binding system where the console class keeps a list of key bindings which can be used to execute commands at a single stroke but I shall have to save that for a post of its own, which will, with any luck, appear in the near future.

Comments

Popular Posts