摘要:該網絡通信系統基于Unity實現,包含以下幾個核心模塊:
- 協議配置:通過XML定義枚舉(如玩家/英雄類型)、數據結構(如PlayerData)及消息協議(如PlayerMsg),支持基礎類型、數組、字典等復雜結構。
- 代碼生成工具:解析XML自動生成C#腳本,包括枚舉類、可序列化的數據結構類(實現字節計算、序列化/反序列化)、消息類及消息池,減少手動編碼。
- 網絡管理器:采用異步Socket實現TCP通信,處理連接、心跳包(間隔2秒)、消息收發及粘包/分包問題,通過消息池動態映射ID與消息類型,結合隊列機制解耦網絡層與業務邏輯。
- 擴展性:支持多命名空間、自動目錄生成,預留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 配置文件自動生成多語言協議代碼,主要功能如下:
-
配置解析:讀取 XML 配置文件(如用戶提供的協議定義),提取枚舉、數據結構和消息定義。
-
代碼生成:
- 通過菜單命令(ProtocolTool / 生成 C# 腳本)觸發,生成 C# 協議類文件
- 支持生成:枚舉類型、數據結構類、消息類、消息池管理類
- 預留了 C++ 和 Java 代碼生成接口(僅打印日志)
-
工具集成:
- 在 Unity 編輯器菜單中添加功能入口
- 生成后自動刷新項目視圖,無需手動操作
-
核心邏輯:
- 使用
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
方法)。 - 提供
GetMessage
和GetHandler
方法,通過反射創建消息實例和處理器。
- 從 XML 中提取所有消息 ID、名稱和命名空間,生成注冊代碼(
- 輸出:消息池管理腳本(
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=4
,string
需計算 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 并生成代碼。 - 依賴項:需提前定義
BaseData
和BaseMsg
抽象類,以及序列化工具方法(如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
。 - 粘包 / 分包處理:
- 先讀取前 8 字節(4 字節消息 ID + 4 字節消息體長度)。
- 根據長度讀取完整消息體,剩余數據緩存至下次解析。
- 通過
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 配置和處理器邏輯,即可快速支持新消息類型。