cocos creator使用protobuf详细方案

一、环境

  • protobufjs:6.x.x、7.x.x版本皆可
  • cocos creator:需使用“导入映射”,建议v3.3及以上版本
  • node参考版本:v16.15.0
  • npm参考版本:8.5.5,安装protobufjs或执行脚本时报错,升级至指定及以上版本再试

二、基本目标

  1. 使用静态加载,单文件(最小化网络请求)
  2. es6模块规范(效率更好)
  3. 导入方式支持导入所有和导入指定模块
  4. 自动导入使用pb及代码提示
  5. 若干工具函数(通用消息编解码、获得pb对象名、获得pb类型、pb克隆)
  6. 支持64位数据
  7. 适配微信小游戏平台

三、安装protobufjs

package.json中dependencies有指定版本则直接使用npm install,否则使用npm install --save protobufjs

需要注意的是protobufjs7需要单独安装protobufcli(npm install --save protobufjs-cli),protobufjs6则在安装protobufjs时默认集成。

四、构建pb流程

1、提供构建protobuf协议指令

package.json

"scripts": {
    "protocli": "node ./tools/build_proto.js",
    "buildproto": "npm run protocli -- ./assets/Proto ./assets/Scripts/Protobuf ./assets/Scripts/Protobuf pb"
  }

2、缩减生成单文件大小

--no-verify --no-convert --no-delimited --no-beautify --no-service
移除不需要的内容,需要通过生成带注释的js文件来生成ts,后可再生成一份不带注释的js文件替换

3、修正模块

解决es6规范default无定义的问题;微信小游戏平台生成代码被混淆后可以根据pb对象获得pb名;64位数据支持

function esModuleCorrect(path) {
    let file = readFileSync(path, { encoding: 'utf8' });
    // let result = file.replace("import * as $protobuf", "import { default as $protobuf }");
    // es导入最终存储在commonjs的内容是另一份对象
    let result = file.replace(`const $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {});`, "const $root = {};");
    result = result.replace(/\$protobuf\./g, "$protobuf.default.");
    let extraContent = [`import Long from 'long';`, `$protobuf.default.util.Long = Long;`, `$protobuf.default.configure();`];
    let dataArray = result.split(/\r\n|\n|\r/gm);
    dataArray.splice(2, 0, ...extraContent);
    writeFileSync(path, dataArray.join('\r\n'));
}

4、输出生成pb文件的package.json

function genEsModuleConfig(path) {
    let config = {"type": "module"};
    writeFileSync(path, JSON.stringify(config, null, 4));
}

五、导入映射

添加文件import-map.json

{
    "imports": {
        "pb": "./assets/Scripts/Protobuf/pb.js"
    }
}

项目设置填上对应import map路径


tsconfig.json修改为

"compilerOptions": {
    "strict": false,
    "baseUrl": "./",
    "paths": {
      "pb": ["./assets/Scripts/Protobuf/pb"]
    },
    "allowSyntheticDefaultImports": true
  }

如果遇到找不到模块 "pb"报错:一般为配置未能刷新,重启cocos creator即可

六、使用

安装依赖:npm install
构建协议:npm run buildproto
使用示例(无需主动import,代码提示自动导入即可,对应pb对象也有代码提示):

import pb from 'pb';//导入全部pb
import { PlayerInfo } from 'pb';//导入指定pb
import Long from 'long';
let message: PlayerInfo = PlayerInfo.create();
message.id = 1;
message.name = "cocos";
message.money = Long.fromString("18446744073709551615");
let buffer  = PlayerInfo.encode(message).finish();
let decoded = PlayerInfo.decode(buffer);

七、pb工具函数

encode

function PbEncode(message: any): Uint8Array {
    let writer = message.constructor.encode(message) as Writer;
    return writer?.finish();
}

decode

function PbDecode<T extends Message>(name: string, arr: Uint8Array): T {
    try {
        let pbType = GetPbTypeByName(name);
        return pbType.decode(arr);
    } catch (e) {
        console.error(`pb decode error, name: ${name}, error: ${e}`);
    }
}

克隆pb(类似其他语言的CopyFrom)

function ClonePb<T>(obj: T | T[]): T | T[] {
    if (typeof obj !== "object" || !obj) return obj;
    let cpy: T;
    if (Array.isArray(obj) || ArrayBuffer.isView(obj)) {
        let len = (obj as T[]).length;
        cpy = new obj.constructor(len);
        for (var i = 0; i < len; ++i) {
            cpy[i] = ClonePb(obj[i]);
        }
    } else {
        cpy = Object.create(obj.constructor.prototype);
        cpy.constructor = obj.constructor;
        for (var i = 0, keys = Object.keys(obj), len = keys.length; i < len; ++i) {
            cpy[keys[i]] = ClonePb(obj[keys[i]]);
        }
    }
    return cpy;
}

根据pb对象获得pb名

function GetPbNameByPb(pb: Function | Object): string {
    let messageName: string = pbToName.get(typeof pb == "object" ? pb.constructor : pb);
    if (messageName == undefined) {
        return "";
    }
    return messageName;
}

根据pb名获得pb类型

function GetPbTypeByName(name: string): any {
    let paths = name.split('.');
    let current = pb;

    for (let i = 0; i < paths.length; ++i) {
        if (current[paths[i]] == undefined) {
            return undefined;
        } else {
            current = current[paths[i]];
        }
    }
    return current;
}

八、示例工程

CocosCreatorProtobuf

26赞

好贴,送你上去

好贴,Mark!!!

有点奇怪, 我的 git base here 。 执行的npm run buildproto时候一直 err’or : no such file or directory, open ‘assets/Proto/*.proto’

我的 .proto 协议明明都在这个文件夹里面.

我用这个示例工程也是这样

我感觉我智商 受到了 伤害, *.proto 我一直以为是读取文件夹内的所有 proto文件。 结果 * 号是让我命名啊。。。。。。。。。。。。。

大佬,那多个proto文件怎么处理?

我还没解决纳, 我的pb 引入一直有问题,搞不明白为什么

照搬官方文档就完事了,有点不完美的是生成的proto.d.ts文件里面会有红线报错但是可以无视它不影响使用,版本官方最新3.7.1

image
-_- 我还是用的以前egret的protobuf
双击就能把.proto生成js和.d.ts

我也能生成,但是引用的时候一直有问题。 2.x 是正常的。

3.x 可不可以不在项目内 npm install,而是用 插件形式把 image

这两个文件夹引入

找到生成报错的原因了,build_proto.js第64行
let protoFuzzypath = join(protoPath, “.proto",)后面加正则,改\为/
let protoFuzzypath = join(protoPath, "
.proto”,).replace(/\/g, “/”);
就这可以找到proto路径了

这个项目 npm install 也 in不下来, 只能 cnpm in。你们是不是

我这边正常的,你再看看

你之前的理解是对的,*在这里就是通配符的意思,读取指定文件夹内的所有proto后缀文件

Windows吗?试试匹配下node和npm的版本呢

应该是高于你提出的版本,npm 什么的还好,
目前在纠结的问题是, 到底要怎么引入协议导出的 .d.ts 文件里命名空间。

我的协议 一直都可以正常导出


然后引用

但是 就是报错 找不到这个 model。

试了一下你的方法和 官方的案例, 都解决不了,目前还在看

总算搞定了。。。 搞了快一天

默认就是支持多proto文件
Proto文件夹中加入对应的proto文件即可(类似player.proto),会全部生成在同一文件里;对应的,文件增加,生成时间也会变长

实际导入的的js文件而不是声明文件,可以参考示例工程再改