美文网首页Cocos Creator
当creator遇上protobufjs—青春升级

当creator遇上protobufjs—青春升级

作者: 张晓衡 | 来源:发表于2018-01-27 17:33 被阅读1349次

    pbkiller1.0已经上线Cocos商店,支持了微信小游戏环境,我录制了一段小视频,演示pbkiller的使用流程和方法。


    pbkiller在微信小游戏中的使用

    在「奎特尔星球」除了介绍插件、工具以外,更重要的是将这些插件、工具的实现原理和方法分享给大家,共同学习一起进步。

    我曾在公众号上发过一篇《微信小游戏protobuf.js快速解决办法》,在这里给大家说声不好意思,这篇文章中的proto加载方案存在缺陷,具体问题如下图所示:

    当a.proto文件中import了b.proto文件,在成功加载a.proto文件后protobufjs内部在解析a.proto时会自动加载b.proto,此时会触发XMLHttpRequest API的调用,导致在微信小游戏环境出现错误。

    一、protobuf.js加载源码分析

    还是从protobuf.js源码入手,我增加了一些注释,方便理解:

    ProtoBuf.loadProtoFile = function(filename, callback, builder) {
        //参数解析,检查callback参数是否有效
        if (callback && typeof callback === 'object') 
            builder = callback,
            callback = null;
        else if (!callback || typeof callback !== 'function')
            callback = null;
        //callback存在,使用异步加载
        if (callback)
            //使用ProtoBuf.Util.fetch函数异步加载,
            //注意这里的写法很不爽,调用fetch函数后立即return了
            return ProtoBuf.Util.fetch(typeof filename === 'string' ? filename : filename["root"]+"/"+filename["file"], function(contents) {
                if (contents === null) {
                    callback(Error("Failed to fetch file"));
                    return;
                }
                try {
                    //加载成功,调用ProtoBuf.loadProto函数解析contents变量,转换为proto对象,通过callback函数返回
                    callback(null, ProtoBuf.loadProto(contents, builder, filename));
                } catch (e) {
                    callback(e);
                }
            });
    
        //callbcak不存在,使用同步方式,
        //通过ProtoBuf.Util.fetch的返回值,获取文件数据
        var contents = ProtoBuf.Util.fetch(typeof filename === 'object' ? filename["root"]+"/"+filename["file"] : filename);
        //加载成功,调用ProtoBuf.loadProto函数解析contents变量,转换为proto对象,通过return返回
        return contents === null ? null : ProtoBuf.loadProto(contents, builder, filename);
    };

    从源码中可以看出,protobufjs有两种加载模式:同步与异步。

    在《当creator遇上protobufjs|相遇》 一文中我们分析过ProtoBuf.Util.fetch函数,这里简单回顾一下:

    浏览器:使用XMLHttpRequest实现的同步、异步的proto文件加载。

    nodejs:异步使用fs.readFile,同步使用fs.readFileSync

    Cocos-JSB:我们介绍了伪装fs模块的办法调用jsb.fileUtils.getStringFromFile来解决。

    微信小游戏环境我的理解是:阉割+定制过的浏览器,它没有提供XMLHttpRequest API,这是导致protobuf.js失败的原因。

    后来我又尝试了在protobufjs 6.x中使用的方案,在ProtoBuf.loadProtoFile函数,使用cc.loader.load代替ProtoBuf.Util.fetch,采用异步加载的方式,同样存在存问题。

    在遇到问题时,以个人的能力不能很好的解决时,去逛一逛论坛是一个不错的想法。当我把问题一提出,第二天就有一位ID叫a1990091的热心朋友提供了一个思路:重写ProtoBuf.Util.fetch函数,在函数中检查当前是否为微信小游戏环境,然后可以利用微信提供的api去实现加载:

    此方法做的非常的漂亮,分别检测了JSB\微信\Web环境,提供不同的加载实现。可对我来说,的遗憾是pbkiller库对外一直提供的是同步加载方法,改为异步加载,对已经使用pbkiller用户不太友好,同步、异步如取舍呢?

    二、救命稻草cc.loader

    发完帖从论坛回到问题上,不能解决估计是睡不着了,头脑中一阵自言自语言,忽然想到cc.loader.getRes同步获取资源的接口与ProtoBuf.Util.fetch的同步方式一样,能否从这里下手呢?

    在这里先简单介绍一下cc.loader下的系列load函数。

    1. cc.loader.load(url, callback)

    cc.loader.load的url参数是从项目发布的根路径开始的完整路径,因此需要借助cc.url.raw函数来获取完整路径。

    例如:加载文件assets/resources/a.json

    cc.loader.load(cc.url.raw('resources/a.json'), (error, json) => {
        cc.load(json);
    });

    cc.loader.load除了可以加载当前项目资源,更重要的能力是加载其它远程服务器上的资源。只需要给出完整路径即可,但在浏览器上使用需要注意跨域问题。

    加载当前项目下resources目录下的资源,使用cc.loader.loadRes更为简单。

    更多用法请参考API文档:

    http://docs.cocos.com/creator/api/zh/classes/loader.html#load

    2. cc.loader.loadRes(url, callback)

    cc.loader.loadRes的url参数路径是以resources为根路径。

    例如:加载文件assets/resources/a.json

    cc.loader.loadRes('a.json', (error, json) => {
        cc.load(json);
    });

    cc.loader.loadRes的用法比cc.loader.load简单很多,也有没那么多参数重载的用法,API文档链接:http://docs.cocos.com/creator/api/zh/classes/loader.html#loadres

    3. cc.loader.loadResDir(url, callback)

    cc.loader.loadResDir顾名思义它是加载一个目录(及子目录),url同样以assets/resources目录作为根路径。

    例如:加载文件 assets/resources/json目录下有a.json、b.json两个文件

    cc.loader.loadResDir('json', (error, array) => {
         //array中包含a.json和b.json的内容
        cc.log(array);
    });

    4. cc.loader.getRes(url)

    cc.loader.getRes是cc.loader家族中唯一的同步资源获取函数。但是它有一个前提,需要被cc.loader.loadXXX加载成功过的资源才能使用,不然它会返回null。

    例如:加载文件 assets/resources/json/a.json

    //jsonA为null
    let jsonA = cc.loader.getRes('json/a.json');
    cc.loader.loadResDir('json', (error) => {
        //此时获取jsonB才有效    
        let jsonB = cc.loader.getRes('json/b.json');    
    });

    这里分享一个查看cc.loader缓存资源的一个方法,在浏览器中运行你的项目,在调试控制台上输入:cc.loader._catch,你会看到如下内容:

    cc.loader._catch对象中的所有资源,都可以使用cc.loader.getRes获取。讲到此处,我猜你已经大概知道怎么使用cc.loader.getRes解决微信小游戏中proto的加载问题了。

    三、cc.loader.getRes移花接木

    从分析cc.loader的系列加载函数,cc.loader.getRes去代替ProtoBuf.Util.fetch,同样使用同步方式,这样pbkiller.loadAll/ pbkiller.loadFromFile的接口用法可以保持不变。

    要想cc.loader.getRes的返回值有效,需要预先将资源加载到cc.loader的缓存中,因此提供了一个pbkiller.preload函数

    let ProtoBuf = require('protobufjs');
    preload(cb) {
        //运行时动态修改ProtoBuf.Util.fetch为cc.loader.getRes    
        ProtoBuf.Util.fetch = cc.loader.getRes.bind(cc.loader); 
        //使用cc.loader.loadResDir加载resources/pb目录所有文件    
        cc.loader.loadResDir('pb', (error, data) => { 
            //通知调用都,预加载完毕
            cb();
        }); 
    }

    简单几行代码解决了所有问题,而且没有修改protobuf.js任何一行源代码。再看下如何使用预加载函数:

    /预先加载proto文件到引擎缓存
    pbkiller.preload(() => { 
      //加载所有proto文件并动态生成proto对象
      let pb = pbkiller.loadAll();
      //实例化proto对象
      let player = new pb.grace.proto.msg.Player();
      ...
    });

    在实际项目中可以提前执行pbkiller.preload,以前所有的pbkiller的用法保持不变,利用javascript的动态属性赋值,特别是可以修改函数指针,基本上可以做到为所欲为,而且不需要修改源代码,有没有觉得特别爽呢?

    四、结语

    pbkiller的内核是protobuf.js,我所做的工作只是将protobuf.js适配到Cocos-JSB和微信小游戏环境,让其能正常工作。希望我的经验能对你有所帮助,愿pbkiller能为你节省时间,提高效率!

    相关文章

      网友评论

        本文标题:当creator遇上protobufjs—青春升级

        本文链接:https://www.haomeiwen.com/subject/mmedixtx.html