专栏首页Creator星球游戏开发社区重磅!H5游戏接入App已经解决了,民间SDK将会崛起!

重磅!H5游戏接入App已经解决了,民间SDK将会崛起!

作者:梦近在咫尺

前文导读

  《各种红包 App 最后都会整合游戏!App+游戏的变现模式分析》一文让晓衡有幸结识到了一位技术大佬「梦近在咫尺」,有时候真的是心想事成!大佬在第2天就写下了这篇文章手把手教大家开发 SDK,将会有更多的民间 SDK 崛起!

1

写在前面

  今年以来,虽然入职的是游戏行业,其实一直在做原生这块的东西,主要是做一个聚合类的 SDK,方便其他厂商快速接入,目前安卓这块已经完成了,发现现在市场上对于小游戏转制 APP 的需求量比较大,其实与我做的聚合 SDK有很多相似之处,因此提取出一部分主要思想,共同探讨,因为本人使用的是Cocos Creator(以下简称CCC)引擎,所以主要参考用例为 CCC,当然本身是支持任意引擎的,当然也包含纯H5游戏。

2

SDK制作

  本文本身需要一定的安卓基础,但考虑到众多没有安卓经验,但是游戏开发者的需求,略微介绍下SDK制作的方式,如果你已有这方面的经验,可以忽略,众多基本介绍请自行百度,下面以主流且力推的 Android Studio (以下简称AS)为例。

首先创建一个工程

  目前官方推荐 kotlin + androidx,但考虑游戏行业的 sdk 多没有跟上,推荐各位选择 java + android.support

将应用设置成库

  如上,我们只是创建了一个应用,这并不是 APP,所以我们需要打开项目(注意工程与项目的概念,与Eclipse不同,简单讲 AS 的根目录就是工程,项目是app、game这类文件夹(本身命名是可自定义的))的 build.gradle,将 appliction 修改成 library,以及删除 applicationId。方便各位看到我修改的地方,请看我注释的两行。

3

编写入口

应用入口

  如果对安卓比较熟悉,就知道库文件是没有上下文的,因此我们要项目传入上下文,相信接过安卓 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
}
  • 高级写法:如果实现该写法,则尽量要保证用户调用 SDK 的时间够早(PS:其实可以要求用户必须在Application里面调用)。该方法的实现思想就是使用 Android 自身的声明周期回调,如下
public static void initSdk(Application app, SdkConfig config){
    // TODO
  app.registerActivityLifecycleCallbacks();
}

编写桥接文件

  虽然前面写了一大堆,其实都是准备工作,在这里才进行到我们的主题,既然原来的方式是用小游戏(js),那么需要在原生使用,则必须要桥接层。本章节主要介绍如何编写。

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脚本

  js对安卓而言仅仅是一个资源,要引擎加载,则还需要引擎加载,H5的话,直接在index中加载该文件即可。而CCC则在main.js中。

调用脚本

  现在我们解决了引擎调用原生的问题,那原生如何调用脚本呢?

如果你是CCC开发人员,你可能马上想到了引擎提供的 Cocos2dxJavascriptJavaBridge.evalString(String str)方法,可是我们是 SDK 啊!总不能还集成 CCC 的引擎层吧,而且还有laya等其它引擎,这该怎么办呢?

其实各位这里就不要陷入死胡同了,我们要做的是提供原生能力,而不是考虑实现,所以这里我们应该提供一个入口给原生,有用户决定 JS 代码如何实现。

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);
            }
        });
    }
});

这样就简单的将跨引擎的事移交(甩锅)给用户了。

3

打包

  打包的方式有多种,可以是 aar,或者流行的 maven 仓库,这个网上博客较多,不在赘述,仅提一句:注意混淆。

4

H5大厅游戏APP如何修改

  这个 SDK 其实更适合那些想把小游戏变成原生游戏的用户使用,但是 H5 大厅模式的,其实原理一样,只不过 SDK 就变成自己使用了,自己集成SDK,然后将主页变成大厅,根据点击实现各H5游戏的实现,即可实现一个简单的H5大厅游戏,当然其中的性能优化、对接就需要各位继续探索了。

5

结尾

  一开始准备写的细节,写着写着突然就不知道该总结到哪,比如 SDK 的集成方式、广告的实现逻辑,如果单独一个章节,感觉又偏离主题,更像一个安卓开发的主题,不写,总感觉重要的东西丢失。只能蜻蜓点水提上几句,只能期待后期继续改进。

  本文的经验来自我在公司的工作内容,因此有些不想关或者涉及重要核心的地方都省略了,另外代码也重新手敲,难免有些错误,敬请谅解。本文可能更加注重的是这个开发思维,更多细节,希望与我一起讨论。

本文分享自微信公众号 - Creator星球游戏开发社区(creator-star),作者:梦近在咫尺

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 狂热「小工」的9款Creator游戏源码及图文教程,等你来拿!

    他在 Cocos 论坛上公开了自己9款小游戏作品,完成度相当之高,是不可多得的Creator学习资源,下面是论坛链接地址:https://forum.cocos...

    张晓衡
  • Creator3D 打砖块子弹发射,以及摄像机平滑移动控制!

    前面一篇教程《Creator3D图文教程【打砖块】》,我们讲了打砖块游戏中的 3D 物体的场景布局、材质资源、物理刚体与碰撞组件,接下来本篇文章重点介绍“子弹的...

    张晓衡
  • KUOKUO的趣味教程 | 小怪物的视野(2)

    本篇承接上一集故事《KUOKUO的趣味教程 | 进击的小怪诞生(1)》,看小怪是如何自我进化的!

    张晓衡
  • 一起用 HTML5 Canvas 做一个简单又骚气的粒子引擎

    前言 好吧,说是“粒子引擎”还是大言不惭而标题党了,离真正的粒子引擎还有点远。废话少说,先看 Demo:http://ol.weixin.qq.com/publ...

    腾讯Bugly
  • 社区开源框架网络模块:ConnectionManager详解

    地址:https://github.com/Golangltd/LollipopCreator

    李海彬
  • three.js 郭先生制作太阳系

    今天郭先生收到评论,想要之前制作太阳系的案例,因为找不到了,于是在vue版本又制作一版太阳系,在线案例请点击three.js制作太阳系(加载时间比较长,请稍等一...

    郭先生的博客
  • vue 中使用threejs

    tianyawhl
  • Flutter开发:TextField常用属性的使用

    在flutter开发过程中,掌握常用组件的使用是必备技能,flutter常用的组件和App开发时候常用的控件基本一模一样,只是使用的方式不一样罢了。

    三掌柜
  • TRTC学习之旅(三)-- 使用vue+ts集成互动直播

    上次我们已经用vue+ts实现了多人会议室的搭建,这次我们继续在上次项目的基础上,实现互动直播功能。

    黑眼圈云豆
  • 冬天到了,分享两款雪花特效代码

    小小鱼儿小小林

扫码关注云+社区

领取腾讯云代金券