Cocos2d-x JSB + Cocos2d-html5 跨平台游戏开发(二)—— 遇到的坑

上一篇blog里总结了选择cocos2d-x javascript binding(后面简称jsb)和cocos2d-html5(后面简称h5)技术开发的原因,这篇总结一下半年来遇到的一些问题。希望能帮到遇到相同问题的人。

jsb和h5开发遇到的问题可以分为下面这几类:

  • javascript语言本身的坑以及对语言不熟悉造成的使用上的错误
  • 引擎本身的bug或者API不兼容
  • jsb和h5实现机制不同导致代码在浏览器里和native应用里行为不一致
  • 移动浏览器上只能使用Canvas模式限制了一些功能

〇、调试方法

掌握调试方法是解决问题的关键,当然对引擎的了解也很重要。调试方法不外乎断点调试、打log以及终极大法:二分注释,断点调试最为方便,但是也不是所有环境都支持。

  1. 如果是h5的问题,直接在浏览器里用调试器下断点或者打log进行调试。
  2. jsb环境下,如果是cocos2dx 3.0可以使用firefox断点调试js,2.x只能在js里打log配合c++的断点调试
  3. 如果是在移动浏览器遇到问题,可以参考这篇blog
    • 部分浏览器支持远程调试,比如ios的safari、android的chrome、UC、Firefox
    • 对于不支持远程调试的环境,比如各种Webview,需要借助Weinrejsconsole

一、javascript语言方面的问题

大多数jsb的开发者都是从C++转过来,而javascript和c++有很多概念上的不同。比如js里是没有类(class)这个概念的,而是使用原型继承的机制。jsb为了使得API和C++在“长相”上比较一致,使用一套用function + prototype模拟类方法。

1. js里的function对象和this指针

《Javascript 面向对象编程》这篇blog,如果搞不清楚会经常遇到this指针不正确的问题。

2. 非基础类型(array/object/function)类成员初始化陷阱:

var MySprite = cc.Sprite.extend({
    data: []
});
var sprite1 = new MySprite();
var sprite2 = new MySprite();
// 所有MySprite实例的data都指向同一个数组
cc.log(sprite1.data == sprite2.data); 

这段代码定义了一个MySprite类,包含一个_data成员数组,然后new了两个实例,这两个实例里的data成员实际上指向的是一个同一个数组,如果一个实例修改了data数组,其他所有实例的data都会变。阅读CCClass.js里基于John Resig’s Simple Class Inheritance的继承实现就会发现原因,new MySprite()会把cc.Sprite.extend的参数对象{data:[]}里每一个属性赋值到新的实例上。所以如果是array/object/function属性,只能把赋初值放到ctor方法里。

二、引擎本身的bug或者API不兼容

jsb+h5开发里经常遇到的一个问题是代码在浏览器里和native应用里行为不一致,大多数是h5正常而jsb出问题,其原因是因为jsb和h5在引擎部分用的是两套代码和实现方式,jsb的实现多了一层c++个js交互的binding层,大大增加了bug得出现几率。

API不兼容相对来说比较好处理,引擎的bug解决起来比较麻烦。H5的问题比较好定位,因为可以通过浏览器调试器断点调试,而native里暂时还不能实现js的单步调试(3.0里已经可以),所以只能在js用log调试配合native代码的断点来debug。

1. cc.Sequence.create如果传入数组会报错

用下面的方法代替

cc.Sequence.createWithArray = function(arr) {
    if (arr.length === 0) {
        return cc.Sequence.create();
    } else if (arr.length === 1) {
        return cc.Sequence.create(arr[0])
    } else if (arr.length === 2) {
        return cc.Sequence.create(arr[0], arr[1]);
    } else {
        var last = arr.pop();
        return cc.Sequence.create(cc.Sequence.createWithArray(arr), last);
    }
};

2. cc.Sprite.setFlipX方法名不一致

jsb里叫setFlipX/setFlipY,而h5里叫setFlippedX和setFlippedY,调用的时候需要做兼容性处理,比如:

this.setFlipX ? this.setFlipX(true) : this.setFlippedX(true)

3. 某些方法只在jsb里或者只在h5里有实现

比如CCPointExtension.js里的很多方法只在h5里有,这种问题处理起来比较简单,因为方法是纯js实现,直接copy到jsb的js代码里就行了,一般是xxx-jsb.js那个入口文件。

再比如cc.Node.boundingBoxToWorld这个方法也是只在h5实现了,但是jsb不是很容易直接用js实现,因为这个方法用到好几个jsb没有的方法,在js里实现还不如直接在C++里实现然后binding到js方便,因为jsb里私有成员和私有方法都是没有绑定的。

4. jsb里有好几个类没有binding

CCRemoveSelf、CCClippingNode没有binding。原因是jsbinding的配置文件里写漏了。手动修改tools/tojs/cocos2dx.ini,把CCClippingNode和CCRemoveSelf加到classes后面,并把CCClippingNode从abstract_classes里去掉。

5. jsb往localStorage里存中文会乱码

需要手动merge这个pull解决: #3901

6. jsb里XMLHttpRequest返回结果中文乱码

和上个问题一样的bug,需要手动merge这个commit解决。

7. jsb里cc.Scheduler.pauseAllTarges和cc.ActionManger.pauseAllRunningActions没有返回暂停的targets数组

这两个函数在处理游戏暂停逻辑的时候还是蛮有用的。要解决这个问题,需要手动修改jsb_cocos2dx_auto.cppjs_cocos2dx_CCActionManager_pauseAllRunningActions方法,将c++方法返回的CCSet转成一个js数组然后返回给js

三、 jsb和h5实现机制不同导致代码在浏览器里和native应用里行为不一致

代码在浏览器里和native应用里行为不一致的原因除了引擎的bug之外,还有一个重要原因:jsb和h5实现机制不同。最常见的就是内存管理的问题。

1. jsb里报“Invalid Native Object”

这个问题几乎所有用jsb的开发者都会遇到,其原因涉及到jsb得工作方式。jsb里的一个cc.Sprite实例实际上是由一个js object + 一个c++的CCSprite实例构成。js object的生命周期由js引擎的垃圾回收器管理,而c++对象则是由cocos2dx的autorelease pool管理。这种工作模式下会出现c++里的CCSprite已经被销毁(比如被removeFromParent了),但是js object因为还有别的地方引用没有回收的情况。在这种情况下再尝试调用Sprite的方法,就会出现“Invalid Native Object”错误。

而在h5里Sprite是个纯js对象,即使从场景里remove掉,只要别的地方有引用,就不会被垃圾回收期销毁,所以不会出现问题。

这个错误本质上还是代码逻辑的问题,在对象生命周期结束后尝试访问对象。如果真到要在remove后访问则必须显式的retain。

2. js里覆盖父类的方法在jsb里无效

这个问题坑了我好几天,我实现了一个MySprite覆盖了cc.Sprite的setPosition和getPosition方法,h5里一切正常,但是jsb里MySprite实例调用的还是原来的setPosition,我的代码根本没有执行,查了几天终于发现jsb在绑定属性时又几个Mask,cocos2dx一般用的是JSPROP_READONLY|JSPROP_ENUMERATE|JSPROP_PERMANENT,代表只读、可遍历、不可删除。一旦被设置为JSPROP_READONLY,就代表无法修改,所以覆盖根本没生效,手动把CCSprite::setPosition的binding代码里的JSPROP_READONLY去掉就ok了。

cocos2dx里使用JSPROP_READONLY本意是防止代码里的错误意外破坏了引擎的功能,但同时进一步增加了jsb和h5的实现差异,而且限制了覆盖父类函数,实乃画蛇添足。

3. 多态特性不能跨域js和c++

这个问题和上个问题都出现在我在js里继承引擎的一个CCNode子类,比如CCSprite,并且尝试覆盖其现有方法实现的情况。一个典型的例子是CCScrollView,他就覆盖了CCNode得addChild、setContentSize等方法。

问题的具体现象是,我自定义的MySprite在js里覆盖了一个方法比如setOpacity,但是引擎的c++代码调用MySprite实例的setOpacity方法还是原来的c++实现,并不会调用js里覆盖的方法。这个原因非常明显,c++的代码调用并没有检查当前的对象是不是一个js controlled的实例,所以根本不知道我在js端覆盖了这个方法。

这个问题带来影响是,在jsb里完全没有办法在js端通过继承和覆盖来改变父类的一些行为。比如下面的代码:我在js离实现了一个PhysicsSprite,覆盖了setPosition方法,在里面增加了同步物理引擎的逻辑。

var PhysicsSprite = cc.Sprite.extend({
    setPosition: function(x, y) {
        cc.log("PhysicsSprite setPosition is called.");
        this._super(x, y);
        this._body.setPos(x, y);
    }
});
var ps = new PhysicsSprite();
// 没有问题,覆盖的setPosition被调用了
ps.setPosition(0, 0);
// 出问题了!覆盖的setPosition没被调用
ps.runAction(cc.MoveTo.create(1, cc.p(100, 100))); 

在jsb环境里,move action的update方法不会调用我在js里实现的setPosition,因为jsb里的Action都是从c++ binding到js得,update是在c++里实现的,所以只能调用到c++里CCSprite的setPosition。

这个问题没有好的解决办法,能绕过只就绕过他,比如上面的问题可以schedule一个update,每帧同步position。实在不行就只能写两份代码,在c++里实现一个PhysicsSprite binding到jsb里,然后再给h5实现一个js版本

以上的问题都是基于cocos2d-x 2.2.3和cocos2d-html 2.2.2版本的。最新的3.0版本有一些问题已经修复了,估计也会有一些新问题。。手上这个项目结束后,应该会开始迁移到3.0版本,希望3.0的坑会少些。

This entry was posted in 技术学习. Bookmark the permalink.

5 Responses to Cocos2d-x JSB + Cocos2d-html5 跨平台游戏开发(二)—— 遇到的坑

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据