当creator遇上protobufjs|青春升级记

pbkiller1.0已经上线Cocos商店,支持了微信小游戏环境,我录制了一段小视频,演示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星球游戏开发社区(creator-star)

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

原始发表时间:2018-01-20

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

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券