用STL构建一个可扩展的控制台系统(一)
作者:Facundo Matias Carreiro
译者:Hydralisk
原文地址:http://www.gamedev.net/reference/articles/article2179.asp
[介绍]
本文假设
你已经对C++语言,虚拟与纯虚拟继承和STL库有了相当的了解。熟悉函数指针,联合和结构会更有助于理解本文。文中将使用面向对象的C++来编写示例代码。我将不会解释关于STL库的细节,但即使你不了解它们也不用惊慌(Panic -_-b:Hydralisk注),我将提供有关文中使用到的STL对象(如std::vector, std::list, std::string)的参考网页的链接。如果你对文中使用到的某些概念不甚了解,请仔细阅读括号中指向的参考网页。如果你对本文有任何疑问或者要和我沟通请发e-mail到fcarreiro@fibertel.com.ar给我,谢谢。
谁适合阅读本文?
本文将介绍如何构建一个抽象的控制台接口用于创建“Quake(游戏天才约翰·卡马克开发的一套经典的FPS游戏)”类型的控制台或者简单的基于文本的控制台系统。
我们将
致力于控制台系统的内部操作和设计,以使它可以适应大多数的需求。
创建一个你可以输入和输出变量、执行命令的控制台。
提供一个例子来解释在什么地方和怎么使用这个控制台系统。
我们将不
介绍怎么实现一个绚丽的控制台界面。
实现一个带控制台的游戏。
所以,本文适合那些想知道如何构建一个复杂的和可扩展的控制台系统的人,如果你希望知道怎么实现一个好看的控制台静请期待本文的第二部分内容;)
如果你还在小板凳上请继续观看本文的第二段,在那我们将讨论让控制台系统更好的工作所需要的几个部分。
[控制台的组成部分]
控制台的组成部分
我们将控制台分成4个主要部分:输入,解析相关的函数,文本缓冲和输出。每一部分都有相关的变量和类用于和系统的其他部分实现交互。
输入
键盘输入:为了能够处理用户的输入以及把输入添加到命令缓冲,用户的按键操作必须被发送到控制台系统。
解析相关
项列表:一个关于可见的命令以及它们的处理函数的列表,变量和其类型也将包含在里面。
命令解析器:我们需要一个东西来分析输入的命令行并执行相应的操作。这就是命令解析器的主要工作。
文本缓冲
命令行缓冲:用户输入的命令行,当按下ENTER键的时候它将被发送到命令解析器,并可能触发一个命令的执行。
输出的历史记录:当执行一个命令时产生的输出将被存储在一个历史记录的缓冲中。
命令行的历史记录:当你执行一个命令时这个命令将被存储在命令行缓冲中,方便以后随时查看和执行以前输入过的命
输出
输出表现:用图形的或者文本的形式来表现控制台的屏幕输出。
流程图

数据从接收键盘输入的函数开始,它调用命令解析器来执行命令,保存到输出缓冲中。渲染的函数可以被派生类改写,为了在屏幕上输出数据,它须有读取文本缓冲的权限。在下一段中我们将解释怎么根据这个设计来实现这个控制台类。
[控制台的规划]
控制台类
我门希望我们写的控制台基类能够有充分的扩展性来用于实现更复杂的控制台类。稍后我将在用法那一段中解释如何使用这个基类(有关虚拟函数的信息请看这里)。
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;
...
};
该例子里包括了控制台类中最重要的几个部分,现在我开始逐个解释该类的这几个部分:
console();
virtual ~console();
这两个函数是类的构造和析构函数。我们在构造函数中初始化成员变量,在析构函数中释放所有的列表和项。为了使用基类也能正确的调用相应派生类的析构函数,该析构函数必须是虚函数。
void addItem(const std::string & strName, void *pointer, console_item_type_t type);
该方程用于向控制台添加项,一个项可以是一条命令或者是一个变量。例如:你输入一个”/quit”到控制台,控制台将退出,如果你输入一个”color red”控制台将把”red”赋给字符串变量”color”,当你输入”color”的时候你也许希望控制台能返回当前该变量的值。我们设置一个“console_item_t”结构来存放项的数值和一个”console_item_type_t”枚举来标志项的类型:
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
};
枚举”console_item_type_t”用于标志项的数据类型,它也可以是个函数。通过添加更多的枚举定义和在其他函数中的相应实现,你可以很方便的支持更多种类的数据类型。
typedef struct
{
std::string name;
console_item_type_t type;
union
{
void *variable_pointer;
console_function function;
};
} console_item_t;
前两个变量很直观,我只解释一下结构中的联合。如果你希望在多个变量中共享部分内存,联合是一个很好的选择。联合中的第一个成员是个指针,一个当项的类型是变量时,它用于指向相应变量。第而个成员是个函数指针。
typedef void (*console_function)(const std::vector<std::string> &);
我们在这给出了”console_function”的类型,改代码表示:所有的命令处理函数必须有一个参数用来传递命令的参数列表并返回一个void类型的值。
因为项在某一时刻只可能是函数指针或者变量指针中的一种,所以我们使用联合来节约部分内存。
现在,我们重新回到控制台类的讲解中,希望你还没睡着。
void setDefaultCommand(console_function func);
当控制台系统找不到一个合适的命令处理函数来处理用户输入的命令时,它将执行缺省的处理。改函数必须在系统开始前被执行。如果你不需要一个特殊的缺省处理函数你们仅仅在其中输出一条错误信息:
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);
...
}
该例中函数会输出一个“该命令不是一个有效命令“的字符串。
void print(const std::string & strText);
print函数只是像输出缓冲中添加该命令输出的文本。
void removeItem(const std::string & strName);
该函数用于通过给出的项的名字从列表中删除一个项。
void passKey(char key);
void passBackSpace();
void passIntro();
这三个函数用来控制键盘的输入:第一个函数用于将字符发送到控制台,如passKey(‘c’);会在控制台中输出一个字符’c’。第二个函数用于删除控制台输入中的最后一个字符(当你按下退格键的时候)。最后一个是用来执行命令行的。
virtual void render() = 0;
这是我们的虚拟渲染接口,在派生类中它将被用来实现控制台的屏幕输出表现。为了确保该基类不被实例化我们把它声明为纯虚函数。
void parseCommandLine();
该函数将在后面用一整段的篇幅来解释。
private:
std::list<std::string> m_commandBuffer;
std::list<console_item_t> m_itemList;
这两个列表主要负责存储命令行缓冲,因为派生类中不需要直接访问它们,所以我将他们声明成私有类型。
std::list<std::string> m_textBuffer;
这个是另外一个列表,主要是用来存放所有的控制台输出历史的记录,当初始化控制台的时候我们可以选择总共用多少行来存储。如果输出超过了缓冲的大小,最前面的记录将被删除。命令行缓冲也有同样的机制。
[核心代码]
解析命令行
现在,我们需要一个函数判断用户的输入是否是个命令。
void console::passIntro()
{ if(m_commandLine.length() > 0) parseCommandLine();
}
passIntro是处理的开始,接着我们调用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;
}
}
}
}
一个很帅的函数不是吗?它非常容易理解,我只解释其中最难的几个部分。
函数开始时将命令行字符串添加到输出缓冲中,做为一个输入命令的一个响应,你也可以禁止它。它只是个额外的功能,即使你把它从代码中删掉也不会影响整个程序的运行。
接着函数把命令行字符串添加到命令行历史记录中。
第三步把命令行分解成一个字符串数组,其中,数组的首个元素是命令的名字,命令的参数紧跟其后。
现在是最复杂的一个部分了,我们在支持的命令的列表中一个接一个的查找和输入的命令相匹配的项,如果没找到,则调用缺省的命令处理函数。
如果我们发现命令行的第一个参数是一个变量名而且没有提供其他任何参数,则表明它是一个查询命令,我们只需要输出变量的值即可。如果还有另一个参数,我们就将参数字符串转换成相应的变量类型并将变量原来的值用新的值替换掉。(请注意参数数组的大小应该是2,因为第一个元素是变量名自身)。
我们也可以直接把参数列表传递给相应的命令行处理函数并执行。注意我们传递的是vector的引用。
[应用]
派生
上面的类只是个最基本的类,你可以添加新的函数和方法来扩展它,让它更有实际用途。下面我简单的介绍一下如何扩展它,而更进一步的讨论将放在文章的第二部分,请不定期地跑到网站来关注一下第二部分是否已经发表:)
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);
...
}
传递按键信息
当你用类似DirectInput, SDL或者其他的方法检测到一个键被按下时,你需要将它传递给控制台处理,下面是伪代码:
char c = get_keypress();
switch(c)
{case BACKSPACE:
console->passBackspace();
break;
case INTRO:
console->passIntro();
break;
default:
console->passKey(c);
break;
}
添加变量
如果你希望让用户通过输入变量名字和值来改变或查询内存中变量的值,只需要这么做:
static std::string user_name;
console->addItem("user_name", &user_name, CTYPE_STRING);
添加命令
控制台的一个优点就是能让用户执行相应的控制命令,通过添加命令到命令列表你可以非常方便让控制台类自动的传递参数列表到处理函数中去。
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); ...
}
这样,当用户输入"/print_args 1 2 hello"的时候控制台就会输出”1,2,hello”。以上就是个简单的例子,它说明了如何访问传递近来的参数数组。
[结论]
嘿嘿,我们学到了啥?
现在,你已经可以利用STL容器的高效和稳定性来设计、编码和使用一个可扩展的复杂控制台系统了。在本文中我们构建了一个控制台的基类,在后续文章中我们将讨论如何实现一个真正的文本控制台系统。我们也可以实现一个典型
的“QUAKE”风格的控制台。人有多大胆,地有多大(原文:The uses of this systems are infinite, the only limit is your imagination)*_*
这里是本文的代码例子,它可以帮助你理解本文中构建的控制台系统。请不要剪切粘贴其中的任何代码,因为这样做对你有害无益,你应该自己去理解它,理解他是如何工作的,然后在重写或者拷贝其中的代码改造成适合你需求的代码。
感谢你阅读本文,我希望它对你构建自己的游戏会有所帮助。
[参考]
如果对于理解本文有所困难,我建议读一本好的C++书籍和一些本文中提到的文章/教程。我将给你一些链接,它们可能并不是最好的学习途径但它们是免费的。我强烈建议里买一些(经济能)力所能及的书籍,没钱就只有蹭免费的喽~