在 Cocos Creator 中使用 Protobufjs(一)
一. 环境准备
我一直在探索Cocos H5正确的开发姿势,目前做javascript项目已经离不开 nodejs、npm或grunt等脚手架工具了。
1.初始化package.json文件
npm init
当新建好cocos-js或creator项目,在项目根目录使用npm init命令,一路回车,将在当前目录创建package.json文件用于nodejs三方模块的管理。关于npm的使用细节网络上有很多教程,在此不用细说。
2. protobufjs模块

本人最早在cocos2dx 2.x时代就开始用protobufjs模块来操纵protobuf一直到现在。所以下面所有内容都是关于protobufjs在cocos creator中的使用,包括原生平台(cocos2d-js也是大同小异)。
安装protobufjs到项目
npm install protobufjs@5 --save
使用npm install命令安装模块,注意我们这里使用的是protobufjs 5.x版本。 虽然protobufjs目前最新的 6.x版本,提供了ts、rpc等功能的支持,但有一个问题是在微信小游戏中不能动态加载proto文件。
安装protobufjs到全局
npm install -g protobufjs@5
使用npm install -g 参数将模块安装到全局,目的主要是方便使用protobufjs提供的pbjs命令行工具。pbjs可以将proto原文件转换成json、js等,以提供不同的加载proto的方式,我们可以根据自己的实际情况选择使用。
二. protobufjs用法
下面是demo中定义的Player.proto文件的内容
syntax = "proto3";
package grace.proto.msg;
message Player {
uint32 id = 1; //唯一ID 首次登录时设置为0,由服务器分配
string name = 2; //显示名字
uint64 enterTime = 3; //登录时间
}
关于proto具体语法细节这里就不多说了,我们重点如何将Player.proto文件中定义的Player对象在js中实例化、属性赋值、序列化、反序列化操作。
1. 静态语言中使用proto文件
在c++/java这类静态语言中使用protobuf通常是使用官方提供的protoc命令将proto文件编译成c++/java代码,像下面这样:
protoc --cpp_out=输出路径 xxx.proto
protoc --java_out=输出路径 xxx.proto
将输出路径的文件导入对应语言的工程中使用。
2. 在creator项目中使用proto文件

javascript是动态语言,可以在运行时产生对象,因此protobufjs提供了更为便捷的动态编译,将proto文件中的对象生成js对象,下面简要讲解一下在creator中具体的使用步骤:
1.加载proto文件并编译生成proto对象
//导入protobufjs模块
let protobuf = require("protobufjs");
//获取一个builder对象
let builder = protobuf.newBuilder();
//使用protobufjs加文件,并与一个builder对象关联
protobuf.protoFromFile('xxx.proto', builder);
protobuf.protoFromFile('yyy.proto', builder);
...
let PB = builder.build('grace.proto.msg');
这步操作主要是使用protobufjs加载、编译proto文件。
2.实例化proto对象与属性赋值
let PB = builder.build('grace.proto.msg')
build函数返回值PB对象中将包含的是在proto中定义所有message对象,现在已经成为js对象,可以被实例化,代码如下:
//实例化Player
let player = new PB.Player();
//属性赋值
player.name = '张三';
player.enterTime = Date.now();
3.proto对象的序列化与反序列化
不说废话,还是直接上代码
...
//使用实例对象上的toArrayBuffer函数将对象序列化为二进制数据
let data = player.toArrayBuffer();
//使用类型对象上的decode函数将二进制数据反序列化为实例对象
let otherPlayer = PB.player.decode(data);
如果幸运你可以在web上使用protobuf了, 为什么只是在web上呢,当你把上面的代码运行在jsb环境下的时候,你会体验到悲催的事情正在发生。
三. 拯救cocos-jsb上的protobufjs
为什么在原生上运行就挂掉了呢?要理解这个问题需要对nodejs\ 浏览器\cocos-jsb这三个javascript的运行宿主环境有一定的了解。
我之前的文章提到过在选择nodejs模块时,要注意是否同时支持nodejs和web,只要是纯js的模块在cocos中一般都可以随便用,比如async、undersocre、lodash等。
protobufjs这个模块是可以很好的在浏览器和nodejs环境上运行的。但运行在cocos-jsb上就会出问题,首先我们要定位到出问题的关键代码:
protobuf.protoFromFile('xxx.proto', builder);
1. 问题分析
从protobuf.protoFromFile函数名上看就知道是要进行文件的加载,一想到文件加载,就涉及到文件操作的api,我们来整理一下不同平台上的文件接口:
| 宿主平台 | 文件接口 | 说明 |
|---|---|---|
| 浏览器 | XMLHttpRequest | 浏览器中动态加载资源、文件等AJAX操作的基础 |
| nodejs | fs.readFile / fs.readFileSync | nodejs上的文件操作模块,底层由c/c++实现 |
| cocos-jsb | jsb.fileUtils.getStringFromFile | cocos-js提供的读取文件内容接口,在不台平台(ios\android\windows)由不同底层api实现 |
看到这里相信很多人已经明白为什么在cocos-jsb上会有问题了,我们再来读一下protobufjs源码,证实下我们的分析。
2. 分析protobufjs源码

找到protobufjs加载文件的主要代码,下面我为源码加上了注释,请认真读一下注释内容:
Util.fetch = function(path, callback) {
//检查callback参数,callback参数决定是否为异步加载
if (callback && typeof callback != 'function')
callback = null;
//运行环境是否为nodejs
if (Util.IS_NODE) {
//加载nodejs的文件系统模块
var fs = require("fs");
//检查是否有callback,存在使用fs.readFile异步函数读取文件内容
if (callback) {
fs.readFile(path, function(err, data) {
if (err)
callback(null);
else
callback(""+data);
});
} else
//使用fs.readFileSync同步函数读取文件内容
try {
return fs.readFileSync(path);
} catch (e) {
return null;
}
} else {
//当不为nodejs运行环境使用XmlHttpRequest加载文件
var xhr = Util.XHR();
//根据callbcak参数是否存在,使用异步还是同步方式
xhr.open('GET', path, callback ? true : false);
// xhr.setRequestHeader('User-Agent', 'XMLHTTP/1.0');
xhr.setRequestHeader('Accept', 'text/plain');
if (typeof xhr.overrideMimeType === 'function') xhr.overrideMimeType('text/plain');
//通过XmlHttpRequest.onreadystatechange事件函数异步获取文件数据
if (callback) {
xhr.onreadystatechange = function() {
if (xhr.readyState != 4) return;
if (/* remote */ xhr.status == 200 || /* local */ (xhr.status == 0 && typeof xhr.responseText === 'string'))
callback(xhr.responseText);
else
callback(null);
};
if (xhr.readyState == 4)
return;
//调用send方法发起AJAX请求
xhr.send(null);
} else {
////调用send方法发起AJAX请求,同步获取文件数据
xhr.send(null);
if (/* remote */ xhr.status == 200 || /* local */ (xhr.status == 0 && typeof xhr.responseText === 'string'))
return xhr.responseText;
return null;
}
}
};
从上面的代码可以看出protobufjs库是为浏览器和nodejs准备的,根本就没考虑过cocos-jsb的存在(吐槽:建议cocos官方提供的接口能模仿nodejs这样能少很多事),所以要在cocos-jsb中使用protobufjs其中的一个办法就是修改protobufjs的源码,如下:
Util.fetch = function(path, callback) {
if (callback && typeof callback != 'function')
callback = null;
//将平台检查代码改为cocos提供的接口
if (cc.sys.isNative) {
//文件读取使用cocos-jsb提供的函数
try {
let data = jsb.fileUtils.getStringFromFile(path);
cc.log(`proto文件内容: {data}`);
return data;
} catch (e) {
return null;
}
} else {
//web端无需修改,略
...
};
我们用cocos的接口将代码修改一下,加载问题就被化解了,问题真的被解决了吗?
不好意思,除了上面要代码外还有一处代码需要修改,源码如下:
BuilderPrototype["import"] = function(json, filename) {
var delim = '/';
// Make sure to skip duplicate imports
if (typeof filename === 'string') {
//这里又出现了平台检查
if (ProtoBuf.Util.IS_NODE)
// require("path")是加载nodejs的path模块,resolve
filename = require("path")['resolve'](filename);
if (this.files[filename] === true)
return this.reset();
this.files[filename] = true;
} else if (typeof filename === 'object') { // Object with root, file.
var root = filename.root;
//这里还要修改
if (ProtoBuf.Util.IS_NODE)
root = require("path")['resolve'](root);
if (root.indexOf("\\") >= 0 || filename.file.indexOf("\\") >= 0)
delim = '\\';
var fname;
//这里还要修改
if (ProtoBuf.Util.IS_NODE)
fname = require("path")['join'](root, filename.file);
else
fname = root + delim + filename.file;
if (this.files[fname] === true)
return this.reset();
this.files[fname] = true;
}
...
}
这里我就不再贴修改代码了,大家自行解决。
四 为protobuf继续填坑
本来写到这里,问题大多已经解决了, 但此时,如果你满怀信心地使用改造后的protobufjs源码,将你的代码运行起来那一刻,我相信绝大多数人会一脸蒙逼。

妈的根本就不行!!看了好多字,好不容易读到这里,不仅在模拟器上跑不起来,在web上同样也跑不起来。
怎么办,为了彻底解决问题,我还得继续写下去。
1. 了解creator动态加载资源的方法
请大家思考一个问题,creator项目中的一张图片,在web与cocos-jsb上他们的文件路径会一样吗?直接使用protobuf.protoFromFile('xxx.proto')去加载一个proto文件会成功吗?
cocos文档中说过要动态加载一个图片资源需要将文件存放在assets/resources目录下,使用如下方法加载:
cc.loader.loadRes('resources/xxx')
尝试将proto文件存放在resources/pb/目录下,用使用以下代码:
protobuf.protoFromFile('resources/pb/xxx.proto')
同样会得到失败的提示,该如何办呢?怎么才能获得正确的资源路径?
算了,不买关子了,写累了直接出答案吧!
protobuf.protoFromFile(cc.url.raw('resources/pb/xxx.proto'));
cc.url.raw这个函数在浏览器、模拟器、手机上会返回不同的资源路径,这才是真正的资源路径,这下代码应该可以正常运行起来了。
2. 更好的解决法办
我一直在探索Cocos H5正确的开发方式,虽然通过修改protobufjs源码的方法可以来解决在cocos-jsb上运行的问题,但这并不是唯一的解决方案。
如何在不修改protobufjs源码的情况下让代码运行起来,以及使用pbjs工具预编译proto文件为JSON和js文件的用法,请继续观注我的系列文章《当Creator遇上protobufjs》!

在 Cocos Creator 中使用 Protobufjs(一)的更多相关文章
- Cocos Creator 中 _worldMatrix 到底是什么(上)
Cocos Creator 中 _worldMatrix 到底是什么(上) 1. (矩阵)Matrix是什么,有什么用 (矩阵)Matrix一个神奇的存在?在开发过程中对里边各项值的含义是不是抓耳挠腮 ...
- Cocos Creator 中 _worldMatrix 到底是什么(中)
Cocos Creator 中 _worldMatrix 到底是什么(中) 1. 中篇摘要 在上篇中主要做了三件事 简单表述了矩阵的基本知识,以及需要涉及到的三角函数知识 推导了图形变换中 位移 .旋 ...
- Cocos Creator中按钮组件数组的使用
Cocos Creator游戏开发中经常使用到按钮,特别是大量按钮的情况,此时使用数组来管理这些按钮就显得更具通用性.我大致走了一下官方的示例,好像没有发现有这个小内容(或者有,但我却是没有找到),于 ...
- kbengine_js_plugins 在Cocos Creator中适配
kbengine_js_plugins 改动(2017/7/6) 由于Cocos Creator使用严格模式的js,而原本的kbengine_js_plugins是非严格模式的,因此为了兼容和方 便C ...
- Cocos Creator 中的动作系统那些事儿
动作系统就是可以在一定的时间内实现位移.旋转.缩放.跳动等各种动作. 需要注意的是,动作系统跟 Cocos Creator 编译器的动画系统不同,动作系统是面向程序员的API接口,而动画系统是通过编译 ...
- cocos creator 中的粒子效果
途中的粒子效果,通过plist文件和png两个文件,创建一个粒子节点,将plist文件拖入到粒子节点的file属性中,然后给custom属性打钩,把png文件拖入到texture属性中即可.
- Cocos Creator中使用事件中心
export class EventCenter { /** 监听数组 */ private listeners = {}; /** * 注册事件 * @param name 事件名称 * @para ...
- cocos creator中粒子效果的使用
就如同上图的星星特效一样,在触碰时产生特效,但是并不销毁节点,因为要使用很多次,因此使用节点池NodePool保存起来的. 以下是使用粒子效果使要使用到的一些基本控制函数: 我的使用:
- 欢乐水杯(happy glass)中流体的一种实现!图文视频讲解 ! Cocos Creator!
使用cocos creator v2.2.2 实现流体效果 ! 图文+视频讲解! 效果预览 实现原理 整体思路是参考论坛中的一个帖子 这款游戏中水的粘连效果在Construct3中利用图层很容易实现, ...
随机推荐
- Aizu-2224Save your cats并查集+最小生成树
Save your cats 题意:存在n个点,有m条边( input中读入的是 边的端点,要先转化为边的长度 ),做一个最小生成树,使得要去除的边的长度总和最小: 思路:利用并查集和求最小生成树的方 ...
- hdu 4825 Xor Sum(01字典树模版题)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4825 题解:一到01字典树的模版题,01字典树就是就是将一些树用二进制放到一个树上这样可以方便对整体异 ...
- hud 3555 Bomb 数位dp
Bomb Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 131072/65536 K (Java/Others) Total Subm ...
- 《MySQL实战45讲》学习笔记3——InnoDB为什么采用B+树结构实现索引
索引的作用是提高查询效率,其实现方式有很多种,常见的索引模型有哈希表.有序列表.搜索树等. 哈希表 一种以key-value键值对的方式存储数据的结构,通过指定的key可以找到对应的value. 哈希 ...
- redis持久化的两种方式RDB和AOF
原文链接:http://www.cnblogs.com/tdws/p/5754706.html Redis的持久化过程中并不需要我们开发人员过多的参与,我们要做的是什么呢?除了深入了解RDB和AOF的 ...
- linux安装python串口工具pyserial遇到不能成功导入的问题
常规方法:pip install pyserial导入serial后提示: 解决方法:apt install python3-serial 参考:https://stackoverflow.com/q ...
- Xpath定位和CssSelector定位的区别
1.Xpath基本语法: (1)"/":从根节点选取,下级节点 (2)"//":任意节点选取 (3)"*":任意标签选取 (4)" ...
- StackOverflow 周报 - 第四周高质量问题的问答(Java、Python)
这是 Stack Overflow 第三周周报,由于本周周四外出,所以只有三篇内容.两篇 Java.一篇 Python.公众号「渡码」为日更,欢迎关注. DAY1. 枚举对象 == 和 equals ...
- mybatis if判断两个值是否相等存在的坑啊
1.使用“==”比较 字符类型 的值 用“==”比较的使用场景: 不管你用的什么类型的变量,只要变量的值是字符类型就用“==” 产生原因: 在mybatis中如果<if>标签用一个“=”判 ...
- App引流增长技术:Deeplink(深度链接)技术
移动互联网时代,信息的分享传播无疑是 App 引流增长的关键,与其花费大量精力和成本找渠道.硬推广,不如从细节下手,用最快最简便的方法实现 Deeplink(深度链接)技术,打破信息孤岛.缩短分享路径 ...