摘要:该网络通信系统基于Unity实现,包含以下几个核心模块:
1
2
该配置文件通过枚举、数据结构和消息定义,构建了游戏中玩家、怪物和系统交互的基础模型。枚举确保类型统一,数据结构支持复杂数据建模,消息机制实现模块间通信,整体设计符合游戏开发中数据配置的典型范式。
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代码");
}
///
/// 获取指定名字的所有子节点 的 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);
}
}
这段代码是一个 Unity 编辑器扩展工具,用于根据 XML 配置文件自动生成多语言协议代码,主要功能如下:
配置解析:读取 XML 配置文件(如用户提供的协议定义),提取枚举、数据结构和消息定义。
代码生成:
工具集成:
核心逻辑:
GenerateCSharp
类处理代码生成逻辑这个工具的设计目标是简化游戏网络协议开发流程,将配置文件自动转换为各语言的代码实现,提高开发效率并减少手动编码错误。
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)
{
//消息ID
idStr = 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 ids = new List();
List names = new List();
List nameSpaces = new List();
foreach (XmlNode dataNode in nodes)
{
//记录所有消息的ID
string id = dataNode.Attributes["id"].Value;
if (!ids.Contains(id))
ids.Add(id);
else
Debug.LogError("存在相同ID的消息" + id);
string name = dataNode.Attributes["name"].Value;
if (!names.Contains(name))
names.Add(name);
else
Debug.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 message = new Dictionary();\r\n" +
"\tprivate Dictionary handlers = new Dictionary();\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);
}
///
/// 获取成员变量声明内容
///
///
///
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";
}
else
bytesNumStr += "\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";
}
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 "bool":
return "ReadBool(bytes, ref index)";
case "string":
return "ReadString(bytes, ref index)";
default:
return "ReadData<" + type + ">(bytes, ref index)";
}
}
}
这段代码是 Unity 中用于自动生成 C# 协议相关脚本的工具类,核心功能是解析 XML 配置文件并生成对应的枚举、数据结构、消息类及消息池管理代码,具体作用如下:
GenerateEnum
)
节点(如玩家类型、怪物类型等)。namespace
)、枚举名(name
)和字段(field
)。csharp
namespace GamePlayer { public enum E_PLAYER_TYPE { MAIN = 1, OTHER } }
GamePlayer/Enum/E_PLAYER_TYPE.cs
)。GenerateData
)
节点(如PlayerData
)。BaseData
抽象类的GetBytesNum
(计算字节长度)、Writing
(序列化)、Reading
(反序列化)方法。short
类型),再循环写入元素;枚举类型会转换为整数存储。GamePlayer/Data/PlayerData.cs
),支持网络传输的数据序列化 / 反序列化。GenerateMsg
)
节点(如PlayerMsg
)。BaseMsg
),包含消息 ID(GetID
方法)、字段序列化 / 反序列化逻辑。PlayerMsgHandler.cs
),用于处理消息逻辑(需手动补充业务代码)。GamePlayer/Msg/PlayerMsg.cs
)和处理器脚本。GenerateMsgPool
)MsgPool
类,维护消息 ID 与消息类型、处理器类型的映射关系。Register
方法)。GetMessage
和GetHandler
方法,通过反射创建消息实例和处理器。Pool/MsgPool.cs
),用于统一管理消息的创建和分发。GetFieldStr
)list
/array
/dic
/enum
/ 基础类型)生成对应的成员变量声明。
list
生成public List list;
,dic
生成public Dictionary dic;
。GetGetBytesNumStr
/GetWritingStr
/GetReadingStr
)int=4
,string
需计算 UTF8 字节长度)。short
,2 字节),再递归计算元素字节数。Writing
):
WriteInt
/WriteShort
等方法将数据写入字节数组,容器类型循环写入元素。Reading
):
ReadInt
/ReadShort
等方法从字节数组读取数据,容器类型先读取长度再循环读取元素,枚举类型通过强制转换还原。Assets/Scripts/Protocol/[命名空间]/Enum/
Assets/Scripts/Protocol/[命名空间]/Data/
Assets/Scripts/Protocol/[命名空间]/Msg/
Assets/Scripts/Protocol/Pool/
GamePlayer/Enum
)。ProtocolTool/生成C#脚本
调用,自动解析 XML 并生成代码。BaseData
和BaseMsg
抽象类,以及序列化工具方法(如WriteInt
/ReadInt
)。该工具通过解析 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;
//和服务器进行连接的 Socket
private Socket socket;
//接受消息用的 缓存容器
private byte[] cacheBytes = new byte[1024 * 1024];
private int cacheNum = 0;
private Queue receiveQueue = new Queue();
//发送心跳消息的间隔时间
private int SEND_HEART_MSG_TIME = 2;
private HeartMsg hearMsg = new HeartMsg();
//消息池对象 用于快速获取消息和处理消息处理类对象
private MsgPool msgPool = new MsgPool();
// Start is called before the first frame update
void 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 frame
void 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);
else
Close();
}
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)
{
//解析ID
msgID = 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 连接、消息收发及消息处理,核心功能如下:
Awake
方法确保全局唯一实例,跨场景保持连接状态。InvokeRepeating
定时发送心跳消息(HeartMsg
),维持长连接。MsgPool
管理消息实例和处理器,实现消息类型与 ID 的动态映射。SocketAsyncEventArgs
实现非阻塞连接,连接成功后立即注册异步接收回调(ReceiveCallBack
)。SocketType.Stream
)。QuitMsg
)后关闭套接字,处理重连逻辑(预留扩展)。Send
方法接受BaseMsg
子类(如PlayerMsg
),自动调用序列化逻辑(Writing
)生成字节数组。SocketAsyncEventArgs
实现非阻塞发送,发送失败时关闭连接。SocketAsyncEventArgs
循环接收数据,存入缓存数组cacheBytes
。MsgPool
根据 ID 动态创建消息实例(如PlayerMsg
),调用反序列化方法(Reading
)。BaseHandler
)存入receiveQueue
,通过Update
帧循环处理,避免阻塞网络线程。PlayerMsgHandler
),将消息实例注入处理器,实现业务逻辑与网络层分离。MsgPool
:维护消息 ID 与类型的映射,通过反射创建实例,避免硬编码switch-case
。BaseMsg
:定义消息序列化(Writing
)、反序列化(Reading
)、获取 ID(GetID
)接口。BaseHandler
:消息处理器基类,持有消息实例(message
属性),子类实现MsgHandler
具体逻辑。BitConverter
解析消息 ID 和长度,确保字节序一致性(默认本地字节序,需根据服务器调整)。receiveQueue
需考虑多线程访问安全(当前仅主线程操作,无需锁)。Writing
/Reading
中添加数据加密(如 AES)或压缩(如 Zlib)逻辑。Close
方法中预留的 “断线重连” 逻辑需补充具体实现。print
输出,可集成更完善的日志记录(如错误等级、消息统计)。该代码实现了基于 TCP 的异步网络通信框架,具备连接管理、心跳维持、自动序列化 / 反序列化、消息分发解耦等功能,适用于实时性要求较高的游戏或应用场景。通过消息池和基类设计,降低了协议扩展的复杂度,开发者只需新增 XML 配置和处理器逻辑,即可快速支持新消息类型。