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
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/82565.shtml
英文地址,請注明出處:http://en.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…