前言
适用人群
本系列将是作者学习cocos2d-x for js 的一些心得体会,想了解这方面可以阅读本教程。
学习前提
在学习本教程之前,你需要对 cocos2d-x c++ js 相关的知识有一定了解。
致谢:http://blog.51cto.com/zt/560
更新日期 | 更新内容 |
2015-07-27 | Cocos2D-x从C++到JS的进阶之路 |
1
迎接脚本时代的到来
首先,我们下载cocos2d-x的最新版本
http://cocos2d-x.googlecode.com/files/cocos2d-2.1beta3-x-2.1.0.zip
我们暂时不使用其他 IDE 了,对于 cocos2d-x 的开发者来说,手头最现成的工具就是vs。用 vs2010 打开工程,蛋疼的发现,里面既没有 js 代码,也没有 c++ 代码。只有一个 spidermonkey 的 js 解释器,还有一个 win32 版 cocos2d-x 的壳。
好吧,首先我们需要导入 js 的源代码,很不幸,这个项目的js源码也没有放在 MoonWarriors 的目录下,而是在 cocos2d-2.1beta3-x-2.1.0\samples\TestJavascript\cocos2d-js-tests\games\MoonWarriors 这个文件夹中 我们把这里面的代码导入到 MoonWarriors 项目中。
好吧,这个目录结构比较蛋疼。但是勉强能看了。 但是又遇到了另外一个问题,我们在 js 源代码中,没法.出来代码,自动完成功能几乎是无效的。番茄只能提示出当前 js 文件中的代码,在其他文件中的代码,没法提示出来。尤其是引擎中的 js 接口。这个实在太要命了。 我查了半天,发现 cocos2d-2.1beta3-x-2.1.0 引擎中,根本没有 js 版本的代码。也就是说,他们只是用 spidermonke y把 js 的接口绑定成了 c++ 的。这可如何是好呢?没代码就没法使用自动完成。
呵呵,之前我们说过,这次是一次联合发布,也就是说各个版本的 js 接口应该是一致的,也就是说我们可以使用 html5 版本的js代码,来协助完成代码提示。 去下一 个html5 引擎 http://cocos2d-x.googlecode.com/files/Cocos2d-html5-v2.1.zip
打开后发现,我们要的代码在这四个文件夹里
把他们也导入到项目中
然后,我们随便打开一个项目源代码,就可以.出来了。自动完成可以使用,哦也。
打完收工。
2
解决在 vs 中修改 js 源文件无效
之前我们讲到了,如何去把 cocos2d-x 引擎自带的 MoonWarriors 例子工程导入源码,然后可以方便学习和编辑。 但是我事后发现,如果修改了 js 代码后,点击调试,运行时仍然是之前的结果,毫无变化。这是怎么回事呢? 仔细观察后,我发现,对于 cocos2d-x for js 来说,js 脚本只是一个资源文件,他在生成工程时,会执行一个批处理,这个批处理的作用就是拷贝资源文件到对应的目录下。
可以看到,只有在生成项目时才会执行这个批处理,也就是说,在项目代码被修改之后。但正如你之前看到的,这是一个 c++ 项目,js 脚本只是作为资源。 那么真的没有办法了吗?其实很简单,vs2010 有一个生成工具选择的功能,对于不同的文件类型使用不同的工具去处理。 我们首先右键选择所有项目中的 js 源码文件,点击属性,可以看到如下
项类型为“不参与生成”,这个就是问题的所在,因为不参与,所以 vs 不会监视这个文件的变化。我们把它改成“自定义生成工具”
然后,我们随便改一下 js 的源码,然后直接点绿三角运行项目。可以看到,项目重新生成,并且 vs 自动复制 js 文件了
打完收工
3
hybrid 开发模式
因为苹果是不允许 app 下载可执行代码的,所以用动态链接库构建插件式引擎并通过网络下载在 iOS 上是无法实现的。但有一种方式是,在引擎内部集成一个脚本解释器,然后把脚本作为资源来下载(脚本是加密的),如此规避苹果的审核条款。这个方式就叫 Hybrid。但这么做没法做到不露痕迹,深层原因应该是,Hybrid 牵扯利益太大,苹果也算睁一只眼闭一只眼。 在 cocos2d 引擎的众多分支中,cocos2d-x 的开发是以 C++ 为核心的。而 cocos2d-x 引擎,就是通过 hybrid 方式来执行 js 代码的。为了执行 js 代码,引擎本身需要一个脚本解释器,引擎集成的脚本解释器就叫:spidermonkey。 spidermonkey 是一个历史悠久的基于 c/c++ 编写的 js 脚本解释器,由 Mozilla 提供,非常有名的 firefox 和 thunderbird 都在用。 cocos2d-x 集成 spidermonkey 的开源协议是 MPL2.0,没有什么限制,你可以放心使用它。 在 AppDelegate::applicationDidFinishLaunching()
函数中,我们可以找到启动脚本引擎的代码:
ScriptingCore* sc = ScriptingCore::getInstance();
sc->addRegisterCallback(register_all_cocos2dx);
sc->addRegisterCallback(register_cocos2dx_js_extensions);
sc->addRegisterCallback(register_CCBuilderReader);
sc->addRegisterCallback(jsb_register_chipmunk);
sc->start();
CCScriptEngineProtocol *pEngine = ScriptingCore::getInstance();
CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);
ScriptingCore::getInstance()->runScript("MoonWarriors-jsb.js");
ScriptCore 是脚本的核心,他就是我们说的那个 JS 解释器。cocos2d-x 把 spidermonkey 的解释器封装了一下,以提供对 cocos2d-x 引擎的相关支持,并简化相应的调用接口。
addRegisterCallback 接口用于添加注册函数,注册函数用于在引擎执行时,绑定相应的代码(从JS往C++的映射代码)。每一个注册函数,对应一个库。现在 cocos2d-x 提供了四个库支持,分别是 cocos2d-x 核心库,cocos2d-x 扩展库,cocosbuilder 支持库,clipmunk 物理引擎库。将来你可以在这里添加注册自己实现的 JS 绑定库,来直接扩展这个 JS 引擎。
start启动脚本引擎
CCScriptEngineManager::sharedManager()->setScriptEngine
这句是将脚本引擎绑定到引擎管理器上,引擎管理器提供对脚本引擎的一个全局访问点,并且也负责对脚本引擎的卸载。
ScriptingCore::getInstance()->runScript("MoonWarriors-jsb.js");
4
cocos2d-x for js 中的继承
对于面向对象语言来说,继承机制是代码复用的基础,很不幸的是 javascript 作为一个基于原型继承的语言,并没有在本身语言层面上直接作出对类继承的支持。 但是 js 语言拥有很强大的表现力。所以一般是 js 的使用者自行设计一套继承机制,这个机制必须包括几个点,对私有访问权限的模拟,对属性和类属性的不同实现,对方法覆盖的支持,对父类被覆盖方法的访问等。 cocos2d-x 中,整合了两套继承机制,看《MoonWarriors》例子中的源码SysMenu.js文件
var SysMenu = cc.Layer.extend({
_ship:null,
ctor:function () {
cc.associateWithNative( this, cc.Layer );
},
init:function () {
var bRet = false;
if (this._super()) {
winSize = cc.Director.getInstance().getWinSize();
var sp = cc.Sprite.create(s_loading);
sp.setAnchorPoint(cc.p(0,0));
this.addChild(sp, 0, 1);
var logo = cc.Sprite.create(s_logo);
logo.setAnchorPoint(cc.p(0, 0));
logo.setPosition(0, 250);
this.addChild(logo, 10, 1);
var newGameNormal = cc.Sprite.create(s_menu, cc.rect(0, 0, 126, 33));
var newGameSelected = cc.Sprite.create(s_menu, cc.rect(0, 33, 126, 33));
var newGameDisabled = cc.Sprite.create(s_menu, cc.rect(0, 33 * 2, 126, 33));
var gameSettingsNormal = cc.Sprite.create(s_menu, cc.rect(126, 0, 126, 33));
var gameSettingsSelected = cc.Sprite.create(s_menu, cc.rect(126, 33, 126, 33));
var gameSettingsDisabled = cc.Sprite.create(s_menu, cc.rect(126, 33 * 2, 126, 33));
var aboutNormal = cc.Sprite.create(s_menu, cc.rect(252, 0, 126, 33));
var aboutSelected = cc.Sprite.create(s_menu, cc.rect(252, 33, 126, 33));
var aboutDisabled = cc.Sprite.create(s_menu, cc.rect(252, 33 * 2, 126, 33));
var newGame = cc.MenuItemSprite.create(newGameNormal, newGameSelected, newGameDisabled, function () {
this.onButtonEffect();
flareEffect(this, this, this.onNewGame);
}.bind(this));
var gameSettings = cc.MenuItemSprite.create(gameSettingsNormal, gameSettingsSelected, gameSettingsDisabled, this.onSettings, this);
var about = cc.MenuItemSprite.create(aboutNormal, aboutSelected, aboutDisabled, this.onAbout, this);
var menu = cc.Menu.create(newGame, gameSettings, about);
menu.alignItemsVerticallyWithPadding(10);
this.addChild(menu, 1, 2);
menu.setPosition(winSize.width / 2, winSize.height / 2 - 80);
this.schedule(this.update, 0.1);
var tmp = cc.TextureCache.getInstance().addImage(s_ship01);
this._ship = cc.Sprite.createWithTexture(tmp,cc.rect(0, 45, 60, 38));
this.addChild(this._ship, 0, 4);
var pos = cc.p(Math.random() * winSize.width, 0);
this._ship.setPosition( pos );
this._ship.runAction(cc.MoveBy.create(2, cc.p(Math.random() * winSize.width, pos.y + winSize.height + 100)));
if (MW.SOUND) {
cc.AudioEngine.getInstance().setMusicVolume(0.7);
cc.AudioEngine.getInstance().playMusic(s_mainMainMusic, true);
}
bRet = true;
}
return bRet;
},
onNewGame:function (pSender) {
var scene = cc.Scene.create();
scene.addChild(GameLayer.create());
scene.addChild(GameControlMenu.create());
cc.Director.getInstance().replaceScene(cc.TransitionFade.create(1.2, scene));
},
onSettings:function (pSender) {
this.onButtonEffect();
var scene = cc.Scene.create();
scene.addChild(SettingsLayer.create());
cc.Director.getInstance().replaceScene(cc.TransitionFade.create(1.2, scene));
},
onAbout:function (pSender) {
this.onButtonEffect();
var scene = cc.Scene.create();
scene.addChild(AboutLayer.create());
cc.Director.getInstance().replaceScene(cc.TransitionFade.create(1.2, scene));
},
update:function () {
if (this._ship.getPosition().y > 480) {
var pos = cc.p(Math.random() * winSize.width, 10);
this._ship.setPosition( pos );
this._ship.runAction( cc.MoveBy.create(
parseInt(5 * Math.random(), 10),
cc.p(Math.random() * winSize.width, pos.y + 480)));
}
},
onButtonEffect:function(){
if (MW.SOUND) {
var s = cc.AudioEngine.getInstance().playEffect(s_buttonEffect);
}
}
});
SysMenu.create = function () {
var sg = new SysMenu();
if (sg && sg.init()) {
return sg;
}
return null;
};
SysMenu.scene = function () {
var scene = cc.Scene.create();
var layer = SysMenu.create();
scene.addChild(layer);
return scene;
};
这个 extend 继承写法由 John Resig 创造,John Resig 是 JS 领域的大神,而且网上有很多粉丝给他编的段子,非常有趣。 例子中使用父类 cc.Layer.extend 方法来启动继承,传入一个对象字面量{},这个字面量可以包含对象属性和对象方法,最终由 extend 来完成接口绑定,返回一个构造函数赋值给 SysMenu。 对于类方法(也就是通常意义上的静态方法),使用的是 js 最传统的方式,直接给构造函数指定属性即可。 这种编写代码的方式非常简单,而且也很优美。更重要的是,这种写法,非常符合 C++ 或 java 程序员的排版审美。 关于继承的理解。js 里面的原型继承和基于类的继承方式截然不同,内部是在维护一个原型链,链上的节点与节点之间是链接关系(注意:不是赋值,也不是拷贝)。可以先看一下《权威指南》那本书是怎么讲的,不过很遗憾,那本书关于原型继承的图解画的不太好……千万不要搞代数式的替换和死记硬背,那样你很难掌握原型链的本质。 另外,强烈推荐三生石上的系列文章《JavaScript 继承详解》
有时间的话,我会把三生石上的文章配一些详细的原型链描述图,这样就可以很容易的掌握 js 的原型链了。
5
John Resiq 的继承写法解析
今天,我们来看看 John Resiq 的继承写法 Simple JavaScript Inheritance。之前已经有很多同行分析过了。这个写法 在cocos2d-x for js 中也被使用,并作了少许改动。我尝试着做一些展开描述。 先贴源码:
cc.Class = function(){};
cc.Class.extend = function (prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function (name, fn) {
return function () {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if (!initializing && this.ctor)
this.ctor.apply(this, arguments);
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.prototype.constructor = Class;
// And make this class extendable
Class.extend = arguments.callee;
return Class;
};
cc.Class = function(){};
做了一个全局构造函数 Class,这个不需要什么解释。
cc.Class.extend = function (prop) {
prop 是一个对象字面量,这个对象包含了子类所需要的全部成员变量和成员方法。 extend 函数就在内部遍历这个字面量的属性,然后将这些属性绑定到一个“新的构造函数”(也就是子类的构造函数)的原型上。
var _super = this.prototype;
注意,js 里面的这个 this 的类型是在调用时指定的,那么这个 this 实际上是父类构造函数对象。比如你写了一个 MyNode 继承自 cc.Node。相应代码是:
var MyNode = cc.Node.extend({
var _super = this.prototype;
...
});
那么这个 this 就是父类 cc.Node。
initializing = true;
var prototype = new this();
initializing = false;
生成父类的对象,用于给子类绑定原型链。但要注意,因为这个时候,什么实参都没有,并不应该给父类对象中的属性进行初始化(构造器参数神马的木有怎么初始化啊喵,这玩意实际是 JS 语言设计上的失误造成的)。所以用一个变量做标记,防止在这个时候进行初始化。相关代码在后面就会看到。
fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
这玩意看起来很乱,这是一个正则对象,右边是一个?表达式,中间添加了一些正则代码。这个对象的作用是,检测子类函数中是否有调用父类的同名方法“_super()”(这种调用父类方式是由 John Resiq 约定的)。但这种检测需要 JS 解释器支持把一个函数转换为字符串的功能,有些解释器是不支持的。所以我们先做一个检测,自己造了一个函数,里面有 xyz,然后用正则的 test 函数在里面搜索 xyz。如果返回 true,表示支持函数转字符串,那么就直接返回/\b_super\b/否则返回/.*/
for (var name in prop) {
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function (name, fn) {
return function () {
var tmp = this._super;
this._super = _super[name];
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
现在重头戏来了,在这个地方我们要把传进来的那个字面量 prop 的属性全都绑定到原型上。这地方又他喵的是一个?表达式,JR 实在太喜欢用这玩意了。首先,forin 把属性拿出来。然后,因为我们添加的功能是“实现像 c++ 那样通过子类来调用父类的同名函数”,那么需要检测父类和子类中是否都有这两个同名函数。用的是这段代码:
typeof prop[name] == "function" && typeof _super[name] == "function"
然后,我们还要检测,子类函数中是否真的使用了_super 去调用了同名的父类函数。这个时候,之前的正则对象 fnTest 出场。继续之前的话题,如果解释器支持函数转字符串,那么 fnTest.test(prop[name]) 可以正常检测,逻辑正常进行;如果不支持,那么 fnTest.test(prop[name]) 始终返回 true。
这玩意什么用处,这是一个优化,如果子类函数真的调父类函数了,就做一个特殊的绑定操作(这个操作我们后面马上讲),如果子类函数没有调父类函数,那么就正常绑定。如果没法判断是否调用了(解释器不支持函数转字符串),直接按照调用了那种情况来处理。虽然损失一些性能,但是可以保证不出问题。
(function (name, fn) {
return function () {
var tmp = this._super;
this._super = _super[name];
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name])
继续,上面的就是我们说的那个特殊的绑定操作。在这里,我们做了一个闭包,这里面的 this 是子类对象,跟之前的那个 this 不一样哦。我们利用闭包的特性,保存了一个 _super,这个 _super 被绑定了父类的同名函数_super[name]。然后我们使用 fn.apply(this, arguments)调用子类函数,并保存返回值。因为这是一个闭包,所以根据语法,我们可以在 fn的实现中调用 _super 函数。
function Class() {
if (!initializing && this.ctor)
this.ctor.apply(this, arguments);
}
Class.prototype = prototype;
Class.prototype.constructor = Class;
Class.extend = arguments.callee;
生成一个 Class 构造函数,这个构造函数作为这个大匿名函数的返回值使用。然后里面就是之前说的,初始化保护,防止在绑定原型链的时候初始化。注意后面那个玩意 ctor,在 cocos2d-x for js 中,真正的初始化是二段构造的那个 init,而不是 ctor。在 cocos2d-x for js 的实现中 ctor 里面会调用一个函数 cc.associateWithNative(this, 父类),这个函数负责后台生成一个 c++ 对象,然后把 c++ 对象和 js 对象绑定到一起。
剩下的是例行公事:绑定子类的原型,修正子类的构造器指向它自己,给子类添加一个同样的 extend 方法。 最后把这个完成的构造函数返回出来。
6
Google 的继承写法解析
cocos2d-x for js 中集成了两套继承写法,一套是 JR 的,一套是 google。公司同事使用过 node.js,对 google 的继承方式比较赞同。我就看了一下 Google 的继承代码。 先贴代码:
// 1) Google "subclasses" borrowed from closure library
// This is the recommended way to do it
//
cc.inherits = function (childCtor, parentCtor) {
/** @constructor */
function tempCtor() {};
tempCtor.prototype = parentCtor.prototype;
childCtor.superClass_ = parentCtor.prototype;
childCtor.prototype = new tempCtor();
childCtor.prototype.constructor = childCtor;
// Copy "static" method, but doesn't generate subclasses.
// for( var i in parentCtor ) {
// childCtor[ i ] = parentCtor[ i ];
// }
cc.inherits 是继承函数,负责链接父类和子类的原型链。非常有趣的是,在这里使用了一个临时构造器,这样就替换了 JR 代码中的 initializing 写法。看起来很舒服。
cc.base = function(me, opt_methodName, var_args) {
var caller = arguments.callee.caller;
if (caller.superClass_) {
// This is a constructor. Call the superclass constructor.
ret = caller.superClass_.constructor.apply( me, Array.prototype.slice.call(arguments, 1));
return ret;
}
var args = Array.prototype.slice.call(arguments, 2);
var foundCaller = false;
for (var ctor = me.constructor;
ctor; ctor = ctor.superClass_ && ctor.superClass_.constructor) {
if (ctor.prototype[opt_methodName] === caller) {
foundCaller = true;
} else if (foundCaller) {
return ctor.prototype[opt_methodName].apply(me, args);
}
}
// If we did not find the caller in the prototype chain,
// then one of two things happened:
// 1) The caller is an instance method.
// 2) This method was not called by the right caller.
if (me[opt_methodName] === caller) {
return me.constructor.prototype[opt_methodName].apply(me, args);
} else {
throw Error(
'cc.base called from a method of one name ' +
'to a method of a different name');
}
};
cc.base 是在子类函数中调用父类同名函数的方法。要使用这个函数,必须是使用过 cc.inherits 进行过链接原型链的类才行。参数方面,me 需要传入 this,其他根据形参表来定。
var caller = arguments.callee.caller;
首先通过,上面的代码获得外层函数的对象。(据说 caller 这个属性已经不再建议使用了,不知道是什么原因)。 然后,如果外层函数是构造函数的话,一定是存在 superClass_ 这个属性的。那么可以用 apply 调用父类的构造器,然后就退出函数执行就可以了。(但是这里为什么会有返回值呢,他喵的构造器返回值不是被运行环境给接管了么?)
var args = Array.prototype.slice.call(arguments, 2);
var foundCaller = false;
for (var ctor = me.constructor;
ctor; ctor = ctor.superClass_ && ctor.superClass_.constructor) {
if (ctor.prototype[opt_methodName] === caller) {
foundCaller = true;
} else if (foundCaller) {
return ctor.prototype[opt_methodName].apply(me, args);
}
}
如果外层函数不是构造函数,那么就是子类的普通函数。后面的代码也很简单,从子类向上往父类上面找,一层一层的遍历构造器,然后再核对同名函数,如果在当前层次找到了对应的函数名,就在下一轮循环中,调用父类的同名函数即可。然后直接返回。
if (me[opt_methodName] === caller) {
return me.constructor.prototype[opt_methodName].apply(me, args);
} else {
throw Error(
'cc.base called from a method of one name ' +
'to a method of a different name');
}
果要调用的那个函数,既不是构造函数,也不是父类中的同名函数。那么只有一种可能,就是这个函数是子类的一个实例上的函数。直接 apply 调用就好了。
再找不到的话,代码就会抽风了。(throw Error)
综上,google 的代码风格非常流畅,可读性也很高。如果 JR 是很黄很暴力,各种奇技淫巧不计其数。那么 google 的代码就是和风细雨,润物细无声。
就我个人而已,非常喜欢 JR 的接口,但是又喜欢 google 的内部实现。矛盾啊,喵了个咪。
另外,google 的代码可以做到很容易的和其他继承机制兼容,但 JR 的就不行,必须已自己为核心来搞才可以的。这些是由他们的实现机制决定的。
目前来说,cocos2d-x for js 使用 JR 的写法,不知道会不会对将来的扩展造成一些问题呢。
7
cxx-generator JS 绑定工具
cxx-generator 是由 Zynga 工程师贡献的 C++ 代码绑定到 js 工具。用于将 cocos2d-x 的 c++ 代码,生成相应的 js 绑定代码(由 c++ 写成),然后将这些函数注册到 spidermonkey 的解释器中。通过将 js 代码映射成 c++ 代码,就可以使用相应的 js 接口了。 所需要的环境 mac os x 系统
python2.7
py-yaml
cheetah (for target language templates)
libclang, from clang 3.1
前三个可以通过 macports 自动安装
macports 下载地址
http://www.macports.org/install.php
注意选择适合你的系统版本,另外该页也注明了安装中常见的系统问题,一共四条。
在安装 macports 时,有可能会卡在最后一分钟,那么需要重启后断网安装即可。
在终端上运行此命令,安装前三个软件
sudo port install python27 py27-yaml py27-cheetah
安装对网络有一定要求,部分地区可能要自备梯子
下载clang
http://llvm.org/releases/3.1/clang+llvm-3.1-x86_64-apple-darwin11.tar.gz
下载NDK
绑定例子中,用到了部分 c++ 标准库接口,所以需要提供相应代码实现,工具中,采用 ndk 实现。不太明白为什么没有直接用 xcode 中的标准库。
http://dl.google.com/android/ndk/android-ndk-r8d-darwin-x86.tar.bz2
第二步,生成绑定代码
复制 userconf.ini.sample 和 user.cfg.sample 并去掉 sample 后缀
添加自己的路径,我的是多系统所以路径有点特别
``` //user.cfg
PYTHON_BIN=/opt/local/bin/python2.7
//userconf.ini
[DEFAULT]
androidndkdir=/Volumes/data/Mac_OS_X/android-ndk-r8b clangllvmdir=/Volumes/data/Mac_OS_X/clang+llvm-3.1-x86_64-apple-darwin11 cxxgeneratordir=/Volumes/data/Workspace/cocos2d-2.1beta3-x-2.1.0/tools/cxx-generator
最后,由终端运行
sudo ./test.sh
生成 simple_test_bindings 文件夹,下面就是绑定好的 c++ 代码了。
## 第三步,集成测试
懒省事直接拿 TestJavaScript 例子开刀,倒入两个文件夹 simple_test 和 simple_test_bindings
在 AppDelegate.cpp 中,倒入头文件
#include "autogentestbindings.hpp"
并注册
sc->addRegisterCallback(register_all_autogentestbindings);
在 tests-boot-jsb.js 中,添加测试代码
var myClass=new ts.SimpleNativeClass(); var myStr=myClass.returnsACString(); cc.log(myStr);
控制台输出
this is a c-string ```
参考文献
https://github.com/funkaster/cxx-generator
http://www.macports.org/install.php
8
JS 脚本语言的优势与一些问题
- 不需声明,甚至匿名方式原地定义。编码量少。 这一条在 C++ 中尤其明显,以绑定一个回调为例,需要声明,定义,调用绑定,三处代码。虽然 C++11 中支持 lambda 表达式,对于回调的写法有很大改进。但是其他地方依然蛋疼。
- 弱类型语言,一般情况下,不需关心实际类型。Debug 时除外。 在使用 C++ 这种强类型语言的开发中,尤其是写功能代码时,类型检查远不如想象中那么有用,很多时候反而是问题根源,编译不通过时,很大一部分时间是在对变量类型,由此还衍生出一些特殊技术手段,比如适配器模式等等。
使用 JS 这种弱类型语言,只要接口名称能对上,那么在对象的函数被调用时就认为是正确的。简单说,只要长得像某一类型就行了,不需要必须是某一类型。
C++11 中 auto 关键字也可以提升编码速度(和 JS 的 var 很类似,可以随时无脑输出),不过看了一下引擎附带的几个例子代码,好像有滥用 auto 的趋势。 - 脚本语言动态扩展能力强,可以不必构造很多临时类型和消息类型。 比如,在大型,全局使用消息机制时,C++ 可能用结构体,自定义类,或者我们以前直接丢J SON 对象过去。在 JS 里面就很简单了,直接扔 JSON 对象吧。 在运行时可以动态给一个对象添加函数和属性,而不需要重新构造新类和初始化。JSON 源自 JS,JSON 是天然的消息对象,非常合适。当然 JSON 有自身的缺点,访问父节点和兄弟节点不太方便。并且 JSON 的结构和二维表没法完全兼容,这是一直让策划和工具程序员头痛的一个问题。
- 语法灵活,可以支持各种编码方式。随机应变。 业界普遍认为面向对象在图像编程是最好的。但对于事件处理逻辑处理AI处理来说,面向对象则是罗嗦的要死。比如,我实在对观察者模式提不起兴趣,Qt 中的信号槽机制优雅的多。又比如我曾经做了一个 A* 算法代码,想改成好用的面向对象方式,发现很痛苦。
JS 很灵活,适合什么样的编码方式,就用什么样的方式。 - 在语言级别天生集成了两种最有用的数据结构,向量和映射表。 记得在 KJava 时代,MIDP 的里面只有很少的数据结构,里面就有向量和哈西表。这两种是最为常用的。JS 在语言层面提供了支持,编码极其方便。
- 脚本语言无需编译,大量节约了开发时间。 如果你在 Mac 上,并且开了虚拟机然后编译 VS 的话,应该有那个恐怖的按小时计算的编译时间长度经验。Clang 虽然速度比 VC 快很多,但是每次如果 clean 一下然后编译几十上百个文件也需要若干分钟。
一些问题:
- 太灵活,更容易出烂代码。
- 调试问题与 IDE 问题。
目前在 cocos2d-x 领域,还缺乏好用的支持 JS 的 IDE 。现在目前暂时还是用 cocos2d-html5 版本做调试(两者的接口已经高度一致化),未来会有基于 c++ 的 IDE 做的 JS 调试插件(比如在 Eclipse 上面的)。 - 善变的 this
this 关键字绝对是 JS 里面的变形金刚。根据不同的上下文,经常会变成其他东西。 这个经常会和回调函数问题纠缠不清,如果再加上闭包,够你喝一壶的。 - 闭包
闭包很强大,无限制传参,抓取快照。 但是闭包本身的问题也不小,首先是阅读和理解上的困难,面向对象的程序员一上来很难理解这东西,从他们的角度看闭包的代码也很丑。 还有就是效率问题,同事测了一下 SpiderMonkey 中的闭包在生成大对象时效率不太高。 目前在 cocos2d-x 前端开发中,为了防止出现问题,对于缺乏经验的程序员,尽量不要使用闭包代码。 我个人在回合制战报,生成动画里是用了一些闭包的,不过那是一次性代码。 - 变量生命周期不明确
变量生命周期问题,因为不需要声明,很多时候也没有特别明显的初始化,并不能通过阅读代码明确知道,一个变量的生存周期,这是所有脚本语言和 GC 语言的特性,有些时候对调试会形成麻烦。 - 原型继承
难以理解的原型继承。熟悉面向对象的人一般都对这个东西莫名其妙。
从静态语言过度到动态脚本语言,一般程序员会疑惑在几个地方,this,闭包,原型继承,以及如何灵活地使用脚本语言的动态性进行编码,我观察了一下,很多人写 JS 像静态语言,还是 c++ 风格或者 Java 风格。
9
JS 与 C++ 的交互 1——JS 代码调用 C++ 代码
之前我们讲过,我们要通过 SpiderMonkey 引擎的注册接口,向 SpiderMonkey 注册相应的从 C++ 到 JS 的绑定函数,这些函数用于把 JS 函数调用代码转换成对应 C++ 函数调用来执行。
//在AppDelegate::applicationDidFinishLaunching函数中
ScriptingCore* sc = ScriptingCore::getInstance();
sc->addRegisterCallback(register_all_cocos2dx);
sc->addRegisterCallback(register_all_cocos2dx_extension);
sc->addRegisterCallback(register_cocos2dx_js_extensions);
sc->addRegisterCallback(register_all_cocos2dx_extension_manual);
sc->addRegisterCallback(jsb_register_chipmunk);
sc->addRegisterCallback(JSB_register_opengl);
sc->addRegisterCallback(jsb_register_system);
sc->addRegisterCallback(MinXmlHttpRequest::_js_register);
sc->addRegisterCallback(register_jsb_websocket);
sc->addRegisterCallback(register_all_cocos2dx_builder);
sc->addRegisterCallback(register_CCBuilderReader);
sc->addRegisterCallback(register_all_cocos2dx_gui);
sc->addRegisterCallback(register_all_cocos2dx_gui_manual);
sc->addRegisterCallback(register_all_cocos2dx_studio);
sc->addRegisterCallback(register_all_cocos2dx_studio_manual);
sc->addRegisterCallback(register_all_cocos2dx_spine);
可以看到上面导入了 Cocos2d-x 的各种库,核心库,扩展,opengl,物理引擎,websocket,CCB 等等等等。
下面我们说 JS 代码如何调用 C++ 代码。 首先,在创建 JS 对象的时候,也会创建一个对应的 C++ 对象。换句话说,JS 对象是和 C++ 对象一一对应的(当然必须是引擎支持的,而且绑定了接口的)。然后,在 JS 对象执行函数时,发生了什么呢? SpiderMonkey 引擎会通过注册的接口,找到对应的 C++ 对象,调用该对象上对应的 C++ 函数。
换句话说,如果有下面的 JS 代码:
var node = cc.Node.create();
node.setVisible(false);
那么经过 SpiderMonkey 执行后,会调用下面的代码:
auto node = CCNode::create();
node->setVisible(false);
当然,SpiderMonkey 远远还不止干了这些,还做了很多事,比如绑定和查找 JS 和 C++ 对象的对应关系,包装参数为对应类型,类型安全检查,返回值包装等等。要知道他干了些什么,直接看引擎代码是更好的选择。
在 Cocos2d-x 3.0 版的引擎中,引擎目录结构进行了大规模重构。
两个脚本语言被放到一个类似的目录中。其中 auto-generated/js-bindings 文件夹是 gxx-generator 工具自动生成的所有 C++ 绑定 JS 代码。而 javascript/bingdings 文件夹是手写的绑定代码,因为工具无法做到完全自动绑定,所以必须有一部分手写的(脚本语言都是这样,习惯就好了,谢谢)。
好,我们继续找刚才说的源代码。打开 jsb_cocos2dx_auto.cpp
JSBool js_cocos2dx_Node_create(JSContext *cx, uint32_t argc, jsval *vp)
{
if (argc == 0) {
cocos2d::Node* ret = cocos2d::Node::create();
jsval jsret = JSVAL_NULL;
do {
if (ret) {
js_proxy_t *proxy = js_get_or_create_proxy<cocos2d::Node>(cx, (cocos2d::Node*)ret);
jsret = OBJECT_TO_JSVAL(proxy->obj);
} else {
jsret = JSVAL_NULL;
}
} while (0);
JS_SET_RVAL(cx, vp, jsret);
return JS_TRUE;
}
JS_ReportError(cx, "js_cocos2dx_Node_create : wrong number of arguments");
return JS_FALSE;
}
这就是 cc.Node.create() 执行时,底层 C++ 跑的代码。所有的通过 JS 调用 C++ 的代码都与这个形式非常一致,首先看函数接口:
第一个参数 JSContext *cx 是 JS 的上下文
第二个参数 uint32_t argc 是 JS 代码中的参数个数,在这个里argc==0
第三个参数 jsval *v p是 JS 代码中的具体参数
继续分析
cocos2d::Node* ret = cocos2d::Node::create();
这个代码再熟悉不过了,标准的 Cocos2d-x 静态工场生成对象的代码
jsval jsret = JSVAL_NULL;
jsval jsret 是这个函数的返回值,这是表示的是一个 JS 对象
js_proxy_t *proxy = js_get_or_create_proxy<cocos2d::Node>(cx, (cocos2d::Node*)ret);
jsret = OBJECT_TO_JSVAL(proxy->obj);
注意这个模板函数,get_or_create,这就是把 JS 对象和 C++ 对象绑到一起的函数。他非常重要,注意 JS 和 C++ 对象是一一对应关系,理解这个特效,有助于我们利用 JS 语言的动态性进行更方便的编程。绑完之后,下面那个函数是用于获得返回值。
最后,函数都要返回一个 JSBool,表面这个函数执行是否成功。如果返回 JS_FALSE,还会通过 JS_ReportError 打印一条报错信息。注意!脚本语言有一个特点,如果函数运行失败了,则该函数后面的函数(在同一作用域中的)都会跳过执行。
继续看下一个函数
JSBool js_cocos2dx_Node_setVisible(JSContext *cx, uint32_t argc, jsval *vp)
{
jsval *argv = JS_ARGV(cx, vp);
JSBool ok = JS_TRUE;
JSObject *obj = JS_THIS_OBJECT(cx, vp);
js_proxy_t *proxy = jsb_get_js_proxy(obj);
cocos2d::Node* cobj = (cocos2d::Node *)(proxy ? proxy->ptr : NULL);
JSB_PRECONDITION2( cobj, cx, JS_FALSE, "js_cocos2dx_Node_setVisible : Invalid Native Object");
if (argc == 1) {
JSBool arg0;
ok &= JS_ValueToBoolean(cx, argv[0], &arg0);
JSB_PRECONDITION2(ok, cx, JS_FALSE, "js_cocos2dx_Node_setVisible : Error processing arguments");
cobj->setVisible(arg0);
JS_SET_RVAL(cx, vp, JSVAL_VOID);
return JS_TRUE;
}
JS_ReportError(cx, "js_cocos2dx_Node_setVisible : wrong number of arguments: %d, was expecting %d", argc, 1);
return JS_FALSE;
}
这个函数和前一个函数的区别是,这个函数有参数,并且他是一个类成员函数(上一个是类静态函数),所以,这里要有 this 指针。
jsval *argv = JS_ARGV(cx, vp);
JSBool ok = JS_TRUE;
JSObject *obj = JS_THIS_OBJECT(cx, vp);
js_proxy_t *proxy = jsb_get_js_proxy(obj);
cocos2d::Node* cobj = (cocos2d::Node *)(proxy ? proxy->ptr : NULL);
JSB_PRECONDITION2( cobj, cx, JS_FALSE, "js_cocos2dx_Node_setVisible : Invalid Native Object");
这一大段函数都在找那个 this 指针。注意,这里面有一个 Cocos2d-x 引擎经常出现的错误提示 Invalid Native Object。底层 C++ 对象被回收了,所以找不到了。
if (argc == 1) {
JSBool arg0;
ok &= JS_ValueToBoolean(cx, argv[0], &arg0);
JSB_PRECONDITION2(ok, cx, JS_FALSE, "js_cocos2dx_Node_setVisible : Error processing arguments");
cobj->setVisible(arg0);
JS_SET_RVAL(cx, vp, JSVAL_VOID);
return JS_TRUE;
}
CCNode::setVisible(xx) 只有一个参数,所以先判断 JS 的参数个数为1。JS_ValueToBoolean 完成 JS 对象到 C++ 对象的转换,注意!这是基本类型的转换,和查找对应的对象指针不同。你在 gxx-generator 生成的代码中会看到大量的这种转换。每次转换都要进行结果判断,如果失败,就打印错误信息。后面是直接调用对应 C++ 对象的 setVisible,以及设置返回值。
很繁琐不是吗?如果这种代码全部手写是不是会死人呢。肯定的吧。所以这些代码都是用脚本生成器做出来的(绝大部分)。
后面我们会继续讲解各种 JS 的绑定代码。
10
JS 与 C++ 的交互 2——JS 与 C++ 的“函数重载”问题
对于 C++ 来说,存在函数重载,例如:
void CCNode::setScale(float scale)
void CCNode::setScale(float scaleX,float scaleY)
这两个函数的函数名是一样的,但是参数表不同。最终在编译器编译后的函数签名不一样。
但是在 JavaScript 中并没有这种机制。怎么破?存在两种情况:
第一种、JS 需要调用重载的 C++ 函数接口 我们就以上面的函数为例,来看看在 cxx-generator 的自动生成代码中,函数重载是如何处理的。打开 jsb_cocos2dx_auto.cpp,找到如下代码:
JSBool js_cocos2dx_Node_setScale(JSContext *cx, uint32_t argc, jsval *vp)
{
jsval *argv = JS_ARGV(cx, vp);
JSBool ok = JS_TRUE;
JSObject *obj = NULL;
cocos2d::Node* cobj = NULL;
obj = JS_THIS_OBJECT(cx, vp);
js_proxy_t *proxy = jsb_get_js_proxy(obj);
cobj = (cocos2d::Node *)(proxy ? proxy->ptr : NULL);
JSB_PRECONDITION2( cobj, cx, JS_FALSE, "js_cocos2dx_Node_setScale : Invalid Native Object");
do {
if (argc == 2) {
double arg0;
ok &= JS_ValueToNumber(cx, argv[0], &arg0);
if (!ok) { ok = JS_TRUE; break; }
double arg1;
ok &= JS_ValueToNumber(cx, argv[1], &arg1);
if (!ok) { ok = JS_TRUE; break; }
cobj->setScale(arg0, arg1);
JS_SET_RVAL(cx, vp, JSVAL_VOID);
return JS_TRUE;
}
} while(0);
do {
if (argc == 1) {
double arg0;
ok &= JS_ValueToNumber(cx, argv[0], &arg0);
if (!ok) { ok = JS_TRUE; break; }
cobj->setScale(arg0);
JS_SET_RVAL(cx, vp, JSVAL_VOID);
return JS_TRUE;
}
} while(0);
JS_ReportError(cx, "js_cocos2dx_Node_setScale : wrong number of arguments");
return JS_FALSE;
}
只是通过 argc 参数简单判断了一下参数个数,然后就执行对应的分支代码就好了。但是如果遇到参数个数相同,而类型不同的情况呢?尚不得而知。
第二种、不需要调用 C++ 函数接口,直接在 JS 层代码中模拟一下函数重载。这个就要利用 JS 语言的一些特性了。我们直接看 Cocos2d-html5 中的对应代码。哦,no,因为 html5 里面关于 CCNode::setScale 函数写了一点杂技代码。所以我们改成看 setPosition 函数吧。也是一样的。
setPosition:function (newPosOrxValue, yValue) {
var locPosition = this._position;
if (arguments.length == 2) {
locPosition._x = newPosOrxValue;
locPosition._y = yValue;
} else if (arguments.length == 1) {
locPosition._x = newPosOrxValue.x;
locPosition._y = newPosOrxValue.y;
}
this.setNodeDirty();
},
可以看到,该代码使用了 JS 的 arguments 来判断参数个数,然后执行对应的分支代码。
好了,重载就说道这里,下篇继续~
11
JS 与 C++ 的交互 3——C++ 和 JS 类型转换
在 SpiderMonkey 执行时,经常要把 JS 中的数据类型转换成 C++ 类型,比如 int,unit,string,各种容器等等。转换之后,才能够给对应的 C++ 函数传递参数,来完成对应的 C++ 函数的调用。反过来也是一样, C++ 的数据类型要返回到 JS 里面,这样 JS 层的代码才能继续跑,也需要把 C++ 类型转换为 JS 类型。
这些“基本数据类型”的转换,是通过预先编写的代码来完成的,cxx-generator 在生成自动 binding 的代码时,如果遇到类型转换,会调用这些写好的这些类型转换代码。了解这些预先写好的类型转换代码是十分必要的,因为有的时候需要调试 bug,有的时候会有一些不能识别的类型,要手写转换代码。
在 Cocos2d-x 引擎中,这些类型转换代码是存放在一个js_manual_conversions.h 和 js_manual_conversions.cpp 的文件中的。
我们看一下头文件,为了便于查看我删掉了模板函数的实现代码。
JSBool jsval_to_opaque( JSContext *cx, jsval vp, void **out );
JSBool jsval_to_int( JSContext *cx, jsval vp, int *out);
JSBool jsval_to_uint( JSContext *cx, jsval vp, unsigned int *out);
JSBool jsval_to_c_class( JSContext *cx, jsval vp, void **out_native, struct jsb_c_proxy_s **out_proxy);
/** converts a jsval (JS string) into a char */
JSBool jsval_to_charptr( JSContext *cx, jsval vp, const char **out);
jsval opaque_to_jsval( JSContext *cx, void* opaque);
jsval c_class_to_jsval( JSContext *cx, void* handle, JSObject* object, JSClass *klass, const char* class_name);
/* Converts a char ptr into a jsval (using JS string) */
jsval charptr_to_jsval( JSContext *cx, const char *str);
JSBool JSB_jsval_typedarray_to_dataptr( JSContext *cx, jsval vp, GLsizei *count, void **data, JSArrayBufferViewType t);
JSBool JSB_get_arraybufferview_dataptr( JSContext *cx, jsval vp, GLsizei *count, GLvoid **data );
// some utility functions
// to native
JSBool jsval_to_ushort( JSContext *cx, jsval vp, unsigned short *ret );
JSBool jsval_to_int32( JSContext *cx, jsval vp, int32_t *ret );
JSBool jsval_to_uint32( JSContext *cx, jsval vp, uint32_t *ret );
JSBool jsval_to_uint16( JSContext *cx, jsval vp, uint16_t *ret );
JSBool jsval_to_long( JSContext *cx, jsval vp, long *out);
JSBool jsval_to_ulong( JSContext *cx, jsval vp, unsigned long *out);
JSBool jsval_to_long_long(JSContext *cx, jsval v, long long* ret);
JSBool jsval_to_std_string(JSContext *cx, jsval v, std::string* ret);
JSBool jsval_to_ccpoint(JSContext *cx, jsval v, cocos2d::Point* ret);
JSBool jsval_to_ccrect(JSContext *cx, jsval v, cocos2d::Rect* ret);
JSBool jsval_to_ccsize(JSContext *cx, jsval v, cocos2d::Size* ret);
JSBool jsval_to_cccolor4b(JSContext *cx, jsval v, cocos2d::Color4B* ret);
JSBool jsval_to_cccolor4f(JSContext *cx, jsval v, cocos2d::Color4F* ret);
JSBool jsval_to_cccolor3b(JSContext *cx, jsval v, cocos2d::Color3B* ret);
JSBool jsval_to_ccarray_of_CCPoint(JSContext* cx, jsval v, cocos2d::Point **points, int *numPoints);
JSBool jsval_to_ccarray(JSContext* cx, jsval v, cocos2d::__Array** ret);
JSBool jsval_to_ccdictionary(JSContext* cx, jsval v, cocos2d::__Dictionary** ret);
JSBool jsval_to_ccacceleration(JSContext* cx,jsval v, cocos2d::Acceleration* ret);
JSBool jsvals_variadic_to_ccarray( JSContext *cx, jsval *vp, int argc, cocos2d::__Array** ret);
// forward declaration
js_proxy_t* jsb_get_js_proxy(JSObject* jsObj);
template <class T>
JSBool jsvals_variadic_to_ccvector( JSContext *cx, jsval *vp, int argc, cocos2d::Vector<T>* ret);
JSBool jsvals_variadic_to_ccvaluevector( JSContext *cx, jsval *vp, int argc, cocos2d::ValueVector* ret);
JSBool jsval_to_ccaffinetransform(JSContext* cx, jsval v, cocos2d::AffineTransform* ret);
JSBool jsval_to_FontDefinition( JSContext *cx, jsval vp, cocos2d::FontDefinition* ret );
template <class T>
JSBool jsval_to_ccvector(JSContext* cx, jsval v, cocos2d::Vector<T>* ret);
JSBool jsval_to_ccvalue(JSContext* cx, jsval v, cocos2d::Value* ret);
JSBool jsval_to_ccvaluemap(JSContext* cx, jsval v, cocos2d::ValueMap* ret);
JSBool jsval_to_ccvaluemapintkey(JSContext* cx, jsval v, cocos2d::ValueMapIntKey* ret);
JSBool jsval_to_ccvaluevector(JSContext* cx, jsval v, cocos2d::ValueVector* ret);
JSBool jsval_to_ssize( JSContext *cx, jsval vp, ssize_t* ret);
JSBool jsval_to_std_vector_string( JSContext *cx, jsval vp, std::vector<std::string>* ret);
JSBool jsval_to_std_vector_int( JSContext *cx, jsval vp, std::vector<int>* ret);
template <class T>
JSBool jsval_to_ccmap_string_key(JSContext *cx, jsval v, cocos2d::Map<std::string, T>* ret);
// from native
jsval int32_to_jsval( JSContext *cx, int32_t l);
jsval uint32_to_jsval( JSContext *cx, uint32_t number );
jsval ushort_to_jsval( JSContext *cx, unsigned short number );
jsval long_to_jsval( JSContext *cx, long number );
jsval ulong_to_jsval(JSContext* cx, unsigned long v);
jsval long_long_to_jsval(JSContext* cx, long long v);
jsval std_string_to_jsval(JSContext* cx, const std::string& v);
jsval c_string_to_jsval(JSContext* cx, const char* v, size_t length = -1);
jsval ccpoint_to_jsval(JSContext* cx, const cocos2d::Point& v);
jsval ccrect_to_jsval(JSContext* cx, const cocos2d::Rect& v);
jsval ccsize_to_jsval(JSContext* cx, const cocos2d::Size& v);
jsval cccolor4b_to_jsval(JSContext* cx, const cocos2d::Color4B& v);
jsval cccolor4f_to_jsval(JSContext* cx, const cocos2d::Color4F& v);
jsval cccolor3b_to_jsval(JSContext* cx, const cocos2d::Color3B& v);
jsval ccdictionary_to_jsval(JSContext* cx, cocos2d::__Dictionary *dict);
jsval ccarray_to_jsval(JSContext* cx, cocos2d::__Array *arr);
jsval ccacceleration_to_jsval(JSContext* cx, const cocos2d::Acceleration& v);
jsval ccaffinetransform_to_jsval(JSContext* cx, const cocos2d::AffineTransform& t);
jsval FontDefinition_to_jsval(JSContext* cx, const cocos2d::FontDefinition& t);
JSBool jsval_to_CGPoint( JSContext *cx, jsval vp, cpVect *out );
jsval CGPoint_to_jsval( JSContext *cx, cpVect p );
#define cpVect_to_jsval CGPoint_to_jsval
#define jsval_to_cpVect jsval_to_CGPoint
template<class T>
js_proxy_t *js_get_or_create_proxy(JSContext *cx, T *native_obj);
template <class T>
jsval ccvector_to_jsval(JSContext* cx, const cocos2d::Vector<T>& v);
template <class T>
jsval ccmap_string_key_to_jsval(JSContext* cx, const cocos2d::Map<std::string, T>& v);
jsval ccvalue_to_jsval(JSContext* cx, const cocos2d::Value& v);
jsval ccvaluemap_to_jsval(JSContext* cx, const cocos2d::ValueMap& v);
jsval ccvaluemapintkey_to_jsval(JSContext* cx, const cocos2d::ValueMapIntKey& v);
jsval ccvaluevector_to_jsval(JSContext* cx, const cocos2d::ValueVector& v);
jsval ssize_to_jsval(JSContext *cx, ssize_t v);
从函数命名来看,很明显是两类:1. xxx_to_jsval 2.jsval_to_xxx。 需要注意的是,这里面曾经潜藏着许多 bug,内存泄漏,转换错误,甚至一些手误代码等。有一些在引擎进化的过程中逐步修复了。
另外,最重要的是,可能有一些 cxx-generator 不支持的转换类型存在,这种不支持的类型转换,就必须要手写代码来实现,例如:我曾经用 map 做了一个简单的只有一个层次的 JSON 对象,作为 talkingdata 的自定义事件追踪函数的参数。
如果你不手写转换代码,cxx-generator 会在无法转换的地方,插入一个编译器警告,但是对于 XCode 来说,基本没有人看那东西,因为编译状态是放在另一个菜单下面的,这是一个隐晦错误。如果你遇到了莫名其妙的代码错误,其中用到一些特殊的新类型,你要去检查你的自动绑定代码,里面很可能有这么一个警告。修复他!然后就可以正常运行了。
下篇继续,应该会讲到回调函数的问题了。
12
回调函数1——按键回调
回调函数是界面交互和接入各种第三方 SDK 的关键所在,因为回调函数的 C++ 代码是不能自动生成的,一切的一切,都需要手写完成。
比较不错的是,Cocos2d-x 引擎对于回调函数提供了完整的包装机制。我们所需要做的就是了解这个机制,并使用他。学习引擎自己的代码例子,可以比较快速准确的上手这一机制。
首先,我们在 Cocos2d-x 3.0 beta 版中,使用他自带的工程创建工具,新建一个跨平台的 JS 项目。按照惯例,这是一个 helloworld 项目。在 XCode 运行时,我们可以看到:
可以看到右下角的回调按钮。我们来看看他是怎么实现的。分成两个过程来做:
一、绑定回调函数过程
首先,我们要去找回调函数 JS 的绑定代码,在 myApp.js 中,init 函数里面,可以看到如下代码:
// add a "close" icon to exit the progress. it's an autorelease object
var closeItem = cc.MenuItemImage.create(
"res/CloseNormal.png",
"res/CloseSelected.png",
function () {
cc.log("close button was clicked.");
},this);
closeItem.setAnchorPoint(cc.p(0.5, 0.5));
var menu = cc.Menu.create(closeItem);
menu.setPosition(cc.p(0, 0));
this.addChild(menu, 1);
closeItem.setPosition(cc.p(size.width - 20, 20));
cc.MenuItemImage.create 函数的第三个参数,绑定了匿名回调函数。第四个参数,传入的是回调函数调用时的 this(如果不理解 JS 的 this 机制,请先阅读一些 JS 的资料)。这些都是意图和作用很明显的 JS 代码,不用细说。
然后,我们去看底层对应执行的 C++ 代码。在 cocos2d_specifics.cpp 文件中,找到 js_cocos2dx_CCMenuItemImage_create 函数。
// "create" in JS
// cc.MenuItemImage.create( normalImage, selectedImage, [disabledImage], callback_fn, [this]
JSBool js_cocos2dx_CCMenuItemImage_create(JSContext *cx, uint32_t argc, jsval *vp)
{
if (argc >= 2 && argc <= 5) {
jsval *argv = JS_ARGV(cx, vp);
JSStringWrapper arg0(argv[0]);
JSStringWrapper arg1(argv[1]);
JSStringWrapper arg2;
bool thirdArgIsString = true;
jsval jsCallback = JSVAL_VOID;
jsval jsThis = JSVAL_VOID;
int last = 2;
if (argc >= 3) {
thirdArgIsString = argv[2].isString();
if (thirdArgIsString) {
arg2.set(argv[2], cx);
last = 3;
}
}
cocos2d::MenuItemImage* ret = cocos2d::MenuItemImage::create(arg0.get(), arg1.get(), std::string(arg2.get()));
if (argc >= 3) {
if (!thirdArgIsString) {
//cc.MenuItemImage.create( normalImage, selectedImage, callback_fn, [this] )
jsCallback = argv[last++];
if (argc == 4) {
jsThis = argv[last];
}
}
else {
//cc.MenuItemImage.create( normalImage, selectedImage, disabledImage, callback_fn, [this] )
if (argc >= 4) {
jsCallback = argv[last++];
if (argc == 5) {
jsThis = argv[last];
}
}
}
}
JSObject *obj = bind_menu_item<cocos2d::MenuItemImage>(cx, ret, jsCallback, jsThis);
JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(obj));
return JS_TRUE;
}
JS_ReportError(cx, "Invalid number of arguments. Expecting: 2 <= args <= 5");
return JS_FALSE;
}
因为在 C++ 层,这是一个重载过的函数,所以他的实现里面有很多参数个数的判断(关于重载问题请参考之前的章节)。过滤掉很多代码,我们直接看关键部分:
if (argc >= 3) {
if (!thirdArgIsString) {
//cc.MenuItemImage.create( normalImage, selectedImage, callback_fn, [this] )
jsCallback = argv[last++];
if (argc == 4) {
jsThis = argv[last];
}
}
else {
//cc.MenuItemImage.create( normalImage, selectedImage, disabledImage, callback_fn, [this] )
if (argc >= 4) {
jsCallback = argv[last++];
if (argc == 5) {
jsThis = argv[last];
}
}
}
}
在这里我们从参数中取出回调函数和 this,分别赋值给 jsCallback 和 jsThis。
JSObject *obj = bind_menu_item<cocos2d::MenuItemImage>(cx, ret, jsCallback, jsThis);
由这句模板函数来实现回调的绑定,四个参数依次是,JS 上下文, cc.MenuItemImage 对应的 C++ 对象,回调函数,和回调函数调用时的 this。
template<class T>
JSObject* bind_menu_item(JSContext *cx, T* nativeObj, jsval callback, jsval thisObj) {
js_proxy_t *p = jsb_get_native_proxy(nativeObj);
if (p) {
addCallBackAndThis(p->obj, callback, thisObj);
return p->obj;
} else {
js_type_class_t *classType = js_get_type_from_native<T>(nativeObj);
assert(classType);
JSObject *tmp = JS_NewObject(cx, classType->jsclass, classType->proto, classType->parentProto);
// bind nativeObj <-> JSObject
js_proxy_t *proxy = jsb_new_proxy(nativeObj, tmp);
JS_AddNamedObjectRoot(cx, &proxy->obj, typeid(*nativeObj).name());
addCallBackAndThis(tmp, callback, thisObj);
return tmp;
}
}
继续看 bind_menu_item 的实现。简单说一下,因为绑定的是一个 JS 函数,所以实际上,需要在 SpiderMonkey 里面做这个绑定操作。传进来的是一个 C++ 对象(CCMenuItemImage 类型),首先找到和这个 C++ 对象对应的 JS 对象。如果找不到,就新建立一个。然后通过函数 addCallBackAndThis 执行绑定。
static void addCallBackAndThis(JSObject *obj, jsval callback, jsval &thisObj)
{
if(callback != JSVAL_VOID) {
ScriptingCore::getInstance()->setReservedSpot(0, obj, callback);
}
if(thisObj != JSVAL_VOID) {
ScriptingCore::getInstance()->setReservedSpot(1, obj, thisObj);
}
}
JSBool ScriptingCore::setReservedSpot(uint32_t i, JSObject *obj, jsval value) {
JS_SetReservedSlot(obj, i, value);
return JS_TRUE;
}
最终我们看到,存储回调函数的方法是通过 SpiderMonkey 的 ReservedSlot 机制。0位存放的是回调函数,1位存放的是回调函数对应的 this。
好,到此为止,回调函数的绑定全部结束。
二、调用回调函数过程
现在我们看从 C++ 层启动JS回调的过程。我们省略掉事件派发机制,直接看按键事件发生时的调用代码。在按键事件发生时,会调用 MenuItemImage 的父类 MenuItem 中的 activate 函数。该函数在 CCMenuItem.cpp 中。
void MenuItem::activate()
{
if (_enabled)
{
if( _callback )
{
_callback(this);
}
if (kScriptTypeNone != _scriptType)
{
BasicScriptData data(this);
ScriptEvent scriptEvent(kMenuClickedEvent,&data);
ScriptEngineManager::getInstance()->getScriptEngine()->sendEvent(&scriptEvent);
}
}
}
非常简单,首先判断按键是否可用。然后如果有 C++ 层回调就调用。如果有脚本层(JS 或 lua)回调,就包装一个 kMenuClickedEvent 事件,然后向对应的脚本引擎发送该事件。
int ScriptingCore::sendEvent(ScriptEvent* evt)
{
if (NULL == evt)
return 0;
JSAutoCompartment ac(_cx, _global);
switch (evt->type)
{
case kNodeEvent:
{
return handleNodeEvent(evt->data);
}
break;
case kMenuClickedEvent:
{
return handleMenuClickedEvent(evt->data);
}
break;
case kTouchEvent:
{
return handleTouchEvent(evt->data);
}
break;
case kTouchesEvent:
{
return handleTouchesEvent(evt->data);
}
break;
case kKeypadEvent:
{
return handleKeypadEvent(evt->data);
}
break;
case kAccelerometerEvent:
{
return handleAccelerometerEvent(evt->data);
}
break;
default:
break;
}
return 0;
}
JS 通过 ScriptingCore::sendEvent 进行事件分发。 kMenuClickedEvent 事件派发给 handleMenuClickedEvent 函数来处理。
int ScriptingCore::handleMenuClickedEvent(void* data)
{
if (NULL == data)
return 0;
BasicScriptData* basicScriptData = static_cast<BasicScriptData*>(data);
if (NULL == basicScriptData->nativeObject)
return 0;
MenuItem* menuItem = static_cast<MenuItem*>(basicScriptData->nativeObject);
js_proxy_t * p = jsb_get_native_proxy(menuItem);
if (!p) return 0;
jsval retval;
jsval dataVal;
js_proxy_t *proxy = jsb_get_native_proxy(menuItem);
dataVal = (proxy ? OBJECT_TO_JSVAL(proxy->obj) : JSVAL_NULL);
executeJSFunctionFromReservedSpot(this->_cx, p->obj, dataVal, retval);
return 1;
}
static void executeJSFunctionFromReservedSpot(JSContext *cx, JSObject *obj,
jsval &dataVal, jsval &retval) {
jsval func = JS_GetReservedSlot(obj, 0);
if (func == JSVAL_VOID) { return; }
jsval thisObj = JS_GetReservedSlot(obj, 1);
JSAutoCompartment ac(cx, obj);
if (thisObj == JSVAL_VOID) {
JS_CallFunctionValue(cx, obj, func, 1, &dataVal, &retval);
} else {
assert(!JSVAL_IS_PRIMITIVE(thisObj));
JS_CallFunctionValue(cx, JSVAL_TO_OBJECT(thisObj), func, 1, &dataVal, &retval);
}
}
再次通过 SpiderMonkey 的 ReservedSlot 机制,取回相应的参数,最后通过 JS_CallFunctionValue 函数完成 JS 层回调函数的调用。
下篇继续
13
回调函数 2
上一篇我们讲了按键回调,这一次我们来说说各种逻辑上的回调函数。
Cocos2d-x 里面一共有三大类回调函数,第一是按键回调 CCMenu 相关的,第二类是定时器相关的回调 Schedule,第三类是 Action 相关的回调 CallFunc。这些回调从最初的引擎版本中就存在着,一直到现在。
一、绑定代码
在 JSB 的解决方案中,对于后两类函数,引擎统一封装成 JSCallbackWrapper 及其子类。
class JSCallbackWrapper: public cocos2d::Object {
public:
JSCallbackWrapper();
virtual ~JSCallbackWrapper();
void setJSCallbackFunc(jsval obj);
void setJSCallbackThis(jsval thisObj);
void setJSExtraData(jsval data);
const jsval& getJSCallbackFunc() const;
const jsval& getJSCallbackThis() const;
const jsval& getJSExtraData() const;
protected:
jsval _jsCallback;
jsval _jsThisObj;
jsval _extraData;
};
JSCallbackWrapper 从名字就可以知道,是 JS 回调函数的包装器。三个接口也一目了然,回调函数,this,外部数据。
// cc.CallFunc.create( func, this, [data])
// cc.CallFunc.create( func )
static JSBool js_callFunc(JSContext *cx, uint32_t argc, jsval *vp)
{
if (argc >= 1 && argc <= 3) {
jsval *argv = JS_ARGV(cx, vp);
std::shared_ptr<JSCallbackWrapper> tmpCobj(new JSCallbackWrapper());
tmpCobj->setJSCallbackFunc(argv[0]);
if(argc >= 2) {
tmpCobj->setJSCallbackThis(argv[1]);
} if(argc == 3) {
tmpCobj->setJSExtraData(argv[2]);
}
CallFuncN *ret = CallFuncN::create([=](Node* sender){
const jsval& jsvalThis = tmpCobj->getJSCallbackThis();
const jsval& jsvalCallback = tmpCobj->getJSCallbackFunc();
const jsval& jsvalExtraData = tmpCobj->getJSExtraData();
bool hasExtraData = !JSVAL_IS_VOID(jsvalExtraData);
JSObject* thisObj = JSVAL_IS_VOID(jsvalThis) ? nullptr : JSVAL_TO_OBJECT(jsvalThis);
JSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJCET
js_proxy_t *proxy = js_get_or_create_proxy<cocos2d::Node>(cx, sender);
jsval retval;
if(jsvalCallback != JSVAL_VOID)
{
if (hasExtraData)
{
jsval valArr[2];
valArr[0] = OBJECT_TO_JSVAL(proxy->obj);
valArr[1] = jsvalExtraData;
JS_AddValueRoot(cx, valArr);
JS_CallFunctionValue(cx, thisObj, jsvalCallback, 2, valArr, &retval);
JS_RemoveValueRoot(cx, valArr);
}
else
{
jsval senderVal = OBJECT_TO_JSVAL(proxy->obj);
JS_AddValueRoot(cx, &senderVal);
JS_CallFunctionValue(cx, thisObj, jsvalCallback, 1, &senderVal, &retval);
JS_RemoveValueRoot(cx, &senderVal);
}
}
// I think the JSCallFuncWrapper isn't needed.
// Since an action will be run by a cc.Node, it will be released at the Node::cleanup.
// By James Chen
// JSCallFuncWrapper::setTargetForNativeNode(node, (JSCallFuncWrapper *)this);
});
js_proxy_t *proxy = js_get_or_create_proxy<cocos2d::CallFunc>(cx, ret);
JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(proxy->obj));
JS_SetReservedSlot(proxy->obj, 0, argv[0]);
if(argc > 1) {
JS_SetReservedSlot(proxy->obj, 1, argv[1]);
}
// if(argc == 3) {
// JS_SetReservedSlot(proxy->obj, 2, argv[2]);
// }
// test->execute();
return JS_TRUE;
}
JS_ReportError(cx, "Invalid number of arguments");
return JS_FALSE;
}
这是 JS 层调用 cc.CallFunc.create 时,底层执行的 C++ 函数,这里面用了一些 C++11 的特性,包括 std::shared_ptr 智能指针和 lambda 表达式(也很简单,不熟悉的童鞋可以自己找资料熟悉下)。
这里面回调函数被封装到了lambda表达式里面,通过=方式引用外部的 tmpCobj 变量,这种方式跟 JS 的闭包非常类似。依然使用 JS_CallFunctionValue 进行函数调用。注意,这种调用方式跟 JS 里面的 apply 方式是很类似的。
这里面有一对函数非常有趣,JS_AddValueRoot 和JS_RemoveValueRoot,这两个函数 JS_CallFunctionValue 调用包起来了。因为这个 valArr 或 senderVal 是在栈上临时生成的,没有指定对应的 root。但是中间又进行了 JS 函数的调用,所以这两个值可能在 JS 函数调用的时候被 SpiderMonkey 虚拟机给垃圾回收掉(可以去看看 JS 的垃圾回收机制原理)。于是我们需要给他们挂一个 root,保护一下,不被回收掉。
二、调用代码
先看一下构造函数
CallFuncN * CallFuncN::create(const std::function<void(Node*)> &func)
{
auto ret = new CallFuncN();
if (ret && ret->initWithFunction(func) ) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
bool CallFuncN::initWithFunction(const std::function<void (Node *)> &func)
{
_functionN = func;
return true;
}
传进来的 lambda 表达式被存为一个 std::function<void(Node*)> 类型。
调用代码异常简单,使用 _functionN 进行调用即可。
void CallFuncN::execute() {
if (_callFuncN) {
(_selectorTarget->*_callFuncN)(_target);
}
else if (_functionN) {
_functionN(_target);
}
}
对比上一篇中的方式,我认为这种调用方式更加合理,因为这种调用方式,对 C++ 层 Core 代码,隐藏了脚本机制。而之前的调用方式是显示通过脚本引擎来调用的。 看完此篇和前篇,我们仔细分析了 Cocos2d-x JSB 里面的回调函数的写法,详细对于读者而言自己实现一个回调函数已经不是什么特别困难的事情。
在刚完成此篇的时候,突然发现有这么一个帖子,讲的也是 JSB 回调函数,写得很不错,还是 IAP 的,可以作为额外阅读参考:
还有一篇可以学习的:
JS 的回调函数的参数构造注记——Web 学习笔记(十八)
关于回调函数的问题,先说这些吧。
下篇继续,我们来讨论一下注册函数的事
14
注册函数
前面的文章中讲过,会调用大量的 addRegisterCallback 函数,向 SpiderMonkey 注册 Cocos2d-x 引擎的函数。
ScriptingCore* sc = ScriptingCore::getInstance();
sc->addRegisterCallback(register_all_cocos2dx);
sc->addRegisterCallback(register_all_cocos2dx_extension);
sc->addRegisterCallback(register_cocos2dx_js_extensions);
sc->addRegisterCallback(register_all_cocos2dx_extension_manual);
sc->addRegisterCallback(jsb_register_chipmunk);
sc->addRegisterCallback(JSB_register_opengl);
sc->addRegisterCallback(jsb_register_system);
sc->addRegisterCallback(MinXmlHttpRequest::_js_register);
sc->addRegisterCallback(register_jsb_websocket);
sc->addRegisterCallback(register_all_cocos2dx_builder);
sc->addRegisterCallback(register_CCBuilderReader);
sc->addRegisterCallback(register_all_cocos2dx_gui);
sc->addRegisterCallback(register_all_cocos2dx_gui_manual);
sc->addRegisterCallback(register_all_cocos2dx_studio);
sc->addRegisterCallback(register_all_cocos2dx_studio_manual);
sc->addRegisterCallback(register_all_cocos2dx_spine);
sc->start();
以 register_all_cocos2dx 注册函数为例,跳转到实现代码:
void register_all_cocos2dx(JSContext* cx, JSObject* obj) {
// first, try to get the ns
JS::RootedValue nsval(cx);
JSObject *ns;
JS_GetProperty(cx, obj, "cc", &nsval);
if (nsval == JSVAL_VOID) {
ns = JS_NewObject(cx, NULL, NULL, NULL);
nsval = OBJECT_TO_JSVAL(ns);
JS_SetProperty(cx, obj, "cc", nsval);
} else {
JS_ValueToObject(cx, nsval, &ns);
}
obj = ns;
js_register_cocos2dx_Action(cx, obj);
js_register_cocos2dx_FiniteTimeAction(cx, obj);
js_register_cocos2dx_ActionInstant(cx, obj);
js_register_cocos2dx_Hide(cx, obj);
js_register_cocos2dx_Node(cx, obj);
js_register_cocos2dx_Scene(cx, obj);
js_register_cocos2dx_TransitionScene(cx, obj);
js_register_cocos2dx_TransitionEaseScene(cx, obj);
js_register_cocos2dx_TransitionMoveInL(cx, obj);
js_register_cocos2dx_TransitionMoveInB(cx, obj);
js_register_cocos2dx_Layer(cx, obj);
js_register_cocos2dx___LayerRGBA(cx, obj);
js_register_cocos2dx_AtlasNode(cx, obj);
js_register_cocos2dx_TileMapAtlas(cx, obj);
js_register_cocos2dx_TransitionMoveInT(cx, obj);
js_register_cocos2dx_TransitionMoveInR(cx, obj);
js_register_cocos2dx_ParticleSystem(cx, obj);
js_register_cocos2dx_ParticleSystemQuad(cx, obj);
js_register_cocos2dx_ParticleSnow(cx, obj);
js_register_cocos2dx_ActionInterval(cx, obj);
js_register_cocos2dx_ActionCamera(cx, obj);
js_register_cocos2dx_ProgressFromTo(cx, obj);
js_register_cocos2dx_MoveBy(cx, obj);
js_register_cocos2dx_MoveTo(cx, obj);
js_register_cocos2dx_JumpBy(cx, obj);
js_register_cocos2dx_ActionEase(cx, obj);
js_register_cocos2dx_EaseBounce(cx, obj);
js_register_cocos2dx_EaseBounceIn(cx, obj);
js_register_cocos2dx_TransitionRotoZoom(cx, obj);
js_register_cocos2dx_Director(cx, obj);
js_register_cocos2dx_Texture2D(cx, obj);
js_register_cocos2dx_EaseElastic(cx, obj);
js_register_cocos2dx_EaseElasticOut(cx, obj);
js_register_cocos2dx_EaseBackOut(cx, obj);
js_register_cocos2dx_TransitionSceneOriented(cx, obj);
js_register_cocos2dx_TransitionFlipX(cx, obj);
js_register_cocos2dx_Spawn(cx, obj);
js_register_cocos2dx_SimpleAudioEngine(cx, obj);
js_register_cocos2dx_SkewTo(cx, obj);
js_register_cocos2dx_SkewBy(cx, obj);
js_register_cocos2dx_TransitionProgress(cx, obj);
js_register_cocos2dx_TransitionProgressVertical(cx, obj);
js_register_cocos2dx_TMXTiledMap(cx, obj);
js_register_cocos2dx_GridAction(cx, obj);
js_register_cocos2dx_Grid3DAction(cx, obj);
js_register_cocos2dx_FadeIn(cx, obj);
js_register_cocos2dx_AnimationCache(cx, obj);
js_register_cocos2dx_FlipX3D(cx, obj);
js_register_cocos2dx_FlipY3D(cx, obj);
js_register_cocos2dx_EaseSineInOut(cx, obj);
js_register_cocos2dx_TransitionFlipAngular(cx, obj);
js_register_cocos2dx_EGLViewProtocol(cx, obj);
js_register_cocos2dx_EGLView(cx, obj);
js_register_cocos2dx_EaseElasticInOut(cx, obj);
js_register_cocos2dx_Show(cx, obj);
js_register_cocos2dx_FadeOut(cx, obj);
js_register_cocos2dx_CallFunc(cx, obj);
js_register_cocos2dx_Waves3D(cx, obj);
js_register_cocos2dx_ParticleFireworks(cx, obj);
js_register_cocos2dx_MenuItem(cx, obj);
js_register_cocos2dx_MenuItemSprite(cx, obj);
js_register_cocos2dx_MenuItemImage(cx, obj);
js_register_cocos2dx_ParticleFire(cx, obj);
js_register_cocos2dx_TransitionZoomFlipAngular(cx, obj);
js_register_cocos2dx_EaseRateAction(cx, obj);
js_register_cocos2dx_EaseIn(cx, obj);
js_register_cocos2dx_EaseExponentialInOut(cx, obj);
js_register_cocos2dx_EaseBackInOut(cx, obj);
js_register_cocos2dx_EaseExponentialOut(cx, obj);
js_register_cocos2dx_SpriteBatchNode(cx, obj);
js_register_cocos2dx_Label(cx, obj);
js_register_cocos2dx_Application(cx, obj);
js_register_cocos2dx_DelayTime(cx, obj);
js_register_cocos2dx_LabelAtlas(cx, obj);
js_register_cocos2dx_LabelBMFont(cx, obj);
js_register_cocos2dx_TransitionFadeTR(cx, obj);
js_register_cocos2dx_TransitionFadeBL(cx, obj);
js_register_cocos2dx_EaseElasticIn(cx, obj);
js_register_cocos2dx_ParticleSpiral(cx, obj);
js_register_cocos2dx_TiledGrid3DAction(cx, obj);
js_register_cocos2dx_FadeOutTRTiles(cx, obj);
js_register_cocos2dx_FadeOutUpTiles(cx, obj);
js_register_cocos2dx_FadeOutDownTiles(cx, obj);
js_register_cocos2dx_TextureCache(cx, obj);
js_register_cocos2dx_ActionTween(cx, obj);
js_register_cocos2dx_TransitionFadeDown(cx, obj);
js_register_cocos2dx_ParticleSun(cx, obj);
js_register_cocos2dx_TransitionProgressHorizontal(cx, obj);
js_register_cocos2dx_TMXObjectGroup(cx, obj);
js_register_cocos2dx_TMXLayer(cx, obj);
js_register_cocos2dx_FlipX(cx, obj);
js_register_cocos2dx_FlipY(cx, obj);
js_register_cocos2dx_TransitionSplitCols(cx, obj);
js_register_cocos2dx_Timer(cx, obj);
js_register_cocos2dx_FadeTo(cx, obj);
js_register_cocos2dx_Repeat(cx, obj);
js_register_cocos2dx_Place(cx, obj);
js_register_cocos2dx_GLProgram(cx, obj);
js_register_cocos2dx_EaseBounceOut(cx, obj);
js_register_cocos2dx_RenderTexture(cx, obj);
js_register_cocos2dx_TintBy(cx, obj);
js_register_cocos2dx_TransitionShrinkGrow(cx, obj);
js_register_cocos2dx_Sprite(cx, obj);
js_register_cocos2dx_LabelTTF(cx, obj);
js_register_cocos2dx_ClippingNode(cx, obj);
js_register_cocos2dx_ParticleFlower(cx, obj);
js_register_cocos2dx_ParticleSmoke(cx, obj);
js_register_cocos2dx_LayerMultiplex(cx, obj);
js_register_cocos2dx_Blink(cx, obj);
js_register_cocos2dx_ShaderCache(cx, obj);
js_register_cocos2dx_JumpTo(cx, obj);
js_register_cocos2dx_ParticleExplosion(cx, obj);
js_register_cocos2dx_TransitionJumpZoom(cx, obj);
js_register_cocos2dx_Touch(cx, obj);
js_register_cocos2dx_AnimationFrame(cx, obj);
js_register_cocos2dx_NodeGrid(cx, obj);
js_register_cocos2dx_TMXLayerInfo(cx, obj);
js_register_cocos2dx_TMXTilesetInfo(cx, obj);
js_register_cocos2dx_GridBase(cx, obj);
js_register_cocos2dx_TiledGrid3D(cx, obj);
js_register_cocos2dx_ParticleGalaxy(cx, obj);
js_register_cocos2dx_Twirl(cx, obj);
js_register_cocos2dx_MenuItemLabel(cx, obj);
js_register_cocos2dx_LayerColor(cx, obj);
js_register_cocos2dx_FadeOutBLTiles(cx, obj);
js_register_cocos2dx_LayerGradient(cx, obj);
js_register_cocos2dx_TargetedAction(cx, obj);
js_register_cocos2dx_RepeatForever(cx, obj);
js_register_cocos2dx_CardinalSplineTo(cx, obj);
js_register_cocos2dx_CardinalSplineBy(cx, obj);
js_register_cocos2dx_TransitionFlipY(cx, obj);
js_register_cocos2dx_TurnOffTiles(cx, obj);
js_register_cocos2dx_TintTo(cx, obj);
js_register_cocos2dx_CatmullRomTo(cx, obj);
js_register_cocos2dx_ToggleVisibility(cx, obj);
js_register_cocos2dx_DrawNode(cx, obj);
js_register_cocos2dx_TransitionTurnOffTiles(cx, obj);
js_register_cocos2dx_RotateTo(cx, obj);
js_register_cocos2dx_TransitionSplitRows(cx, obj);
js_register_cocos2dx_TransitionProgressRadialCCW(cx, obj);
js_register_cocos2dx_ScaleTo(cx, obj);
js_register_cocos2dx_TransitionPageTurn(cx, obj);
js_register_cocos2dx_BezierBy(cx, obj);
js_register_cocos2dx_BezierTo(cx, obj);
js_register_cocos2dx_Menu(cx, obj);
js_register_cocos2dx_SpriteFrame(cx, obj);
js_register_cocos2dx_ActionManager(cx, obj);
js_register_cocos2dx_TransitionFade(cx, obj);
js_register_cocos2dx_TransitionZoomFlipX(cx, obj);
js_register_cocos2dx_SpriteFrameCache(cx, obj);
js_register_cocos2dx_TransitionCrossFade(cx, obj);
js_register_cocos2dx_Ripple3D(cx, obj);
js_register_cocos2dx_TransitionSlideInL(cx, obj);
js_register_cocos2dx_TransitionSlideInT(cx, obj);
js_register_cocos2dx_StopGrid(cx, obj);
js_register_cocos2dx_ShakyTiles3D(cx, obj);
js_register_cocos2dx_PageTurn3D(cx, obj);
js_register_cocos2dx_Grid3D(cx, obj);
js_register_cocos2dx_TransitionProgressInOut(cx, obj);
js_register_cocos2dx_EaseBackIn(cx, obj);
js_register_cocos2dx_SplitRows(cx, obj);
js_register_cocos2dx_Follow(cx, obj);
js_register_cocos2dx_Animate(cx, obj);
js_register_cocos2dx_ShuffleTiles(cx, obj);
js_register_cocos2dx_ProgressTimer(cx, obj);
js_register_cocos2dx_ParticleMeteor(cx, obj);
js_register_cocos2dx_EaseInOut(cx, obj);
js_register_cocos2dx_TransitionZoomFlipY(cx, obj);
js_register_cocos2dx_ScaleBy(cx, obj);
js_register_cocos2dx_Lens3D(cx, obj);
js_register_cocos2dx_Animation(cx, obj);
js_register_cocos2dx_TMXMapInfo(cx, obj);
js_register_cocos2dx_EaseExponentialIn(cx, obj);
js_register_cocos2dx_ReuseGrid(cx, obj);
js_register_cocos2dx_MenuItemAtlasFont(cx, obj);
js_register_cocos2dx_Liquid(cx, obj);
js_register_cocos2dx_OrbitCamera(cx, obj);
js_register_cocos2dx_ParticleBatchNode(cx, obj);
js_register_cocos2dx_Component(cx, obj);
js_register_cocos2dx_TextFieldTTF(cx, obj);
js_register_cocos2dx_ParticleRain(cx, obj);
js_register_cocos2dx_Waves(cx, obj);
js_register_cocos2dx_EaseOut(cx, obj);
js_register_cocos2dx_MenuItemFont(cx, obj);
js_register_cocos2dx_TransitionFadeUp(cx, obj);
js_register_cocos2dx_EaseSineOut(cx, obj);
js_register_cocos2dx_JumpTiles3D(cx, obj);
js_register_cocos2dx_MenuItemToggle(cx, obj);
js_register_cocos2dx_RemoveSelf(cx, obj);
js_register_cocos2dx_SplitCols(cx, obj);
js_register_cocos2dx_MotionStreak(cx, obj);
js_register_cocos2dx_RotateBy(cx, obj);
js_register_cocos2dx_FileUtils(cx, obj);
js_register_cocos2dx_ProgressTo(cx, obj);
js_register_cocos2dx_TransitionProgressOutIn(cx, obj);
js_register_cocos2dx_CatmullRomBy(cx, obj);
js_register_cocos2dx_Sequence(cx, obj);
js_register_cocos2dx_Shaky3D(cx, obj);
js_register_cocos2dx_TransitionProgressRadialCW(cx, obj);
js_register_cocos2dx_EaseBounceInOut(cx, obj);
js_register_cocos2dx_TransitionSlideInR(cx, obj);
js_register_cocos2dx___NodeRGBA(cx, obj);
js_register_cocos2dx_ParallaxNode(cx, obj);
js_register_cocos2dx_Scheduler(cx, obj);
js_register_cocos2dx_EaseSineIn(cx, obj);
js_register_cocos2dx_WavesTiles3D(cx, obj);
js_register_cocos2dx_TransitionSlideInB(cx, obj);
js_register_cocos2dx_Speed(cx, obj);
js_register_cocos2dx_ShatteredTiles3D(cx, obj);
}
首先看到的是从根对象中获取一个“cc”属性(如果获取不到,就新建一个),因为 JS 中没有名字空间的概念,所以我们使用一个 cc 对象来表示类似的功能。所有的类型和函数都是这个 cc 对象下面的属性。在 Cocos2d-x 3.0 中,C++ 层面,类名去掉了 CC 的前缀,和 js 保持一致。
然后就是一大堆子函数,每个函数都负责注册一个对应的类。打开 js_register_cocos2dx_Sprite,这个函数负责注册 Sprite 类。
打开 js_register_cocos2dx_Sprite 的实现代码
void js_register_cocos2dx_Sprite(JSContext *cx, JSObject *global) {
jsb_cocos2d_Sprite_class = (JSClass *)calloc(1, sizeof(JSClass));
jsb_cocos2d_Sprite_class->name = "Sprite";
jsb_cocos2d_Sprite_class->addProperty = JS_PropertyStub;
jsb_cocos2d_Sprite_class->delProperty = JS_DeletePropertyStub;
jsb_cocos2d_Sprite_class->getProperty = JS_PropertyStub;
jsb_cocos2d_Sprite_class->setProperty = JS_StrictPropertyStub;
jsb_cocos2d_Sprite_class->enumerate = JS_EnumerateStub;
jsb_cocos2d_Sprite_class->resolve = JS_ResolveStub;
jsb_cocos2d_Sprite_class->convert = JS_ConvertStub;
jsb_cocos2d_Sprite_class->finalize = js_cocos2d_Sprite_finalize;
jsb_cocos2d_Sprite_class->flags = JSCLASS_HAS_RESERVED_SLOTS(2);
static JSPropertySpec properties[] = {
{0, 0, 0, JSOP_NULLWRAPPER, JSOP_NULLWRAPPER}
};
static JSFunctionSpec funcs[] = {
JS_FN("setSpriteFrame", js_cocos2dx_Sprite_setSpriteFrame, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setTexture", js_cocos2dx_Sprite_setTexture, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getTexture", js_cocos2dx_Sprite_getTexture, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setFlippedY", js_cocos2dx_Sprite_setFlippedY, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setFlippedX", js_cocos2dx_Sprite_setFlippedX, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getBatchNode", js_cocos2dx_Sprite_getBatchNode, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getOffsetPosition", js_cocos2dx_Sprite_getOffsetPosition, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("removeAllChildrenWithCleanup", js_cocos2dx_Sprite_removeAllChildrenWithCleanup, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("updateQuadVertices", js_cocos2dx_Sprite_updateQuadVertices, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("updateTransform", js_cocos2dx_Sprite_updateTransform, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setTextureRect", js_cocos2dx_Sprite_setTextureRect, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isFrameDisplayed", js_cocos2dx_Sprite_isFrameDisplayed, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getAtlasIndex", js_cocos2dx_Sprite_getAtlasIndex, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setBatchNode", js_cocos2dx_Sprite_setBatchNode, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setDisplayFrameWithAnimationName", js_cocos2dx_Sprite_setDisplayFrameWithAnimationName, 2, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setTextureAtlas", js_cocos2dx_Sprite_setTextureAtlas, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getSpriteFrame", js_cocos2dx_Sprite_getSpriteFrame, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isDirty", js_cocos2dx_Sprite_isDirty, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setAtlasIndex", js_cocos2dx_Sprite_setAtlasIndex, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setDirty", js_cocos2dx_Sprite_setDirty, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isTextureRectRotated", js_cocos2dx_Sprite_isTextureRectRotated, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getTextureRect", js_cocos2dx_Sprite_getTextureRect, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getTextureAtlas", js_cocos2dx_Sprite_getTextureAtlas, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isFlippedX", js_cocos2dx_Sprite_isFlippedX, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isFlippedY", js_cocos2dx_Sprite_isFlippedY, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setVertexRect", js_cocos2dx_Sprite_setVertexRect, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("ctor", js_cocos2d_Sprite_ctor, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FS_END
};
static JSFunctionSpec st_funcs[] = {
JS_FN("create", js_cocos2dx_Sprite_create, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("createWithTexture", js_cocos2dx_Sprite_createWithTexture, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("createWithSpriteFrameName", js_cocos2dx_Sprite_createWithSpriteFrameName, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("createWithSpriteFrame", js_cocos2dx_Sprite_createWithSpriteFrame, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FS_END
};
jsb_cocos2d_Sprite_prototype = JS_InitClass(
cx, global,
jsb_cocos2d_Node_prototype,
jsb_cocos2d_Sprite_class,
dummy_constructor<cocos2d::Sprite>, 0, // no constructor
properties,
funcs,
NULL, // no static properties
st_funcs);
// make the class enumerable in the registered namespace
JSBool found;
JS_SetPropertyAttributes(cx, global, "Sprite", JSPROP_ENUMERATE | JSPROP_READONLY, &found);
// add the proto and JSClass to the type->js info hash table
TypeTest<cocos2d::Sprite> t;
js_type_class_t *p;
std::string typeName = t.s_name();
if (_js_global_type_map.find(typeName) == _js_global_type_map.end())
{
p = (js_type_class_t *)malloc(sizeof(js_type_class_t));
p->jsclass = jsb_cocos2d_Sprite_class;
p->proto = jsb_cocos2d_Sprite_prototype;
p->parentProto = jsb_cocos2d_Node_prototype;
_js_global_type_map.insert(std::make_pair(typeName, p));
}
}
看起来比较长,其实很简单,我们一段一段分析。
jsb_cocos2d_Sprite_class = (JSClass *)calloc(1, sizeof(JSClass));
jsb_cocos2d_Sprite_class->name = "Sprite";
jsb_cocos2d_Sprite_class->addProperty = JS_PropertyStub;
jsb_cocos2d_Sprite_class->delProperty = JS_DeletePropertyStub;
jsb_cocos2d_Sprite_class->getProperty = JS_PropertyStub;
jsb_cocos2d_Sprite_class->setProperty = JS_StrictPropertyStub;
jsb_cocos2d_Sprite_class->enumerate = JS_EnumerateStub;
jsb_cocos2d_Sprite_class->resolve = JS_ResolveStub;
jsb_cocos2d_Sprite_class->convert = JS_ConvertStub;
jsb_cocos2d_Sprite_class->finalize = js_cocos2d_Sprite_finalize;
首先,我们构造了一个 JSClass 对象,这个对象保存了一部分 Sprite 类的相关信息(注意,只是一部分而已)。其中包括类名,还有大量函数的占位符 JS_XXXStub,这些函数是在一定情况下被调用的,如:添加删除属性,查看修改属性等等。这块其实不用特别关注,因为使用的都是 SpiderMonkey 自带的缺省实现。Cocos2d-x 引擎只是在最后把 finalize 函数替换成自己的函数了。最后那个参数表示这个类,有几个 Reserved Slots 槽,这东西我们在之前讲回调函数的时候见过。
static JSPropertySpec properties[] = {
{0, 0, 0, JSOP_NULLWRAPPER, JSOP_NULLWRAPPER}
};
static JSFunctionSpec funcs[] = {
JS_FN("setSpriteFrame", js_cocos2dx_Sprite_setSpriteFrame, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setTexture", js_cocos2dx_Sprite_setTexture, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getTexture", js_cocos2dx_Sprite_getTexture, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setFlippedY", js_cocos2dx_Sprite_setFlippedY, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setFlippedX", js_cocos2dx_Sprite_setFlippedX, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getBatchNode", js_cocos2dx_Sprite_getBatchNode, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getOffsetPosition", js_cocos2dx_Sprite_getOffsetPosition, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("removeAllChildrenWithCleanup", js_cocos2dx_Sprite_removeAllChildrenWithCleanup, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("updateQuadVertices", js_cocos2dx_Sprite_updateQuadVertices, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("updateTransform", js_cocos2dx_Sprite_updateTransform, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setTextureRect", js_cocos2dx_Sprite_setTextureRect, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isFrameDisplayed", js_cocos2dx_Sprite_isFrameDisplayed, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getAtlasIndex", js_cocos2dx_Sprite_getAtlasIndex, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setBatchNode", js_cocos2dx_Sprite_setBatchNode, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setDisplayFrameWithAnimationName", js_cocos2dx_Sprite_setDisplayFrameWithAnimationName, 2, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setTextureAtlas", js_cocos2dx_Sprite_setTextureAtlas, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getSpriteFrame", js_cocos2dx_Sprite_getSpriteFrame, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isDirty", js_cocos2dx_Sprite_isDirty, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setAtlasIndex", js_cocos2dx_Sprite_setAtlasIndex, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setDirty", js_cocos2dx_Sprite_setDirty, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isTextureRectRotated", js_cocos2dx_Sprite_isTextureRectRotated, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getTextureRect", js_cocos2dx_Sprite_getTextureRect, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("getTextureAtlas", js_cocos2dx_Sprite_getTextureAtlas, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isFlippedX", js_cocos2dx_Sprite_isFlippedX, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("isFlippedY", js_cocos2dx_Sprite_isFlippedY, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("setVertexRect", js_cocos2dx_Sprite_setVertexRect, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("ctor", js_cocos2d_Sprite_ctor, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FS_END
};
static JSFunctionSpec st_funcs[] = {
JS_FN("create", js_cocos2dx_Sprite_create, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("createWithTexture", js_cocos2dx_Sprite_createWithTexture, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("createWithSpriteFrameName", js_cocos2dx_Sprite_createWithSpriteFrameName, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("createWithSpriteFrame", js_cocos2dx_Sprite_createWithSpriteFrame, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FS_END
};
然后是填写大量的参数,包括属性,静态属性,函数,静态函数。注意,因为 Cocos2d-x 的类完全使用 Setter 和 Getter,所以一般是没有属性和静态属性的。比较重要的是静态函数和普通函数。我们看一下宏函数 JS_FN。他的第一个参数是函数名,这个和 C++ 层的函数命名是一致的,第二个参数在 SpiderMonkey 调用 JS 层对应的 C++ 函数时的回调函数,这个函数我们之前的文章中分析过。第三个参数是函数调用时的参数个数。最后一个参数,是一些访问特性,JSPROP_PERMANENT 表示不可删除,JSPROP_ENUMERATE 表示在枚举时可见(JS 的 for 遍历)。
jsb_cocos2d_Sprite_prototype = JS_InitClass(
cx, global,
jsb_cocos2d_Node_prototype,
jsb_cocos2d_Sprite_class,
dummy_constructor<cocos2d::Sprite>, 0, // no constructor
properties,
funcs,
NULL, // no static properties
st_funcs);
// make the class enumerable in the registered namespace
JSBool found;
JS_SetPropertyAttributes(cx, global, "Sprite", JSPROP_ENUMERATE | JSPROP_READONLY, &found);
因为 JS 使用的是原型继承,那么我们需要构造一个原型,需要的参数也很多,都是我们上面配置好的,上下文,父对象,原型,JSClass 对象,各种属性和函数。然后,会自动把这个原型设置为 global(就是之前的 cc 对象)的一个属性。最后,设置好访问特性。
TypeTest<cocos2d::Sprite> t;
js_type_class_t *p;
std::string typeName = t.s_name();
if (_js_global_type_map.find(typeName) == _js_global_type_map.end())
{
p = (js_type_class_t *)malloc(sizeof(js_type_class_t));
p->jsclass = jsb_cocos2d_Sprite_class;
p->proto = jsb_cocos2d_Sprite_prototype;
p->parentProto = jsb_cocos2d_Node_prototype;
_js_global_type_map.insert(std::make_pair(typeName, p));
}
最后这段代码是 Cocos2d-x 引擎自己做的一个设计,把类型信息存到一个 map 里,这个设计以后会经常见到。可以用来查询,另外在 JS 虚拟机清空时,也用来遍历删除对应的类型信息。做法是先放到一个 map 里,然后 cleanup 时遍历这个 map 即可。就不赘述了。
15
傀儡构造函数
上篇我们以 Sprite 为例,分析了注册函数。但其中我们似乎遗漏了一个地方,那就是构造函数。因为 Cocos2d-x 在 C++ 层使用的是工场函数来生成对象,而不是构造函数。所以在 JS 层代码中,也需要有相应的对应机制来处理这件事。
看一下 jsb_cocos2dx_auto.hpp
extern JSClass *jsb_cocos2d_Sprite_class;
extern JSObject *jsb_cocos2d_Sprite_prototype;
JSBool js_cocos2dx_Sprite_constructor(JSContext *cx, uint32_t argc, jsval *vp);
void js_cocos2dx_Sprite_finalize(JSContext *cx, JSObject *obj);
void js_register_cocos2dx_Sprite(JSContext *cx, JSObject *global);
void register_all_cocos2dx(JSContext* cx, JSObject* obj);
这声明了几个重要的对象和函数。JSClass 对象和原型对象、注册函数、自己实现的 finalize 的 Stub 等。但是我们发现js_cocos2dx_Sprite_constructor 构造函数并没有对应的实现代码,仅仅是一个声明而已。
需要注意的是,根据 JS 的原型继承,我们在生成 jsb_cocos2d_Sprite_prototype 原型时,需要传入一个构造函数,而构造函数 js_cocos2dx_Sprite_constructor 又是未实现的,那么他是如何做到的呢?
在 js_register_cocos2dx_Sprite 函数中查看生成 jsb_cocos2d_Sprite_prototype 原型的代码:
jsb_cocos2d_Sprite_prototype = JS_InitClass(
cx, global,
jsb_cocos2d_Node_prototype,
jsb_cocos2d_Sprite_class,
dummy_constructor<cocos2d::Sprite>, 0, // no constructor
properties,
funcs,
NULL, // no static properties
st_funcs);
注意到第五个参数是一个模板函数 dummy_constructor,字面意思是傀儡构造函数。
看一下这个模板函数的定义
template<class T>
static JSBool dummy_constructor(JSContext *cx, uint32_t argc, jsval *vp) {
JS::RootedValue initializing(cx);
JSBool isNewValid = JS_TRUE;
JSObject* global = ScriptingCore::getInstance()->getGlobalObject();
isNewValid = JS_GetProperty(cx, global, "initializing", &initializing) && JSVAL_TO_BOOLEAN(initializing);
if (isNewValid)
{
TypeTest<T> t;
js_type_class_t *typeClass = nullptr;
std::string typeName = t.s_name();
auto typeMapIter = _js_global_type_map.find(typeName);
CCASSERT(typeMapIter != _js_global_type_map.end(), "Can't find the class type!");
typeClass = typeMapIter->second;
CCASSERT(typeClass, "The value is null.");
JSObject *_tmp = JS_NewObject(cx, typeClass->jsclass, typeClass->proto, typeClass->parentProto);
JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(_tmp));
return JS_TRUE;
}
JS_ReportError(cx, "Don't use `new cc.XXX`, please use `cc.XXX.create` instead! ");
return JS_FALSE;
}
这个函数首先使用了 JS::RootedValue 类型的量来判断 GlobalObject 对象是否初始化完毕。JS::RootedValue 具体的原理暂时不用深究,你只需要知道这是 SpiderMonkey 引擎的一种内存管理方式即可。
然后使用了一个非常有趣的技巧,用一个模板类 TypeTest t,取出对应的类型名。这是一个很不错的写法,能够不破坏函数签名,使得函数能够匹配 JS_InitClass 的参数类型,又能够在不同的上下文中里面获得需要的信息。我们看一下 TypeTest 的实现,这种写法在很多时候有很大的借鉴意义!
template< typename DERIVED >
class TypeTest
{
public:
static const char* s_name()
{
// return id unique for DERIVED
// ALWAYS VALID BUT STRING, NOT INT - BUT VALID AND CROSS-PLATFORM/CROSS-VERSION COMPATBLE
// AS FAR AS YOU KEEP THE CLASS NAME
return typeid( DERIVED ).name();
}
};
最后我们在 _js_global_type_map 里查询对应的类型,取出相应的参数来调用 JS_NewObject 函数,生成对应的对象并设置为返回值。
16
使用 cocos2d-console 工具转换脚本为字节码
从 Cocos2D-X v2.1.4 版本开始,增加了 Cocos2D-console 命令行工具,该工具的其中一个功能是:把 .js 文件转换为 .jsc 文件,该文件是字节码格式,可以提高代码的安全性。
使用这个工具的方式很简单。以引擎自带的 TestJavaScript 项目为例: 首先我们 cd 到 Cocos2D-console 的目录
goldliontekiMacBook-Pro:~ goldlion$ cd /Users/goldlion/Documents/developer/cocos2d-x-3.0beta/tools/cocos2d-console/console
然后可以看到里面有很多 .py 脚本
cocos2d_jscompile.py
cocos2d_version.py
cocos2d.py
cocos2d_new.py
其中 cocos2d.py 是我们要使用的主脚本文件。使用命令 ./cocos2d.py jscompile --help 查看编译字节码的命令格式
goldliontekiMacBook-Pro:console goldlion$ ./cocos2d.py jscompile --help
Usage: cocos2d.py jscompile -s src_dir -d dst_dir [-c -o COMPRESSED_FILENAME -j COMPILER_CONFIG]
Options:
-h, --help show this help message and exit
-s SRC_DIR_ARR, --src=SRC_DIR_ARR
source directory of js files needed to be compiled,
supports mutiple source directory
-d DST_DIR, --dst=DST_DIR
destination directory of js bytecode files to be
stored
-c, --use_closure_compiler
Whether to use closure compiler to compress all js
files into just a big file
-o COMPRESSED_FILENAME, --output_compressed_filename=COMPRESSED_FILENAME
Only available when '-c' option was True
-j COMPILER_CONFIG, --compiler_config=COMPILER_CONFIG
The configuration for closure compiler by using JSON,
please refer to compiler_config_sample.json
参数非常简单,一个输入目录,一个输出目录,后面加一组可选参数。该工具在遍历 .js 文件时支持文件夹递归访问,在输出 .jsc 文件时支持按照源文件夹的结构全部新建文件夹。易用性还是不错的。
对 TestJavaScript 其中一个文件夹 ExtensionsTest 使用 Cocos2D-console 工具进行加密来测试。输出路径设置为桌面
./cocos2d.py jscompile -s /Users/goldlion/Documents/developer/cocos2d-x-3.0beta/samples/Javascript/Shared/tests/ExtensionsTest -d /Users/goldlion/Desktop/ExtensionsTest
打开输出的 ExtensionsTest 文件夹看到,所有 .js 都变成了 .jsc,并且体积都大幅度减小。
下面说一下可选参数,可选参数的意思是使用 closure compiler 工具压缩代码为一个文件。
COMPRESSED_FILENAME 是压缩后的文件名,最好使用 xxx.js,因为工具会自动再后面加个 c
COMPILER_CONFIG 是压缩时调用的配置文件,需要根据项目需求自己填写,在 bin 目录下有一个做好的缺省例子可以使用,compiler_config_sample.json
我并不建议使用这种做法,因为:
1. 如果将所有脚本都压缩为一个文件,那么每次更新都要重新下载这个文件,对于一些对省流量要求很高的公司不适合。
2. 压缩的目的是隐藏文件目录结构,但是这个工具只压缩了脚本部分,对于图片,动画,数据,音频视频等等都是不考虑的。而一般开发的方式需要把所有资源都压缩成一个文件,线更新时只下载更新档,通过程序将更新档中的文件打入到大文件中。注意这涉及到二进制级别的比较删除以及合并,需要做非常仔细的设计,
更多信息请访问
http://wiki.jikexueyuan.com/project/cocos2d-x-from-cplusplus-js/




