Console Variables And Commands
by (11 July 2000)



Return to The Archives
Introduction


Most games these days have a console. A console basically consists of a log window and an input mechanism which allows users to enter commands and have results displayed on the given log window. Console variables are basically game variables which can be accessed and/or modified by typing out their name in the console with optional parameters. For example, if there is a console variable called "name". Then the user can see its value just by entering "name" into the console. Depending on how the system is laid out, the user might be able to change "name" to a desired value, or have it flagged for changes at a certain time by entering something like "name Bob".

Similarly console commands are commands which you can execute through the console. The game can define a command called "map" which would consider the second parameter of the entered string to be the name of the map, and then attempt to load it. For example, when the user enters "map hallsofconfusion", the game code would attempt to load a map called "hallsofconfusion".

As you can guess, console commands and variables can be a huge aid in debugging while development by allowing you to view and modify game variables and behavior during runtime. They also improve the end user experience by providing more customizability with little effort from the developer.

I'll try to show how to implement a simple console manager class in C++ in this article. I'll ignore the aspects of drawing the log window and handling input, because these are tied up with how your game engine implements input and rendering. The sample application for this article is a simple MFC dialog which basically just shows how to use the console specific code. If you do need to look at how I've implemented the console in our project, then you can get the complete source code of our game engine at the link at the end of the article.

Lets consider console commands first of all. When a user enters a console command, the Console class needs to execute the function its associated with which will optionally handle the parameters entered by the user. In order to accomplish this, the Console class needs to know the name of the command and it needs to know what function to call for that particular command. This is easy if the class which defines the console command registers it with the console class. The only things the class would need to provide to the console class would be the name of the command, and a callback mechanism to have the proper member function executed when the name is entered in the console. This would be easy in C, because you could simply pass a function pointer and have the console call that when needed. But its not possible to give a pointer to a member function of a class in C++ without making it static. But static member funcs can only access static class data, so unless you start making everything static in your class, which doesn't seem a good idea, your console command will be limited to only accessing static data and functions of the given class.

The OOP way would be to define an interface which declares a prototype of the command handler function. The class can then just register itself as the owner of the console command. And the console class will only need to store a pointer to the interface and call the HandleCommand function on it, when the user enters something which matches the name of the command.


struct I_ConHandler {
virtual void HandleCommand(HCMD cmdId, const CParms &parms) = 0;
};
 


The only problem with this technique is that it will limit a class to just one console Command, because it can obviously implement the Handler interface just once. Thatís where the "HCMD" variable in the above code comes into play. The class uses that to differentiate between all the commands it has registered. HCMD is just an integer id for a command. The class associates a console command with an ID when it registered it, and the Console class makes sure that the HandleCommand function is called the proper id set, allowing the class to figure out which command should be handled. CParms is just a utility class which allows the handler function to access the parameters of the string entered by the user. The command registration function exported by the console class is prototyped as


class CConsole {
.....
void RegisterCommand(const char *cmdname,HCMD id, I_ConHandler * handler);
....
};
 


Letís go through an example. The class registers a command like this


const int CMD_MAP = 1;
GetConsole()->RegisterCommand("map", CMD_MAP, this);
 


Here "map" is the name of the command. CMD_MAP is the id the class internally associates with the command. And "this" just tells the console, that the class itself is the handler of the command. So when someone types "map test" in the console, the console sees that "map" is a registered command, finds the appropriate handler, and then calls the HandleCommand function with cmdID set to CMD_MAP. The handler function can then access different parameters via the CParms object, and do whatever it wants with them. Another advantage to this approach is that you can register more than one commands with the same id.


const int CMD_QUIT = 2;
GetConsole()->RegisterCommand("quit", CMD_QUIT, this);
GetConsole()->RegisterCommand("exit", CMD_QUIT, this);
 


This will let the user to enter either "quit" or "exit" and have the same code executed. Pretty simple, isn't it. Lets move on to Console variables. There are a little more complicated. The biggest problem with them is that they are dynamic. This would be fine in simple circumstances, but if you are creating a variable which is allocated in a DLL and then registering it with your Console class in your executable, which will probably modify it during a session, then we can run into memory issues. The DLL might be linked to one runtime library and the executable with another. This will cause all sorts of problems, and if you are using something like the CRT library, it will most likely start reporting memory leaks as variables are modified between modules. One way to get around this is to use a base variable class which defines pure virtual functions to be used to change its data. And then include the definition of a derived variable class in each DLL so that the calls to modify a console variable's data always map to the function in its own module. Alternately, you can always make sure that all console variables only get created and modified in the same module. This could be accomplished by only keeping the pointers to console variables in the owner class and have the creation, modification and deletion done in the Console class. The variable registration function of the Console class would then just return a pointer to the newly created console variable.

If you have a better solution to this problem then please let me know and I'll update the article with the information giving full credit. However, you can ignore all that if your console variables will always exist within the same module. The following is the declaration of my console variable base class.


enum CVarFlags
{
	CVAR_ARCHIVE = 1,   //write this to config on exit and load on startup
	CVAR_LATCH = 2,       //only change when unlatched.
	CVAR_READONLY = 4 //never change
};

enum CVarType { CVAR_UNDEFINED, CVAR_INT, CVAR_FLOAT, CVAR_STRING, CVAR_BOOL };

class CVarBase { public: union { float fval; int ival; bool bval; };

char * name; char * string; int flags;

virtual void ForceSet(const char *varval)=0; virtual void ForceSet(float val)=0; virtual void ForceSet(int val)=0; virtual void Set(const char *varval)=0; virtual void Set(float val)=0; virtual void Set(int val)=0; virtual void Unlatch() = 0; virtual void Reset() = 0;

protected: enum { CVAR_MAXSTRINGLEN = 512 }; friend class CConsole;

char * latched_string; char * default_string; CVarType type; I_ConHandler * handler; };


The code should be fairly self-explanatory. The union of fval, ival and bval maintains the proper value of the Cvar. The string always keeps that value in string form so that it can be displayed easily by the console. The flags variable defines the behavior of the Cvar. The latched_string caches a value which will be applied to the Cvar only when its unlatched. The default_string is the value it is initially registered with in the code so it can be restored to that if the need be.

Now we can expand the I_ConHandler interface to also provide a Cvar handling mechanism.


struct I_ConHandler
{
	virtual void HandleCommand(HCMD cmdId, const CParms &parms) = 0;
	virtual bool HandleCVar(const CVarBase *cvar, const CParms &parms) = 0;
};
 


Instead of using IDs for CVars, the HandleCVar func implemented by a class uses the address of the CVarBase pointer passed to it to figure out which Cvar is being accessed. This function can act both as a handler and a validation function. A return value of true will allow any proposed changes to be made to the given Cvar, and a return value of false will deny them. For example lets say you have a Cvar named "bpp" and you want the user to be able to change the colordepth to 16 or 32 bpp using that Cvar. Then in your handler/validation function, you would first make sure that the user did indeed enter "bpp 16" or "bpp 32", then if the parameters were okay, you can change the display to the proper depth. If the change is successful, then just return true, and the "bpp" cvar will be updated to the new value. And if the change was unsuccessful, or the user entered invalid parameters, then just return false and perhaps show an error message, and the cvar will retain its original value. If you don't want any validation or handling for a CVar, and want it to be accessible and modifiable freely, then you don't even need to register it with a Handler.

Now we can add the CVar registration function to the Console class. Its definition becomes somewhat like this


class CConsole {
.....

void RegisterCVar(CVarBase * var, I_ConHandler * handler=0); void RegisterCommand(const char *cmdname,HCMD id, I_ConHandler * handler); .... };


If no handler is specified, then the CVar behaves just accordingly to the flags it was created with and no extra validation is performed. The following is a simple test class which shows how everything plugs together.


class CTest : public I_ConHandler
{
public:

CTest() : m_cvPlayerName("name", "John Doe", CVAR_STRING, CVAR_ARCHIVE), m_cvPlayerAge("age","35", CVAR_INT, CVAR_ARCHIVE), m_cvAlive("alive", "1", CVAR_BOOL, CVAR_READONLY |CVAR_ARCHIVE), m_cvMhz("mhz", "450.0", CVAR_FLOAT, CVAR_LATCH | CVAR_ARCHIVE), m_cvFree("myvar","1", CVAR_INT, 0)

{ GetConsole()->RegisterCVar(&m_cvPlayerName,this); GetConsole()->RegisterCVar(&m_cvPlayerAge,this); GetConsole()->RegisterCVar(&m_cvMhz,this); GetConsole()->RegisterCVar(&m_cvAlive); GetConsole()->RegisterCVar(&m_cvFree);

GetConsole()->RegisterCommand("quit",CMD_QUIT,this); GetConsole()->RegisterCommand("exit",CMD_QUIT,this); GetConsole()->RegisterCommand("restart",CMD_RESTART,this); GetConsole()->RegisterCommand("mbox",CMD_MESSAGEBOX,this); }

~CTest() {}

void HandleCommand(HCMD cmdId, const CParms &parms) { switch(cmdId) { case CMD_QUIT: AfxGetMainWnd()->PostMessage(WM_QUIT); break; case CMD_MESSAGEBOX: AfxMessageBox(parms.String()); break; case CMD_RESTART: m_cvMhz.Unlatch(); break; } }

bool HandleCVar(const CVarBase * cvar, const CParms &parms) { if(cvar == reinterpret_cast(&m_cvPlayerAge)) { if(parms.NumTokens() > 1) { int age = parms.IntTok(1); if(age > 10 && age < 40) return true; ComPrintf("Invalid value for age\n"); } } else if(cvar == reinterpret_cast(&m_cvMhz)) { if(parms.NumTokens() > 1) { int mhz = parms.IntTok(1); if(mhz > 20 && mhz < 1000) return true; ComPrintf("Invalid Mhz\n"); } } else if(cvar == reinterpret_cast(&m_cvPlayerName)) { //add validation for name here return true; } return false; }

private: CVar m_cvPlayerName; CVar m_cvPlayerAge; CVar m_cvAlive; CVar m_cvMhz; CVar m_cvFree; };


The entire mechanism should be pretty clear by now. Most of the improvement can come in the Console class. You can add nifty features like command completion and buffering etc. The sample MFC dialog app is available with source below, and the source for a full blown version can be downloaded from the Void website at http://www.thepeel.com/void. Feel free to contact me if you have any suggestions, questions or improvements.

Download The Sample Application (With Source Code)

 

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