Cocos2d-JS 跨平台的调用方法
Cocos2d-JS 跨平台的调用方法
首先从我们ProjectM的开发背景开始,项目采用的Cocos2d-JS 引擎是为了发布到包括Web平台,iOS,Android,Mac,Windows等全平台。理论上只需修改少量代码就可打包为原生性能表现的混合游戏,支持全平台的同时也保证性能需求。
接下来就通过在ProjectM项目接入终端统一登录库的方法来看一下终端平台不同语言之间调用。
- Android终端平台
虽然使用JS开发在Web端验证之后在终端平台功能基本一致,但是终端设备上的登录模块是统一接入第三方SDK,必须针对不同平台单独接入。一般情况下第三方为了一次开发支持多个平台,业务逻辑使用C++开放,在封装成不同平台库提供接入。接下来就是接入第三方SDK库时涉及到的各种平台的调用方式。
1.1 从JS层调用到Java层
在用户点击登录按钮的时候,JS层响应点击事件然后通过系统平台的判断调用反射方法。
使用cocos2d-js 3.0以上版本中的一个新特性,在android平台上可以通过反射直接在js中调用java的静态方法:
if (cc.sys.OS_ANDROID == cc.sys.os) {
console.log("current platform is: cc.sys.OS_ANDROID");
jsb.reflection.callStaticMethod("org/cocos2dx/javascript/AppActivity", "requestLogin", "(Ljava/lang/String;)V", strPlatform);
return true;
}
在jsb.reflection.callStaticMethod方法中,需要传入对应Java的类名、方法名和参数返回值类型以及参数名称。对应的AppActivity类中的方法实现如下
public static void requestLogin(final String strPlatform) {
IMLogger.d("requestLogin platform = " + strPlatform);
Bundle bundleMsg = new Bundle();
bundleMsg.putString("key", strPlatform);
// send message
Message message = s_handler.obtainMessage();
message.what = HANDLER_LOGIN_REQ;
message.setData(bundleMsg);
s_handler.sendMessage(message);
}
通过消息机制来处理请求,立即让callStaticMethod返回。如果在此处需要有UI层的界面显示,需要启用UI线程来操作
AppActivity.this.runOnUiThread(new Runnable() {
......
}
因为在android平台,cocos的渲染和JS的逻辑是在GL线程中进行的,而android本身的UI更新是在app的UI线程进行的。
1.2 从Java层到C++层
上面提到了从JS层调用到Java层方法之后,再调用native 方法调用到C++层。
public native static void login();
调用到对应JNI接口实现
JNIEXPORT void JNICALL Java_org_cocos2dx_javascript_AppActivity_login(JNIEnv *, jclass);
接着就是具体的登录业务逻辑C++层的实现了。
添加登录模块过程中遇到了一个编译错误,是多个jni_onload导致的。查看了一下发现在引擎层的JNI接口文件里面也有实现 frameworksjs-bindingscocos2d-xcocosplatformandroidjavaactivity-android.cpp
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JniHelper::setJavaVM(vm);
return JNI_VERSION_1_4;
}
屏蔽掉了此处的实现,然后修改为使用main.cpp添加的jni_onload并且在里面添加了必要的初始化操作。
1.3 再从C++层到Java层
登录请求会设置观察者,在登录返回回调的时候调用观察者的回调接口返回登录结果信息。然后同样通过JNI接口调用到Java层方法:
jmethodID jshowstate = env->GetStaticMethodID("org/cocos2dx/javascript/AppActivity", "onLoginRsp", "(Ljava/lang/String;)V");
jstring jresult = env->NewStringUTF(result.c_str());
......
1.4 再从Java层到JS层
调用到Java层的静态方法之后
public static void onLoginRsp(final String strRet) {
Log.d(TAG, "onLoginRsp strRet = " + strRet);
Bundle bundleMsg = new Bundle();
bundleMsg.putString("key", strRet);
// send message
Message message = s_handler.obtainMessage();
message.what = HANDLER_LOGIN_RSP;
message.setData(bundleMsg);
s_handler.sendMessage(message);
}
在处理此处回调的时候需要注意的是,因为cocos的渲染和JS的逻辑是在GL线程中进行的,因此需要在GL线程中调用JS的方法。
case HANDLER_LOGIN_RSP:
Log.d(TAG, "msg.what == HANDLER_LOGIN_RSP");
String strRet = msg.getData().getString("key");
final String jsCallStr = String.format("onGameLoginRsp("%s");", strRet);
// call JS method, must be in GL thread
AppActivity.this.runOnGLThread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "runOnGLThread: jsCallStr == " + jsCallStr);
Cocos2dxJavascriptJavaBridge.evalString(jsCallStr);
}
});
break;
这里利用功能强大的evalString,它可以执行任何js代码,并且它可以访问到js代码中的对象。在JS代码中添加全局的函数onGameLoginRsp接收回调的返回参数和内容:
// login response from mobile device
function onGameLoginRsp(loginret) {
console.log("call from Java and IOS, onGameLoginRsp");
// connect to the server with response
var accessToken = loginret;
Login_Data.accessToken = accessToken;
console.log("onGameLoginRsp: Login_Data.accessToken= " + accessToken);
......
}
到这里Cocos2d-JS 在Android平台Native层的调用就完整跑了一遍了。
- IOS终端平台
对IOS平台存在同样的需求,需要从JS层调用到OC层然后再回调到JS层。
调用基本原理是向SpiderMonkey注册相应的从C++到JS的绑定函数,如引擎的一些方法
bool AppDelegate::applicationDidFinishLaunching()
...
ScriptingCore* sc = ScriptingCore::getInstance();
sc->addRegisterCallback(register_all_cocos2dx);
sc->addRegisterCallback(register_cocos2dx_js_core);
...
这里以最简单的创建一个Node 对象为例, JS代码如下:
var node = cc.Node.create();
node.setVisible(false);
那么经过SpiderMonkey执行后,会调用下面的代码:
auto node = CCNode::create();
node->setVisible(false);
还包括绑定和查找JS和C++对象的对应关系,包装参数为对应类型,类型安全检查,返回值包装等等...
调用到 frameworksjs-bindingsbindingsautojsb_cocos2dx_auto.cpp
JSBool js_cocos2dx_Node_create(JSContext *cx, uint32_t argc, jsval *vp)
{
if (argc == 0) {
cocos2d::Node* ret = cocos2d::Node::create(); //call C++ native
jsval jsret = JSVAL_NULL; // 一个JS对象
...
if (ret) {
// 模板函数 get_or_create 这就是把JS对象和C++对象绑到一起的函数
js_proxy_t *proxy = js_get_or_create_proxy<cocos2d::Node>(cx, (cocos2d::Node*)ret);
jsret = OBJECT_TO_JSVAL(proxy->obj);
...
}
绑定之后就可以通过对应的接口传入参数实现调用。
2.1 JS层到OC的调用方法
Cocos2d-JS3.0以上版本也提供了在iOS和Mac上js直接调用Objective-C的方法,调用方式如下:
console.log("current login platform is:" + strPlatform);
jsb.reflection.callStaticMethod("NativeClass", "requestLogin:", strPlatform);
同样是输入类名和方法名以及参数。对应OBC中的实现就是
@implementation NativeClass
// request login
+(void) requestLogin: (NSString *)strp {
NSLog(@"requestLogin(): strPlatform = %@", strp);
......
}
需要注意的是,JS到OC的反射仅支持OC中类的静态方法。当带有参数的时候,需要将对应的:也带上。参数应该为"requestLogin:",不要漏掉了他们之间的: 号。
2.2 OC到JS层的回调方法
这里使用到的是ScriptingCore::getInstance()->evalString 方法,与android 平台调用机制类似。
Native层回调返回strRet 对象,经过格式转换之后传入evalString 方法,调用到JS的对象和方法。
// call the js function
std::string jsCallStr = cocos2d::StringUtils::format("onGameLoginRsp("%s");", strRet.c_str());
NSLog(@"jsCallStr = %s", jsCallStr.c_str());
jsval ret;
ScriptingCore::getInstance()->evalString(jsCallStr.c_str(), &ret);
NSLog(@"jsCallStr = %s", jsCallStr.c_str());
调用到JS层的全局方法,就是上面Android平台调用的同一个方法,处理回调
// login response from mobile device
function onGameLoginRsp(loginret) {
console.log("call from Java and IOS, onGameLoginRsp");
......
}
小结:在接入第三方库或者开发公共功能模块的时候,都会用到上述的跨平台的调用方法。通常在Native层完成底层业务逻辑再接入到不同终端,通过JSB的反射和evalString 方法可以方便的到达我们的目的。
这里还转载一些使用过程中会很容易遇到的坑,在使用反射调用java代码的一些注意事项。示例如下:
var webUrl = "http://www.baidu.com";
if(cc.sys.os == cc.sys.OS_ANDROID){
this.addKeyListener();
this.webViewId = jsb.reflection.callStaticMethod("org/cocos2dx/lib/Cocos2dxWebViewHelper", "createWebView", "()I");
if(this.webViewId < 0)
return ;
jsb.reflection.callStaticMethod("org/cocos2dx/lib/Cocos2dxWebViewHelper", "setScalesPageToFit", "(IZ)V", this.webViewId, true);
jsb.reflection.callStaticMethod("org/cocos2dx/lib/Cocos2dxWebViewHelper", "loadUrl", "(ILjava/lang/String;)V", this.webViewId, webUrl);
}
遇到崩溃:
编译Android版本运行,在显示完百度主页以后,程序就崩溃了
错误:标识的是c++端,没有实现Java_org_cocos2dx_lib_Cocos2dxWebViewHelper_didFinishLoading 函数, 但是实际上已经在cocos2d-x/cocos/ui/UIWebViewImpl-android.cpp中实现了,并且已经编译进去了。
这是为什么呢?
答案其实是,我们在Cocos2d-JS 3.2版本的时候,做过的安装包体积自动缩减功能在作怪。
我们在安装包体积缩减的时候采用了 LOCAL_STATIC_LIBRARIES 的方式链接c++各个库文件。
这种链接方式会在在连接静态连接库的时候移除"dead code",就是调用者模块永远都不会用到的代码段和变量。
只是在js脚本中引用的webview对象,而在c++代码中并没有使用webview对象,这就导致了c++在生成so链接库的时候,将webview这部分代码给移除了。
解决方案:
只需要在我们的c++代码中显示调用以下即可。
在appDelegate.cpp中引入头文件,引入
#include "ui/UIWebView.h"
在函数 applicationDidFinishLaunching最后,加上
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
cocos2d::experimental::ui::WebView::create();
#endif