今年以来,虽然入职的是游戏行业,其实一直在做原生这块的东西,主要是做一个聚合类的SDK,方便其他厂商快速接入,目前安卓这块已经完成了,发现现在市场上对于小游戏转制APP的需求量比较大,其实与我做的聚合SDK有很多相似之处,因此提取出一部分主要思想,共同探讨,因为本人使用的是CocosCreator(以下简称CCC)引擎,所以主要参考用例为CCC,当然本身是支持任意引擎的,当然也包含纯H5游戏。
本文本身需要一定的安卓基础,但考虑到众多没有安卓经验,但是游戏开发者的需求,略微介绍下SDK制作的方式,如果你已有这方面的经验,可以忽略,众多基本介绍请自行百度,下面以主流且力推的AndroidStudio(以下简称AS)为例。
目前官方推荐kotlin+androidx,但考虑游戏行业的sdk多没有跟上,推荐各位选择java+android.support
如上,我们只是创建了一个应用,这并不是APP,所以我们需要打开项目(注意工程与项目的概念,与Eclipse不同,简单讲AS的根目录就是工程,项目是app、game这类文件夹(本身命名是可自定义的))的build.gradle,将appliction修改成library,以及删除applicationId。方便各位看到我修改的地方,请看我注释的两行。
如果对安卓比较熟悉,就知道库文件是没有上下文的,因此我们要项目传入上下文,相信接过安卓SDK的人都知道,sdk初始化常常是***.init/initSdk(Context context);因此我们也要编写我们的应用入口。
public static void init(Application app, String appId){
// TODO
}
public static void init(Application app, SdkConfig config){
// TODO
}
public class SdkConfig{
private String appId;
// TODO
public static class Builder {
private String appId;
public Builder setAppId(String appId){
this.appId = appid;
return this;
}
public SdkConfig build(){
SdkConfig config = new SdkConfig();
config.appId = appId;
// TODO
return config;
}
}
}
这样的好处是能够扩展参数,且是目前流行的链式语法。
类比CCC的组件声明周期,Android也有一套声明周期,一些统计事件可能需生命周期的回调,所以我们也要提供接口。
public static void onResume(Contextcontext){
// TODO
}
public static void initSdk(Application app, SdkConfig config){
// TODO
app.registerActivityLifecycleCallbacks();
}
虽然前面写了一大堆,其实都是准备工作,在这里才进行到我们的主题,既然原来的方式是用小游戏(js),那么需要在原生使用,则必须要桥接层。本章节主要介绍如何编写。
js脚本的主要作用是实现微信的api,然后借助各引擎的交互连接sdk,为了使用结构清晰,推荐各位根据各平台独立编写一个单独的js文件。例如我编写的针对CCC的脚本文件(部分):
(function(){
var clzName = "com/dc/sdk/DCJSBridge";
function BannerAd(id, style) {
var self = this;
this.id = id;
this.showResolve = null;
this.showReject = null;
this.loadCallbacks = [];
this.resizeCallbacks = [];
this.errorCallbacks = [];
this.style = {
width: style.width,
height: style.height,
_top: style.top,
_left: style.left
};
this.style.__defineGetter__('top', function () { return this._top });
this.style.__defineSetter__('top', function (top) {
try {
jsb.reflection.callStaticMethod(clzName, "setBannerAdTop", "(II)V", self.id, top);
} catch (e) { }
self._top = top;
});
this.style.__defineGetter__('left', function () { return this._left });
this.style.__defineSetter__('left', function (left) {
try {
jsb.reflection.callStaticMethod(clzName, "setBannerAdLeft", "(II)V", self.id, left);
} catch (e) { }
self._left = left;
});
/**
* 显示 banner 广告
* @returns {Promise} banner 广告显示操作的结果
*/
this.show = function () {
return new Promise(function (resolve, reject) {
self.showResolve = resolve;
self.showReject = reject;
try {
jsb.reflection.callStaticMethod(clzName, "showBannerAd", "(I)V", self.id);
} catch (e) {
reject();
}
});
};
/**
* 监听 banner 广告尺寸变化事件
* @param {Function} callback banner 广告尺寸变化事件的回调函数
*/
this.onResize = function (callback) {
this.resizeCallbacks.push(callback);
};
/**
* 监听 banner 广告加载事件
* @param {Function} callback banner 广告加载事件的回调函数
*/
this.onLoad = function (callback) {
this.loadCallbacks.push(callback);
};
/**
* 监听 banner 广告错误事件
* @param {Function} callback banner 广告错误事件的回调函数
*/
this.onError = function (callback) {
this.errorCallbacks.push(callback);
};
/**
* 取消监听 banner 广告尺寸变化事件
* @param {Function} callback banner 广告尺寸变化事件的回调函数
*/
this.offResize = function (callback) {
if (null == callback) {
this.resizeCallbacks = [];
return;
}
for (var i = 0; i < this.resizeCallbacks.length; i++) {
if (callback == this.resizeCallbacks[i]) {
this.resizeCallbacks.splice(i, 1);
i--;
break;
}
}
};
/**
* 取消监听 banner 广告加载事件
* @param {Function} callback banner 广告加载事件的回调函数
*/
this.offLoad = function (callback) {
if (null == callback) {
this.loadCallbacks = [];
return;
}
for (var i = 0; i < this.loadCallbacks.length; i++) {
if (callback == this.loadCallbacks[i]) {
this.loadCallbacks.splice(i, 1);
i--;
break;
}
}
};
/**
* 取消监听 banner 广告错误事件
* @param {Function} callback banner 广告错误事件的回调函数
*/
this.offError = function (callback) {
if (null == callback) {
this.errorCallbacks = [];
return;
}
for (var i = 0; i < this.errorCallbacks.length; i++) {
if (callback == this.errorCallbacks[i]) {
this.errorCallbacks.splice(i, 1);
i--;
break;
}
}
};
/**
* 隐藏 banner 广告
*/
this.hide = function () {
try {
jsb.reflection.callStaticMethod(clzName, "hideBannerAd", "(I)V", this.id);
} catch (e) {
}
};
/**
* 销毁 banner 广告
*/
this.destroy = function () {
try {
jsb.reflection.callStaticMethod(clzName, "destroyBannerAd", "(I)V", this.id);
} catch (e) {
}
};
return this;
}
var dc = window.dc || function(){};
dc.getSystemInfoSync = function(){
try {
var str = jsb.reflection.callStaticMethod(clzName, "getSystemInfo", "()Ljava/lang/String;");
// 防止特殊字符等,进行了Base64编码
if(null == str || "" == str) {
return null;
}
var msg = atob(str);
return msg;
} catch(e) {
return null;
}
};
var dc.bannerAds = [];
dc.createBannerAd = function (object) {
// TODO 判断是否缺少必须参数
try {
// 如果想性能高点,则使用对应参数
// var id = jsb.reflection.callStaticMethod(clzName, "createBannerAd", "(Ljava/lang/String;IIIII)I", object.adUnitId, object.adIntervals || 30, object.style.left, object.style.top, object.style.width, object.style.height);
// 如果想扩展方便,则可以直接传字符串,然后在java层进行解析
var id = jsb.reflection.callStaticMethod(clzName, "createBannerAd", "(Ljava/lang/String;)I", JSON.stringify(object));
if (id < 0) {
return null;
}
dc.bannerAds[id] = new BannerAd(id, object.style);
return dc.bannerAds[id];
} catch (e) {
return null;
}
};
})();
各引擎调用的方法基本类似,各位如果有对应的经验,其实非常简单。
这里其实是工作的一大重点,但是考虑各位这篇文章应该有基本的Sdk集成经验,而且工作相对重复且多,本文可能没法进行详述。
需要注意的是,这是sdk开发,我们不能像以往接sdk一样,某个地方突然想调用一下js就调用,而应该集中起来,先调用java,然后由java调用对应的js代码,好处是java原生游戏其实也能用这套逻辑,另外一点是下面提到的调用脚本的实现。
推荐各位多使用代理类,实现各个方法的接口,然后由代理类去调用各个sdk的具体实现。
js对安卓而言仅仅是一个资源,要引擎加载,则还需要引擎加载,H5的话,直接在index中加载该文件即可。而CCC则在main.js中。
现在我们解决了引擎调用原生的问题,那原生如何调用脚本呢?如果你是CCC开发人员,你可能马上想到了引擎提供的Cocos2dxJavascriptJavaBridge.evalString(String str);方法,可是我们是Sdk啊,总不能还集成CCC的引擎层吧,而且还有laya等其它引擎,这该怎么办呢?其实各位这里就不要陷入死胡同了,我们要做的是提供原生能力,而不是考虑实现,所以这里我们应该提供一个入口给原生,有用户决定js代码如何实现。
public interface JSEval {
void callJS(String code);
}
private static List<JSEval> evals = new ArrayList<>();
public static void registerJSEval(JSEval eval){
evals.add(eval);
}
DCAgent.registerJSEval(new DCAgent.JSEval() {
@Override
public void callJS(final String jsCode) {
runOnGLThread(new Runnable() {
@Override
public void run() {
Cocos2dxJavascriptJavaBridge.evalString(jsCode);
}
});
}
});
这样就简单的将跨引擎的事移交(甩锅)给用户了。
打包的方式有多种,可以是aar,或者流行的maven仓库,这个网上博客较多,不在赘述,仅提一句:注意混淆。
这个sdk其实更适合那些想把小游戏变成原生游戏的用户使用,但是H5大厅模式的,其实原理一样,只不过sdk就变成自己使用了,自己集成sdk,然后将主页变成大厅,根据点击实现各H5游戏的实现,即可实现一个简单的H5大厅游戏,当然其中的性能优化、对接就需要各位继续探索了。
一开始准备写的细节,写着写着突然就不知道该总结到哪,比如sdk的集成方式,广告的实现逻辑,如果单独一个章节,感觉又偏离主题,更像一个安卓开发的主题,不写,总感觉重要的东西丢失。只能蜻蜓点水提上几句,只能期待后期继续改进。
本文的经验来自我在公司的工作内容,因此有些不想关或者涉及重要核心的地方都省略了,另外代码也重新手敲,难免有些错误,敬请谅解。本文可能更加注重的是这个开发思维,更多细节,希望与我一起讨论。