Unity多人游戏大厅开发:NetworkLobbyManager核心原理与实战指南 1. 项目概述为什么需要一个游戏大厅如果你做过或者玩过多人在线游戏尤其是那些需要组队、匹配的竞技或合作游戏一定会对“大厅”这个概念不陌生。它就像是游戏开始前的一个“准备室”或“集结区”。在这个虚拟空间里玩家们可以创建房间、加入房间、聊天、选择角色、调整装备并等待所有玩家都准备就绪后再一同进入真正的游戏场景。这个看似简单的功能背后却串联起了网络同步、状态管理、场景切换、玩家数据传递等一系列复杂逻辑。在Unity的生态中早期实现多人游戏主要依赖UNetUnity Networking系统而其中的NetworkLobbyManager组件就是官方提供的一个快速构建游戏大厅的“脚手架”。虽然官方已经明确UNet是一个已弃用Deprecated的解决方案并正在力推新的Netcode for GameObjects但理解NetworkLobbyManager的设计思想和实现方式对于学习多人游戏的核心架构依然具有极高的价值。它就像一本经典的教科书虽然技术栈可能过时但其中关于“大厅”状态机、玩家预制体切换、准备就绪同步等核心概念是跨网络框架通用的。这篇笔记我将结合自己实际项目中的踩坑经验带你彻底拆解Unity的Lobby API特别是NetworkLobbyManager。我会从零开始手把手演示如何构建一个可运行的、基础的多人游戏大厅并深入剖析其内部的工作流程、关键回调函数以及那些官方文档里不会写的“坑”。无论你是想维护一个基于UNet的老项目还是为学习新的Netcode打下坚实的理论基础这篇文章都能给你提供清晰的路径和实用的代码。2. 核心组件与工作流拆解在动手写代码之前我们必须先理解NetworkLobbyManager这个核心组件是如何运作的。它不是一个黑盒子而是一个设计精巧的状态管理器。2.1 NetworkLobbyManager大厅的总指挥NetworkLobbyManager继承自NetworkManager这意味着它具备了所有基础的网络管理功能如启动主机、服务器、客户端。在此基础上它额外添加了“大厅”这一层抽象。你可以把它想象成一场晚宴的“管家”NetworkManager负责租用场地、准备餐具网络连接。NetworkLobbyManager则负责在宴会开始前在休息厅大厅场景接待客人玩家确认他们的座位玩家索引和饮食偏好自定义数据等所有人都到齐并表示“可以开始了”准备就绪再引导大家正式进入宴会厅游戏场景。它的Inspector面板有几个关键字段需要配置Lobby Scene: 大厅场景的名字。所有玩家连接后首先进入的场景。Play Scene: 游戏主场景的名字。当所有玩家准备就绪后将切换至此场景。Lobby Player Prefab: 大厅玩家预制体。玩家在大厅场景中的“化身”。它必须挂载NetworkLobbyPlayer组件。Game Player Prefab: 游戏玩家预制体。玩家在游戏场景中的“化身”。它就是一个标准的网络对象需要挂载NetworkIdentity。Minimum Players: 最小玩家数。这是触发“所有玩家准备就绪”检查的前提条件。例如设置为2则至少需要2个客户端连接后系统才会开始检查每个客户端的“准备就绪”状态。注意Minimum Players指的是已连接的客户端数量而不是“已准备就绪的玩家数量”。这是一个非常容易混淆的点。即使只有一个玩家按了准备只要连接数达到最小值且所有连接的玩家都准备就绪游戏就会开始。2.2 两种玩家预制体LobbyPlayer 与 GamePlayer这是NetworkLobbyManager设计中最精妙也最容易出错的部分。它明确区分了玩家在两个不同阶段的“身份”。LobbyPlayer大厅玩家存在周期从玩家客户端成功连接大厅场景开始到离开大厅场景进入游戏或断开连接结束。核心组件NetworkLobbyPlayer。这个组件提供了一个readyToBegin的布尔值用来同步该玩家是否已准备就绪。它还有几个重要的客户端回调OnClientEnterLobby(),OnClientExitLobby(),OnClientReady(bool readyState)。作用在大厅中代表玩家处理准备状态、可能包含角色选择、装备配置等UI交互。它的数据是临时的仅存在于大厅阶段。GamePlayer游戏玩家存在周期从大厅切换至游戏场景时创建直到游戏结束返回大厅或断开连接时被销毁。核心组件标准的NetworkIdentity及你的游戏逻辑组件如PlayerMovement,PlayerHealth。作用在游戏场景中代表玩家执行游戏核心逻辑。它需要从LobbyPlayer那里“继承”或“获取”玩家在大厅中做出的选择。关键流程当所有LobbyPlayer都标记为readyToBegin后NetworkLobbyManager会自动执行ServerChangeScene切换到Play Scene。在切换场景后对于每一个LobbyPlayer服务器都会自动实例化一个GamePlayer预制体来替换它。原来的LobbyPlayer对象会被销毁。这就是为什么我们需要一个机制把大厅里的数据比如选择的角色颜色、职业传递给游戏里的角色。2.3 核心工作流程与回调函数NetworkLobbyManager提供了一整套虚方法回调函数允许我们在关键节点插入自定义逻辑。理解这些回调的触发时机至关重要。服务器端关键回调OnLobbyStartHost()/OnLobbyStopHost(): 当大厅主机启动/停止时调用。OnLobbyServerConnect(NetworkConnection conn): 当有客户端成功连接到大厅服务器时调用。此时客户端的LobbyPlayer还未创建。OnLobbyServerSceneChanged(string sceneName): 大厅服务器加载了新场景后调用。OnLobbyServerCreateLobbyPlayer(NetworkConnection conn, int playerControllerId): 当服务器需要为连接的客户端创建LobbyPlayer时调用。你可以重写它以使用自定义的创建逻辑。OnLobbyServerCreateGamePlayer(NetworkConnection conn, GameObject lobbyPlayer):这是最重要的回调之一。当服务器要为某个LobbyPlayer创建其对应的GamePlayer时调用。参数lobbyPlayer就是即将被替换的那个大厅玩家对象。在这里你必须将LobbyPlayer上的数据复制或传递给新创建的GamePlayer对象。OnLobbyServerPlayersReady(): 当所有连接的玩家都准备就绪时调用。默认实现就是在这里触发场景切换。客户端关键回调OnLobbyClientEnter(): 客户端进入大厅场景时调用包括第一次进入和从游戏场景返回。OnLobbyClientExit(): 客户端离开大厅场景时调用切换到游戏场景时。OnLobbyClientReady(bool readyState): 当本地玩家的准备状态发生变化时调用。一个常见的误解很多人以为OnLobbyServerPlayersReady里要写开始游戏的逻辑。其实不然它的默认实现已经处理了场景切换。你需要重写的地方通常是OnLobbyServerCreateGamePlayer用于数据传递。3. 实战构建一个最简单的多人游戏大厅理论说得再多不如动手做一遍。我们来构建一个最小可用的Demo一个大厅场景玩家可以连接、准备然后一起进入一个游戏场景并且能把在大厅里选择的颜色带到游戏里。3.1 场景与基础设置创建场景创建两个场景分别命名为LobbyScene和GameScene。在File - Build Settings中将这两个场景拖入确保LobbyScene在第一位。创建大厅管理器在LobbyScene中创建一个空的GameObject命名为NetworkLobbyManager。为其添加NetworkLobbyManager组件。配置管理器将Lobby Scene字段设置为LobbyScene。将Play Scene字段设置为GameScene。Minimum Players暂时设为1方便我们单机测试。添加简易UI为了方便测试我们使用Unity旧的UI系统UGUI在LobbyScene中创建一个Canvas上面放两个按钮“Start Host”启动主机客户端和“Start Client”仅启动客户端。再放一个文本显示状态。为NetworkLobbyManager对象也添加一个NetworkManagerHUD组件这样我们也能通过默认的UI进行网络操作。3.2 创建LobbyPlayer预制体创建对象在场景中创建一个UI Image作为玩家图标或一个3D胶囊体命名为LobbyPlayerPrefab。为其添加NetworkLobbyPlayer组件和NetworkIdentity组件NetworkLobbyPlayer依赖于NetworkIdentity。添加自定义数据创建一个C#脚本LobbyPlayerData.cs挂载到该预制体上。using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; public class LobbyPlayerData : NetworkLobbyPlayer { [SyncVar(hook OnColorChanged)] public Color playerColor Color.white; // 同步玩家选择的颜色 public Image playerIcon; // 在Inspector中关联一个UI Image // 当playerColor这个SyncVar在客户端发生变化时会自动调用此方法 void OnColorChanged(Color newColor) { playerColor newColor; if (playerIcon ! null) playerIcon.color playerColor; } // 当这个玩家对象在客户端被实例化并进入大厅时调用 public override void OnClientEnterLobby() { base.OnClientEnterLobby(); Debug.Log($Player {netId} entered lobby.); // 可以在这里初始化UI比如显示“等待准备”文字 } // 当准备状态变化时调用 public override void OnClientReady(bool readyState) { base.OnClientReady(readyState); Debug.Log($Player {netId} ready state: {readyState}); // 可以更新UI比如把按钮文字从“准备”改成“取消准备” } // 这个方法可以由UI按钮调用 public void CmdToggleReady() { if (!isLocalPlayer) return; // 只有本地玩家自己能操作 readyToBegin !readyToBegin; SendReadyToBeginMessage(); // 通知服务器准备状态变化 } // 这个方法可以由UI按钮调用用于改变颜色 public void CmdChangeColor(Color newColor) { if (!isLocalPlayer) return; playerColor newColor; } }注意CmdToggleReady和CmdChangeColor方法需要由UI按钮事件触发。你需要为预制体创建相应的UI按钮并绑定这些方法。制作预制体将场景中的LobbyPlayerPrefab对象拖入Project视图创建为预制体。然后从场景中删除这个实例。关联预制体回到NetworkLobbyManager组件的Inspector将Lobby Player Prefab字段设置为刚刚创建的预制体。3.3 创建GamePlayer预制体创建对象在GameScene中或在一个临时位置创建一个代表游戏内玩家的对象比如一个胶囊体(Capsule)命名为GamePlayerPrefab。添加组件为其添加NetworkIdentity组件。将Local Player Authority勾选上这样该玩家对象的部分权限会下发给对应的客户端。添加游戏逻辑脚本创建一个C#脚本GamePlayer.cs。using UnityEngine; using UnityEngine.Networking; public class GamePlayer : NetworkBehaviour { [SyncVar] public Color playerColor Color.white; // 需要从大厅传递过来的颜色 private Renderer rendererComp; void Start() { rendererComp GetComponentRenderer(); if (rendererComp ! null) rendererComp.material.color playerColor; // 如果是本地玩家可以添加额外的控制逻辑比如移动脚本 if (isLocalPlayer) { // 例如gameObject.AddComponentPlayerMovement(); Debug.Log(This is the local game player.); } } // 一个简单的移动示例仅本地玩家执行 void Update() { if (!isLocalPlayer) return; float moveX Input.GetAxis(Horizontal) * Time.deltaTime * 5.0f; float moveZ Input.GetAxis(Vertical) * Time.deltaTime * 5.0f; transform.Translate(moveX, 0, moveZ); } }制作预制体将GamePlayerPrefab做成预制体并从场景中删除实例。关联预制体在NetworkLobbyManager组件的Inspector中将Game Player Prefab字段设置为这个预制体。3.4 关键一步重写数据传递回调现在LobbyPlayer和GamePlayer都有了但它们是两个完全独立的对象。当从大厅切换到游戏时NetworkLobbyManager会销毁LobbyPlayer并创建GamePlayer但默认情况下GamePlayer.playerColor仍然是默认的白色。我们需要告诉管理器如何传递数据。创建一个新的C#脚本CustomLobbyManager.cs将其挂载到之前创建的NetworkLobbyManager游戏对象上它会自动替换或与原有组件协同工作。实际上我们是继承并扩展它。using UnityEngine; using UnityEngine.Networking; public class CustomLobbyManager : NetworkLobbyManager { // 重写最重要的方法当为大厅玩家创建游戏玩家时 public override GameObject OnLobbyServerCreateGamePlayer(NetworkConnection conn, GameObject lobbyPlayer) { // 1. 首先获取或创建游戏玩家对象调用基类方法完成默认的实例化 // 基类方法会根据conn的playerControllerId找到正确的生成位置Start Position GameObject gamePlayer base.OnLobbyServerCreateGamePlayer(conn, lobbyPlayer); // 2. 从大厅玩家对象上获取我们自定义的数据组件 LobbyPlayerData lobbyData lobbyPlayer.GetComponentLobbyPlayerData(); // 3. 从刚创建的游戏玩家对象上获取游戏逻辑组件 GamePlayer gamePlayerLogic gamePlayer.GetComponentGamePlayer(); // 4. 进行数据传递 if (lobbyData ! null gamePlayerLogic ! null) { // 将大厅中选择的颜色赋值给游戏玩家。 // 注意GamePlayer.playerColor 是 [SyncVar]这个赋值操作在服务器进行会自动同步到所有客户端。 gamePlayerLogic.playerColor lobbyData.playerColor; } else { Debug.LogWarning(Failed to transfer data from lobby to game player. Components missing.); } // 5. 返回创建好的游戏玩家对象 return gamePlayer; } // 可选重写其他回调以添加日志方便调试 public override void OnLobbyServerPlayersReady() { Debug.Log(All players are ready! Switching to game scene.); base.OnLobbyServerPlayersReady(); // 必须调用基类方法它内部会触发场景切换 } public override void OnLobbyClientEnter() { base.OnLobbyClientEnter(); Debug.Log(Client entered the lobby.); } }重要挂载此脚本后确保NetworkLobbyManager组件上的Lobby Player Prefab和Game Player Prefab字段仍然正确关联。这个自定义脚本继承了所有功能并覆盖了数据传递的部分。3.5 运行测试确保LobbyScene是激活场景。点击Unity编辑器运行按钮。在Game视图中点击NetworkManagerHUD提供的“Start Host”按钮。这会在编辑器内启动一个主机同时是服务器和第一个客户端。你会看到第一个LobbyPlayer被创建出来。通过它的UI如果你做了的话点击“准备”。因为Minimum Players设为1所以一旦这个玩家准备游戏就会立即开始。观察控制台日志你会看到“All players are ready!”和场景切换到GameScene。在GameScene中你应该能看到一个胶囊体GamePlayer并且它的颜色应该继承自大厅如果你在大厅改了颜色。停止运行。再次运行这次先“Start Host”然后再单独运行一个游戏实例通过File - Build and Run或者使用Unity的ParrelSync等工具。在第二个实例中使用NetworkManagerHUD的“Connect”连接到第一个实例的地址通常是localhost。现在你应该能看到两个玩家可以在大厅准备然后一起进入游戏场景。4. 深入解析状态同步与自定义UI基础流程跑通后我们会发现默认的NetworkManagerHUD非常简陋而且NetworkLobbyPlayer自带的readyToBegin同步逻辑是黑盒。要做一个实用的大厅我们必须自己掌控UI和状态同步。4.1 手动管理“准备”状态虽然NetworkLobbyPlayer有readyToBegin和SendReadyToBeginMessage()但它的UI反馈和触发条件我们控制不了。更常见的做法是我们自己定义一个同步的状态变量并完全控制其UI。修改LobbyPlayerData.cs脚本public class LobbyPlayerData : NetworkLobbyPlayer { [SyncVar(hook OnColorChanged)] public Color playerColor Color.white; [SyncVar(hook OnMyReadyStateChanged)] public bool isPlayerReady false; // 我们自定义的准备状态 public Image playerIcon; public Text playerNameText; // 显示玩家名字 public Button readyButton; // 准备按钮 public Text readyButtonText; public override void OnStartLocalPlayer() { base.OnStartLocalPlayer(); // 只有本地玩家才需要设置UI交互 if (readyButton ! null) { readyButton.onClick.RemoveAllListeners(); readyButton.onClick.AddListener(ToggleReadyState); } CmdSetPlayerName(Player_ netId); // 给服务器发送一个默认名字 } [Command] void CmdSetPlayerName(string name) { // 服务器收到命令可以在这里进行名字查重等逻辑然后通过TargetRpc同步给所有人 RpcUpdatePlayerName(name); } [ClientRpc] void RpcUpdatePlayerName(string name) { // 所有客户端更新UI if (playerNameText ! null) playerNameText.text name; } void ToggleReadyState() { if (!isLocalPlayer) return; CmdSetReadyState(!isPlayerReady); } [Command] void CmdSetReadyState(bool ready) { isPlayerReady ready; // 通知大厅管理器检查是否所有人都准备好了 // 我们需要一个引用指向大厅管理器 CustomLobbyManager lobbyManager NetworkManager.singleton as CustomLobbyManager; if (lobbyManager ! null) { // 这里可以调用一个自定义的方法让管理器去遍历所有玩家检查状态 lobbyManager.CheckAllPlayersReady(); } } void OnMyReadyStateChanged(bool newReadyState) { isPlayerReady newReadyState; UpdateReadyButtonUI(); } void UpdateReadyButtonUI() { if (readyButtonText ! null) { readyButtonText.text isPlayerReady ? Cancel Ready : Ready Up; } // 也可以改变按钮颜色 // if (readyButton ! null) readyButton.image.color isPlayerReady ? Color.green : Color.white; } void OnColorChanged(Color newColor) { playerColor newColor; if (playerIcon ! null) playerIcon.color playerColor; } // ... 其他方法 ... }同时需要在CustomLobbyManager中添加一个CheckAllPlayersReady方法public class CustomLobbyManager : NetworkLobbyManager { // ... 之前的数据传递代码 ... public void CheckAllPlayersReady() { // 只有服务器才有权判断 if (!NetworkServer.active) return; // 检查最小玩家数是否满足 if (numPlayers minPlayers) { Debug.Log($Not enough players connected. Current: {numPlayers}, Required: {minPlayers}); return; } // 遍历所有大厅玩家检查自定义的isPlayerReady状态 bool allReady true; foreach (var conn in NetworkServer.connections) { if (conn null) continue; foreach (var playerController in conn.playerControllers) { if (playerController null || playerController.gameObject null) continue; var lobbyPlayer playerController.gameObject.GetComponentLobbyPlayerData(); if (lobbyPlayer ! null !lobbyPlayer.isPlayerReady) { allReady false; break; } } if (!allReady) break; } if (allReady) { Debug.Log(All players are ready according to custom check!); // 调用基类方法触发场景切换。这会使用内置的readyToBegin逻辑。 // 为了兼容我们也可以在这里直接设置所有玩家的内置readyToBegin为true然后调用基类的检查。 // 更干净的做法是完全接管准备逻辑直接在这里调用ServerChangeScene(playScene); ServerChangeScene(playScene); } } }这种方式给了我们最大的灵活性但也要处理更多的状态同步细节。4.2 大厅玩家列表UI一个完整的大厅需要显示所有已连接的玩家及其状态是否准备、角色选择等。这需要用到NetworkManager中的NetworkServer.connections列表或通过自定义的网络消息来同步玩家列表。一个相对简单的实现是创建一个LobbyUIManager脚本它作为一个只在客户端存在的单例负责管理大厅的UI。using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; public class LobbyUIManager : MonoBehaviour { public static LobbyUIManager Instance; public Transform playerListContent; // 一个UI Layout Group的Transform public GameObject playerListItemPrefab; // 用于显示每个玩家信息的UI预制体 private Dictionaryuint, LobbyPlayerListItem playerListItems new Dictionaryuint, LobbyPlayerListItem(); void Awake() { if (Instance null) Instance this; else Destroy(gameObject); } // 这个方法由每个LobbyPlayerData在OnStartLocalPlayer或OnClientEnterLobby时调用 public void AddPlayerToList(LobbyPlayerData playerData) { if (playerListItems.ContainsKey(playerData.netId.Value)) return; GameObject itemGO Instantiate(playerListItemPrefab, playerListContent); LobbyPlayerListItem item itemGO.GetComponentLobbyPlayerListItem(); if (item ! null) { item.Setup(playerData); playerListItems[playerData.netId.Value] item; } } public void RemovePlayerFromList(uint netId) { if (playerListItems.TryGetValue(netId, out LobbyPlayerListItem item)) { Destroy(item.gameObject); playerListItems.Remove(netId); } } public void UpdatePlayerReadyState(uint netId, bool isReady) { if (playerListItems.TryGetValue(netId, out LobbyPlayerListItem item)) { item.UpdateReadyState(isReady); } } }然后LobbyPlayerData脚本需要与这个UI管理器交互public class LobbyPlayerData : NetworkLobbyPlayer { // ... 之前的变量 ... public override void OnStartClient() { base.OnStartClient(); // 当这个玩家对象在任何客户端生成时将其添加到UI列表 // 注意需要在所有客户端执行以构建完整的列表 if (LobbyUIManager.Instance ! null) LobbyUIManager.Instance.AddPlayerToList(this); } public override void OnNetworkDestroy() { base.OnNetworkDestroy(); // 当玩家对象被销毁时如断开连接从UI列表中移除 if (LobbyUIManager.Instance ! null) LobbyUIManager.Instance.RemovePlayerFromList(netId.Value); } void OnMyReadyStateChanged(bool newReadyState) { isPlayerReady newReadyState; UpdateReadyButtonUI(); // 通知UI管理器更新显示 if (LobbyUIManager.Instance ! null) LobbyUIManager.Instance.UpdatePlayerReadyState(netId.Value, newReadyState); } // ... 其他代码 ... }5. 常见问题、踩坑记录与进阶思考在实际使用NetworkLobbyManager的过程中你会遇到各种各样的问题。下面是我总结的一些典型坑点和解决方案。5.1 场景加载与玩家对象生命周期混乱问题游戏场景加载后发现GamePlayer没有生成或者生成了但位置不对、数据丢失。排查检查Play Scene设置确保NetworkLobbyManager的Play Scene字段填写正确且该场景已添加到Build Settings。检查Start PositionsGamePlayer预制体生成时会寻找场景中带有NetworkStartPosition组件的对象。确保你的游戏场景中有至少一个这样的对象通常是空物体否则玩家可能会生成在世界原点(0,0,0)。重写方法是否正确调用基类在重写OnLobbyServerCreateGamePlayer时**务必调用base.OnLobbyServerCreateGamePlayer(conn, lobbyPlayer)**来执行默认的创建和生成逻辑然后再进行自定义数据赋值。直接Instantiate而不调用基类方法会导致网络身份和连接关联出错。数据传递时机确保在OnLobbyServerCreateGamePlayer中传递数据。不要在OnLobbyServerPlayersReady中传递那时GamePlayer还没创建。5.2 “准备就绪”逻辑不触发问题所有玩家都点了准备但游戏不开始。排查理解Minimum Players这是已连接客户端数的阈值不是准备人数的阈值。如果Minimum Players2但只有一个客户端连接即使他准备了游戏也不会开始。内置vs自定义状态如果你使用了自定义的isPlayerReady但游戏开始逻辑仍然依赖内置的readyToBegin那么你需要确保在自定义状态为true时也调用SendReadyToBeginMessage()或者像我们之前那样完全接管开始游戏的逻辑直接调用ServerChangeScene。网络命令权限CmdSetReadyState这类Command只有本地玩家对象isLocalPlayer为true才能成功调用。确保你的UI按钮触发的是本地玩家对象上的方法。5.3 从游戏返回大厅时的问题问题一局游戏结束后调用ServerChangeScene回到大厅场景玩家状态没有重置或者出现了重复的玩家对象。解决方案状态重置在CustomLobbyManager中重写OnLobbyServerSceneLoadedForPlayer方法注意这是从游戏场景切换回大厅场景时为每个玩家重新创建LobbyPlayer时调用。在这里你可以将玩家的自定义LobbyPlayerData如isPlayerReady重置为默认值。对象清理NetworkLobbyManager的默认行为是当从游戏场景返回大厅时会销毁GamePlayer并为每个连接重新创建一个新的LobbyPlayer。这个过程是自动的。如果你发现有残留对象检查是否在游戏场景中有非玩家网络对象没有被正确清理。确保所有在游戏场景中动态生成的网络对象都在游戏结束时通过NetworkServer.Destroy()销毁了。5.4 关于UNet弃用与未来方向现状正如Unity手册明确指出的UNet是已弃用的方案。这意味着它不会再获得新功能更新且未来的Unity版本可能会移除对其的支持。学习价值尽管如此通过UNet的NetworkLobbyManager学习大厅模式的设计理念——状态分离LobbyPlayer/GamePlayer、场景切换同步、准备状态机、数据传递——这些概念是通用的与底层网络库无关。迁移路径如果你要开发新项目强烈建议使用Unity官方新的多人游戏解决方案Netcode for GameObjects(NGO) 或更成熟的第三方方案如Mirror一个高度兼容UNet API但更强大、维护活跃的开源库。对于NGO你需要自己实现类似的大厅逻辑因为官方没有提供开箱即用的高级大厅组件。而Mirror则直接提供了NetworkLobbyManager的移植版本其API与UNet几乎一致迁移成本极低且社区资源丰富是目前许多Unity多人游戏项目的首选。最后的建议如果你正在维护一个UNet老项目理解本文内容至关重要。如果你要启动新项目请直接使用Mirror或深入研究NGO。将本文的知识视为理解“大厅”这一设计模式的基石而非特定API的使用手册。无论底层网络如何变化优秀的架构思想总是相通的。