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能为你节省时间,提高效率!