cocos2d-x开发,包括核心模块接口开发和脚本部分的业务逻辑实现.从上层应用需求开始说,脚本在做业务逻辑实现的时候, 很多时候都需要依赖底层的接口功能,但是不是所有的人都可以游刃有余的去明白该怎么使用下层的接口,这不仅仅是语言的区别,其中还包括接口开发者在设计之初就期望的使用方式问题,所以一般都应该提供接口使用demo,由于我都是使用lua,所以我习惯的称之为unittest,接口功能测试则是以testcase的方式呈现.其实也就只是一种叫法.
先来说一下为什么我会做这样子的区分,我见过一些项目,要不然就是不提供使用范例,就是将各种测试都写在项目的入口文件那里.假如我们的引擎进入luaEngine之后进入的是src/main.lua文件中.那么按照很多没有分离好的项目的做法就会出现在main.lua文件中做一个变量.然后判断是进入游戏,还是进入测试,还是进入工具模式等等.程序员嘛,我们可以不纠结于这个实现的过程,结果也算是实现了各个方面的功能.不过,我还是要来分析一下: 项目开发毕竟是多个部分合作开发的,我们并不希望策划在使用我们提供的工具的时候还要自己手动去改配置文件.另外,由于初期就是为了方便将各种测试都堆叠在项目的某个文件里面,当然好的习惯不一定会被大家学习,这种坏习惯就很容易传染.后面每一个人就都会习惯这种简单的测试方法,认为这种测试堆叠的方法是理所当然的.最后的结果就是,堆测试的文件基本就是没法看了.
最后也是最主要的一点,我认真思考了一下,做下层接口,写范例代码是为了什么?其实就是作为对api不足的补充,更好的是某种情况下,达成某正规范,是可以让大家很快的熟悉接口的使用方法.我始终坚信,接口的设计者在实现的时候就应该会想到该如何更好的去使用自己写的接口.上面说了很多,希望能够准确的表达我的意思, 如果看不懂, 可以看下面我的思路以及实现,相信还是很容易看得懂的.
我先想到的是将除了项目业务逻辑实现部分的其他相关,像是工具呀,接口范例呀这些都分离出来,要做到这样子并不难,无非是在项目脚本目录的同级目录穿件一个新的文件夹,另外提供一个新的入口文件.就像下面这样子:
1 frameworks 2 runtime 3 res 4 src 5 --main.lua --我们通常看到的游戏入口文件 6 --Tests 7 --TestsController.lua -- 测试入口 8 --Tools -- 如果需要,工具部分入口.
结合我前面的出发点,我不希望更改脚本或者是配置文件做到这样子.不过也好解决,我们都知道,无论是在win下还是在linux下面,应用程序在运行的时候是可以解析命令行参数的.当然在Linux下面就是解析main函数的argc和argv,而在win下面也是同样,我们可以解析lpCmdLine,或者是__argc,__targv(tchar.h).关于win下面的这部分因为涉及到unicode的问题,所以我才会写成这个样子的,不过大家应该都会明白的.说到这里,应该很好明白如何去实现这部分的功能了.我以win32为例,代码如下:
1 AppDelegate.h 2 public: 3 void setCmdLine(const std::string& cmdline) 4 { 5 this->M_cmdline_ = cmdline; 6 } 7 std::string getCmdLine() const 8 { 9 return this->M_cmdline_; 10 } 11 private: 12 std::string getCmdByFlag(const std::string& flag, const std::string& defaultValue); 13 std::string M_cmdline_;
这是引擎封装好的入口代理,这里我添加了一个私有属性,用来装从命令行读取的参数.不管读到了什么,都一并装过来.不过,既然是我写,我当然知道我希望的格式是什么样子的了. “xiaoyan.exe -esrc/Tests/TestsController.lua”
就像是这个样子的,解析的时候我只要去解析-e后面的flag属性字段,我就知道要运行什么模块的代码了.好了,我做了一下Unicode字符串到ansi的转换,如下:
1 win32 main.cpp 2 // create the application instance 3 AppDelegate app; 4 if(__argc > 1) 5 { 6 #if(UNICODE) 7 std::wstring wcmdLine(lpCmdLine); 8 const wchar_t* wparams = wcmdLine.c_str(); 9 size_t size = wcmdLine.size() * 2 + 1; 10 char* tmp = new char[size]; 11 memset(tmp, 0, size); 12 wcstombs(tmp, wparams, size); 13 std::string cmdline(tmp); 14 app.setCmdLine(cmdline); 15 delete[] tmp; 16 #else 17 std::string cmdLine(lpCmdLine); 18 app.setCmdLine(cmdline); 19 #endif 20 } 21 int ret = Application::getInstance()->run();
虽然我不懂win32 sdk,但是简单看看unicode处理,还是很容易写出上面的这些代码.现在,我拿到了我想要的命令行参数,也做了相应的跨平台转化处理,放在std::string,解析部分就简单了,直接给实现:
1 std::string AppDelegate::getCmdByFlag(const std::string& flag, const std::string& defaultValue) 2 { 3 4 if(this->M_cmdline_.empty()) { return defaultValue; } 5 std::size_t index = this->M_cmdline_.find(flag); 6 if(index == std::string::npos) { return defaultValue; } 7 std::size_t nextIndex = this->M_cmdline_.find("-", index+flag.size()); 8 if(nextIndex == std::string::npos) { return this->M_cmdline_.substr(index+flag.size(), std::string::npos); } 9 else { return this->M_cmdline_.substr(index+flag.size(), nextIndex-index-flag.size()); } 10 return defaultValue; 11 }
经过上面的变动,我就可以去修改我的引擎入口实现了,不废话,直接看吧.
1 bool AppDelegate::applicationDidFinishLaunching() 2 { 3 auto engine = LuaEngine::getInstance(); 4 ScriptEngineManager::getInstance()->setScriptEngine(engine); 5 std::string entranceFile = getCmdByFlag("-e", "main.lua"); 6 if (engine->executeScriptFile(entranceFile.c_str())) { 7 return false; 8 } 9 10 return true; 11 }
我这边只是添加了一个接口测试模块,所以并没有什么逻辑判断之类的,不过我相信,即使是再多添加一些功能,也是很容易做的,但是不是修改上面这里的入口,而是简单的在运行时候提供我们想要运行的模块入口文件就行了.因为涉及到工作目录的问题,我在测试的时候是使用vs做的,不过,我也写好了sublime text配置部分的,一并给出来.我先是修改了build system,添加了一个Test的子模块:(可以对照上一篇文章看一下,如果不嫌麻烦)
1 { 2 "cmd": ["C:\\Users\\Administrator\\Desktop\\xiaoyan\\scripts\\compile-win32.cmd"], 3 "working_dir": "C:\\Users\\Administrator\\Desktop\\xiaoyan\\xiaoyan", 4 "shell": true, 5 "encoding":"utf-8", 6 "variants": 7 [ 8 { 9 "cmd" : ["start","C:\\Users\\Administrator\\Desktop\\xiaoyan\\xiaoyan\\runtime\\win32\\xiaoyan.exe"], 10 "name": "Run", 11 }, 12 { --添加一个Test,带有参数 13 "cmd" : ["start","C:\\Users\\Administrator\\Desktop\\xiaoyan\\xiaoyan\\runtime\\win32\\xiaoyan.exe","-esrc/Tests/TestsController.lua"], 14 "name": "Test", 15 } 16 ] 17 }
如上面我在注释中给出的说明一样, 后面加入了Test附加的参数,也就是测试模块的入口.好吧,做到这里就只剩下最后的琐碎了.先去添加一个快捷键:
1 [ 2 { "keys": ["alt+f1"], "command": "toggle_side_bar" }, 3 4 { "keys": ["f5"], "command": "build" }, 5 { "keys": ["f10"], "command": "build", "args": {"variant": "Run"} }, 6 { "keys": ["f8"], "command": "build", "args": {"variant": "Test"} }, 7 ]
好吧,做到这个份上了, src/Tests/TestController.lua的测试源码就不用给了吧? 我相信还是很容易想到如何做的.该说说我为什么要这样做,而这么做和分离又有什么关系?在说这之前,我先说一下如何用,一样的,知道如何用就知道优点在哪里,自然也就明白了好处.可以参照Lua-tests的例子,是一种很好实现Tests的方法,不过,当然我们不会写的这么复杂.做到这样,可能还不完善,但是分离的工作相信很容易就看到成效了,在开发中有什么不明白的,先去翻阅Tests下面的TestCase,如果需要添加接口,将写好的接口测试加入TestCase提交就好了.
到这里,分离部分我就做完了,对于分离后下面的测试实现,相信不同的有见解的程序员会有不同的做法,那些就自由发挥了.其实我这样做的目的,是为了测试下层模块接口使用的.在没有确定游戏类型和需求之前,盲目的去做下层架构设计相关的工作,会造成后期开发很多问题.