Protobuf 使用(unity)

工具&环境

1.vscode https://code.visualstudio.com
2.node.js http://nodejs.cn/download/
3.npm(node.js)
4.Google.Protobuf https://github.com/protocolbuffers/protobuf
5.protoc (windows平台编译工具)https://github.com/protocolbuffers/protobuf/releases/download/v3.17.3/protoc-3.17.3-win64.zip
6.protobuf Csharp https://github.com/protocolbuffers/protobuf/tree/master/csharp
7.Protoc Gen Typescript (转ts配置)https://www.npmjs.com/package/protoc-gen-ts

无关知识点

1)package.json npm项目配置 文档

npm init //创建默认package.json配置

以下是我本地项目配置

{
  "name": "protobufdemo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "google-protobuf": "^3.17.0",
    "ts-node": "^10.2.1",
    "typescript": "^4.3.5"
  },
  "devDependencies": {
    "source-map-support": "^0.5.19",
    "@types/google-protobuf": "^3.15.2",
    "ts-protoc-gen": "^0.15.1-pre.a71b34e",
    "@swc-node/core": "^1.3.0",
    "fs-extra": "^9.1.0",
    "uglify-js": "^3.13.1"
  }
}

注意两种依赖

dependencies,在生产环境中需要用到的依赖
devDependencies,在开发、测试环境中用到的依赖

在开发新项目时 运行 npm install 就会根据配置自动下载安装依赖文件。
在配置外还有两种安装方式
npm install xxx 本地安装
npm install -g xxx 全局安装 (默认会安装到npm全局目录下:C:\Users\jsfa\AppData\Roaming\npm\node_modules
如果安装时同时还想将module添加到package依赖 npm install xxx -save
添加到Dev的命令类似 npm install xxx -save -dev

自定义npm 命令 script

image.png

在package目录控制台命令 npm run test 就会输出一个我们定义的报错信息
通过这样的方式可以做很多事情,比如快捷打包,自动执行,复制文件
image.png

2)Vscode配置文件 settings.json

自定义一些设置,文件筛选啊之类的

调试(debug)和启动(run)的配置项 launch.json

image.png

如果我想调试typescript代码,添加下面的代码

   "version": "0.2.0",
    "configurations": [
        {
            "name": "Current TS File",
            "type": "node",
            "request": "launch",
            "args": [
                "${workspaceRoot}/TsProj/Test/Test.ts"
            ],
            "runtimeArgs": [
                "--nolazy",
                "-r",
                "ts-node/register"
            ],
            "sourceMaps": true,
            "cwd": "${workspaceRoot}/TsProj",
            "protocol": "inspector",
            "console": "integratedTerminal",
            "internalConsoleOptions": "neverOpen"
        }
}

点击调试就会自动编译运行工作空间下Test.ts脚本
如果新项目没有这个脚本,添加的方式很多比如
Create a launch.json file

image.png

如果想调试Unity 可以安装Vscode 插件Debugger for Unity

image.png

安装好之后 选择创建json.file 在下拉列表中选择unityDebugger
image.png

然后当当当当(三声),多了一堆东西
image.png

调试
image.png

运行调试 运行unity 就可以愉快打断点调试了
有可能还要先Attach一下
image.png

3)TypeScript 配置文件 tsconfig.json 文档链接

初始化配置文件
1.在跟目录手动创建tsconfig.json文件
2控制台运行 tsc --init命令
这是我本地项目配置

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "sourceMap": true,
    "noImplicitAny": true,
    "typeRoots": [
      "../Assets/Puerts/Typing",
      "../Assets/Gen/Typing",
      "./node_modules/@types"
    ],
    "outDir": "dist",
    "suppressImplicitAnyIndexErrors": true,
    "experimentalDecorators": true
  }
}

还记得上面package配置里的Scripts: build

image.png

执行npm run build就可以以我配置的选项执行编译typescript脚本。

Protobuf 使用(本文主题)

protobuf的概念上面有文档链接

  1. 创建proto文件


    image.png

    TestData.proto 这就是我们需要的protobuf源文件了
    可以下载这个插件,方便编写protobuf协议


    image.png
syntax = "proto3";
option csharp_namespace =  "Dx"; 
message TestData {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;
}
  1. 将.proto生成特定语言能够使用的数据结构
    这是我们下载好的编译文件,将bin目录添加到环境变量中,然后控制台测试 protoc --version
    image.png

    image.png

    在vs目录也好,控制台也好
    a.C#
    protoc --csharp_out=输出目录 源文件目录\*.proto
    b.js
    protoc --js_out=输出目录 源文件目录\*.proto
    c.TypeScript
    这个比较特殊 我还没找到原有的生成方式,所以这里要用到 Protoc Gen Typescript
    可以通过npm直接安装
npm install ts-protoc-gen

然后就可以执行

protoc -I=sourcedir --ts_out=dist myproto.proto \\生成ts的protobuf结构

这里有可能会报错,找不到google.protobuf 模块,但项目里明明有,可以通过全局安装
npm install -g google-protobuf 解决 (这里的问题我还没搞明白,但暂时这么解决了)
ts-protoc-gen有两种转化方式

protoc  --ts_out=.\Assets\Proto .\Assets\Proto\TestData.proto
protoc --plugin=protoc-gen-ts=.\node_modules\ts-protoc-gen\bin\protoc-gen-ts.cmd --proto_path=. --ts_out=.\Assets\Proto .\Assets\Proto\TestData.proto

这两种方式生成的ts结构是不同的

TestData.ts ts的协议文件包含所有的变量 类型 序列化 反序列化方法。。。

/**
 * Generated by the protoc-gen-ts.  DO NOT EDIT!
 * compiler version: 3.7.1
 * source: Assets/Proto/TestData.proto
 * git: https://github.com/thesayyn/protoc-gen-ts
 * buymeacoffee: https://www.buymeacoffee.com/thesayyn
 *  */
import * as pb_1 from "google-protobuf";
export class TestData extends pb_1.Message {
    constructor(data?: any[] | {
        name?: string;
        id?: number;
        email?: string;
        phones?: TestData.PhoneNumber[];
    }) {
        super();
        pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [4], []);
        if (!Array.isArray(data) && typeof data == "object") {
            if ("name" in data && data.name != undefined) {
                this.name = data.name;
            }
            if ("id" in data && data.id != undefined) {
                this.id = data.id;
            }
            if ("email" in data && data.email != undefined) {
                this.email = data.email;
            }
            if ("phones" in data && data.phones != undefined) {
                this.phones = data.phones;
            }
        }
    }
    get name() {
        return pb_1.Message.getField(this, 1) as string;
    }
    set name(value: string) {
        pb_1.Message.setField(this, 1, value);
    }
    get id() {
        return pb_1.Message.getField(this, 2) as number;
    }
    set id(value: number) {
        pb_1.Message.setField(this, 2, value);
    }
    get email() {
        return pb_1.Message.getField(this, 3) as string;
    }
    set email(value: string) {
        pb_1.Message.setField(this, 3, value);
    }
    get phones() {
        return pb_1.Message.getRepeatedWrapperField(this, TestData.PhoneNumber, 4) as TestData.PhoneNumber[];
    }
    set phones(value: TestData.PhoneNumber[]) {
        pb_1.Message.setRepeatedWrapperField(this, 4, value);
    }
  
...还有好长好长的代码...

        serializeBinary(): Uint8Array {
            return this.serialize();
        }
        static deserializeBinary(bytes: Uint8Array): PhoneNumber {
            return PhoneNumber.deserialize(bytes);
        }
    }
}

TestData_pb.ts ts 协议的声明文件 同样包含所有的 变量 类型 序列化反序列化方法

// package: 
// file: TestData.proto

import * as jspb from "google-protobuf";

export class TestData extends jspb.Message {
  getName(): string;
  setName(value: string): void;

  getId(): number;
  setId(value: number): void;

  getEmail(): string;
  setEmail(value: string): void;

  clearPhonesList(): void;
  getPhonesList(): Array<TestData.PhoneNumber>;
  setPhonesList(value: Array<TestData.PhoneNumber>): void;
  addPhones(value?: TestData.PhoneNumber, index?: number): TestData.PhoneNumber;

  serializeBinary(): Uint8Array;
  toObject(includeInstance?: boolean): TestData.AsObject;
  static toObject(includeInstance: boolean, msg: TestData): TestData.AsObject;
  static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
  static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
  static serializeBinaryToWriter(message: TestData, writer: jspb.BinaryWriter): void;
  static deserializeBinary(bytes: Uint8Array): TestData;
  static deserializeBinaryFromReader(message: TestData, reader: jspb.BinaryReader): TestData;
}

export namespace TestData {
  export type AsObject = {
    name: string,
    id: number,
    email: string,
    phonesList: Array<TestData.PhoneNumber.AsObject>,
  }

  export class PhoneNumber extends jspb.Message {
    getNumber(): string;
    setNumber(value: string): void;

    getType(): TestData.PhoneTypeMap[keyof TestData.PhoneTypeMap];
    setType(value: TestData.PhoneTypeMap[keyof TestData.PhoneTypeMap]): void;

    serializeBinary(): Uint8Array;
    toObject(includeInstance?: boolean): PhoneNumber.AsObject;
    static toObject(includeInstance: boolean, msg: PhoneNumber): PhoneNumber.AsObject;
    static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
    static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
    static serializeBinaryToWriter(message: PhoneNumber, writer: jspb.BinaryWriter): void;
    static deserializeBinary(bytes: Uint8Array): PhoneNumber;
    static deserializeBinaryFromReader(message: PhoneNumber, reader: jspb.BinaryReader): PhoneNumber;
  }

  export namespace PhoneNumber {
    export type AsObject = {
      number: string,
      type: TestData.PhoneTypeMap[keyof TestData.PhoneTypeMap],
    }
  }

  export interface PhoneTypeMap {
    MOBILE: 0;
    HOME: 1;
    WORK: 2;
  }

  export const PhoneType: PhoneTypeMap;
}

在import 对应路径下的 TestData之后 ,就可以开始使用了

import { TestData } from "./Assets/Proto/TestData_pb";//这里要指向使用的协议文件,我这里指向的协议文件
class main{
    public x:TestData=new TestData();

    constructor(id:number,name:string){
        this.x.setId(id);
        this.x.setName(name);
    }
}

function Read(data:ArrayBuffer){
    let ret = TestData.deserializeBinary(new Uint8Array(data));
    console.log(ret.getId());
    console.log(ret.getName());
}

let m = new main(12,"dx");
Read(m.x.serializeBinary());

我们可以直接在ts脚本中创建对应的协议数据类型,读取写入,序列化之后传输。
但当我们将ts编译为js文件时,两种方式的流程有所不同
TestData.ts
main.ts通过tsc 编译后,将自动将生成TestData.js脚本,里面包含全部的协议信息,所以编译过后,能正常调用,程序正常运行。
TestData_pb.ts由于它只是声明文件,
所以我们在编程阶段可以正常使用方法,添加数据而不会报错,但将主程序编译之后,并不会生成对应的协议.js文件,所以运行程序会出现

image.png

所以需要我们主动生成对应的.js脚本:
在生成协议时同时生成js脚本

D:\SoftWare\protoc-3.7.1-win64\bin\protoc.exe --plugin=protoc-gen-ts=D:\Project\UnityProject\ProtobufDemo\node_modules/.bin/protoc-gen-ts.cmd --proto_path=. --ts_out=. --js_out=import_style=commonjs,binary:. .\TestData.proto

程序目录\protoc.exe
--plugin=protoc-gen-ts=protoc-gen-ts.cmd目录\protoc-gen-ts.cmd
--proto_path=依赖的proto文件目录(import"**.proto"
--ts_out=声明文件输出目录
--js_out=import_style=commonjs,binary:js文件输出格式及目录
协议文件输入路径

这样我们就同时得到声明文件.d.ts.js文件了,可以正常编译使用

一般在应用场景中,我们更多的使用第二种方式来在ts部分使用protobuf协议,因为声明文件更简洁明了,诸如此类巴拉巴拉(反正用就完事了)

d. 其他语言请查看官方文档
3 ) proto文件有了,对应平台转化的协议也有了,现在就是使用了,以Unity为例
将转换的协议 TestData.cs导入Unity

image.png

然后报错
image.png

因为我们项目中缺少 Google.Protobuf
打开github,选择C#,download
image.png

image.png

bulidall
image.png

image.png

将以下文件导入unity plugins\protobuf 目录
image.png

image.png

编写测试代码
main.cs

using Google.Protobuf; //数据转二进制要用到 ToByteArray()  方法
public class Main : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        TestData data = new TestData();
        data.Name="dx";
        data.Id=1234;
        var s = data.ToByteArray();
        SendS(s);
    }
    public void SendS(byte[] data){
        Debug.Log(data);
        RecvS(data);
    }
    public void RecvS(byte[] data){
        TestData rData = TestData.Parser.ParseFrom(data);
        Debug.Log(rData.Name);
        Debug.Log(rData.Id);
    }
}

结果


image.png

成功!
其他语言也类似,只是序列化和反序列化的方法可以稍微有差别

补充(大概率是我数据转换处理基础太差造成的原因)
这里出现了Cs端Ts端传输byte[] 数据 协议转化失败,没法正常取数据的情况,最后通过PuertsArrayBuffer将cs端的二进制数据先转为ArrayBuffer,最后在ts部分转换为协议数据解决

public static Puerts.ArrayBuffer GetPlayerRecord()
        {
            Protocol.PlayerRecord data = PlayerRecord.Instance.GetPlayerRecordPb();
           
            if (data != null) return new Puerts.ArrayBuffer(data.ToByteArray());
            return null;
        }
export function InitPlayerRecord (data: ArrayBuffer) {
        let playerRecord  = PlayerRecord .deserializeBinary(new Uint8Array(data));
    }

抛开协议也能通过在ts端通过jsonobj的方式解析数据,前提是你完全知道协议内容

let obj = JSON.parse(data);
        let mrlist = obj['mapRecords'];
        for (let i = 0; i < mrlist.length; i++) {
            let record=mrlist[i];
            UpdateRecordByJson(record["area"],record["floor"],record["status"],record["time"],record["resetTime"],record["boughttimes"],record["weeklyboughttimes"]);
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容