小议游戏控制台

>_ 引子

  近来一直在搞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,要说的就这么多了,最后还是那句:下次再见吧~~~

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大宽宽的碎碎念

如何深入理解开源项目从小代码集看起聚焦请先看文档关注资源的生命周期找一个好工具建立调试环境看代码很累,要坚持

34860
来自专栏腾讯技术工程官方号的专栏

大道至简—GO语言最佳实践

被称为GO语言之父的Rob Pike说,你是否同意GO语言,取决于你是认可少就是多,还是少就是少。

3.1K120
来自专栏北京马哥教育

高性能服务器架构里的隐藏秘密

作者:Coder李海波 来源:http://blog.csdn.net/marising/article/details/5186643 在提到服务器架构时,...

36940
来自专栏乐沙弥的世界

Python简介

版权声明:本文为博主原创文章,欢迎扩散,扩散请务必注明出处。

21830
来自专栏iOS122-移动混合开发研究院

记一个同时支持模糊匹配和静态推导的Atom语法补全插件的开发过程: 序

简介 过去的一周,都睡的很晚,终于做出了Atom上的APICloud语法提示与补全插件:apicloud_autocomplete.个中滋味,感觉还是有必要记录...

20160
来自专栏Golang语言社区

在 Go 语言中,如何正确的使用并发

Glyph Lefkowitz最近写了一篇启蒙文章,其中他详细的说明了一些关于开发高并发软件的挑战,如果你开发软件但是没有阅读这篇问题,那么我建议你阅读一篇。这...

21300
来自专栏Java成长之路

mo9 2年java面试总结

mo9是一家做数字货币交易所的公司,在4月份的时候自己去mo9参加了java开发的面试。mo9的面试更加注重基础,问了很多java基础方面的知识。下面将面试的一...

15020
来自专栏我是攻城师

程序员最恐怖的梦魇是什么?

27040
来自专栏Golang语言社区

【Go 语言社区】在 Go 语言中,如何正确的使用并发

Glyph Lefkowitz最近写了一篇启蒙文章,其中他详细的说明了一些关于开发高并发软件的挑战,如果你开发软件但是没有阅读这篇问题,那么我建议你阅读一篇。这...

37290
来自专栏况鹰的专栏

obs源码分析第二篇:庖丁解牛

上一篇《obs源码分析第一篇--踏石留印》简单介绍了obs的由来和工程构成,这一篇我将剖析一下obs二代的代码内部,就不来文艺气息了,直接上猛料。本文将按照数据...

1.9K00

扫码关注云+社区

领取腾讯云代金券