前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >重磅!H5游戏接入App已经解决了,民间SDK将会崛起!

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

作者头像
张晓衡
发布2020-07-21 14:30:30
1.9K0
发布2020-07-21 14:30:30
举报

作者:梦近在咫尺

前文导读

  《各种红包 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);因此我们也要编写我们的应用入口。

  • 入门级
代码语言:javascript
复制
public static void init(Application app, String appId){
  // TODO
}
  • 个人推荐
代码语言:javascript
复制
public static void init(Application app, SdkConfig config){
  // TODO
}
代码语言:javascript
复制
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 也有一套声明周期,一些统计事件可能需生命周期的回调,所以我们也要提供接口。

  • 初级写法:简单的方式是由用户自行调用,比如
代码语言:javascript
复制
public static void onResume(Contextcontext){
    // TODO
}
  • 高级写法:如果实现该写法,则尽量要保证用户调用 SDK 的时间够早(PS:其实可以要求用户必须在Application里面调用)。该方法的实现思想就是使用 Android 自身的声明周期回调,如下
代码语言:javascript
复制
public static void initSdk(Application app, SdkConfig config){
    // TODO
  app.registerActivityLifecycleCallbacks();
}

编写桥接文件

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

JS脚本

  JS脚本的主要作用是实现微信的 API,然后借助各引擎的交互连接 SDK,为了使用结构清晰,推荐各位根据各平台独立编写一个单独的js文件。例如我编写的针对CCC的脚本文件(部分):

代码语言:javascript
复制
(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 回调监听

代码语言:javascript
复制
public interface JSEval {
    void callJS(String code);
}

传入监听

代码语言:javascript
复制
private static List<JSEval> evals = new ArrayList<>();
public static void registerJSEval(JSEval eval){
    evals.add(eval);
}

用户层实现

代码语言:javascript
复制
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 的集成方式、广告的实现逻辑,如果单独一个章节,感觉又偏离主题,更像一个安卓开发的主题,不写,总感觉重要的东西丢失。只能蜻蜓点水提上几句,只能期待后期继续改进。

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Creator星球游戏开发社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 首先创建一个工程
  • 将应用设置成库
  • 应用入口
  • 生命周期
  • 编写桥接文件
    • JS脚本
      • 原生代码
        • 加载JS脚本
          • 调用脚本
            • JS 回调监听
            • 传入监听
            • 用户层实现
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档