Unity网络开发实践项目

摘要:该网络通信系统基于Unity实现,包含以下几个核心模块:

  1. 协议配置:通过XML定义枚举(如玩家/英雄类型)、数据结构(如PlayerData)及消息协议(如PlayerMsg),支持基础类型、数组、字典等复杂结构。
  2. 代码生成工具:解析XML自动生成C#脚本,包括枚举类、可序列化的数据结构类(实现字节计算、序列化/反序列化)、消息类及消息池,减少手动编码。
  3. 网络管理器:采用异步Socket实现TCP通信,处理连接、心跳包(间隔2秒)、消息收发及粘包/分包问题,通过消息池动态映射ID与消息类型,结合队列机制解耦网络层与业务逻辑。
  4. 扩展性:支持多命名空间、自动目录生成,预留C++/Java接口,确保协议修改后代码自动同步,提升开发效率。
    整体设计实现了高内聚、低耦合的网络通信框架,适用于游戏等实时交互场景。
<?xml version="1.0" encoding="UTF-8"?>
<messages><!--枚举配置规则--><enum name="E_PLAYER_TYPE" namespace="GamePlayer"><field name="MAIN">1</field><field name="OTHER"/></enum><enum name="E_HERO_TYPE" namespace="GamePlayer"><field name="MAIN"/><field name="OTHER"/></enum><enum name="E_MONSTER_TYPE" namespace="GameMonster"><field name="NORMAL">2</field><field name="BOSS"/></enum><!--数据结构类配置规则--><data name="PlayerData" namespace="GamePlayer"><field type="int" name="id"/><field type="float" name="atk"/><field type="bool" name="sex"/><field type="long" name="lev"/><field type="array" data="int" name="arrays"/><field type="list" T="int" name="list"/><field type="dic" Tkey="int" Tvalue="string" name="dic"/><field type="enum" data="E_HERO_TYPE" name="heroType"/></data><!--消息类类配置规则--><message id="1001" name="PlayerMsg" namespace="GamePlayer"><field type="int" name="playerID"/><field type="PlayerData" name="data"/></message><message id="1002" name="HeartMsg" namespace="GameSystem"/><message id="1003" name="QuitMsg" namespace="GameSystem"/>
</messages>

该配置文件通过枚举、数据结构和消息定义,构建了游戏中玩家、怪物和系统交互的基础模型。枚举确保类型统一,数据结构支持复杂数据建模,消息机制实现模块间通信,整体设计符合游戏开发中数据配置的典型范式。

using System.Collections;
using System.Collections.Generic;
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(){//1.读取xml相关的信息//XmlNodeList list = GetNodes("enum");//2.根据这些信息 去拼接字符串 生成对应的脚本//生成对应的枚举脚本generateCSharp.GenerateEnum(GetNodes("enum"));//生成对应的数据结构类脚本generateCSharp.GenerateData(GetNodes("data"));//生成对应的消息类脚本generateCSharp.GenerateMsg(GetNodes("message"));//生成消息池generateCSharp.GenerateMsgPool(GetNodes("message"));//刷新编辑器界面 让我们可以看到生成的内容 不需要手动进行刷新了AssetDatabase.Refresh();}[MenuItem("ProtocolTool/生成C++脚本")]private static void GenerateC(){Debug.Log("生成C++代码");}[MenuItem("ProtocolTool/生成Java脚本")]private static void GenerateJava(){Debug.Log("生成Java代码");}/// <summary>/// 获取指定名字的所有子节点 的 List/// </summary>/// <param name="nodeName"></param>/// <returns></returns>private static XmlNodeList GetNodes(string nodeName){XmlDocument xml = new XmlDocument();xml.Load(PROTO_INFO_PATH);XmlNode root = xml.SelectSingleNode("messages");return root.SelectNodes(nodeName);}
}

这段代码是一个 Unity 编辑器扩展工具,用于根据 XML 配置文件自动生成多语言协议代码,主要功能如下:

  1. 配置解析:读取 XML 配置文件(如用户提供的协议定义),提取枚举、数据结构和消息定义。

  2. 代码生成

    • 通过菜单命令(ProtocolTool / 生成 C# 脚本)触发,生成 C# 协议类文件
    • 支持生成:枚举类型、数据结构类、消息类、消息池管理类
    • 预留了 C++ 和 Java 代码生成接口(仅打印日志)
  3. 工具集成

    • 在 Unity 编辑器菜单中添加功能入口
    • 生成后自动刷新项目视图,无需手动操作
  4. 核心逻辑

    • 使用GenerateCSharp类处理代码生成逻辑
    • 通过 XPath 查询 XML 节点,提取协议定义信息

这个工具的设计目标是简化游戏网络协议开发流程,将配置文件自动转换为各语言的代码实现,提高开发效率并减少手动编码错误。

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using UnityEngine;public class GenerateCSharp
{//协议保存路径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 enumField in enumFields){fieldStr += "\t\t" + enumField.Attributes["name"].Value;if (enumField.InnerText != "")fieldStr += " = " + enumField.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("枚举生成结束");}//生成数据结构类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" + $"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseData\r\n" +"\t{\r\n" +$"{fieldStr}" +"\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" +"\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" +"\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("数据结构类生成结束");}//生成消息类public void GenerateMsg(XmlNodeList nodes){string idStr = "";string namespaceStr = "";string classNameStr = "";string fieldStr = "";string getBytesNumStr = "";string writingStr = "";string readingStr = "";foreach (XmlNode dataNode in nodes){//消息IDidStr = dataNode.Attributes["id"].Value;//命名空间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" +$"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseMsg\r\n" +"\t{\r\n" +$"{fieldStr}" +"\t\tpublic override int GetBytesNum()\r\n" +"\t\t{\r\n" +"\t\t\tint num = 8;\r\n" +//这个8代表的是 消息ID的4个字节 + 消息体长度的4个字节$"{getBytesNumStr}" +"\t\t\treturn num;\r\n" +"\t\t}\r\n" +"\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" +"\t\t\tWriteInt(bytes, GetID(), ref index);\r\n" +"\t\t\tWriteInt(bytes, bytes.Length - 8, ref index);\r\n" +$"{writingStr}" +"\t\t\treturn bytes;\r\n" +"\t\t}\r\n" +"\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\tpublic override int GetID()\r\n" +"\t\t{\r\n" +"\t\t\treturn " + idStr + ";\r\n" +"\t\t}\r\n" +"\t}\r\n" +"}";//保存为 脚本文件//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Msg/";//如果不存在这个文件夹 则创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为枚举脚本文件File.WriteAllText(path + classNameStr + ".cs", dataStr);//生成处理器脚本//判断处理器脚本是否存在 如果存在就不要覆盖 避免把写过的逻辑处理代码覆盖了//如果想要改变,就把没用的脚本删了,再生成就会是新的if (File.Exists(path + classNameStr + "Handler.cs"))continue;string handlerStr = $"namespace {namespaceStr}\r\n" +"{\r\n" + $"\tpublic class {classNameStr}Handler : BaseHandler"+"\t{\r\n"+"\t\tpublic override void MsgHandler()\r\n"+"\t\t{\r\n"+$"\t\t\t{classNameStr} msg = message as {classNameStr};\r\n"+"\t\t}\r\n"+"\t}\r\n"+"}\r\n";//把消息处理器类的内容保存到本地File.WriteAllText(path + classNameStr + "Handler.cs", handlerStr);}Debug.Log("消息类生成结束");}//生成消息池类//主要就是ID和消息类型以及消息处理器类型的对应关系public void GenerateMsgPool(XmlNodeList nodes){List<string> ids = new List<string>();List<string> names = new List<string>();List<string> nameSpaces = new List<string>();foreach (XmlNode  dataNode in nodes){//记录所有消息的IDstring id = dataNode.Attributes["id"].Value;if (!ids.Contains(id))ids.Add(id);elseDebug.LogError("存在相同ID的消息" + id);string name = dataNode.Attributes["name"].Value;if (!names.Contains(name))names.Add(name);elseDebug.LogError("存在同名的消息" + name + ",建议即使在不同的命名空间下也使用不同的消息名字");string msgNameSpace = dataNode.Attributes["namespace"].Value;if (!nameSpaces.Contains(msgNameSpace))nameSpaces.Add(msgNameSpace);}//获取所有需要引用的命名空间 拼接好string nameSpaceStr = "";for (int i = 0; i < nameSpaces.Count; i++)nameSpaceStr += $"using {nameSpaces[i]};\r\n";//获取所有消息注册相关内容string registerStr = "";for (int i = 0; i < ids.Count; i++)registerStr += $"\t\tRegister({ids[i]},typeof({names[i]}),typeof({names[i]}Handler));\r\n";string msgPoolStr = "using System;\r\n" +"using System.Collections.Generic;\r\n" +nameSpaceStr +"public class MsgPool\r\n" +"{\r\n" +"\tprivate Dictionary<int, Type> message = new Dictionary<int, Type>();\r\n" +"\tprivate Dictionary<int, Type> handlers = new Dictionary<int, Type>();\r\n" +"\tpublic MsgPool ()\r\n" +"\t{\r\n" +registerStr +"\t}\r\n" +"\tprivate void Register(int id,Type messageType,Type handlerType)\r\n" +"\t{\r\n" +"\t\tmessage.Add(id, messageType);\r\n" +"\t\thandlers.Add(id, handlerType);\r\n" +"\t}\r\n" +"\tpublic BaseMsg GetMessage(int id)\r\n" +"\t{\r\n" +"\t\tif (!message.ContainsKey(id))\r\n" +"\t\t\treturn null;\r\n" +"\t\treturn Activator.CreateInstance(message[id]) as BaseMsg;\r\n" +"\t}\r\n" +"\tpublic BaseHandler GetHandler(int id)\r\n" +"\t{\r\n" +"\t\tif (!handlers.ContainsKey(id))\r\n" +"\t\t\treturn null;\r\n" +"\t\treturn Activator.CreateInstance(handlers[id]) as BaseHandler;\r\n" +"\t}\r\n" +"}\r\n";string path = SAVE_PATH + "/Pool/";if (!Directory.Exists(path))Directory.CreateDirectory(path);File.WriteAllText(path + "MsgPool.cs", msgPoolStr);}/// <summary>/// 获取成员变量声明内容/// </summary>/// <param name="fields"></param>/// <returns></returns>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;}//拼接 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;bytesNumStr += "\t\t\tnum += 2;\r\n";//+2 是为了节约字节数 用一个short去存储信息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]") + ";\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";//+2 是为了节约字节数 用一个short去存储信息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]") + ";\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";//+2 是为了节约字节数 用一个short去存储信息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") + ";\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tvalue, name + "[key]") + ";\r\n";bytesNumStr += "\t\t\t}\r\n";}elsebytesNumStr += "\t\t\tnum += " + GetValueBytesNum(type, name) + ";\r\n";}return bytesNumStr;}//获取 指定类型的字节数private string GetValueBytesNum(string type, string name){//这里我没有写全 所有的常用变量类型 你可以根据需求去添加switch (type){case "int":case "float":case "enum":return "4";case "long":return "8";case "byte":case "bool":return "1";case "short":return "2";case "string":return "4 + Encoding.UTF8.GetByteCount(" + name + ")";default:return name + ".GetBytesNum()";}}//拼接 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 "short":return "WriteShort(bytes, " + name + ", ref index);";case "long":return "WriteLong(bytes, " + name + ", ref index);";case "float":return "WriteFloat(bytes, " + name + ", ref index);";case "bool":return "WriteBool(bytes, " + name + ", ref index);";case "string":return "WriteString(bytes, " + name + ", ref index);";case "enum":return "WriteInt(bytes, Convert.ToInt32(" + name + "), ref index);";default:return "WriteData(bytes, " + name + ", ref index);";}}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";}elsereadingStr += "\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 "bool":return "ReadBool(bytes, ref index)";case "string":return "ReadString(bytes, ref index)";default:return "ReadData<" + type + ">(bytes, ref index)";}}
}

这段代码是 Unity 中用于自动生成 C# 协议相关脚本的工具类,核心功能是解析 XML 配置文件并生成对应的枚举、数据结构、消息类及消息池管理代码,具体作用如下:

1. 代码生成核心逻辑

1.1 枚举生成(GenerateEnum
  • 输入:XML 中所有<enum>节点(如玩家类型、怪物类型等)。
  • 处理
    • 提取命名空间(namespace)、枚举名(name)和字段(field)。
    • 自动拼接枚举代码字符串(包含字段名和值),例如:

      csharp

      namespace GamePlayer { public enum E_PLAYER_TYPE { MAIN = 1, OTHER } }
      
  • 输出:按命名空间分层存储的枚举脚本(如GamePlayer/Enum/E_PLAYER_TYPE.cs)。
1.2 数据结构类生成(GenerateData
  • 输入:XML 中所有<data>节点(如PlayerData)。
  • 处理
    • 解析字段类型(基础类型、数组、列表、字典、枚举),生成对应的成员变量声明。
    • 自动实现BaseData抽象类的GetBytesNum(计算字节长度)、Writing(序列化)、Reading(反序列化)方法。
    • 例如,数组 / 列表会先写入长度(short类型),再循环写入元素;枚举类型会转换为整数存储。
  • 输出:数据结构类脚本(如GamePlayer/Data/PlayerData.cs),支持网络传输的数据序列化 / 反序列化。
1.3 消息类生成(GenerateMsg
  • 输入:XML 中所有<message>节点(如PlayerMsg)。
  • 处理
    • 生成消息类(继承BaseMsg),包含消息 ID(GetID方法)、字段序列化 / 反序列化逻辑。
    • 自动生成消息处理器脚本(如PlayerMsgHandler.cs),用于处理消息逻辑(需手动补充业务代码)。
    • 消息协议格式:前 8 字节固定为消息 ID(4 字节)和消息体长度(4 字节),后续为具体字段数据。
  • 输出:消息类脚本(如GamePlayer/Msg/PlayerMsg.cs)和处理器脚本。
1.4 消息池生成(GenerateMsgPool
  • 功能:创建MsgPool类,维护消息 ID 与消息类型、处理器类型的映射关系。
  • 处理
    • 从 XML 中提取所有消息 ID、名称和命名空间,生成注册代码(Register方法)。
    • 提供GetMessageGetHandler方法,通过反射创建消息实例和处理器。
  • 输出:消息池管理脚本(Pool/MsgPool.cs),用于统一管理消息的创建和分发。

2. 辅助工具方法

2.1 字段解析(GetFieldStr
  • 作用:根据字段类型(list/array/dic/enum/ 基础类型)生成对应的成员变量声明。
    • 例如:list<int>生成public List<int> list;dic<int, string>生成public Dictionary<int, string> dic;
2.2 字节计算与序列化 / 反序列化(GetGetBytesNumStr/GetWritingStr/GetReadingStr
  • 字节计算
    • 基础类型直接返回固定字节数(如int=4string需计算 UTF8 字节长度)。
    • 容器类型(列表 / 数组 / 字典)先写入长度(short,2 字节),再递归计算元素字节数。
  • 序列化(Writing
    • 通过WriteInt/WriteShort等方法将数据写入字节数组,容器类型循环写入元素。
  • 反序列化(Reading
    • 通过ReadInt/ReadShort等方法从字节数组读取数据,容器类型先读取长度再循环读取元素,枚举类型通过强制转换还原。

3. 目录结构与文件管理

  • 输出路径
    • 枚举:Assets/Scripts/Protocol/[命名空间]/Enum/
    • 数据结构:Assets/Scripts/Protocol/[命名空间]/Data/
    • 消息类:Assets/Scripts/Protocol/[命名空间]/Msg/
    • 消息池:Assets/Scripts/Protocol/Pool/
  • 自动创建目录:若路径不存在,自动创建文件夹(如GamePlayer/Enum)。
  • 避免覆盖:消息处理器脚本若已存在则跳过生成,防止覆盖手动编写的逻辑。

4. 工具集成与使用

  • 触发方式:通过 Unity 编辑器菜单ProtocolTool/生成C#脚本调用,自动解析 XML 并生成代码。
  • 依赖项:需提前定义BaseDataBaseMsg抽象类,以及序列化工具方法(如WriteInt/ReadInt)。
  • 扩展能力:预留了生成 C++/Java 代码的接口(当前仅打印日志,需进一步实现)。

总结

该工具通过解析 XML 配置文件,自动化生成游戏开发中所需的协议相关 C# 代码,涵盖枚举定义、数据结构序列化、消息通信和消息池管理,显著减少手动编码工作量,提高开发效率,尤其适用于需要频繁修改协议的网络通信场景。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using GamePlayer;
using GameSystem;
using UnityEngine;public class NetAsyncMgr : MonoBehaviour
{private static NetAsyncMgr instance;public static NetAsyncMgr Instance => instance;//和服务器进行连接的 Socketprivate Socket socket;//接受消息用的 缓存容器private byte[] cacheBytes = new byte[1024 * 1024];private int cacheNum = 0;private Queue<BaseHandler> receiveQueue = new Queue<BaseHandler>();//发送心跳消息的间隔时间private int SEND_HEART_MSG_TIME = 2;private HeartMsg hearMsg = new HeartMsg();//消息池对象 用于快速获取消息和处理消息处理类对象private MsgPool msgPool = new MsgPool();// Start is called before the first frame updatevoid Awake(){instance = this;//过场景不移除DontDestroyOnLoad(this.gameObject);//客户端循环定时给服务端发送心跳消息InvokeRepeating("SendHeartMsg", 0, SEND_HEART_MSG_TIME);}private void SendHeartMsg(){if (socket != null && socket.Connected)Send(hearMsg);}// Update is called once per framevoid Update(){if (receiveQueue.Count > 0){//目标二:不要每次添加了新消息 就在这里去处理对应消息的逻辑//更加自动化的去处理他们 并且不要在网络层这来处理//通过消息处理者基类对象 调用处理方法 以后无论添加多少消息 都不用修改了receiveQueue.Dequeue().MsgHandler();}}//连接服务器的代码public void Connect(string ip, int port){if (socket != null && socket.Connected)return;IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.RemoteEndPoint = ipPoint;args.Completed += (socket, args) =>{if(args.SocketError == SocketError.Success){print("连接成功");//收消息SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();receiveArgs.SetBuffer(cacheBytes, 0, cacheBytes.Length);receiveArgs.Completed += ReceiveCallBack;this.socket.ReceiveAsync(receiveArgs);}else{print("连接失败" + args.SocketError);}};socket.ConnectAsync(args);}//收消息完成的回调函数private void ReceiveCallBack(object obj, SocketAsyncEventArgs args){if(args.SocketError == SocketError.Success){HandleReceiveMsg(args.BytesTransferred);//继续去收消息args.SetBuffer(cacheNum, args.Buffer.Length - cacheNum);//继续异步收消息if (this.socket != null && this.socket.Connected)socket.ReceiveAsync(args);elseClose();}else{print("接受消息出错" + args.SocketError);//关闭客户端连接Close();}}public void Close(bool isSelf=false){if(socket != null){QuitMsg msg = new QuitMsg();socket.Send(msg.Writing());socket.Shutdown(SocketShutdown.Both);socket.Disconnect(false);socket.Close();socket = null;}//不是自己主动断开连接的if(!isSelf){//短线重连,弹出一个面板}}public void SendTest(byte[] bytes){SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.SetBuffer(bytes, 0, bytes.Length);args.Completed += (socket, args) =>{if (args.SocketError != SocketError.Success){print("发送消息失败" + args.SocketError);Close();}};this.socket.SendAsync(args);}public void Send(BaseMsg msg){if(this.socket != null && this.socket.Connected){byte[] bytes = msg.Writing();SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.SetBuffer(bytes, 0, bytes.Length);args.Completed += (socket, args) =>{if (args.SocketError != SocketError.Success){print("发送消息失败" + args.SocketError);Close();}};this.socket.SendAsync(args);}else{Close();}}//处理接受消息 分包、黏包问题的方法private void HandleReceiveMsg(int receiveNum){int msgID = 0;int msgLength = 0;int nowIndex = 0;cacheNum += receiveNum;while (true){//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断msgLength = -1;//处理解析一条消息if (cacheNum - nowIndex >= 8){//解析IDmsgID = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;//解析长度msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;}if (cacheNum - nowIndex >= msgLength && msgLength != -1){//解析消息体//BaseMsg baseMsg = null;//BaseHandler handler = null;//目标一:不需要每次手动的去添加代码//添加了消息后 根据这个ID 就能自动的去根据ID得到对应的消息类 来进行反序列化//switch (msgID)//{//    case 1001://        baseMsg = new PlayerMsg();//        handler = new PlayerMsgHandler();//        baseMsg.Reading(cacheBytes, nowIndex);//        handler.message = baseMsg;//        break;//}//if (baseMsg != null)//    receiveQueue.Enqueue(handler);//得到一个指定ID的消息类对象 只不过是用父类装子类BaseMsg baseMsg = msgPool.GetMessage(msgID);if(baseMsg !=null){//反序列化baseMsg.Reading(cacheBytes, nowIndex);BaseHandler baseHandler = msgPool.GetHandler(msgID);baseHandler.message = baseMsg; }nowIndex += msgLength;if (nowIndex == cacheNum){cacheNum = 0;break;}}else{if (msgLength != -1)nowIndex -= 8;//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);cacheNum = cacheNum - nowIndex;break;}}}private void OnDestroy(){Close(true);}
}

这段代码是 Unity 中实现的异步网络通信管理器,用于处理客户端与服务器的 TCP 连接、消息收发及消息处理,核心功能如下:

1. 单例模式与初始化

  • 单例实例:通过Awake方法确保全局唯一实例,跨场景保持连接状态。
  • 心跳机制:通过InvokeRepeating定时发送心跳消息(HeartMsg),维持长连接。
  • 消息池依赖:使用MsgPool管理消息实例和处理器,实现消息类型与 ID 的动态映射。

2. 网络连接管理

2.1 连接服务器
  • 异步连接:通过SocketAsyncEventArgs实现非阻塞连接,连接成功后立即注册异步接收回调(ReceiveCallBack)。
  • 参数配置:支持传入 IP 和端口,创建 TCP 流式套接字(SocketType.Stream)。
2.2 关闭连接
  • 优雅断开:发送退出消息(QuitMsg)后关闭套接字,处理重连逻辑(预留扩展)。
  • 错误处理:连接 / 收发失败时自动关闭连接,触发可能的重连机制。

3. 消息收发与序列化

3.1 发送消息
  • 通用接口Send方法接受BaseMsg子类(如PlayerMsg),自动调用序列化逻辑(Writing)生成字节数组。
  • 异步发送:通过SocketAsyncEventArgs实现非阻塞发送,发送失败时关闭连接。
3.2 接收消息
  • 异步接收:使用SocketAsyncEventArgs循环接收数据,存入缓存数组cacheBytes
  • 粘包 / 分包处理
    1. 先读取前 8 字节(4 字节消息 ID + 4 字节消息体长度)。
    2. 根据长度读取完整消息体,剩余数据缓存至下次解析。
    3. 通过MsgPool根据 ID 动态创建消息实例(如PlayerMsg),调用反序列化方法(Reading)。

4. 消息处理流程

  • 队列解耦:接收的消息处理器(BaseHandler)存入receiveQueue,通过Update帧循环处理,避免阻塞网络线程。
  • 动态分发:通过消息池获取处理器(如PlayerMsgHandler),将消息实例注入处理器,实现业务逻辑与网络层分离。

5. 核心组件与依赖

  • MsgPool:维护消息 ID 与类型的映射,通过反射创建实例,避免硬编码switch-case
  • 协议基类
    • BaseMsg:定义消息序列化(Writing)、反序列化(Reading)、获取 ID(GetID)接口。
    • BaseHandler:消息处理器基类,持有消息实例(message属性),子类实现MsgHandler具体逻辑。
  • 工具方法:使用BitConverter解析消息 ID 和长度,确保字节序一致性(默认本地字节序,需根据服务器调整)。

6. 扩展与优化点

  • 线程安全receiveQueue需考虑多线程访问安全(当前仅主线程操作,无需锁)。
  • 加密与压缩:可在Writing/Reading中添加数据加密(如 AES)或压缩(如 Zlib)逻辑。
  • 连接重试Close方法中预留的 “断线重连” 逻辑需补充具体实现。
  • 日志系统:当前仅用print输出,可集成更完善的日志记录(如错误等级、消息统计)。

总结

该代码实现了基于 TCP 的异步网络通信框架,具备连接管理、心跳维持、自动序列化 / 反序列化、消息分发解耦等功能,适用于实时性要求较高的游戏或应用场景。通过消息池和基类设计,降低了协议扩展的复杂度,开发者只需新增 XML 配置和处理器逻辑,即可快速支持新消息类型。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/82565.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV CUDA模块图像过滤------创建一个 Sobel 滤波器函数createSobelFilter()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 该函数用于创建一个 Sobel 滤波器&#xff0c;用于在 GPU 上进行边缘检测。它基于图像的梯度计算&#xff1a; dx 表示对 x 方向求导的阶数&…

【JavaSE】枚举和注解学习笔记

枚举和注解 -枚举 规定多选一数据类型的解决方案-枚举 枚举对应英文(enumeration,简写 enum) 2)枚举是一组常量的集合。 3)可以这里理解:枚举属于一种特殊的类&#xff0c;里面只包含一组有限的特定的对象。 枚举的两种实现方式 自定义实现枚举 使用enum关键字实现枚举 自…

Spark SQL进阶:解锁大数据处理的新姿势

目录 一、Spark SQL&#xff0c;为何进阶&#xff1f; 二、进阶特性深剖析 2.1 窗口函数&#xff1a;数据洞察的新视角 2.2 高级聚合&#xff1a;挖掘数据深度价值 2.3 自定义函数&#xff08;UDF 和 UDTF&#xff09;&#xff1a;拓展功能边界 三、性能优化实战 3.1 数…

如何利用 Conda 安装 Pytorch 教程 ?

如何利用 Conda 安装 Pytorch 教程 &#xff1f; 总共分为六步走&#xff1a; &#xff08;1&#xff09;第一步&#xff1a;验证conda 环境是否安装好&#xff1f; 1) conda -V2) conda --version&#xff08;2&#xff09;第二步&#xff1a;查看现有环境 conda env list…

什么是HTTP

HTTP&#xff08;HyperText Transfer Protocol&#xff09;是万维网数据通信的基础协议&#xff0c;作为应用层协议具有以下关键特性&#xff1a; 客户端-服务器模型&#xff1a;基于请求/响应模式 无状态协议&#xff1a;默认不保留通信状态 可扩展性&#xff1a;通过首部字…

2025-05-27 学习记录--Python-模块

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、模块 ⭐️ &#xff08;一&#xff09;模块的导入与使用 &#x1f36d; 模块的导入&#xff1a;&#x1f41d; 模块 就好比…

leetcode 131. Palindrome Partitioning

目录 一、题目描述 二、方法1、回溯法每次暴力判断回文子串 三、方法2、动态规划回溯法 一、题目描述 分割回文子串 131. Palindrome Partitioning 二、方法1、回溯法每次暴力判断回文子串 class Solution {vector<vector<string>> res;vector<string>…

重构开发范式!飞算JavaAI革新Spring Cloud分布式系统开发

分布式系统凭借高可用性、可扩展性等核心优势&#xff0c;成为大型软件项目的标配架构。Spring Cloud作为Java生态最主流的分布式开发框架&#xff0c;虽被广泛应用于微服务架构搭建&#xff0c;但其传统开发模式却面临效率瓶颈——从服务注册中心配置到网关路由规则编写&#…

python 生成复杂表格,自动分页等功能

py&#xff54;&#xff48;&#xff4f;&#xff4e; 生成复杂表格&#xff0c;自动分页等功能 解决将Python中的树形目录数据转换为Word表格&#xff0c;并生成带有合并单元格的检测报告的问题。首先&#xff0c;要解决“tree目录数据”和“Word表格互换”&#xff0c;指将树…

根据Cortex-M3(包括STM32F1)权威指南讲解MCU内存架构与如何查看编译器生成的地址具体位置

首先我们先查看官方对于Cortex-M3预定义的存储器映射 1.存储器映射 1.1 Cortex-M3架构的存储器结构 内部私有外设总线&#xff1a;即AHB总线&#xff0c;包括NVIC中断&#xff0c;ITM硬件调试&#xff0c;FPB, DWT。 外部私有外设总线&#xff1a;即APB总线&#xff0c;用于…

Linux中硬件信息查询利器——lshw命令详解!

lshw&#xff08;List Hardware&#xff09;是 Linux 系统下的一款命令行工具&#xff0c;用于全面检测并显示详细的硬件信息。它能够报告 CPU、内存、主板、存储设备、显卡、网络设备等几乎所有硬件组件的详细信息&#xff0c;适用于系统管理、故障排查和硬件兼容性检查等场景…

用llama3微调了一个WiFiGPT 用于室内定位

一段话总结 本文提出WiFiGPT,一种基于Decoder-Only Transformer(如LLaMA 3)的室内定位系统,通过将WiFi遥测数据(如CSI、FTM、RSSI)转换为文本序列进行端到端训练,无需手工特征工程即可实现高精度定位。实验表明,WiFiGPT在LOS环境中实现亚米级精度(MAE低至0.90米),在…

大模型系列22-MCP

大模型系列22-MCP 玩转 MCP 协议&#xff1a;用 Cline DeepSeek 接入天气服务什么是 MCP&#xff1f;环境准备&#xff1a;VScode Cline DeepSeek**配置 DeepSeek 模型&#xff1a;****配置 MCP 工具****uvx是什么&#xff1f;****安装 uv&#xff08;会自动有 uvx 命令&…

Go语言Map的底层原理

概念 map 又称字典&#xff0c;是一种常用的数据结构&#xff0c;核心特征包含下述三点&#xff1a; &#xff08;1&#xff09;存储基于 key-value 对映射的模式&#xff1b; &#xff08;2&#xff09;基于 key 维度实现存储数据的去重&#xff1b; &#xff08;3&#x…

循环神经网络(RNN):原理、架构与实战

循环神经网络&#xff08;Recurrent Neural Network, RNN&#xff09;是一类专门处理序列数据的神经网络&#xff0c;如时间序列、自然语言、音频等。与前馈神经网络不同&#xff0c;RNN 引入了循环结构&#xff0c;能够捕捉序列中的时序信息&#xff0c;使模型在不同时间步之间…

java 项目登录请求业务解耦模块全面

登录是统一的闸机&#xff1b; 密码存在数据库中&#xff0c;用的是密文&#xff0c;后端加密&#xff0c;和数据库中做对比 1、UserController public class UserController{Autowiredprivate IuserService userservicepublic JsonResult login(Validated RequestBody UserLo…

【手写数据库核心揭秘系列】第9节 可重入的SQL解析器,不断解析Structure Query Language,语言翻译好帮手

可重入的SQL解析器 文章目录 可重入的SQL解析器一、概述 二、可重入解析器 2.1 可重入设置 2.2 记录状态的数据结构 2.3 节点数据类型定义 2.4 头文件引用 三、调整后的程序结构 四、总结 一、概述 现在就来修改之前sqlscanner.l和sqlgram.y程序,可以不断输入SQL语句,循环执…

微软开源bitnet b1.58大模型,应用效果测评(问答、知识、数学、逻辑、分析)

微软开源bitnet b1.58大模型,应用效果测评(问答、知识、数学、逻辑、分析) 目 录 1. 前言... 2 2. 应用部署... 2 3. 应用效果... 3 1.1 问答方面... 3 1.2 知识方面... 4 1.3 数字运算... 6 1.4 逻辑方面... …

用HTML5+JavaScript实现汉字转拼音工具

用HTML5JavaScript实现汉字转拼音工具 前一篇博文&#xff08;https://blog.csdn.net/cnds123/article/details/148067680&#xff09;提到&#xff0c;当需要将拼音添加到汉字上面时&#xff0c;用python实现比HTML5JavaScript实现繁琐。在这篇博文中用HTML5JavaScript实现汉…

鸿蒙OSUniApp 开发的动态背景动画组件#三方框架 #Uniapp

使用 UniApp 开发的动态背景动画组件 前言 在移动应用开发中&#xff0c;动态背景动画不仅能提升界面美感&#xff0c;还能增强用户的沉浸感和品牌辨识度。无论是登录页、首页还是活动页&#xff0c;恰到好处的动态背景都能让产品脱颖而出。随着鸿蒙&#xff08;HarmonyOS&am…