在 Unix 文化中,有这样一种理念,Happy Hacking!使用 Cocos2d-x/C++ 写过一些游戏,其绑定的脚本语言,用的也不少,脚本语言的一个好处就是快速开发,你无需明白它之运行机理,便可容易的完成所想要的效果,三天上手,五天就能写出像样的程序来,C++ 则不然,其各种语言细节特性,各种开发技巧,内存管理等细枝末叶 ~
计算机不会魔法,在一叶看来其内容,只有 “知、或者不知” ,没有 “懂、或者不懂”,“知或者不知”来自于你的学习历程与经验,至于“不懂”么,我还没接触到的领域内容,我都不懂,哈 ~ Cocos2d-x 脚本引擎也用过一段时间了,但其运行机理还不明白,就使用而言也无需明白,不过于在意细节的实现,可能更好的从宏观角度把握整体。过去只是对其“存疑”(对于这里的“不懂”,一叶通常美其名曰:要学会存疑 :p ),而现在想要对其运行机制多了解一些,那就只有一步步去探究喽,要了解到什么程度,那就随意了~
对 Cocos2d-x 的运行机制只是略懂一二,C++ 的场景由 C++ 运行,脚本呢,先开启脚本引擎,让后将控制权交由脚本代码执行,在这过程中发生了什么,由脚本所控制的元素和 C++ 有什么不同,或者说它的本质是什么!这之前一叶一直说的是脚本引擎,而非具体那种脚本引擎,lua 或者 js (jsvascript)的引擎实现!凭着对已有知识的了解和直觉,很多疑问和可能性随之而来,它所支持的脚本语言有两种,此两种的共同之处是什么,其使用了脚本绑定技术,什么是绑定 ?各种对象在内存中如何分布,如何配合在一起工作。
为了增加探究过程的趣味性,所以一叶试想着能不能让 Cocos2d-x 现今所支持的两种脚本引擎同时运行(lua and js),然后确定是否能在三者之间(C++, js, lua)访问同一个内存元素,如果行,便弄出来,即便不能做到,那也无所谓,这其中的过程比结果更有意思,不是吗 ~
两种脚本引擎同时运行
这里使用了 cocos2d-x-3.0alpha0-pre 版本,原因有二。一者:这是最新版本,反正是折腾,顺便了解一下 3.0 的新特性和 代码 style ,其二:3.0 对 三大开发平台(windows, linux ,mac),两大运行平台(android, ios)的支持更好更全一些,比如,lua 可以跑在 mac 上面,这一点最新的 2.2 版本不行(lua )。这样的选择,可以让我在当前系统(Mac OS X) 系统下,直接运行看效果,而不用开模拟器或者虚拟机,使过程更为方便。(过去使用 Linux 作为开发环境,也很方便,一叶的博客也有其具体的开发环境搭建配置等)而 windows 系统,几年前就几乎不怎么用了,各种不顺手。
3.0 中去除了使用项目模板来构建项目,而改为使用脚本创建,支持的平台如下(这个脚本是 github 上最新的版本),观其关键代码:
从这里脚本看出 cpp 和 lua ,对五个平台已经全面支持,javascript 对 linux 还没有支持(脚本上是这样),相比 -x 2.x 版本支持更好,更全面,其代码也经过重构,更模块化,还有很多 C++ 11 的新特性,这里同样也期待 3.0 早日成熟,达到实用阶段 :p
为了简化操作步骤,一叶尽量利用现有的环境内容,使用 XCode打开 samples 项目,这里包含了所有项目内容,起初一叶打算基于 HelloCpp 项目做扩展,添加脚本支持,来实现自己的功能,也许是我对 XCode 环境了解不够,通过手动配置,让它能够支持 lua 扩展,但是 js 扩展却没能跑起来(没理由 lua 可以 js 却不行,知道一定是哪里配置有误),此时看来,在已有的项目中添加 lua 比 js 成功几率要大,固转而使用 HelloLua 项目添加 js 扩展支持,以达到项目组织上能够独立运行 js 引擎 或者 lua 引擎。(注:使用 HelloCpp 作为扩展,主要想看看怎么配置方便,如果都好配置,就基于 Cpp ,如果其中一个配置难,或者没通过,就基于没有通过配置的已有实现,扩展它。比如,这里 lua 通过 js 却没有,那就基于已有的 HelloLua 扩展支持 js,更为节省精力,这是策略问题 :p)
以上只是让项目同时支持 js 或者 lua 的运行,比如只跑 lua,或者只跑 js,但是 两者却不能同时跑,如果在代码中,同时用到了 lua 和 js 的支持库,在编译时会报错。这是因为在绑定的时候,两种脚本引擎分别实现了自己的 GLNode 。
同时使用两种引擎,就意味着同时用到两者的库依赖,而重复的类型定义导致编译通不过,所以只能根据需要 hack 源码了,如果修改,二者修改其一,看修改哪个方便,引用的地方少。如下是使用 Emacs 的 find-grep 命令,在 scripting 目录搜索 “GLNode” 关键字的结果。(Emacs 是一叶的必备工具,这里搜索出的结果可以快速定位代码位置)
目测感觉两者差不多,而且代码量也不多,所以修改其中之一的 GLNode 类名称是可行的,总共八十多行结果,修改一种就四十行左右,而且其中出现的 “GLNode” 关键字,并不是都需要修改,有的是绑定到脚本引擎内部的,我们只需要修改其 C++ 端绑定的类型就 OK 了,一叶修改了 lua 绑定中的 GLNode 名字为 LuaGLNode,然后定位到 LuaOpengl 和相关文件(两个文件左右),查找并替换其中一部分 “GLNode” 关键字代码,这里借助 Emacs ,递进式的关键字查询替换,通过匹配规则,一个个过滤,手动选择是替换还是不替换,最后替换了所有 C++ 中 Lua 端的 GLNode 类名实现(替换的内容很少,比我想象中要少)。
到这里就已经完成能够让两种脚本引擎同时运行的方法。并且一叶修改了入口函数,在 AppDelegate 入口处,添加修改:
可以看到这里有三个方法,runCpp 、runLua 和 runJsb,这三者可单独独立运行,也可同时运行,在脚本实现不同的 log 打印,便能知晓,但如如果你在这三处同时运行了一个场景的话,那么根据场景的运行规则,后面运行的场景会入栈,替换之前场景的运行,三者的运行顺序,可以随意修改,并观其运行结果。为了测试这一点,我们同时运行 runLua、runCpp 和 runJsb ,然后使用一个全局定时器(请看 实现 『Cocos2d-x 全局定时器』 一文),每三秒钟弹出一个场景。而每一个场景的内部不同,这样我们便能看见首先运行了 runJsb 场景,三秒后 runLua ,之后 runCpp 场景,最终三秒后,所有场景弹出,Game Over 了。
脚本绑定技术的特性
以上通过对源代码的修改,一个例子,让 lua 和 js 两种脚本引擎同时运行,由于在任何时候只能有一个场景运行,所以,无论由 C++、lua 还是 js 来启动游戏,另外两种语言将会得不倒执行的权利,但是从另一个侧面,全局定时器它得的运行并不依赖场景的运行,这对我们研究程序执行过程中对象的特性提供了方便,前文通过一个 C++ 端实现的全局定时器,不论你运行的是不是脚本,是什么脚本,都不影响它之运行,那么我们就能能用这个定时器去定期的调用各种脚本,以让这三种脚本语言同时运行,在这样一个过程中,去验证对它们的内存分布,操作机制的的情况等 ~
一叶在 C++ 端开启了一个定时器,且由 C++ 运行了第一个场景,然后修改全局定时器的定时调用实现,添加对 lua 和 js 的脚本调用:
这是全局定时器的实现,它在当前运行的场景中添加了一个 Label ,label 的内容 “一叶 v 5~“,并且通过一个执行技术 count 来决定当前执行的是 C++ 还是 lua 或者 js,在不同的语言中,修改同一个元素的大小 Scale,看看 js 和 lua 的 method 文件实现:
以上的代码都很简单,各种语言的逻辑一样,首先或者当前运行着的场景,然后通过 tag 获取场景中的元素 Label,再之修改它的大小,这样将程序运行,便能看见此 Label 定时改变大小,而且是三种大小状态不停的切换,可见已经完成了我们之前的目标,双开脚本引擎,用不语言控制同一个元素(文章最后给出所有代码)。
绑定技术的特性浅析
说道脚本绑定技术,这里可以插入一个新的内容来说,Cocos2d-html5 版本!作为对比,更为明了,h5 的实现和 jsb 的实现显然不同,h5 是跑在浏览器上的,jsb 是跑在移动终端上的。但是它们都用统一的接口实现,即用 js 写的游戏(同一套代码)即能够跑在浏览器上,又能够能跑在手机的 js 引擎上,这两者之间表面相同,本质不同 ,也是隐藏了内部实现,提供统一的接口让写程序更为简单。有兴趣的朋友可以去了解一下。在 Emacs 内置的 lisp 语言函数中有很多性能要求比较高的是使用 c 语言实现的,但在调用的时候全然不知(不知道就对了)。
而 jsb 和 lua 之间:知其不同,是见其表,知其皆同,是知其本,舍不同而观其同,可游心于物之初,哈。 在脚本引擎库中,以 C++ 实现的类型为基本,通过动态往虚拟机(引擎环境,或者上下文对象)里添加类型定义并绑定,每钟脚本类型都有其对应的 C++ 类型作为依据,通过脚本创建的对象最终被映射为调用 C++ 创建对象,而在 C++ 中创建的对象,也可以在脚本中随时获取,并修改其属性,当然其内部还有复杂的内存管理解决方案 (特别是本文中这样混合形的运行时环境,其对象生命周期就更复杂了,关系到引擎内部实现的细节,由不同语言创建的对象,由谁管理,由谁释放等等),在需要之时可以深究,而这里显然没有必要(这里引擎双开仅作学习之用),宏观角度考量,内存管理无非是定义一套规则,或是规范,这样能保证出错误的最小可能性(在 『Cocos2d-x 内存管理浅说』 , 『Cocos2d-x 内存管理的一种实现』, 『深入理解 Cocos2d-x 内存管理』) 几篇文章有怎样通过编程规范来尽量避免内存出现的问题)。
如果我们需要添加一种新脚本绑定实现,比如使用 lisp 语言作为绑定 (Emacs 用户首先想到的就是 lisp 了),那么我们需要一个 lisp 运行时环境的实现,然后通过函数绑定,javascript 和 lua 的第一类型是 函数(First-class Function),它们都有很强的函数式语言特性,其封装的 Cocos2d-x 调用方式不过是语法糖衣,看起来像面相对象而已,此点 js 表现更甚。所以对于 lisp 实现来说,是可行的,至于最后写起来是否顺手不得而知,目测如果实现,写法更像 lua 对 Cocos2d-x 的 style,可能很好使,可能很糟糕 :P 也许使用对象形的脚本语言更加合宜。
非吾小天下 宏观而已
想要玩转这里脚本引擎,那么你至少会绑定,知道怎么绑定,其具体步骤 ?jsb 怎么手动绑定,lua 怎么手动绑定,而且还有自动绑定脚本,jsb 的内部使用了 spidermonkey 开源的 js 引擎,lua 还可以开 jit,其运行环境有很多复杂的上下文参数,各种错综复杂,提出各种专有的概念,有着各自不同的游戏规则,其内部的细枝末叶是对具体问题的解决方案,然而有的时候我们并不知道需要解决的问题由来,一环套着一环,我在这里却避而不谈,一方面是因为我也不知 :p ,另一方面是因为不想让我或者别人陷入这样那样的泥沼中去,从宏观的角度去看问题,或者抽象,在 lua 和 js 这两种脚本引擎中,其特点为何,能达到什么样的效果,在本文的操作过程,并没有什么复杂的步骤,修改了一处编译报错问题,引擎双开,操作同一对象。避开了很多各脚本的内部实现细节内容。把复杂的问题简单化 ~
关于本文的内容,一叶会将代码项目提交到 github 以供参考(在文章最后给出1),内容不多,组织凌乱(时不时想修改,临时添加以看不同的测试效果),所以就将就着看了,哈,我的博文从不倾向给出一个完整的解决方案,以思路为重,其过程比结果更为重要。