using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
namespace Mirror
{
///
/// This is a specialized NetworkManager that includes a networked lobby.
///
///
/// The lobby has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkLobbyPlayer component be on the lobby player objects.
/// NetworkLobbyManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkLobbyManager, there are new virtual functions on the NetworkLobbyManager that begin with "OnLobby". These should be used on classes derived from NetworkLobbyManager instead of the virtual functions on NetworkManager.
/// The OnLobby*() functions have empty implementations on the NetworkLobbyManager base class, so the base class functions do not have to be called.
///
[AddComponentMenu("Network/NetworkLobbyManager")]
[HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkLobbyManager.html")]
public class NetworkLobbyManager : NetworkManager
{
public struct PendingPlayer
{
public NetworkConnection conn;
public GameObject lobbyPlayer;
}
[Header("Lobby Settings")]
[FormerlySerializedAs("m_ShowLobbyGUI")]
[SerializeField]
internal bool showLobbyGUI = true;
[FormerlySerializedAs("m_MinPlayers")]
[SerializeField]
int minPlayers = 1;
[FormerlySerializedAs("m_LobbyPlayerPrefab")]
[SerializeField]
NetworkLobbyPlayer lobbyPlayerPrefab;
///
/// The scene to use for the lobby. This is similar to the offlineScene of the NetworkManager.
///
[Scene]
public string LobbyScene;
///
/// The scene to use for the playing the game from the lobby. This is similar to the onlineScene of the NetworkManager.
///
[Scene]
public string GameplayScene;
///
/// List of players that are in the Lobby
///
[FormerlySerializedAs("m_PendingPlayers")]
public List pendingPlayers = new List();
///
/// These slots track players that enter the lobby.
/// The slotId on players is global to the game - across all players.
///
public List lobbySlots = new List();
///
/// True when all players have submitted a Ready message
///
public bool allPlayersReady;
public override void OnValidate()
{
// always >= 0
maxConnections = Mathf.Max(maxConnections, 0);
// always <= maxConnections
minPlayers = Mathf.Min(minPlayers, maxConnections);
// always >= 0
minPlayers = Mathf.Max(minPlayers, 0);
if (lobbyPlayerPrefab != null)
{
NetworkIdentity identity = lobbyPlayerPrefab.GetComponent();
if (identity == null)
{
lobbyPlayerPrefab = null;
Debug.LogError("LobbyPlayer prefab must have a NetworkIdentity component.");
}
}
base.OnValidate();
}
internal void ReadyStatusChanged()
{
int CurrentPlayers = 0;
int ReadyPlayers = 0;
foreach (NetworkLobbyPlayer item in lobbySlots)
{
if (item != null)
{
CurrentPlayers++;
if (item.readyToBegin)
ReadyPlayers++;
}
}
if (CurrentPlayers == ReadyPlayers)
CheckReadyToBegin();
else
allPlayersReady = false;
}
///
///
///
/// Connection of the client
public override void OnServerReady(NetworkConnection conn)
{
if (LogFilter.Debug) Debug.Log("NetworkLobbyManager OnServerReady");
base.OnServerReady(conn);
if (conn != null && conn.playerController != null)
{
GameObject lobbyPlayer = conn.playerController.gameObject;
// if null or not a lobby player, dont replace it
if (lobbyPlayer != null && lobbyPlayer.GetComponent() != null)
SceneLoadedForPlayer(conn, lobbyPlayer);
}
}
void SceneLoadedForPlayer(NetworkConnection conn, GameObject lobbyPlayer)
{
if (LogFilter.Debug) Debug.LogFormat("NetworkLobby SceneLoadedForPlayer scene: {0} {1}", SceneManager.GetActiveScene().name, conn);
if (SceneManager.GetActiveScene().name == LobbyScene)
{
// cant be ready in lobby, add to ready list
PendingPlayer pending;
pending.conn = conn;
pending.lobbyPlayer = lobbyPlayer;
pendingPlayers.Add(pending);
return;
}
GameObject gamePlayer = OnLobbyServerCreateGamePlayer(conn);
if (gamePlayer == null)
{
// get start position from base class
Transform startPos = GetStartPosition();
gamePlayer = startPos != null
? Instantiate(playerPrefab, startPos.position, startPos.rotation)
: Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
gamePlayer.name = playerPrefab.name;
}
if (!OnLobbyServerSceneLoadedForPlayer(lobbyPlayer, gamePlayer))
return;
// replace lobby player with game player
NetworkServer.ReplacePlayerForConnection(conn, gamePlayer);
}
///
/// CheckReadyToBegin checks all of the players in the lobby to see if their readyToBegin flag is set.
/// If all of the players are ready, then the server switches from the LobbyScene to the PlayScene - essentially starting the game. This is called automatically in response to NetworkLobbyPlayer.SendReadyToBeginMessage().
///
public void CheckReadyToBegin()
{
if (SceneManager.GetActiveScene().name != LobbyScene) return;
if (minPlayers > 0 && NetworkServer.connections.Count(conn => conn.Value != null && conn.Value.playerController.gameObject.GetComponent().readyToBegin) < minPlayers)
{
allPlayersReady = false;
return;
}
pendingPlayers.Clear();
allPlayersReady = true;
OnLobbyServerPlayersReady();
}
void CallOnClientEnterLobby()
{
OnLobbyClientEnter();
foreach (NetworkLobbyPlayer player in lobbySlots)
if (player != null)
{
player.OnClientEnterLobby();
}
}
void CallOnClientExitLobby()
{
OnLobbyClientExit();
foreach (NetworkLobbyPlayer player in lobbySlots)
if (player != null)
{
player.OnClientExitLobby();
}
}
#region server handlers
///
///
///
/// Connection of the client
public override void OnServerConnect(NetworkConnection conn)
{
if (numPlayers >= maxConnections)
{
conn.Disconnect();
return;
}
// cannot join game in progress
if (SceneManager.GetActiveScene().name != LobbyScene)
{
conn.Disconnect();
return;
}
base.OnServerConnect(conn);
OnLobbyServerConnect(conn);
}
///
///
///
/// Connection of the client
public override void OnServerDisconnect(NetworkConnection conn)
{
if (conn.playerController != null)
{
NetworkLobbyPlayer player = conn.playerController.GetComponent();
if (player != null)
lobbySlots.Remove(player);
}
allPlayersReady = false;
foreach (NetworkLobbyPlayer player in lobbySlots)
{
if (player != null)
player.GetComponent().readyToBegin = false;
}
if (SceneManager.GetActiveScene().name == LobbyScene)
RecalculateLobbyPlayerIndices();
base.OnServerDisconnect(conn);
OnLobbyServerDisconnect(conn);
}
///
///
///
/// Connection of the client
///
public override void OnServerAddPlayer(NetworkConnection conn, AddPlayerMessage extraMessage)
{
if (SceneManager.GetActiveScene().name != LobbyScene) return;
if (lobbySlots.Count == maxConnections) return;
allPlayersReady = false;
if (LogFilter.Debug) Debug.LogFormat("NetworkLobbyManager.OnServerAddPlayer playerPrefab:{0}", lobbyPlayerPrefab.name);
GameObject newLobbyGameObject = OnLobbyServerCreateLobbyPlayer(conn);
if (newLobbyGameObject == null)
newLobbyGameObject = (GameObject)Instantiate(lobbyPlayerPrefab.gameObject, Vector3.zero, Quaternion.identity);
NetworkLobbyPlayer newLobbyPlayer = newLobbyGameObject.GetComponent();
lobbySlots.Add(newLobbyPlayer);
RecalculateLobbyPlayerIndices();
NetworkServer.AddPlayerForConnection(conn, newLobbyGameObject);
}
void RecalculateLobbyPlayerIndices()
{
if (lobbySlots.Count > 0)
{
for (int i = 0; i < lobbySlots.Count; i++)
{
lobbySlots[i].index = i;
}
}
}
///
///
///
///
public override void ServerChangeScene(string sceneName)
{
if (sceneName == LobbyScene)
{
foreach (NetworkLobbyPlayer lobbyPlayer in lobbySlots)
{
if (lobbyPlayer == null) continue;
// find the game-player object for this connection, and destroy it
NetworkIdentity identity = lobbyPlayer.GetComponent();
NetworkIdentity playerController = identity.connectionToClient.playerController;
NetworkServer.Destroy(playerController.gameObject);
if (NetworkServer.active)
{
// re-add the lobby object
lobbyPlayer.GetComponent().readyToBegin = false;
NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, lobbyPlayer.gameObject);
}
}
}
else
{
if (dontDestroyOnLoad)
{
foreach (NetworkLobbyPlayer lobbyPlayer in lobbySlots)
{
if (lobbyPlayer != null)
{
lobbyPlayer.transform.SetParent(null);
DontDestroyOnLoad(lobbyPlayer);
}
}
}
}
base.ServerChangeScene(sceneName);
}
///
///
///
///
public override void OnServerSceneChanged(string sceneName)
{
if (sceneName != LobbyScene)
{
// call SceneLoadedForPlayer on any players that become ready while we were loading the scene.
foreach (PendingPlayer pending in pendingPlayers)
SceneLoadedForPlayer(pending.conn, pending.lobbyPlayer);
pendingPlayers.Clear();
}
OnLobbyServerSceneChanged(sceneName);
}
///
///
///
public override void OnStartServer()
{
if (string.IsNullOrEmpty(LobbyScene))
{
Debug.LogError("NetworkLobbyManager LobbyScene is empty. Set the LobbyScene in the inspector for the NetworkLobbyMangaer");
return;
}
if (string.IsNullOrEmpty(GameplayScene))
{
Debug.LogError("NetworkLobbyManager PlayScene is empty. Set the PlayScene in the inspector for the NetworkLobbyMangaer");
return;
}
OnLobbyStartServer();
}
///
///
///
public override void OnStartHost()
{
OnLobbyStartHost();
}
///
///
///
public override void OnStopServer()
{
lobbySlots.Clear();
base.OnStopServer();
}
///
///
///
public override void OnStopHost()
{
OnLobbyStopHost();
}
#endregion
#region client handlers
///
///
///
public override void OnStartClient()
{
if (lobbyPlayerPrefab == null || lobbyPlayerPrefab.gameObject == null)
Debug.LogError("NetworkLobbyManager no LobbyPlayer prefab is registered. Please add a LobbyPlayer prefab.");
else
ClientScene.RegisterPrefab(lobbyPlayerPrefab.gameObject);
if (playerPrefab == null)
Debug.LogError("NetworkLobbyManager no GamePlayer prefab is registered. Please add a GamePlayer prefab.");
else
ClientScene.RegisterPrefab(playerPrefab);
OnLobbyStartClient();
}
///
///
///
/// Connection of the client
public override void OnClientConnect(NetworkConnection conn)
{
OnLobbyClientConnect(conn);
CallOnClientEnterLobby();
base.OnClientConnect(conn);
}
///
///
///
/// Connection of the client
public override void OnClientDisconnect(NetworkConnection conn)
{
OnLobbyClientDisconnect(conn);
base.OnClientDisconnect(conn);
}
///
///
///
public override void OnStopClient()
{
OnLobbyStopClient();
CallOnClientExitLobby();
if (!string.IsNullOrEmpty(offlineScene))
{
// Move the LobbyManager from the virtual DontDestroyOnLoad scene to the Game scene.
// This let's it be destroyed when client changes to the Offline scene.
SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene());
}
}
///
///
///
///
public override void OnClientChangeScene(string newSceneName)
{
if (LogFilter.Debug) Debug.LogFormat("OnClientChangeScene from {0} to {1}", SceneManager.GetActiveScene().name, newSceneName);
if (SceneManager.GetActiveScene().name == LobbyScene && newSceneName == GameplayScene && dontDestroyOnLoad && NetworkClient.isConnected)
{
if (NetworkClient.connection != null && NetworkClient.connection.playerController != null)
{
GameObject lobbyPlayer = NetworkClient.connection.playerController.gameObject;
if (lobbyPlayer != null)
{
lobbyPlayer.transform.SetParent(null);
DontDestroyOnLoad(lobbyPlayer);
}
else
Debug.LogWarningFormat("OnClientChangeScene: lobbyPlayer is null");
}
}
else
if (LogFilter.Debug) Debug.LogFormat("OnClientChangeScene {0} {1}", dontDestroyOnLoad, NetworkClient.isConnected);
}
///
///
///
/// Connection of the client
public override void OnClientSceneChanged(NetworkConnection conn)
{
if (SceneManager.GetActiveScene().name == LobbyScene)
{
if (NetworkClient.isConnected)
CallOnClientEnterLobby();
}
else
CallOnClientExitLobby();
base.OnClientSceneChanged(conn);
OnLobbyClientSceneChanged(conn);
}
#endregion
#region lobby server virtuals
///
/// This is called on the host when a host is started.
///
public virtual void OnLobbyStartHost() { }
///
/// This is called on the host when the host is stopped.
///
public virtual void OnLobbyStopHost() { }
///
/// This is called on the server when the server is started - including when a host is started.
///
public virtual void OnLobbyStartServer() { }
///
/// This is called on the server when a new client connects to the server.
///
/// The new connection.
public virtual void OnLobbyServerConnect(NetworkConnection conn) { }
///
/// This is called on the server when a client disconnects.
///
/// The connection that disconnected.
public virtual void OnLobbyServerDisconnect(NetworkConnection conn) { }
///
/// This is called on the server when a networked scene finishes loading.
///
/// Name of the new scene.
public virtual void OnLobbyServerSceneChanged(string sceneName) { }
///
/// This allows customization of the creation of the lobby-player object on the server.
/// By default the lobbyPlayerPrefab is used to create the lobby-player, but this function allows that behaviour to be customized.
///
/// The connection the player object is for.
/// The new lobby-player object.
public virtual GameObject OnLobbyServerCreateLobbyPlayer(NetworkConnection conn)
{
return null;
}
///
/// This allows customization of the creation of the GamePlayer object on the server.
/// By default the gamePlayerPrefab is used to create the game-player, but this function allows that behaviour to be customized. The object returned from the function will be used to replace the lobby-player on the connection.
///
/// The connection the player object is for.
/// A new GamePlayer object.
public virtual GameObject OnLobbyServerCreateGamePlayer(NetworkConnection conn)
{
return null;
}
// for users to apply settings from their lobby player object to their in-game player object
///
/// This is called on the server when it is told that a client has finished switching from the lobby scene to a game player scene.
/// When switching from the lobby, the lobby-player is replaced with a game-player object. This callback function gives an opportunity to apply state from the lobby-player to the game-player object.
///
/// The lobby player object.
/// The game player object.
/// False to not allow this player to replace the lobby player.
public virtual bool OnLobbyServerSceneLoadedForPlayer(GameObject lobbyPlayer, GameObject gamePlayer)
{
return true;
}
///
/// This is called on the server when all the players in the lobby are ready.
/// The default implementation of this function uses ServerChangeScene() to switch to the game player scene. By implementing this callback you can customize what happens when all the players in the lobby are ready, such as adding a countdown or a confirmation for a group leader.
///
public virtual void OnLobbyServerPlayersReady()
{
// all players are readyToBegin, start the game
ServerChangeScene(GameplayScene);
}
#endregion
#region lobby client virtuals
///
/// This is a hook to allow custom behaviour when the game client enters the lobby.
///
public virtual void OnLobbyClientEnter() { }
///
/// This is a hook to allow custom behaviour when the game client exits the lobby.
///
public virtual void OnLobbyClientExit() { }
///
/// This is called on the client when it connects to server.
///
/// The connection that connected.
public virtual void OnLobbyClientConnect(NetworkConnection conn) { }
///
/// This is called on the client when disconnected from a server.
///
/// The connection that disconnected.
public virtual void OnLobbyClientDisconnect(NetworkConnection conn) { }
///
/// This is called on the client when a client is started.
///
/// The connection for the lobby.
public virtual void OnLobbyStartClient() { }
///
/// This is called on the client when the client stops.
///
public virtual void OnLobbyStopClient() { }
///
/// This is called on the client when the client is finished loading a new networked scene.
///
/// The connection that finished loading a new networked scene.
public virtual void OnLobbyClientSceneChanged(NetworkConnection conn) { }
///
/// Called on the client when adding a player to the lobby fails.
/// This could be because the lobby is full, or the connection is not allowed to have more players.
///
public virtual void OnLobbyClientAddPlayerFailed() { }
#endregion
#region optional UI
///
/// virtual so inheriting classes can roll their own
///
public virtual void OnGUI()
{
if (!showLobbyGUI)
return;
if (SceneManager.GetActiveScene().name != LobbyScene)
return;
GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS");
}
#endregion
}
}