>_ 引子
近来一直在搞iOS平台游戏,所用引擎则是cocos2dx,不少时间接触下来,感觉是愈来愈喜欢了:),虽然起初引擎稍显简陋,目前也仍然和商业引擎存在差距,但鉴于引擎“资质优异”、社区活跃,每次更新都有不少令人欣喜的特性,再加上全世界热心Programmer们的各类扩展,cocos2dx(或者说cocos2d家族吧,也许更确切些)确实是愈来愈强大了,而自己则像前面所说的,愈来愈喜欢他了:)
可惜虽然cocos2dx用的顺手,但是随着开发的深入,自己倒是发现了另一个相较引擎使用可能更加费时的工作,那就是游戏调整:譬如简单的一个界面Slide,虽然内部的逻辑简单,但是想要得到一个令人满意的操作感觉,仍然需要不少时间的来回调整,传统的硬编码“技术”早已过时,Config之类的参数配置支持早已成为“标配”,但即便如此,很多时候我们仍然需要重启游戏以再次读取数据,脚本的出现进一步扩展了游戏的“自由度”,但往往也不过是更高级的Config罢了,即便做到了热更新,我们仍然需要付出来回转换编辑的代价,而目前移动平台上的“真机调试”,则限制更大,仅为调整一个参数我们可能就需要重新生成游戏多次,所耗费的大量无意时间着实令人厌烦;另外的,一些逻辑的动态调整也很难完成,譬如Render信息,很多时候非常有用,但也有不少时间我们并不关心,如何方便的开关也是一个难题,尤其是在上述的真机调试中,问题更加明显……
每每遇到此类问题的时候,我都会不由自主的怀念起以前使用过的CE中的游戏控制台,相比上面所言的种种方法,运用这类游戏中内建的Console来调整一个参数那便非常的方便了,简单的几下按键就可以完成,迅速高效直观,这也是为什么目前大多数的PC游戏或者引擎都内建有Console的原因,只是可惜的是,cocos2dx目前并未内建支持,网上稍稍google了一阵,确实也发现了不少控制台的类库实现(譬如这个),但往往都仅针对PC平台,无论设计和实现上都与我的需求有所距离,大抵都不能简单的实行“拿来主义”……
思来想去,觉得与其成天怨天尤人,不如借鉴一下“前辈们”的劳动成果,自己简单的来实现一个移动端的Console,一方面算是解决当前问题、锻炼己身技术,另一方面也算给有过类似问题困扰的朋友一些参考吧……
>_ 设计
游戏控制台大概是起源于大神约翰卡马克的《Quake》,从他以后有不少人对此做了一些扩展或者改变,但相互之间的操作机制都基本类似,某种程度上说,这些游戏控制台中的操作也很类似于OS中的命令行,拿CE中的Console为例,大概的操作分为以下几种:
~:唤出游戏控制台
普通字符:输入字符
Backspace:删除当前光标前的字符
Enter:确认输入
↑:上一个历史输入
↓:下一个历史输入
← : 前移光标
→ : 后移光标
Tab : 自动补全(前缀)
就PC输入而言,上述操作方便快捷,非常流畅,但是想要将他们一股脑儿的搬到移动平台上,亦或者说cocos2dx上,那便有些令人犯难了:
首先,cocos2dx虽然支持Text Input,其扩展Edit Box功能更全,但是从功能上来讲其支持的程度非常有限,譬如箭头、Tab之类的按键操作,即便在其Win32平台的实现中也并未提供(被简单过滤掉了,当然,你可以修改源码……),而在像iOS一般的移动平台中,原生的键盘甚至都不提供这些按键,想要获取这些按键信息基本没门(当然,你可以自己实现一个定制键盘……);再者,如何唤出控制台也是个问题,相比PC上简单的一个~按键,移动端则一般都没有提供类似的输入机制,如何有效的开启移动平台上的控制台也值得思考……
不过好在这些问题从相对的角度来考虑,很多便不再是问题了:诚然,在移动平台上我们并没有完整的按键支持,但是相应的,PC平台上也欠缺移动平台上提供的其他操作(譬如Touch),如果我们不再纠结于按键,而改用其他操作(譬如Touch)来控制Console的话,那么很多问题便解决了,毕竟PC是PC,移动平台是移动平台,有些事情尽管内部原理一致,但是实际实施时也要因地制宜才可 :)基于以上观点,新版的Console操作修改如下:
特定按钮(或者固定Touch范围): 开启控制台
普通字符:输入字符
Backspace:删除当前光标前的字符
Enter:确认输入
向上Slide:上一个历史输入
向下Slide:下一个历史输入
向左Slide: 前移光标
向右Slide: 后移光标
Tap: 自动补全(模糊)
此处除了操作方式有所改变以外,自动补全功能也根据我的个人经验作了一些修改,就CE中的自动补全而言,其采用的是标准的前缀匹配:即譬如你输入了Rel,系统便可以自动匹配到Reload,但是如果输入了Rle或者Rlo之类的字符串则无法匹配(因为不是Reload前缀)。实际使用中,我一般可以大概记得某个命令中的一些字符,但是并不能够完全准确无误的记住这些命令的前缀,再加上时有发生的输入误差,往往导致自动补全功能表现的不尽人意……为了Console的顺畅使用,在此我便索性将游戏控制台中一般的前缀匹配修改为模糊匹配,以期能够得到更好的使用和匹配效果 :)
>_ 实现
OK,基于上面的Console设计,我们来简单看一下实现吧 :
结构上来说,Console代码实现遵循MVC框架,其中的Model为ConsoleDataManager,View为ConsoleView,Controller则是ConsoleController,让我们简单的一个个浏览一下:
首先,让我们来看一看两个Console的基本结构:ConsoleVariable和ConsoleCommand。
顾名思义,ConsoleVariable其实就是控制台参数,而ConsoleCommand则代表控制台命令,实现过程中我曾经试图将这两者统一为ConsoleElement之类的结构,不过后来简单尝试之后,发现ConsoleVariable和ConsoleCommand的相关概念还是有不少区别,统一接口虽然在实现上简洁了一些,但在使用上不甚清晰,所以最终还是分开了,这便导致目前不少接口必须同时支持两个类型,稍稍有些冗余:)OK,闲话少叙,让我们来看看他们的头文件:
//! console variable
class ConsoleVariable
{
public:
ConsoleVariable(const std::string& name, const ConsoleValue& value, ConsoleVarFunc varFunc = NULL, const std::string& help = "");
//! set variable name
void SetName(const std::string& name);
//! get variable name
const std::string& GetName() const;
//! set variable value
void SetValue(const ConsoleValue& value);
//! get variable value
const ConsoleValue& GetValue() const;
//! set variable help
void SetHelp(const std::string& help);
//! get variable help
const std::string& GetHelp() const;
//! set variable func
void SetFunc(ConsoleVarFunc varFunc);
//! get variable func
ConsoleVarFunc GetFunc() const;
// implement details here
// ......
};
可以看到,ConsoleVariable其实非常简单,实现上仅是一个简单的Setter和Getter,结构上大概由Name(命名)、Value(数值)、Func(回调函数)和Help(帮助信息)组成。而ConsoleCommand则更加简单,同样仅有一些存取函数以及一个简单的执行函数,并且组成更简单:分别是Name(命名)、Func(回调函数)和Help(帮助信息):
//! console commands
class ConsoleCommand
{
public:
ConsoleCommand(const std::string& name, ConsoleCmdFunc cmdFunc, const std::string& help = "");
//! set command name
void SetName(const std::string& name);
//! get command name
const std::string& GetName() const;
//! set command function
void SetFunc(ConsoleCmdFunc cmdFunc);
//! get command function
ConsoleCmdFunc GetFunc() const;
//! set command help
void SetHelp(const std::string& help);
//! get command help
const std::string& GetHelp() const;
//! execute command
void Execute(const ConsoleCmdArgs* cmdArgs);
// implement details here
// ......
};
OK,Console基本结构介绍完毕,接下来就是我们MVC框架中的Model了,即ConsoleDataManager:
class ConsoleDataManager
{
public:
//! simple singleton implementation
static ConsoleDataManager* GetSingleton();
public:
~ConsoleDataManager();
//! init method
bool Init();
//! release method
//! call this before quit
void Release();
//! add console variable
ConsoleVariable* AddCVar(const std::string& name, int value, ConsoleVarFunc varFunc = NULL, const std::string& help = "");
ConsoleVariable* AddCVar(const std::string& name, float value, ConsoleVarFunc varFunc = NULL, const std::string& help = "");
ConsoleVariable* AddCVar(const std::string& name, const std::string& value, ConsoleVarFunc varFunc = NULL, const std::string& help = "");
//! get console variable
ConsoleVariable* GetCVar(const std::string& name);
//! remove console variable
void RemoveCVar(const std::string& name);
//! add console command
ConsoleCommand* AddCCmd(const std::string& name, ConsoleCmdFunc cmdFunc, const std::string& help = "");
//! get console command
ConsoleCommand* GetCCmd(const std::string& name);
//! remove console command
void RemoveCCmd(const std::string& name);
//! get similar cvars, return values are arranged by similarity
std::vector<CVarSimilarInfo> GetSimilarCVars(const std::string& name);
//! get similar ccmds, return values are arranged by similarity
std::vector<CCmdSimilarInfo> GetSimilarCCmds(const std::string& name);
//! dump console variable
void DumpCVar(IConsoleVariableDumper* dumper);
//! dump console command
void DumpCCmd(IConsoleCommandDumper* dumper);
// implement details here
// ......
};
相信大家根据上面注释已经大概了解了ConsoleDataManager的基本接口结构,需要特别提一下的便是以下几点:1.ConsoleDataManager是一个Singleton,获取十分简单,但是释放就不那么直观了,目前的实现比较简单,我们需要在退出程序时(或者其他适当时刻)释放ConsoleDataManager的资源,即调用其Release方法;2.GetSimilarCVars(GetSimilarCCmds)返回所有与所给字符串参数相似的字符串,并且按照相似度升序排序(即最相似的字符串置于最后),返回方式使用了简单的std::vector by value的方式,效率不高,有时间可以尝试一下C++11中右值引用 ,或者直接修改接口:)
好了,接下来便是MVC中的View,即ConsoleView:
//! console view interface
class ConsoleView
{
public:
virtual ~ConsoleView() {};
//! set console action delegate
virtual void SetActionDelegate(ConsoleActionDelegate* delegate) = 0;
//! get console action delegate
virtual ConsoleActionDelegate* GetActionDelegate() = 0;
//! set line max char count
virtual void SetLineMaxCharCount(size_t maxCount) = 0;
//! get line max char count
virtual size_t GetLineMaxCharCount() const = 0;
//! output an new line
virtual void OutputLine(const std::string& line) = 0;
//! output input string
virtual void OutputInput(const std::string& input) = 0;
//! set prompt position
virtual void SetPromptPos(size_t pos) = 0;
//! get prompt position
virtual size_t GetPromptPos() const = 0;
};
可以看到,ConsoleView仅是一个虚类(或者说接口类吧),实际中我们必须实现自己的ConsoleView才能正确显示我们的Console信息,而源码中的类型ConsoleViewCocos2dx便是ConsoleView的cocos2dx版本实现,虽然期间细节不少,但在概念上来讲也仅仅是实现了上面的接口定义,并没有什么特别的地方,有兴趣进一步了解的朋友可以参看源码 :)
最后,让我们来看看ConsoleController,即MVC中的Controller:
class ConsoleController: public ConsoleActionDelegate
{
public:
virtual ~ConsoleController() {};
//! init method
virtual bool Init() = 0;
//! release method
virtual void Release() = 0;
//! ConsoleActionDelegate
//
//virtual bool OnEvent(const ConsoleActionData& data) = 0;
//
// NOTE: now we just support one view for simple and clear
//! set view
virtual void SetView(ConsoleView* view) = 0;
//! get view
virtual ConsoleView* GetView() = 0;
};
可以看到,ConsoleController同ConsoleView一样,也仅是一个接口类,实际使用中我们仍然需要实现自己的Controller才能达到对Console的控制,不过好在Controller的控制逻辑基本相似,所以具体实现中除了有上面的ConsoleController以外,还有一个ConsoleControllerBase,其直接继承于ConsoleController,并封装了这些基本的Console操作,譬如如何处理输入、删除等等,当然其中细节也有不少,但概念上都是用于完成这些操作,有兴趣的朋友可以仔细看看代码,在此就不细述了 :)
好了,程序的基本结构我们大概过了一遍,接下来的问题便是如何使用它了,一般情况下,你需要首先将GameConsole中的各个源码文件加入你的cocos2dx工程,然后在适当位置调用:
// add game console at the top
//
addChild(ConsoleCocos2dxLayer::create(), TOP_Z_ORDER);
//
然后就结束了,就这么简单 :)
目前代码简单实现了两个参数和两个命令以供测试,有兴趣的朋友可以试一试 :)
renderInfo : (参数)显示或者关闭Render信息
gameFPS : (参数)设定游戏FPS
Dump : (命令)输出所有参数和命令
EXIT : (命令)退出程序
最后,相关源码可以在此取得,and have fun with console :)
>_ 花絮
1. 曾经考虑过同时支持多个Controller和View的代码框架,后来觉得基本没有必要,除了增加一些开发难度,偶尔可以装装X以外,基本没有什么实际用途,所以很快便放弃了;再者目前的已有代码实现都以可读性和整洁性为第一指导准则,待优化的地方还有不少,BUG自然也不太可能避免,如果有兴趣的朋友优化了实现、找到了BUG亦或者实现了更好的Console,请务必告知,以便改正学习 :)
2. 目前因为条件所限,代码仅在Win32和iOS上测试了一番,其他平台问题暂时不得而知。就Win32平台而言,操作感觉个人觉得马马虎虎(不过模糊补全功能还是很对我的胃口:)),流畅度上还是不及传统的全键盘操作,虽然修改引擎基本可以实现这项功能,不过考虑到所需付出(个人不太喜欢侵入性代码……),觉得还是作罢为好;另外的iOS平台,操作就比较费劲了,由于cocos2dx内部过滤了键盘存在时的Touch信息,导致每次我都必须关闭键盘才能触发Console的Touch逻辑,十分不便,在此我曾尝试去除了这些Touch过滤,相应Console的操作便非常之流畅了(虽然是侵入式代码……),有兴趣的朋友可以试试。
3. 关于Console外观,似乎长久以来都是那副黑底白字的模样,的确这在某种程度上说也足够了,不过因为个人原因,我还是倾向于更加美观一些的样子,譬如这样:
当然,我们还可以做得更好一些,譬如这样:
甚至个人文艺色彩更重一些(纯属个人倾向而已……),BTW,此处期待一下FF10高清中文版 :)
>_ exit
OK,要说的就这么多了,最后还是那句:下次再见吧~~~