1. 项目概述:为什么需要一个游戏大厅?
如果你做过或者玩过多人在线游戏,尤其是那些需要组队、匹配的竞技或合作游戏,一定会对“大厅”这个概念不陌生。它就像是游戏开始前的一个“准备室”或“集结区”。在这个虚拟空间里,玩家们可以创建房间、加入房间、聊天、选择角色、调整装备,并等待所有玩家都准备就绪后,再一同进入真正的游戏场景。这个看似简单的功能,背后却串联起了网络同步、状态管理、场景切换、玩家数据传递等一系列复杂逻辑。
在Unity的生态中,早期实现多人游戏主要依赖UNet(Unity 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 = GetComponent<Renderer>(); if (rendererComp != null) rendererComp.material.color = playerColor; // 如果是本地玩家,可以添加额外的控制逻辑,比如移动脚本 if (isLocalPlayer) { // 例如:gameObject.AddComponent<PlayerMovement>(); 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.GetComponent<LobbyPlayerData>(); // 3. 从刚创建的游戏玩家对象上获取游戏逻辑组件 GamePlayer gamePlayerLogic = gamePlayer.GetComponent<GamePlayer>(); // 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.GetComponent<LobbyPlayerData>(); 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 Dictionary<uint, LobbyPlayerListItem> playerListItems = new Dictionary<uint, 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.GetComponent<LobbyPlayerListItem>(); 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 Positions:GamePlayer预制体生成时,会寻找场景中带有
NetworkStartPosition组件的对象。确保你的游戏场景中有至少一个这样的对象(通常是空物体),否则玩家可能会生成在世界原点(0,0,0)。 - 重写方法是否正确调用基类:在重写
OnLobbyServerCreateGamePlayer时,**务必调用base.OnLobbyServerCreateGamePlayer(conn, lobbyPlayer)**来执行默认的创建和生成逻辑,然后再进行自定义数据赋值。直接Instantiate而不调用基类方法会导致网络身份和连接关联出错。 - 数据传递时机:确保在
OnLobbyServerCreateGamePlayer中传递数据。不要在OnLobbyServerPlayersReady中传递,那时GamePlayer还没创建。
- 检查Play Scene设置:确保
5.2 “准备就绪”逻辑不触发
- 问题:所有玩家都点了准备,但游戏不开始。
- 排查:
- 理解
Minimum Players:这是已连接客户端数的阈值,不是准备人数的阈值。如果Minimum Players=2,但只有一个客户端连接,即使他准备了,游戏也不会开始。 - 内置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的使用手册。无论底层网络如何变化,优秀的架构思想总是相通的。