1. 程式人生 > >Cocos Creator 中使用 protobufjs

Cocos Creator 中使用 protobufjs

  為了在CocosCreator 中使用 protobuf,NRatel走了不少彎路。

  protobufjs開源地址

  一、安裝 

  前置條件,安裝Node.js 、npm。

  檢視候選版本:

npm view protobufjs versions

  新建一個專案目錄,用來轉換.proto為.js。執行 npm init -y 初始化專案。

  選擇需要的版本安裝,(這裡用的是6.8.8版):

npm install --save-dev [email protected]

  執行後,將出現node_modules目錄,

  要執行的轉換命令檔案為:node_modules\.bin\pbjs.cmd, 內容如下:

::當前目錄是否存在node.exe
@IF EXIST "%~dp0\node.exe" (
  ::使用node執行pbjs進行檔案轉換
  "%~dp0\node.exe"  "%~dp0\..\protobufjs\bin\pbjs" %*
) ELSE (
  @SETLOCAL
  ::將環境變數PATHEXT中的JS刪除
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  ::使用node執行pbjs進行檔案轉換
  node  "%~dp0\..\protobufjs\bin\pbjs" %*
)

  實際上它最終是,用 node執行了 node_modules\protobufjs\bin\pbjs檔案。

  二、使用

  protobufjs 提供了多種使用方式,但通常主要還是採用 生成靜態.js使用和動態載入.proto檔案使用這兩種方式。

  注意, protobufjs 依賴了  long.jsbytebuffer.js。放入工程即可。

  1.靜態方式(推薦!)

  1). 執行命令獲取幫助,確認引數含義和用法。避免版本改變導致用法改變導致的錯誤:

node_modules\.bin\pbjs -h

  2). 執行命令進行 .proto檔案到.js的轉換操作:

  注意,要執行的檔案從上層目錄開始執行時,在windows下為反斜槓間隔。

  這裡的版本中的 -t 指定目標格式;-w 指定模組引用規範;-o指定輸入輸出檔案。具體以pbjs -h中的說明為準。

node_modules\.bin\pbjs -t static-module -w commonjs -o protores.js *.proto

  3). 示例:

  .proto原始檔。

// 日誌
package log;

// 提交評論
message comment_C {
    optional string msg = 1; // 評論內容
}

  可能需要修改protores.js檔案頂部 對protibuf.js的引用(修改引用路徑,改為自己使用的版本)。

//var $protobuf = require("protobufjs/minimal");
var $protobuf = require("protobuf");

  將使用方法封裝為更通用、更易用的方式。

let protores = require("protores");

let Pbjs6 = class Pbjs6 {
	//packageName: package名
	//msgTypeName: 訊息型別名
	static Encode(packageName, msgTypeName, data) {
		var msgType = protores[packageName][msgTypeName];
		var msg = msgType.create(data);
		var bytes = msgType.encode(msg).finish();
		return bytes;
	}

	static Decode(packageName, msgTypeName, bytes) {
		var msgType = protores[packageName][msgTypeName];
		var msg = msgType.decode(bytes);
		var data = msgType.toObject(msg, {
			longs: Number,		//long預設轉換為Number型別
			enums: String,
			bytes: String,
			// see ConversionOptions
		});
		return data;
	}
}

module.exports = Pbjs6;

  測試:

let Pbjs6 = require("Pbjs6");
let bytes = Pbjs6.Encode("log", "comment_C", { msg: "NRatel" });
cc.log("bytes: ", bytes);  //Uint8Array(14) [10, 12, 110, 105, 101, 104, 111, 110, 103, 113, 105, 97, 110, 103]

let data = Pbjs6.Decode("log", "comment_C", bytes);
cc.log("data: ", data);  //{ msg: "NRatel" }

  2.動態方式(不推薦!)

  為什麼要動態載入?,為了包體越小越好(微信小遊戲中包體要求4M的限制)。

  protobufjs6.x的動態用法:

  注意:微信小遊戲中不可用, 因為其內部使用Es6的Function。而微信禁止了動態生成程式碼的行為。

let Assets = require("Assets");
let protobuf6 = require("protobuf");  //6.x的protobufjs

let Pbjs6 = class Pbjs6 {
    static s_ProtoRootMap = new Map();

    static LoadAll(protoDir) {
        return new Promise((resolve, reject) => {
            //二次封裝的ccc載入目錄的方法
            Assets.LoadDir_ReturnWithUrls(protoDir).then((object) => {
                let { resArray, urls } = object;
                for (let index in resArray) {
                    let path = urls[index];
                    let key = path.substr(path.lastIndexOf('/') + 1, path.length);
                    //生成protoRoot並放入Map, 每個proto檔案對應一個protoRoot
                    let protoRoot = protobuf6.parse(resArray[index]).root;
                    this.s_ProtoRootMap.set(key, protoRoot);
                }
                return resolve();
            });
        });
    }

    static Encode(packageName, msgTypeName, data) {
        //根據packageName找到對應的protoRoot
        let root = this.s_ProtoRootMap.get(packageName);
        cc.assert(typeof (root) != "undefined" && root != null, "未找到該protoRoot, 請確保已提前載入, packageName: ", packageName);
        //根據protoRoot和msgTypeName找到訊息型別。
        let msgType = root.lookupType(msgTypeName);
        //根據訊息型別檢查資料
        let error = msgType.verify(data);
        cc.assert(error == null, "data資料型別檢查失敗!", error);
        //根據實際資料建立訊息提,並encode為bytes
        let msg = msgType.create(data);
        let bytes = msgType.encode(msg).finish();

        return bytes;
    }

    static Decode(packageName, msgTypeName, bytes) {
        //根據packageName找到對應的protoRoot
        let root = this.s_ProtoRootMap.get(packageName);
        cc.assert(typeof (root) != "undefined" && root != null, "未找到該protoRoot,請確保已提前載入, packageName: ", packageName);
        //根據protoRoot和msgTypeName找到訊息型別。
        var msgType = root.lookupType(msgTypeName);
        //根據實際bytes解析出原始資料
        var msg = msgType.decode(bytes);
        var data = msgType.toObject(msg, {
            longs: Number,
            enums: String,
            bytes: String,
            // see ConversionOptions
        });
        return data;
    }
};

module.exports = Pbjs6;

  protobuf5.x的動態用法:

  注意:無法處理import了其他proto檔案的proto檔案。

let Assets = require("Assets");
let protobuf5 = require("protobuf");    //5.x的protobufjs

let Pbjs5 = class Pbjs5 {
    static s_ProtoRootMap = new Map();

    static LoadAll(protoDir) {
        return new Promise((resolve, reject) => {
            //二次封裝的ccc載入目錄的方法
            Assets.LoadDir_ReturnWithUrls(protoDir)
                .then((object) => {
                    let { resArray, urls } = object;
                    for (let index in resArray) {
                        let path = urls[index];
                        let key = path.substr(path.lastIndexOf('/') + 1, path.length);
                        let root = protobuf5.loadProto(resArray[index]).build(key);
                        this.s_ProtoRootMap.set(key, root);
                    }
                    return resolve();
                });
        });
    }

    // 快捷式Encode
    // 傳入msg對應的data
    static Encode(packageName, msgTypeName, data) {
        let root = this.s_ProtoRootMap.get(packageName);
        cc.assert(typeof (root) === "object" && root != null, "未找到protoPackage:" + packageName);
        let Message = root[msgTypeName];
        cc.assert(typeof (Message) === "function" && Message.$type.className === "Message", "未找到Message定義, packageName: " + packageName + ", msgTypeName: " + msgTypeName);
        let msg = new Message();
        for (const p in data) {
            if (data.hasOwnProperty(p)) {
                msg.set(p, data[p], false);
            }
        }
        let bytes = new Uint8Array(msg.encode().toBuffer());
        return bytes;
    }

    // 面向物件式Encode。
    // 在callback中 對msg 的欄位逐個 set 進行Encode。
    // 可以呼叫set(key, value), 也可以直接呼叫set_欄位名(value), 欄位命名規則為:同時支援下劃線格式和駝峰格式。
    static EncodeOO(packageName, msgTypeName, callback) {
        let root = this.s_ProtoRootMap.get(packageName);
        cc.assert(typeof (root) === "object" && root != null, "未找到protoPackage:" + packageName);
        let Message = root[msgTypeName];
        cc.assert(typeof (Message) === "function" && Message.$type.className === "Message", "未找到Message定義, packageName: " + packageName + ", msgTypeName: " + msgTypeName);
        let msg = new Message();
        msg = callback(msg);
        let bytes = new Uint8Array(msg.encode().toBuffer());
        return bytes;
    }

    static Decode(packageName, msgTypeName, bytes) {
        let root = this.s_ProtoRootMap.get(packageName);
        cc.assert(typeof (root) === "object" && root != null, "未找到protoPackage:" + packageName);
        let Message = root[msgTypeName];
        cc.assert(typeof (Message) === "function" && Message.$type.className === "Message", "未找到Message定義, packageName: " + packageName + ", msgTypeName: " + msgTypeName);

        let msg = Message.decode(bytes);
        return msg;
    }
};

module.exports = Pbjs5;