UN5L3——协议(消息)生成

协议(消息)生成主要做什么

协议生成 主要是使用配置文件中读取出来的信息,动态的生成对应语言的代码文件
每次添加消息或者数据结构类时,我们不需要再手写代码了
我们不仅可以生成C#脚本文件,还可以根据需求生成别的语言的文件

根据配置生成脚本的文件的主要思路就是,按规则拼接字符串
只要有数据和规则,我们就可以动态的创建脚本文件

制作功能前的准备工作

协议生成是不会在发布后使用的功能,主要是在开发时使用,所以我们在Unity当中可以把它作为一个编辑器功能来做
因此我们可以专门新建一个Editor文件夹(专门放编辑器相关内容,不会发布),在其中放置配置文件、自动生成相关脚本文件

创建一个生成协议(消息)脚本的工具类,并利用[MenuItem()]​​创建选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEditor;
using UnityEngine;

public class ProtocolTool
{
[MenuItem("ProtocolTool/生成C#脚本")]
private static void GenerateCSharp()
{
Debug.Log("生成C#代码");
}

[MenuItem("ProtocolTool/生成C++脚本")]
private static void GenerateCPP()
{
Debug.Log("生成C++代码");
}

[MenuItem("ProtocolTool/生成Java脚本")]
private static void GenerateJava()
{
Debug.Log("生成Java代码");
}
}

由于创建脚本代码逻辑较多,因此新声明一个类GenerateCSharp​用来实现脚本代码生成,由ProtocolTool​类调用
GenerateCSharp​实现三个方法分别用来生成枚举脚本,数据类脚本,消息类脚本,这些方法需要传入对应的XML节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.IO;
using System.Xml;
using UnityEngine;

public class GenerateCSharp
{
//协议保存路径
private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";

public void GenerateEnum(XmlNodeList nodes)
{
//TODO.. 生成枚举脚本
}

//TODO.. 生成数据类消息类脚本
}

因此,在ProtocolTool​中实现读取指定名字子节点List的方法,再传入到GenerateCSharp​的方法内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System.Xml;
using UnityEditor;
using UnityEngine;

public class ProtocolTool
{
//配置文件所在路径
private static string PROTO_INFO_PATH = Application.dataPath + "/Editor/ProtocolTool/ProtocolInfo.xml";
private static GenerateCSharp generateCSharp = new GenerateCSharp();

[MenuItem("ProtocolTool/生成C#脚本")]
private static void GenerateCSharp()
{
generateCSharp.GenerateEnum(GetNodes("enum"));
//TODO.. 生成数据类消息类脚本
AssetDatabase.Refresh();
}

[MenuItem("ProtocolTool/生成C++脚本")]
private static void GenerateCPP()
{
Debug.Log("生成C++代码");
}

[MenuItem("ProtocolTool/生成Java脚本")]
private static void GenerateJava()
{
Debug.Log("生成Java代码");
}

// 获取指定名字的所有子节点的List
private static XmlNodeList GetNodes(string nodeName)
{
XmlDocument xml = new XmlDocument();
xml.Load(PROTO_INFO_PATH);
XmlNode root = xml.SelectSingleNode("messages");
return root.SelectNodes(nodeName);
}
}

制作生成枚举功能

  1. 读取xml枚举相关信息

    遍历传入的枚举节点,读取其名字与命名空间名,再遍历枚举节点下的每个字段节点,读取其字段名与默认值
    其中,字段的字符串可以提前拼接好,添加合适缩进和换行(使用\r\n​)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public void GenerateEnum(XmlNodeList nodes)
    {
    //生成枚举脚本的逻辑
    string namespaceStr = "";
    string enumNameStr = "";
    string fieldStr = "";
    //遍历所有的枚举节点
    foreach (XmlNode enumNode in nodes)
    {
    namespaceStr = enumNode.Attributes["namespace"].Value; //获取命名空间
    enumNameStr = enumNode.Attributes["name"].Value; //获取枚举名
    //遍历枚举下的所有字段消息
    XmlNodeList enumFields = enumNode.SelectNodes("field");
    fieldStr = ""; //新枚举需要清空上一次拼接的字段字符串
    foreach (XmlNode fieldNode in enumFields)
    {
    fieldStr += "\t\t" + fieldNode.Attributes["name"].Value; //添加字段名
    if (fieldNode.InnerText != "")
    fieldStr += " = " + fieldNode.InnerText; //如果有值添加值
    fieldStr += ",\r\n"; //换行
    }
    }
    }
  2. 根据枚举相关信息 拼接字符串

    得到所有可变消息后,按照C#的代码格式,添加合适的关键词,缩进,换行,将所有可变内容拼接起来得到完整的代码字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public void GenerateEnum(XmlNodeList nodes)
    {
    //生成枚举脚本的逻辑
    string namespaceStr = "";
    string enumNameStr = "";
    string fieldStr = "";
    //遍历所有的枚举节点
    foreach (XmlNode enumNode in nodes)
    {
    namespaceStr = enumNode.Attributes["namespace"].Value; //获取命名空间
    enumNameStr = enumNode.Attributes["name"].Value; //获取枚举名
    //遍历枚举下的所有字段消息
    XmlNodeList enumFields = enumNode.SelectNodes("field");
    fieldStr = ""; //新枚举需要清空上一次拼接的字段字符串
    foreach (XmlNode fieldNode in enumFields)
    {
    fieldStr += "\t\t" + fieldNode.Attributes["name"].Value; //添加字段名
    if (fieldNode.InnerText != "")
    fieldStr += " = " + fieldNode.InnerText; //如果有值添加值
    fieldStr += ",\r\n"; //换行
    }
    //对所有的可变的内容进行拼接
    string enumStr = $"namespace {namespaceStr}\r\n" + //命名空间
    "{\r\n" +
    $"\tpublic enum {enumNameStr}\r\n" + //枚举声明
    "\t{\r\n" +
    $"{fieldStr}" + //添加字段字符串
    "\t}\r\n" +
    "}";
    }
    }
  3. 生成枚举脚本文件

    将拼接得到的代码字符串保存到指定路径下,每个命名空间一个文件夹,所有同一命名空间下的枚举脚本放一个文件夹

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    //协议保存路径
    private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";

    public void GenerateEnum(XmlNodeList nodes)
    {
    //生成枚举脚本的逻辑
    string namespaceStr = "";
    string enumNameStr = "";
    string fieldStr = "";
    //遍历所有的枚举节点
    foreach (XmlNode enumNode in nodes)
    {
    namespaceStr = enumNode.Attributes["namespace"].Value; //获取命名空间
    enumNameStr = enumNode.Attributes["name"].Value; //获取枚举名
    //遍历枚举下的所有字段消息
    XmlNodeList enumFields = enumNode.SelectNodes("field");
    fieldStr = ""; //新枚举需要清空上一次拼接的字段字符串
    foreach (XmlNode fieldNode in enumFields)
    {
    fieldStr += "\t\t" + fieldNode.Attributes["name"].Value; //添加字段名
    if (fieldNode.InnerText != "")
    fieldStr += " = " + fieldNode.InnerText; //如果有值添加值
    fieldStr += ",\r\n"; //换行
    }
    //对所有的可变的内容进行拼接
    string enumStr = $"namespace {namespaceStr}\r\n" + //命名空间
    "{\r\n" +
    $"\tpublic enum {enumNameStr}\r\n" + //枚举声明
    "\t{\r\n" +
    $"{fieldStr}" + //添加字段字符串
    "\t}\r\n" +
    "}";
    //保存文件的路径
    string path = SAVE_PATH + namespaceStr + "/Enum/";
    //若不存在就创建
    if (!Directory.Exists(path))
    Directory.CreateDirectory(path);
    //字符串保存 存储为枚举脚本文件
    File.WriteAllText(path + enumNameStr + ".cs", enumStr);
    }

    Debug.Log("枚举生成结束");
    }

制作生成数据类功能

分析制作数据结构类的构成

数据结构的类的全部内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System.Text;

//玩家数据类
public class PlayerData : BaseData
{
public string name;
public int atk;
public int lev;

public override int GetBytesNum()
{
return 4 + 4 + 4 + Encoding.UTF8.GetBytes(name).Length;
}

public override int Reading(byte[] bytes, int beginIndex = 0)
{
int index = beginIndex;
name = ReadString(bytes, ref index);
atk = ReadInt(bytes, ref index);
lev = ReadInt(bytes, ref index);
return index - beginIndex;
}

public override byte[] Writeing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
WriteString(bytes, name, ref index);
WriteInt(bytes, atk, ref index);
WriteInt(bytes, lev, ref index);
return bytes;
}
}

通过分析可知,我们需要根据字段生成成员变量声明,
再根据各个成员变量的长度生成GetBytesNum()​,根据各个成员变量的类型生成Reading()​和Writeing()

制作生成数据结构类步骤

声明一个导入所有<data>​节点生成数据结构类的方法,在ProtocolTool​中调用

1
2
3
4
5
6
7
8
[MenuItem("ProtocolTool/生成C#脚本")]
private static void GenerateCSharp()
{
generateCSharp.GenerateEnum(GetNodes("enum"));
generateCSharp.GenerateData(GetNodes("data"));
// TODO.. 生成消息类
AssetDatabase.Refresh();
}
  1. 生成成员变量声明

    假设我们要生成这样的类声明和成员变量声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    namespace GamePlayer
    {
    public class PlayerData : BaseData
    {
    public int id;
    public float atk;
    public bool sex;
    public long lev;
    public int[] arrays;
    public List<int> list;
    public Dictionary<int, string> dic;
    public E_HERO_TYPE heroType;
    }
    }

    遍历传入的所有<data>​节点,读取命名及所在命名空间,完成类的声明,然后在类的声明中先声明各个变量
    数据类变量的声明是可变的,因此使用一个单独的字符串变量装载,该字符串变量包括所有的变量声明语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public void GenerateData(XmlNodeList nodes)
    {
    string namespaceStr = "";
    string classNameStr = "";
    string fieldStr = "";

    foreach (XmlNode dataNode in nodes)
    {
    namespaceStr = dataNode.Attributes["namespace"].Value;
    classNameStr = dataNode.Attributes["name"].Value;
    XmlNodeList fields = dataNode.SelectNodes("field"); //获取所有的字段消息
    fieldStr = GetFieldStr(fields); //根据字段获取变量声明相关字符串
    string dataStr = $"using System;\r\n" +
    $"using System.Collections.Generic;\r\n" +
    $"using System.Text;\r\n" +
    $"using UnityEngine;\r\n\r\n" +
    //命名空间
    $"namespace {namespaceStr}\r\n" +
    "{\r\n" +
    //类声明
    $"\tpublic class {classNameStr} : BaseData\r\n" +
    "\t{\r\n" +
    //拼接各个变量声明的字符串
    $"{fieldStr}\r\n" +
    //TODO.. GetBytesNum声明
    //TODO.. Writing声明
    //TODO.. Reading声明
    "\t}\r\n" +
    "}";
    //保存为 脚本文件
    //保存文件的路径
    string path = SAVE_PATH + namespaceStr + "/Data/";
    //若不存在就创建
    if (!Directory.Exists(path))
    Directory.CreateDirectory(path);
    File.WriteAllText(path + classNameStr + ".cs", dataStr);
    }
    Debug.Log("数据结构类生成结束");
    }

    生成声明各个变量的语句逻辑较为复杂,因此另外声明一个方法GetFieldStr​,
    该方法需要导入<data>​节点下的所有的<field>​节点,遍历解析,生成所有数据类变量的声明
    其中list​、array​、dic​、enum​类型,它们需要另外识别类型,因此需要读取特定的属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    //拼接各个成员变量的方法
    private string GetFieldStr(XmlNodeList fields)
    {
    string fieldStr = "";
    //遍历所有的字段节点,生成对应的遍历声明语句
    foreach (XmlNode field in fields)
    {
    string type = field.Attributes["type"].Value; //变量类型
    string fieldName = field.Attributes["name"].Value; //变量名
    //列表声明语句
    if (type == "list")
    {
    string T = field.Attributes["T"].Value;
    fieldStr += $"\t\tpublic List<{T}> ";
    }
    //数组声明语句
    else if (type == "array")
    {
    string data = field.Attributes["data"].Value;
    fieldStr += $"\t\tpublic {data}[] ";
    }
    //字典声明语句
    else if (type == "dic")
    {
    string TKey = field.Attributes["TKey"].Value;
    string TValue = field.Attributes["TValue"].Value;
    fieldStr += $"\t\tpublic Dictionary<{TKey}, {TValue}> ";
    }
    //枚举声明语句
    else if (type == "enum")
    {
    string data = field.Attributes["data"].Value;
    fieldStr += $"\t\tpublic {data} ";
    }
    //变量声明语句
    else
    {
    fieldStr += $"\t\tpublic {type} ";
    }
    fieldStr += fieldName + ";\r\n";
    }
    return fieldStr;
    }
  2. 生成GetBytesNum​获取字节数函数

    假设我们要生成这样的GetBytesNum​方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    namespace GamePlayer
    {
    public class PlayerData : BaseData
    {
    public int id;
    public float atk;
    public bool sex;
    public long lev;
    public int[] arrays;
    public List<int> list;
    public Dictionary<int, string> dic;
    public E_HERO_TYPE heroType;

    public override int GetBytesNum()
    {
    int num = 0;
    num += 4; // id : sizeof(int)
    num += 4; // atk : sizeof(float)
    num += 1; // sex : sizeof(bool)
    num += 8; // lev : sizeof(long)
    num += 2; // arrays.Length : sizeof(short)
    for (int i = 0; i < arrays.Length; ++i)
    num += 4; // arrays : sizeof(int)
    num += 2; // list.Count : sizeof(short)
    for (int i = 0; i < list.Count; ++i)
    num += 4; // list : sizeof(int)
    num += 2; // dic.Count : sizeof(short)
    foreach (int key in dic.Keys)
    {
    num += 4; // dic : sizeof(int, string)
    num += 4 + Encoding.UTF8.GetByteCount(dic[key]);
    }
    num += 4; // heroType : sizeof(enum)
    return num;
    }
    }
    }

    GetBytesNum​的声明语句是可变的,因此将其可变部分使用一个单独的字符串变量装载,该字符串变量包括所有可变的GetBytesNum​方法声明语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    public void GenerateData(XmlNodeList nodes)
    {
    string namespaceStr = "";
    string classNameStr = "";
    string fieldStr = "";
    string getBytesNumStr = "";

    foreach (XmlNode dataNode in nodes)
    {
    namespaceStr = dataNode.Attributes["namespace"].Value;
    classNameStr = dataNode.Attributes["name"].Value;
    XmlNodeList fields = dataNode.SelectNodes("field"); //获取所有的字段消息
    fieldStr = GetFieldStr(fields); //根据字段获取变量声明相关字符串
    //根据所有字段,对GetBytesNum函数中的字符串内容进行拼接 返回结果
    getBytesNumStr = GetGetBytesNumStr(fields);
    string dataStr = $"using System;\r\n" +
    $"using System.Collections.Generic;\r\n" +
    $"using System.Text;\r\n" +
    $"using UnityEngine;\r\n\r\n" +
    //命名空间
    $"namespace {namespaceStr}\r\n" +
    "{\r\n" +
    //类声明
    $"\tpublic class {classNameStr} : BaseData\r\n" +
    "\t{\r\n" +
    //拼接各个变量声明的字符串
    $"{fieldStr}\r\n" +
    //GetBytesNum声明
    $"\t\tpublic override int GetBytesNum()\r\n" +
    "\t\t{\r\n" +
    "\t\t\tint num = 0;\r\n" +
    $"{getBytesNumStr}" +
    "\t\t\treturn num;\r\n" +
    "\t\t}\r\n\r\n" +
    //TODO.. Writing声明
    //TODO.. Reading声明
    "\t}\r\n" +
    "}";
    //保存为 脚本文件
    //保存文件的路径
    string path = SAVE_PATH + namespaceStr + "/Data/";
    //若不存在就创建
    if (!Directory.Exists(path))
    Directory.CreateDirectory(path);
    File.WriteAllText(path + classNameStr + ".cs", dataStr);
    }
    Debug.Log("数据结构类生成结束");
    }

    生成GetBytesNum​的语句逻辑较为复杂,因此另外声明一个方法GetGetBytesNumStr
    该方法需要遍历所有的字段,识别类型,根据字段的类型获取对应的字节数,再将生成字节数累加语句,
    最终使得生成的GetBytesNum​方法可以获取消息类所有成员变量的总长度

    其中,基础类型包括枚举的变量的字节数是固定的,因此可以直接再另外声明GetValueBytesNum​用于获取各种基本类型的字节数

    list​、array​、dic​的字节数是可变的,我们需要像字符串那样,
    +2​用来存储它们元素的数量,在根据list​、array​、dic​的类型来循环累加字节数
    这样反序列化可以前通过前两个字节来确认元素数量,再根据数量循环反序列化各个元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    //拼接GetBytesNum函数的方法
    private string GetGetBytesNumStr(XmlNodeList fields)
    {
    string bytesNumStr = "";
    string type = "";
    string name = "";
    foreach (XmlNode field in fields)
    {
    type = field.Attributes["type"].Value;
    name = field.Attributes["name"].Value;
    if (type == "list")
    {
    string T = field.Attributes["T"].Value;
    //+2 是为了节约字节数,用一个short去存储消息
    bytesNumStr += $"\t\t\tnum += 2;\t\t// {name}.Count : sizeof(short)\r\n";
    bytesNumStr += $"\t\t\tfor (int i = 0; i < {name}.Count; ++i)\r\n";
    //这里使用的是 name + [i] 目的是获取list当中的元素传入进行使用
    bytesNumStr += $"\t\t\t\tnum += {GetValueBytesNum(T, $"{name}[i]")};\t// {name} : sizeof({T})\r\n";
    }
    else if (type == "array")
    {
    string data = field.Attributes["data"].Value;
    //+2 是为了节约字节数,用一个short去存储消息
    bytesNumStr += $"\t\t\tnum += 2;\t\t// {name}.Length : sizeof(short)\r\n";
    bytesNumStr += $"\t\t\tfor (int i = 0; i < {name}.Length; ++i)\r\n";
    //这里使用的是 name + [i] 目的是获取list当中的元素传入进行使用
    bytesNumStr += $"\t\t\t\tnum += {GetValueBytesNum(data, $"{name}[i]")};\t// {name} : sizeof({data})\r\n";
    }
    else if (type == "dic")
    {
    string TKey = field.Attributes["TKey"].Value;
    string TValue = field.Attributes["TValue"].Value;
    //+2 是为了节约字节数,用一个short去存储消息
    bytesNumStr += $"\t\t\tnum += 2;\t\t// {name}.Count : sizeof(short)\r\n";
    bytesNumStr += $"\t\t\tforeach ({TKey} key in {name}.Keys)\r\n";
    bytesNumStr += "\t\t\t{\r\n";
    bytesNumStr += $"\t\t\t\tnum += {GetValueBytesNum(TKey, $"key")};\t// {name} : sizeof({TKey}, {TValue})\r\n";
    bytesNumStr += $"\t\t\t\tnum += {GetValueBytesNum(TValue, $"{name}[key]")};\r\n";
    bytesNumStr += "\t\t\t}\r\n";
    }
    else
    bytesNumStr += $"\t\t\tnum += {GetValueBytesNum(type, name)};\t\t// {name} : sizeof({type})\r\n";
    }
    return bytesNumStr;
    }

    private string GetValueBytesNum(string type, string name)
    {
    switch (type)
    {
    case "int":
    case "float":
    case "enum":
    return "4";
    case "byte":
    case "bool":
    return "1";
    case "short":
    return "2";
    case "long":
    case "double":
    return "8";
    case "string":
    return $"4 + Encoding.UTF8.GetByteCount({name})";
    default:
    return $"{name}.GetBytesNum()";
    }
    }
  3. 生成Writing​序列化函数

    假设我们要生成这样的Writing​序列化函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    namespace GamePlayer
    {
    public class PlayerData : BaseData
    {
    public int id;
    public float atk;
    public bool sex;
    public long lev;
    public int[] arrays;
    public List<int> list;
    public Dictionary<int, string> dic;
    public E_HERO_TYPE heroType;

    public override int GetBytesNum()
    {
    int num = 0;
    num += 4; // id : sizeof(int)
    num += 4; // atk : sizeof(float)
    num += 1; // sex : sizeof(bool)
    num += 8; // lev : sizeof(long)
    num += 2; // arrays.Length : sizeof(short)
    for (int i = 0; i < arrays.Length; ++i)
    num += 4; // arrays : sizeof(int)
    num += 2; // list.Count : sizeof(short)
    for (int i = 0; i < list.Count; ++i)
    num += 4; // list : sizeof(int)
    num += 2; // dic.Count : sizeof(short)
    foreach (int key in dic.Keys)
    {
    num += 4; // dic : sizeof(int, string)
    num += 4 + Encoding.UTF8.GetByteCount(dic[key]);
    }
    num += 4; // heroType : sizeof(enum)
    return num;
    }

    public override byte[] Writing()
    {
    int index = 0;
    byte[] bytes = new byte[GetBytesNum()];
    WriteInt(bytes, id, ref index);
    WriteFloat(bytes, atk, ref index);
    WriteBool(bytes, sex, ref index);
    WriteLong(bytes, lev, ref index);
    WriteShort(bytes, (short)arrays.Length, ref index);
    for (int i = 0; i < arrays.Length; ++i)
    WriteInt(bytes, arrays[i], ref index);
    WriteShort(bytes, (short)list.Count, ref index);
    for (int i = 0; i < list.Count; ++i)
    WriteInt(bytes, list[i], ref index);
    WriteShort(bytes, (short)dic.Count, ref index);
    foreach (int key in dic.Keys)
    {
    WriteInt(bytes, key, ref index);
    WriteString(bytes, dic[key], ref index);
    }
    WriteInt(bytes, Convert.ToInt32(heroType), ref index);
    return bytes;
    }
    }
    }

    Writing​的声明语句也是可变的,因此将其可变部分使用一个单独的字符串变量装载,该字符串变量包括所有可变的Writing​方法声明语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    public void GenerateData(XmlNodeList nodes)
    {
    string namespaceStr = "";
    string classNameStr = "";
    string fieldStr = "";
    string getBytesNumStr = "";
    string writingStr = "";

    foreach (XmlNode dataNode in nodes)
    {
    namespaceStr = dataNode.Attributes["namespace"].Value;
    classNameStr = dataNode.Attributes["name"].Value;
    XmlNodeList fields = dataNode.SelectNodes("field"); //获取所有的字段消息
    fieldStr = GetFieldStr(fields); //根据字段获取变量声明相关字符串
    //根据所有字段,对GetBytesNum函数中的字符串内容进行拼接 返回结果
    getBytesNumStr = GetGetBytesNumStr(fields);
    //通过所有字段,对Writing函数中的字符串内容进行拼接 返回结果
    writingStr = GetWritingStr(fields);
    string dataStr = $"using System;\r\n" +
    $"using System.Collections.Generic;\r\n" +
    $"using System.Text;\r\n" +
    $"using UnityEngine;\r\n\r\n" +
    //命名空间
    $"namespace {namespaceStr}\r\n" +
    "{\r\n" +
    //类声明
    $"\tpublic class {classNameStr} : BaseData\r\n" +
    "\t{\r\n" +
    //拼接各个变量声明的字符串
    $"{fieldStr}\r\n" +
    //GetBytesNum声明
    $"\t\tpublic override int GetBytesNum()\r\n" +
    "\t\t{\r\n" +
    "\t\t\tint num = 0;\r\n" +
    $"{getBytesNumStr}" +
    "\t\t\treturn num;\r\n" +
    "\t\t}\r\n\r\n" +
    //Writing声明
    $"\t\tpublic override byte[] Writing()\r\n" +
    "\t\t{\r\n" +
    "\t\t\tint index = 0;\r\n" +
    $"\t\t\tbyte[] bytes = new byte[GetBytesNum()];\r\n" +
    $"{writingStr}" +
    "\t\t\treturn bytes;\r\n" +
    "\t\t}\r\n\r\n" +
    //TODO.. Reading声明
    "\t}\r\n" +
    "}";
    //保存为 脚本文件
    //保存文件的路径
    string path = SAVE_PATH + namespaceStr + "/Data/";
    //若不存在就创建
    if (!Directory.Exists(path))
    Directory.CreateDirectory(path);
    File.WriteAllText(path + classNameStr + ".cs", dataStr);
    }
    Debug.Log("数据结构类生成结束");
    }

    生成Writing​的语句逻辑较为复杂,因此另外声明一个方法GetWritingStr
    该方法需要遍历所有的字段,识别类型,根据字段的类型,生成对应的序列化语句
    最终使得生成的Writing​方法可以将消息类对象自己进行序列化

    由于已经封装了基础类型变量的序列化方法,可以直接调用,因此再声明GetFieldWritingStr​来生成调用序列化基础类型变量方法的字符串

    list​、array​、dic​的序列化语句是可变的,我们需要像字符串那样,
    先生成序列化它们的元素的数量Short​的语句,再生成循环序列化其元素的语句,
    这样反序列化就会先得到数量,在根据数量反序列化多少个元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    //拼接Writing函数的方法
    private string GetWritingStr(XmlNodeList fields)
    {
    string writingStr = "";
    string type = "";
    string name = "";
    foreach (XmlNode field in fields)
    {
    type = field.Attributes["type"].Value;
    name = field.Attributes["name"].Value;
    if (type == "list")
    {
    string T = field.Attributes["T"].Value;
    writingStr += $"\t\t\tWriteShort(bytes, (short){name}.Count, ref index);\r\n";
    writingStr += $"\t\t\tfor (int i = 0; i < {name}.Count; ++i)\r\n";
    writingStr += $"\t\t\t\t{GetFieldWritingStr(T, $"{name}[i]")}\r\n";
    }
    else if (type == "array")
    {
    string data = field.Attributes["data"].Value;
    writingStr += $"\t\t\tWriteShort(bytes, (short){name}.Length, ref index);\r\n";
    writingStr += $"\t\t\tfor (int i = 0; i < {name}.Length; ++i)\r\n";
    writingStr += $"\t\t\t\t{GetFieldWritingStr(data, $"{name}[i]")}\r\n";
    }
    else if (type == "dic")
    {
    string TKey = field.Attributes["TKey"].Value;
    string TValue = field.Attributes["TValue"].Value;
    writingStr += $"\t\t\tWriteShort(bytes, (short){name}.Count, ref index);\r\n";
    writingStr += $"\t\t\tforeach ({TKey} key in {name}.Keys)\r\n";
    writingStr += "\t\t\t{\r\n";
    writingStr += $"\t\t\t\t{GetFieldWritingStr(TKey, "key")}\r\n";
    writingStr += $"\t\t\t\t{GetFieldWritingStr(TValue, $"{name}[key]")}\r\n";
    writingStr += "\t\t\t}\r\n";
    }
    else
    {
    writingStr += $"\t\t\t{GetFieldWritingStr(type, name)}\r\n";
    }
    }
    return writingStr;
    }

    //获取各种基本类型的序列化语句字符串
    private string GetFieldWritingStr(string type, string name)
    {
    switch (type)
    {
    case "byte":
    return $"WriteByte(bytes, {name}, ref index);";
    case "int":
    return $"WriteInt(bytes, {name}, ref index);";
    case "enum":
    return $"WriteInt(bytes, Convert.ToInt32({name}), ref index);";
    case "short":
    return $"WriteShort(bytes, {name}, ref index);";
    case "long":
    return $"WriteLong(bytes, {name}, ref index);";
    case "float":
    return $"WriteFloat(bytes, {name}, ref index);";
    case "double":
    return $"WriteDouble(bytes, {name}, ref index);";
    case "bool":
    return $"WriteBool(bytes, {name}, ref index);";
    case "string":
    return $"WriteString(bytes, {name}, ref index);";
    default:
    return $"WriteData(bytes, {name}, ref index);";
    }
    }
  4. 生成Reading​反序列化函数

    假设我们要生成这样的Reading​反序列化函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    namespace GamePlayer
    {
    public class PlayerData : BaseData
    {
    public int id;
    public float atk;
    public bool sex;
    public long lev;
    public int[] arrays;
    public List<int> list;
    public Dictionary<int, string> dic;
    public E_HERO_TYPE heroType;

    public override int GetBytesNum()
    {
    int num = 0;
    num += 4; // id : sizeof(int)
    num += 4; // atk : sizeof(float)
    num += 1; // sex : sizeof(bool)
    num += 8; // lev : sizeof(long)
    num += 2; // arrays.Length : sizeof(short)
    for (int i = 0; i < arrays.Length; ++i)
    num += 4; // arrays : sizeof(int)
    num += 2; // list.Count : sizeof(short)
    for (int i = 0; i < list.Count; ++i)
    num += 4; // list : sizeof(int)
    num += 2; // dic.Count : sizeof(short)
    foreach (int key in dic.Keys)
    {
    num += 4; // dic : sizeof(int, string)
    num += 4 + Encoding.UTF8.GetByteCount(dic[key]);
    }
    num += 4; // heroType : sizeof(enum)
    return num;
    }

    //省略Writing

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
    int index = beginIndex;
    id = ReadInt(bytes, ref index);
    atk = ReadFloat(bytes, ref index);
    sex = ReadBool(bytes, ref index);
    lev = ReadLong(bytes, ref index);
    short arraysLength = ReadShort(bytes, ref index);
    arrays = new int[arraysLength];
    for (int i = 0; i < arraysLength; ++i)
    arrays[i] = ReadInt(bytes, ref index);
    list = new List<int>();
    short listCount = ReadShort(bytes, ref index);
    for (int i = 0; i < listCount; ++i)
    list.Add(ReadInt(bytes, ref index));
    dic = new Dictionary<int, string>();
    short dicCount = ReadShort(bytes, ref index);
    for (int i = 0; i < dicCount; ++i)
    dic.Add(ReadInt(bytes, ref index), ReadString(bytes, ref index));
    heroType = (E_HERO_TYPE)ReadInt(bytes, ref index);
    return index - beginIndex;
    }
    }
    }

    Reading​的声明语句也是可变的,因此将其可变部分使用一个单独的字符串变量装载,该字符串变量包括所有可变的Reading​方法声明语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    public void GenerateData(XmlNodeList nodes)
    {
    string namespaceStr = "";
    string classNameStr = "";
    string fieldStr = "";
    string getBytesNumStr = "";
    string writingStr = "";
    string readingStr = "";

    foreach (XmlNode dataNode in nodes)
    {
    namespaceStr = dataNode.Attributes["namespace"].Value;
    classNameStr = dataNode.Attributes["name"].Value;
    XmlNodeList fields = dataNode.SelectNodes("field"); //获取所有的字段消息
    fieldStr = GetFieldStr(fields); //根据字段获取变量声明相关字符串
    //根据所有字段,对GetBytesNum函数中的字符串内容进行拼接 返回结果
    getBytesNumStr = GetGetBytesNumStr(fields);
    //根据所有字段,对Writing函数中的字符串内容进行拼接 返回结果
    writingStr = GetWritingStr(fields);
    //根据所有字段,对Reading函数中的字符串内容进行拼接 返回结果
    readingStr = GetReadingStr(fields);
    string dataStr = $"using System;\r\n" +
    $"using System.Collections.Generic;\r\n" +
    $"using System.Text;\r\n" +
    $"using UnityEngine;\r\n\r\n" +
    //命名空间
    $"namespace {namespaceStr}\r\n" +
    "{\r\n" +
    //类声明
    $"\tpublic class {classNameStr} : BaseData\r\n" +
    "\t{\r\n" +
    //拼接各个变量声明的字符串
    $"{fieldStr}\r\n" +
    //GetBytesNum声明
    $"\t\tpublic override int GetBytesNum()\r\n" +
    "\t\t{\r\n" +
    "\t\t\tint num = 0;\r\n" +
    $"{getBytesNumStr}" +
    "\t\t\treturn num;\r\n" +
    "\t\t}\r\n\r\n" +
    //Writing声明
    $"\t\tpublic override byte[] Writing()\r\n" +
    "\t\t{\r\n" +
    "\t\t\tint index = 0;\r\n" +
    $"\t\t\tbyte[] bytes = new byte[GetBytesNum()];\r\n" +
    $"{writingStr}" +
    "\t\t\treturn bytes;\r\n" +
    "\t\t}\r\n\r\n" +
    //Reading声明
    $"\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +
    "\t\t{\r\n" +
    "\t\t\tint index = beginIndex;\r\n" +
    $"{readingStr}" +
    "\t\t\treturn index - beginIndex;\r\n" +
    "\t\t}\r\n" +
    "\t}\r\n" +
    "}";
    //保存为 脚本文件
    //保存文件的路径
    string path = SAVE_PATH + namespaceStr + "/Data/";
    //若不存在就创建
    if (!Directory.Exists(path))
    Directory.CreateDirectory(path);
    File.WriteAllText(path + classNameStr + ".cs", dataStr);
    }
    Debug.Log("数据结构类生成结束");
    }

    生成Reading​的语句逻辑较为复杂,因此另外声明一个方法GetReadingStr
    该方法需要遍历所有的字段,识别类型,根据字段的类型,生成对应的反序列化语句
    最终使得生成的Reading​方法可以从字节数组中反序列化对象

    由于已经封装了基础类型变量的反序列化方法,可以直接调用,因此再声明GetFieldReadingStr​来生成调用反序列化基础类型变量方法的字符串

    其中enum​的反序列化实际上是反序列化一个int​值,再强转为对应枚举

    list​、array​、dic​的反序列化语句是可变的,我们需要像字符串那样,
    先生成反序列化它们的元素的数量Short​的语句,再生成循环反序列化元素的语句,
    这样反序列化就会先得到数量,在根据数量反序列化多少个元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    //拼接Reading函数的方法
    private string GetReadingStr(XmlNodeList fields)
    {
    string readingStr = "";
    string type = "";
    string name = "";
    foreach (XmlNode field in fields)
    {
    type = field.Attributes["type"].Value;
    name = field.Attributes["name"].Value;
    if (type == "list")
    {
    string T = field.Attributes["T"].Value;
    readingStr += $"\t\t\t{name} = new List<{T}>();\r\n";
    readingStr += $"\t\t\tshort {name}Count = ReadShort(bytes, ref index);\r\n";
    readingStr += $"\t\t\tfor (int i = 0; i < {name}Count; ++i)\r\n";
    readingStr += $"\t\t\t\t{name}.Add({GetFieldReadingStr(T)});\r\n";

    }
    else if (type == "array")
    {
    string data = field.Attributes["data"].Value;
    readingStr += $"\t\t\tshort {name}Length = ReadShort(bytes, ref index);\r\n";
    readingStr += $"\t\t\t{name} = new {data}[{name}Length];\r\n";
    readingStr += $"\t\t\tfor (int i = 0; i < {name}Length; ++i)\r\n";
    readingStr += $"\t\t\t\t{name}[i] = {GetFieldReadingStr(data)};\r\n";
    }
    else if (type == "dic")
    {
    string TKey = field.Attributes["TKey"].Value;
    string TValue = field.Attributes["TValue"].Value;
    readingStr += $"\t\t\t{name} = new Dictionary<{TKey}, {TValue}>();\r\n";
    readingStr += $"\t\t\tshort {name}Count = ReadShort(bytes, ref index);\r\n";
    readingStr += $"\t\t\tfor (int i = 0; i < {name}Count; ++i)\r\n";
    readingStr += $"\t\t\t\t{name}.Add({GetFieldReadingStr(TKey)}, {GetFieldReadingStr(TValue)});\r\n";
    }
    else if (type == "enum")
    {
    string data = field.Attributes["data"].Value;
    readingStr += $"\t\t\t{name} = ({data})ReadInt(bytes, ref index);\r\n";
    }
    else
    {
    readingStr += $"\t\t\t{name} = {GetFieldReadingStr(type)};\r\n";
    }
    }
    return readingStr;
    }

    //获取各种基本类型的反序列化语句字符串
    private string GetFieldReadingStr(string type)
    {
    switch (type)
    {
    case "byte":
    return $"ReadByte(bytes, ref index)";
    case "int":
    return $"ReadInt(bytes, ref index)";
    case "short":
    return $"ReadShort(bytes, ref index)";
    case "long":
    return $"ReadLong(bytes, ref index)";
    case "float":
    return $"ReadFloat(bytes, ref index)";
    case "double":
    return $"ReadDouble(bytes, ref index)";
    case "bool":
    return $"ReadBool(bytes, ref index)";
    case "string":
    return $"ReadString(bytes, ref index)";
    default:
    return $"ReadData<{type}>(bytes, ref index)";
    }
    }

制作生成消息类功能

消息类本身与数据类十分类似,只是多了一个包含消息ID和消息体长度的头数据
因此我们可以方便的复用之前在制作数据类功能时声明的方法,只是在生成语句方面需要添加一些特有的内容

假设要生成下面的消息类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
namespace GamePlayer
{
public class PlayerMsg : BaseMessage
{
public int playerID;
public PlayerData data;

public override int GetID()
{
return 1001;
}

public override int GetBytesNum()
{
int num = 0;
num += 4; // ID : size(int)
num += 4; // messageBodyLength : size(int)
num += 4; // playerID : sizeof(int)
num += data.GetBytesNum(); // data : sizeof(PlayerData)
return num;
}

public override byte[] Writing()
{
int index = 0;
int bytesNum = GetBytesNum();
byte[] bytes = new byte[bytesNum];
WriteInt(bytes, GetID(), ref index);
WriteInt(bytes, bytesNum - 8, ref index);
WriteInt(bytes, playerID, ref index);
WriteData(bytes, data, ref index);
return bytes;
}

public override int Reading(byte[] bytes, int beginIndex = 0)
{
int index = beginIndex;
playerID = ReadInt(bytes, ref index);
data = ReadData<PlayerData>(bytes, ref index);
return index - beginIndex;
}
}
}

分析可以发现,相对于数据类,
消息类的获取字节长度上多了获取消息ID和消息体的字节长度,序列化方法上多了序列化ID和消息体长度的逻辑,多了一个可以获取消息ID的方法

因此可以复制之前生成数据类的语句,在此基础上稍作修改即可完成生成消息类的逻辑

1
2
3
4
5
6
7
8
[MenuItem("ProtocolTool/生成C#脚本")]
private static void GenerateCSharp()
{
generateCSharp.GenerateEnum(GetNodes("enum"));
generateCSharp.GenerateData(GetNodes("data"));
generateCSharp.GenerateMessage(GetNodes("message"));
AssetDatabase.Refresh();
}

相对于数据类作修改的地方:

  • 需要额外从<message>​节点的属性内获取消息ID
  • 添加额外的GetID​方法,该方法唯一可变的地方是之前获取的消息ID
  • GetBytesNum​中,添加固定的累加消息头字节长度的语句
  • Writing​中,添加固定的序列化消息ID和消息体长度的语句
  • 修改存储路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public void GenerateMessage(XmlNodeList nodes)
{
string namespaceStr = "";
string classNameStr = "";
string idStr = "";
string fieldStr = "";
string getBytesNumStr = "";
string writingStr = "";
string readingStr = "";

foreach (XmlNode dataNode in nodes)
{
namespaceStr = dataNode.Attributes["namespace"].Value;
classNameStr = dataNode.Attributes["name"].Value;
idStr = dataNode.Attributes["id"].Value;
XmlNodeList fields = dataNode.SelectNodes("field");
fieldStr = GetFieldStr(fields);
//通过某个方法,对GetBytesNum函数中的字符串内容进行拼接 返回结果
getBytesNumStr = GetGetBytesNumStr(fields);
//通过某个方法,对Writing函数中的字符串内容进行拼接 返回结果
writingStr = GetWritingStr(fields);
readingStr = GetReadingStr(fields);
string messageStr = $"using System;\r\n" +
$"using System.Collections.Generic;\r\n" +
$"using System.Text;\r\n" +
$"using UnityEngine;\r\n\r\n" +
//命名空间
$"namespace {namespaceStr}\r\n" +
"{\r\n" +
//类声明
$"\tpublic class {classNameStr} : BaseMessage\r\n" +
"\t{\r\n" +
//各个变量声明
$"{fieldStr}\r\n" +
//GetID声明
"\t\tpublic override int GetID()\r\n" +
"\t\t{\r\n" +
$"\t\t\treturn {idStr};\r\n" +
"\t\t}\r\n\r\n" +
//GetBytesNum声明
$"\t\tpublic override int GetBytesNum()\r\n" +
"\t\t{\r\n" +
"\t\t\tint num = 0;\r\n" +
//添加固定的消息头长度
"\t\t\tnum += 4;\t\t// ID : size(int)\r\n" +
"\t\t\tnum += 4;\t\t// messageBodyLength : size(int)\r\n" +
$"{getBytesNumStr}" +
"\t\t\treturn num;\r\n" +
"\t\t}\r\n\r\n" +
//Writing声明
$"\t\tpublic override byte[] Writing()\r\n" +
"\t\t{\r\n" +
"\t\t\tint index = 0;\r\n" +
"\t\t\tint bytesNum = GetBytesNum();\r\n" +
$"\t\t\tbyte[] bytes = new byte[bytesNum];\r\n" +
//反序列化固定的消息头
$"\t\t\tWriteInt(bytes, GetID(), ref index);\r\n" +
$"\t\t\tWriteInt(bytes, bytesNum - 8, ref index);\r\n" +
$"{writingStr}" +
"\t\t\treturn bytes;\r\n" +
"\t\t}\r\n\r\n" +
//Reading声明
$"\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +
"\t\t{\r\n" +
"\t\t\tint index = beginIndex;\r\n" +
$"{readingStr}" +
"\t\t\treturn index - beginIndex;\r\n" +
"\t\t}\r\n" +
"\t}\r\n" +
"}";
//保存为 脚本文件
//保存文件的路径
string path = SAVE_PATH + namespaceStr + "/Message/";
//若不存在就创建
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
File.WriteAllText(path + classNameStr + ".cs", messageStr);
}
Debug.Log("消息类生成结束");
}