ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity网络通信优化:sproto协议与RPC框架的轻量级集成实践

Unity网络通信优化:sproto协议与RPC框架的轻量级集成实践 1. 项目概述为什么Unity开发者需要关注sproto-Unity如果你正在用Unity做网络游戏或者任何需要客户端与服务器通信的应用那么“RPC通讯”这个词对你来说肯定不陌生。无论是玩家移动同步、技能释放、聊天消息还是更复杂的游戏状态同步背后都离不开远程过程调用。Unity官方有Netcode、Mirror等方案社区也有各种Socket库但当你真正上手时往往会遇到一堆头疼的问题协议定义繁琐、序列化性能低下、代码生成工具链复杂还有最要命的——不同平台尤其是移动端和WebGL的兼容性问题。我自己在项目里就踩过不少坑。比如用Unity自带的[RPC]现在演进为更通用的[Rpc]属性时光是搞明白SendTo.Server、SendTo.NotServer这些枚举的含义以及如何防止客户端恶意调用服务器专属的RPC就花了不少时间。就像网络讨论里那个经典问题“怎么阻止玩家2黑掉客户端然后广播说玩家1死了” 这背后是严肃的服务器权威性和网络安全问题。再比如当项目需要与已有的、非Unity技术栈的后端服务比如用Go、Java写的游戏服务器通信时如何设计一个双方都能高效理解、且易于维护的通信协议又是一个巨大的挑战。这就是我今天想跟你详细聊聊sproto-Unity这个开源项目的原因。它不是另一个庞大的网络框架而是一个专注于解决“协议层”问题的轻量级工具。它的核心价值在于借助云风大神的sproto协议描述语言和编解码库为Unity带来了一套极其高效、简洁的RPC通信基础方案。说“亲测免费”一点不夸张它完全开源集成过程清晰并且实实在在地解决了我在实际开发中遇到的协议定义混乱和性能瓶颈问题。接下来我会带你从设计思路到实操细节完整地走一遍让你也能在自己的项目里用上这个利器。2. 核心设计思路sproto协议与Unity的珠联璧合在深入代码之前我们得先搞清楚sproto-Unity到底解决了什么根本问题以及它是如何思考的。很多网络模块的复杂度其实源于“协议层”和“业务层”的纠缠不清。2.1 传统Unity RPC的痛点分析以Unity Netcode for GameObjects (NGO) 为例它的[Rpc]系统很方便你只需要在方法上加个标签框架就会帮你处理网络发送和调用。但它的便利性背后隐藏着几个限制协议黑箱序列化和反序列化过程对开发者是透明的你很难深度优化网络包大小尤其是传输复杂结构如嵌套类、字典时。强耦合性RPC方法签名方法名、参数列表本身就是协议。一旦修改必须保证所有客户端和服务器的代码同步更新否则就会导致连接断开或运行时错误。跨语言支持弱NGO主要服务于Unity客户端之间的通信或者Unity客户端与用C#编写的专用游戏服务器通信。如果你的后端是Go、Python、C等其他语言就需要自己实现一套复杂的消息对接层。安全性依赖框架如网络讨论所示防止客户端滥用RPC如调用本应由服务器发起的SendTo.NotServer方法需要依赖框架的验证机制。虽然可以通过检查RpcParams.Receive.SenderClientId来手动实现但这增加了业务代码的复杂度。2.2 sproto的核心思想协议先行代码生成sproto-Unity的解决方案非常经典协议先行。它不关心你底层是用Socket、WebSocket还是其他传输层它只关心你传输的“数据”是什么格式。使用sproto语言定义协议你需要在一个.sproto文本文件中像定义结构体一样定义你的所有网络消息和RPC过程。例如.PlayerMove { player_id 0 : integer x 1 : integer y 2 : integer } .PlayerAttack { attacker_id 0 : integer skill_id 1 : integer target_id 2 : integer } # 定义一个RPC服务 game 1 { move 1 : PlayerMove - boolean attack 2 : PlayerAttack - boolean }这里.PlayerMove和.PlayerAttack是数据类型game是一个服务包含move和attack两个RPC方法。-后面是返回值类型。这种定义方式独立于任何编程语言清晰且无二义性。代码生成sproto-Unity提供了工具可以将这个.sproto文件一键生成对应的C#类以及服务器端可能需要的Go、Lua等语言代码。生成的C#类包含完整的序列化打包和反序列化解包方法。与传输层解耦你的网络管理模块比如自己封装的Socket管理器只需要负责发送和接收二进制流(byte[])。收到数据后调用sproto-Unity生成的解码器就能得到结构化的C#对象反之将C#对象交给编码器就能得到二进制流用于发送。这种设计的巨大优势在于性能sproto的二进制编码非常紧凑序列化/反序列化速度极快特别适合对实时性要求高的游戏。清晰度协议文件是唯一的权威定义客户端和服务器开发者都以此为准极大减少了沟通误解。灵活性后端技术栈可以自由选择。只要后端能用sproto库Go、C、Lua、Python等都有成熟实现就能与Unity客户端无缝通信。安全你可以在服务器端对收到的任何消息进行强类型校验后再处理业务逻辑清晰。客户端无法发送协议中未定义的消息格式。2.3 与Unity现有生态的融合策略sproto-Unity并没有试图取代Unity的NGO或Mirror。相反它可以与它们协作。例如你可以用NGO处理Unity对象网络同步、房间管理等高层逻辑而用sproto-Unity来处理自定义的游戏业务RPC如购买道具、提交任务等。对于不需要复杂状态同步的棋牌类、回合制游戏甚至可以直接基于sproto-Unity和简单的TCP/WebSocket来自主构建整个网络层这样项目依赖更轻控制力更强。3. 环境准备与项目集成理论讲完了我们动手把它集成到Unity项目里。整个过程非常轻量。3.1 获取sproto-Unity最直接的方式是通过Unity的Package Manager从Git URL添加打开Unity进入Window - Package Manager。点击左上角的“”号选择“Add package from git URL...”。输入sproto-Unity仓库的URL例如https://github.com/lovelycat777/sproto-Unity.git。你可以去GitHub搜索“sproto-Unity”找到当前最活跃的仓库。点击“Add”。Unity会自动下载并导入这个包。注意有些仓库可能包含了sproto的C核心库源码需要编译。如果导入后遇到与原生插件Native Plugin相关的错误请检查仓库的README看是否需要手动运行一个构建脚本或者为不同平台Windows、macOS、Android、iOS下载预编译好的插件。3.2 安装与配置代码生成器sproto-Unity的核心工具是一个C#编写的代码生成器。它通常以控制台应用程序的形式提供。定位生成器在导入的Package目录下通常位于Packages/sproto-unity或Assets/Plugins/sproto-Unity找到名为sproto-cs-generator.exeWindows或sproto-cs-generatormacOS/Linux的文件。也可能是一个C#项目文件(.csproj)需要你自行编译。准备协议文件在你的项目目录中例如Assets/Proto创建你的.sproto文件比如game.sproto内容就是上一节我们举例的那些协议定义。生成代码命令行方式打开终端切换到生成器所在目录执行命令# 假设生成器在 Tools 目录协议文件在 Assets/Proto ./sproto-cs-generator ../../Assets/Proto/game.sproto ../../Assets/Scripts/Net/Proto/GameProto.cs这个命令会将game.sproto编译成C#代码并输出到指定的GameProto.cs文件路径。Unity Editor菜单集成推荐更高效的方式是写一个简单的Editor脚本在Unity中创建右键菜单。例如using UnityEditor; using System.Diagnostics; using System.IO; public class SprotoGenerator { [MenuItem(Tools/Sproto/Generate C# Code)] public static void Generate() { string generatorPath Path.GetFullPath(Packages/sproto-unity/Tools/sproto-cs-generator.exe); string protoPath Path.GetFullPath(Assets/Proto/game.sproto); string outputPath Path.GetFullPath(Assets/Scripts/Net/Proto/GameProto.cs); ProcessStartInfo startInfo new ProcessStartInfo { FileName generatorPath, Arguments $\{protoPath}\ \{outputPath}\, UseShellExecute false, RedirectStandardOutput true, CreateNoWindow true }; using (Process process Process.Start(startInfo)) { string output process.StandardOutput.ReadToEnd(); process.WaitForExit(); UnityEngine.Debug.Log($Sproto Code Generation Output:\n{output}); } AssetDatabase.Refresh(); // 刷新Unity资源数据库 } }这样你只需要在Unity Editor点击Tools/Sproto/Generate C# Code就能一键生成。每次修改.sproto文件后记得运行一次。3.3 关键依赖与初始化生成的GameProto.cs文件里核心是sproto的运行时库。你需要确保sproto-Unity的运行时DLL通常是spromolib.dll或libspromolib.so等在项目的Plugins目录下并且针对不同平台设置正确。在游戏启动时例如在GameManager的Awake方法中你需要初始化sproto的协议管理器using Sproto; public class NetworkManager : MonoBehaviour { void Awake() { // 注册你的协议文件。这里假设生成的代码里有一个静态类 GameProto 包含了协议描述字节流。 Protocol.Register(GameProto.ProtocolBuffer); // ProtocolBuffer 是生成器根据你的 .sproto 文件生成的二进制描述数据。 } }这个初始化过程主要是让sproto运行时知道你定义了哪些类型和RPC服务为后续的打包解包做准备。4. 核心使用流程定义、生成、发送、接收现在我们通过一个完整的“玩家移动”例子把整个流程串起来。4.1 第一步精确定义协议game.sproto协议定义是重中之重要考虑扩展性。我们设计一个简单的移动协议。# 基础向量类型可复用 .Vector2 { x 0 : integer y 1 : integer } # 玩家移动请求客户端 - 服务器 .PlayerMoveRequest { player_id 0 : integer position 1 : Vector2 timestamp 2 : integer # 用于客户端预测与服务器回滚校验 } # 玩家移动广播服务器 - 所有客户端 .PlayerMoveBroadcast { player_id 0 : integer position 1 : Vector2 validated 2 : boolean # 服务器是否验证通过 } # 定义游戏世界的RPC服务 world 1 { # 客户端请求移动服务器返回一个布尔值表示是否接受 request_move 1 : PlayerMoveRequest - boolean # 服务器广播玩家移动无返回值 broadcast_move 2 : PlayerMoveBroadcast }实操心得字段后面的数字如0,1是标签tag一旦确定切勿随意修改因为它是二进制编码的标识。新增字段请使用新的、从未用过的标签。将常用结构如Vector2,ItemInfo定义为独立类型利于复用。为关键字段添加注释。4.2 第二步生成C#代码并解读运行代码生成器后你会得到GameProto.cs。打开它你会看到类似下面的自动生成代码已简化// 这是自动生成的代码请勿手动修改。 namespace Sproto.Game { public partial class Vector2 : SprotoTypeBase { public int x; // tag: 0 public int y; // tag: 1 // ... 序列化、反序列化、CalculateSize等方法 } public partial class PlayerMoveRequest : SprotoTypeBase { public int player_id; // tag: 0 public Vector2 position; // tag: 1 public int timestamp; // tag: 2 // ... } public partial class PlayerMoveBroadcast : SprotoTypeBase { public int player_id; // tag: 0 public Vector2 position; // tag: 1 public bool validated; // tag: 2 // ... } // RPC 存根Stub类用于发起请求和处理响应 public class world { public static SprotoRpc.Request request_move(PlayerMoveRequest request, SprotoRpc.ResponseDelegatebool callback) { ... } public static void broadcast_move(PlayerMoveBroadcast request) { ... } // 以及对应的分发处理方法 public static void Dispatch(int tag, byte[] buffer, SprotoRpc.ResponseDelegateobject callback) { ... } } // 协议描述字节流 public static class GameProto { public static byte[] ProtocolBuffer new byte[] { /* 一大串二进制数据 */ }; } }生成器为你做了最关键的两件事1. 创建了强类型的C#类2. 生成了这些类与二进制流相互转换的所有底层代码。world类里的request_move和broadcast_move方法就是你要调用的RPC入口。4.3 第三步客户端发送RPC请求假设我们有一个玩家控制的角色当摇杆输入时我们需要向服务器发送移动请求。using Sproto.Game; public class PlayerController : MonoBehaviour { private int myPlayerId 1001; private SprotoRpc rpcClient; // 假设这是你封装的网络层对象内部持有Socket连接 void Update() { // 获取输入简化示例 Vector2 input new Vector2(Input.GetAxis(Horizontal), Input.GetAxis(Vertical)); if (input.magnitude 0.1f) { SendMoveRequest(input); } } void SendMoveRequest(Vector2 dir) { // 1. 构造请求对象 var request new PlayerMoveRequest { player_id myPlayerId, position new Vector2 { x (int)(dir.x * 100), y (int)(dir.y * 100) }, // 假设放大100倍传输 timestamp (int)(Time.time * 1000) // 毫秒时间戳 }; // 2. 通过生成的RPC存根方法创建请求并指定回调函数 SprotoRpc.Request rpcReq world.request_move(request, OnMoveResponse); // 3. 将rpcReq对象序列化成二进制数据并通过你的网络层发送出去 byte[] data rpcReq.Pack(); rpcClient.Send(data); // rpcClient.Send 是你自己实现的网络发送方法 } void OnMoveResponse(bool success, SprotoRpc.Responsebool response) { // 处理服务器响应 if (response.Error ! null) { Debug.LogError($Move request failed: {response.Error}); return; } bool isAccepted response.Result; if (isAccepted) { Debug.Log(Server accepted our move.); // 可以立即进行客户端预测移动 } else { Debug.Log(Server rejected our move.); // 可能需要回退到服务器确认的位置 } } }注意事项world.request_move这个调用并不会直接发送网络数据。它只是根据协议定义帮你构造了一个格式正确的RPC请求对象包含请求类型、会话ID、序列化的请求体等。真正的网络发送逻辑需要你自己实现rpcClient.Send。这种设计让sproto-Unity可以灵活适配任何传输层。4.4 第四步服务器处理与广播服务器端可能是用Go、C#等编写同样使用sproto库来解码和编码。这里以概念性伪代码展示逻辑// Go 语言服务器端示例 (使用 go-sproto 库) package main import ( github.com/hiroakis/go-sproto ) func handleClientMessage(data []byte, client *Client) { // 1. 解码RPC请求 rpcReq, err : sproto.DecodeRpcRequest(data) if err ! nil { /* 处理错误 */ } // 2. 根据RPC类型分发 switch rpcReq.Type { case world.RequestMoveTag: // 这个Tag值由生成器确定 var req PlayerMoveRequest sproto.Unmarshal(rpcReq.Request, req) // 3. 业务逻辑验证 if !validateMove(client.PlayerId, req.Position, req.Timestamp) { // 构造失败响应 rpcResp : sproto.NewRpcResponse(rpcReq.Session, false) sendToClient(client, rpcResp.Pack()) return } // 4. 更新游戏状态并准备广播给所有玩家 updatePlayerPosition(req.PlayerId, req.Position) // 5. 先给请求的客户端一个成功响应 rpcResp : sproto.NewRpcResponse(rpcReq.Session, true) sendToClient(client, rpcResp.Pack()) // 6. 构造广播消息 broadcastMsg : PlayerMoveBroadcast{ PlayerId: req.PlayerId, Position: req.Position, Validated: true, } // 使用生成的广播RPC方法构造数据 broadcastData : world.BroadcastMove(broadcastMsg).Pack() // 7. 发送给除自己外的所有客户端或所有客户端 broadcastToAll(client, broadcastData) } }服务器端的流程清晰地展示了“请求-响应-广播”的完整闭环。sproto保证了双方数据结构的一致性。4.5 第五步客户端处理服务器广播回到Unity客户端我们需要在网络层收到数据后将其分发给对应的RPC处理器。public class NetworkManager : MonoBehaviour { private SprotoRpc rpcProcessor new SprotoRpc(); void Start() { // 假设你的Socket收到数据 yourSocket.OnDataReceived OnNetworkData; } void OnNetworkData(byte[] data) { // 1. 尝试解码为RPC消息 var rpcMsg SprotoRpc.Message.Unpack(data); if (rpcMsg null) { Debug.LogError(Invalid RPC message received.); return; } // 2. 根据RPC消息类型进行分发 // 如果是响应有session交给rpcProcessor处理会自动触发之前注册的回调如OnMoveResponse // 如果是请求服务器发来的如广播需要手动调用协议层的Dispatch if (rpcMsg.IsResponse()) { rpcProcessor.Dispatch(rpcMsg); } else { // 这里是处理服务器主动发来的请求例如广播 // world.Dispatch 是生成器创建的方法它会根据tag调用对应的处理函数 world.Dispatch(rpcMsg.Tag, rpcMsg.Data, (error, result) { if (error ! null) { /* 处理错误 */ } // 对于广播result可能是null因为广播没有返回值 OnServerBroadcast(rpcMsg.Tag, result); }); } } void OnServerBroadcast(int tag, object result) { switch (tag) { case world.BroadcastMoveTag: // 假设生成器定义了这样一个常量 PlayerMoveBroadcast broadcast result as PlayerMoveBroadcast; if (broadcast ! null) { // 更新其他玩家的位置 FindPlayer(broadcast.player_id).SetPosition(broadcast.position); } break; } } }通过这个Dispatch机制我们将网络二进制流、sproto协议、以及游戏业务逻辑优雅地连接了起来。5. 性能优化与高级技巧sproto-Unity本身已经非常高效但在实际大型项目中我们还可以从以下几个层面进行深度优化。5.1 减少GC垃圾回收压力频繁创建消息对象和字节数组会引发GC在移动端可能导致卡顿。使用对象池对频繁使用的消息对象如PlayerMoveRequest、Vector2进行池化管理。public class MessagePoolT where T : SprotoTypeBase, new() { private StackT pool new StackT(); public T Get() { return pool.Count 0 ? pool.Pop() : new T(); } public void Release(T obj) { // 重置对象状态 // 对于SprotoTypeBase可能需要手动清空字段或依赖生成器提供的Reset方法 if (obj is IResettable resettable) resettable.Reset(); pool.Push(obj); } } // 使用 var requestPool new MessagePoolPlayerMoveRequest(); var req requestPool.Get(); // ... 填充req ... // 发送并等待完成后 requestPool.Release(req);复用字节数组在网络层可以预先分配一个大小合理的环形缓冲区Ring Buffer来接收数据避免每次new byte[]。5.2 协议设计进阶可选字段与默认值sproto支持可选字段和默认值合理使用可以压缩数据包。.PlayerInfo { id 0 : integer name 1 : string level 2 : integer [default 1] # 默认值如果为1可以不传输 guild_name 3 : string [optional] # 可选字段如果没有可以不传输 vip_expire_time 4 : integer [optional] }在生成C#代码中可选字段会以NullableT或类似方式呈现在序列化时如果为null或默认值则不会占用二进制空间。5.3 与Unity Job System/Burst Compiler结合对于需要处理大量网络实体状态的游戏如千人同屏可以在收到批量更新广播后将反序列化得到的数据结构转换成NativeArray然后利用Job System进行并行处理再用Burst编译提升性能。using Unity.Collections; using Unity.Jobs; using Unity.Burst; [BurstCompile] struct UpdatePositionsJob : IJobParallelFor { public NativeArrayint PlayerIds; public NativeArrayVector2Int Positions; // 使用Vector2Int存储位置 public NativeArrayVector3 OutputWorldPositions; // 输出到世界坐标 public void Execute(int index) { // 将网络整数坐标转换为世界坐标例如除以一个缩放因子 OutputWorldPositions[index] new Vector3(Positions[index].x / 100f, 0, Positions[index].y / 100f); } } // 在主线程中调度Job public void ProcessBatchMoveBroadcast(ListPlayerMoveBroadcast broadcasts) { var count broadcasts.Count; var playerIds new NativeArrayint(count, Allocator.TempJob); var positions new NativeArrayVector2Int(count, Allocator.TempJob); var output new NativeArrayVector3(count, Allocator.TempJob); for (int i 0; i count; i) { playerIds[i] broadcasts[i].player_id; positions[i] new Vector2Int(broadcasts[i].position.x, broadcasts[i].position.y); } var job new UpdatePositionsJob { PlayerIds playerIds, Positions positions, OutputWorldPositions output }; JobHandle handle job.Schedule(count, 64); handle.Complete(); // 使用output中的数据更新Transform... playerIds.Dispose(); positions.Dispose(); output.Dispose(); }重要提示这属于高级优化技巧适用于性能瓶颈确实出现在网络数据处理环节的情况。大部分中小型项目sproto自身的效率已经足够。6. 常见问题、调试技巧与避坑指南即使方案再优雅实际开发中总会遇到问题。下面是我在项目实践中总结的一些典型问题和解决方法。6.1 协议版本管理与兼容性问题游戏上线后需要新增功能如何在修改.sproto文件如新增字段、新增RPC时不导致老版本客户端崩溃解决方案向后兼容规则绝不修改已存在字段的Tag和类型。只新增字段并且总是定义为[optional]或[default ...]。新增RPC方法使用新的、未使用过的方法ID。版本号协商在连接握手阶段客户端和服务器交换协议版本号。服务器可以根据客户端版本决定是否启用新功能或者回退到旧的消息格式。双端同步务必使用版本控制工具如Git严格管理.sproto文件确保客户端和服务器仓库中的协议定义文件始终保持同步。可以将生成C#/Go代码的步骤作为CI/CD流水线的一环。6.2 调试与日志网络问题难以复现完善的日志至关重要。十六进制转储在发送和接收数据的底层将二进制流(byte[])以十六进制格式打印出来。public static string BytesToHex(byte[] bytes) { return BitConverter.ToString(bytes).Replace(-, ); } // 在发送前和接收后调用 Debug.Log($Send Data: {BytesToHex(data)});当出现解析错误时对比发送和接收的十六进制数据能快速定位是网络丢包、粘包还是序列化问题。包装异常在sproto的Pack()和Unpack()方法调用处添加try-catch并附加上下文信息如消息类型、发送者。try { var obj SomeSprotoType.Unpack(networkData); } catch (Exception e) { Debug.LogError($Failed to unpack message. DataLen:{networkData.Length}, Hex:{BytesToHex(networkData)}, Error:{e}); }6.3 处理网络粘包与半包sproto-Unity只负责协议编解码不处理TCP的流式特性。你必须在传输层解决粘包问题。经典长度前缀法在发送sproto编码后的字节数组前先发送其长度固定字节如4字节的int。// 发送端 byte[] protoData rpcReq.Pack(); byte[] lengthBytes BitConverter.GetBytes(protoData.Length); byte[] finalData new byte[lengthBytes.Length protoData.Length]; Buffer.BlockCopy(lengthBytes, 0, finalData, 0, lengthBytes.Length); Buffer.BlockCopy(protoData, 0, finalData, lengthBytes.Length, protoData.Length); socket.Send(finalData); // 接收端需要有一个缓冲区进行累积 private Listbyte receiveBuffer new Listbyte(); private int expectedLength -1; void OnSocketData(byte[] chunk) { receiveBuffer.AddRange(chunk); while (true) { if (expectedLength -1 receiveBuffer.Count 4) { // 读取长度头 expectedLength BitConverter.ToInt32(receiveBuffer.GetRange(0, 4).ToArray(), 0); receiveBuffer.RemoveRange(0, 4); } if (expectedLength ! -1 receiveBuffer.Count expectedLength) { // 取出一个完整的sproto消息包 byte[] packet receiveBuffer.GetRange(0, expectedLength).ToArray(); receiveBuffer.RemoveRange(0, expectedLength); ProcessPacket(packet); // 交给sproto-unity处理 expectedLength -1; // 重置准备读取下一个包 } else { break; // 数据不够等待下次接收 } } }6.4 WebGL平台的特别注意事项如果你需要发布到WebGL而服务器使用TCP可能会遇到浏览器限制。通常需要后端支持WebSocket或者通过WSSWebSocket Secure连接。传输层适配在Unity WebGL中不能直接使用System.Net.Sockets。你需要使用UnityEngine.Networking中的WebSocket类或者第三方兼容WebGL的Socket库。sproto-Unity的兼容性确保sproto-Unity的运行时核心库C/C部分有编译为WebAssemblyWASM的版本或者它有一个纯C#的实现版本。大多数情况下其核心编解码算法是纯C#的可以跨平台运行。性能WebGL下的C#执行效率低于原生平台但sproto的轻量级特性使其仍然是一个好选择。避免在WebGL端每帧处理海量网络消息。6.5 与Unity Netcode for GameObjects (NGO) 的混合使用正如开头所说sproto-Unity和NGO可以共存。一个清晰的架构划分是NGO负责玩家生成/销毁、基础Transform同步NetworkTransform、客户端预测与服务器调和、RPC权限管理利用[ServerRpc]、[ClientRpc]。sproto-Unity负责复杂的游戏业务逻辑RPC如交易、邮件、排行榜数据请求、与非Unity后端服务如账号中心、支付网关的通信。在这种架构下你的NetworkManager需要同时初始化NGO和sproto-Unity并分别处理来自不同通道的消息。这提供了最大的灵活性既能利用NGO在游戏对象同步上的成熟方案又能享受sproto在复杂业务协议上的高效与跨语言优势。
返回列表