using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Netcode;
using Unity.Services.Lobbies.Models;
using Unity.XR.CoreUtils.Bindings.Variables;
using UnityEngine;
using Unity.Services.Lobbies;
using UnityEditor;
namespace XRMultiplayer
{
#if USE_FORCED_BYTE_SERIALIZATION
///
/// Workaround for a bug introduced in NGO 1.9.1.
///
/// Delete this class once the bug is fixed in NGO.
class ForceByteSerialization : NetworkBehaviour
{
NetworkVariable m_ForceByteSerialization;
}
#endif
///
/// Manages the high level connection for a networked game session.
///
[RequireComponent(typeof(LobbyManager)), RequireComponent(typeof(AuthenticationManager))]
public class XRINetworkGameManager : NetworkBehaviour
{
///
/// Determines the current state of the networked game connection.
///
///
/// None: No connection state.
/// Authenticating: Currently authenticating.
/// Authenticated: Authenticated.
/// Connecting: Currently connecting to a lobby.
/// Connected: Connected to a lobby.
///
public enum ConnectionState
{
None,
Authenticating,
Authenticated,
Connecting,
Connected
}
///
/// Max amount of players allowed when creating a new room.
///
public const int maxPlayers = 20;
///
/// Singleton Reference for access to this manager.
///
public static XRINetworkGameManager Instance => s_Instance;
static XRINetworkGameManager s_Instance;
///
/// OwnerClientId that gets set for the local player when connecting to a game.
///
public static ulong LocalId;
///
/// Authentication Id that gets passed once Authenticated.
///
public static string AuthenicationId;
///
/// Internal Room Code set by Lobby.
///
public static string ConnectedRoomCode;
///
/// Current connected region set by Lobby and Relay.
///
public static string ConnectedRoomRegion;
///
/// Bindable Variable that gets updated when changing the the currently connected room.
///
public static BindableVariable ConnectedRoomName = new("");
///
/// Bindable Variable that gets updated when the local player changes name.
///
public static BindableVariable LocalPlayerName = new("Player");
///
/// Bindable Variable that gets updated when the local player changes color.
///
public static BindableVariable LocalPlayerColor = new(Color.white);
///
/// Bindable Variable that gets updated when a player connects or disconnects from a networked game.
///
public static IReadOnlyBindableVariable Connected
{
get => m_Connected;
}
static BindableVariable m_Connected = new BindableVariable(false);
///
/// Bindable Variable that gets updated throughout the authentication and connection process.
/// See
///
public static IReadOnlyBindableVariable CurrentConnectionState
{
get => m_ConnectionState;
}
static BindableEnum m_ConnectionState = new BindableEnum(ConnectionState.None);
///
/// Auto connects to the player to a networked game session once they connect to a lobby.
/// Uncheck if you want to handle joining a networked session separately.
///
public bool autoConnectOnLobbyJoin { get => m_AutoConnectOnLobbyJoin; }
[SerializeField] bool m_AutoConnectOnLobbyJoin = true;
///
/// Flag for updating positional voice chat.
///
///
/// This will be removed in the future with the Vivox v16 update.
///
public bool positionalVoiceChat = false;
///
/// Action for when a player connects or disconnects.
///
public Action playerStateChanged;
///
/// Action for when connection status is updated.
///
public Action connectionUpdated;
///
/// Action for when connection fails.
///
public Action connectionFailedAction;
///
/// Lobby Manager handles the Lobby and Relay work between players.
///
public LobbyManager lobbyManager => m_LobbyManager;
LobbyManager m_LobbyManager;
///
/// Lobby Manager handles the Lobby and Relay work between players.
///
public AuthenticationManager authenticationManager => m_AuthenticationManager;
AuthenticationManager m_AuthenticationManager;
///
/// List that handles all current players by ID.
/// Useful for getting specific players.
/// See
///
readonly List m_CurrentPlayerIDs = new();
///
/// Flagged whenever the application is in the process of shutting down.
///
bool m_IsShuttingDown = false;
const string k_DebugPrepend = "[Network Game Manager] ";
///
/// See .
///
protected virtual async void Awake()
{
// Check for existing singleton reference. If once already exists early out.
if (s_Instance != null)
{
Utils.Log($"{k_DebugPrepend}Duplicate XRINetworkGameManager found, destroying.", 2);
Destroy(gameObject);
return;
}
s_Instance = this;
// Check for Lobby Manager, if none exist, early out.
if (TryGetComponent(out m_LobbyManager) && TryGetComponent(out m_AuthenticationManager))
{
m_LobbyManager.OnLobbyFailed += ConnectionFailed;
}
else
{
Utils.Log($"{k_DebugPrepend}Missing Managers, Disabling Component", 2);
enabled = false;
return;
}
#if UNITY_EDITOR
if(!CloudProjectSettings.projectBound)
{
Utils.Log($"{k_DebugPrepend}Project has not been linked to Unity Cloud." +
"\nThe VR Multiplayer Template utilizes Unity Gaming Services and must be linked to Unity Cloud." +
"\nGo to Settings -> Project Settings -> Services and link your project.", 2);
return;
}
#endif
// Initialize bindable variables.
m_Connected.Value = false;
// Update connection state.
m_ConnectionState.Value = ConnectionState.Authenticating;
// Wait for Authentication to complete.
bool signedIn = await Authenticate();
if (!signedIn)
{
Utils.Log($"{k_DebugPrepend}Failed to Authenticate.", 1);
ConnectionFailed("Failed to Authenticate.");
PlayerHudNotification.Instance.ShowText($"Failed to Authenticate.");
}
else
{
// Update connection state.
m_ConnectionState.Value = ConnectionState.Authenticated;
}
}
///
/// See .
///
protected virtual void Start()
{
NetworkManager.Singleton.OnClientStopped += OnLocalClientStopped;
}
///
/// See .
///
public override void OnDestroy()
{
base.OnDestroy();
ShutDown();
}
///
/// See .
///
private void OnApplicationQuit()
{
ShutDown();
}
async void ShutDown()
{
if (m_IsShuttingDown) return;
m_IsShuttingDown = true;
// Remove callbacks
if (NetworkManager.Singleton != null)
{
NetworkManager.Singleton.OnClientStopped -= OnLocalClientStopped;
}
// Shutdown lobby if owner, remove from lobby if not owner.
await m_LobbyManager.RemovePlayerFromLobby(AuthenicationId);
}
public async Task Authenticate()
{
return await m_AuthenticationManager.Authenticate();
}
public bool IsAuthenticated()
{
return AuthenticationManager.IsAuthenticated();
}
///
/// Called from XRINetworkPlayer once they have spawned.
///
/// Sets based on from the local player
public virtual void LocalPlayerConnected(ulong localPlayerId)
{
m_Connected.Value = true;
LocalId = localPlayerId;
PlayerHudNotification.Instance.ShowText($"Status: Connected");
}
///
/// Called when disconnected from any networked game.
///
///
/// Local player id.
///
protected virtual void OnLocalClientStopped(bool id)
{
m_Connected.Value = false;
m_CurrentPlayerIDs.Clear();
PlayerHudNotification.Instance.ShowText($"Status: Disconnected");
// Check if authenticated on disconnect.
if (IsAuthenticated())
{
m_ConnectionState.Value = ConnectionState.Authenticated;
}
else
{
m_ConnectionState.Value = ConnectionState.None;
}
}
///
/// Finds all 's existing in the scene and gets the
/// based on for that player.
///
///
/// of the player.
///
///
/// Out .
///
///
/// Returns true based on whether or not a player with that Id exists.
///
public virtual bool GetPlayerByID(ulong id, out XRINetworkPlayer player)
{
// Find all existing players in scene. This is a workaround until NGO exposes client side player list (2.x I believe - JG).
XRINetworkPlayer[] allPlayers = FindObjectsByType(FindObjectsSortMode.None);
//Loops through existing players and returns true if player with id is found.
foreach (XRINetworkPlayer p in allPlayers)
{
if (p.NetworkObject.OwnerClientId == id)
{
player = p;
return true;
}
}
player = null;
return false;
}
[ContextMenu("Show All NetworkClients")]
void ShowAllNetworkClients()
{
foreach (var client in NetworkManager.Singleton.ConnectedClients)
{
Debug.Log($"Client: {client.Key}, {client.Value.PlayerObject.name}");
}
}
///
/// This function will set the player ID in the list and
/// invokes the callback .
///
/// of the joined player.
/// Called from .
public virtual void PlayerJoined(ulong playerID)
{
// If playerID is not already registered, then add.
if (!m_CurrentPlayerIDs.Contains(playerID))
{
m_CurrentPlayerIDs.Add(playerID);
playerStateChanged?.Invoke(playerID, true);
}
else
{
Utils.Log($"{k_DebugPrepend}Trying to Add a player ID [{playerID}] that already exists", 1);
}
}
///
/// Called from .
///
/// of the player who left.
public virtual void PlayerLeft(ulong playerID)
{
// Check to make sure player has been registerd.
if (m_CurrentPlayerIDs.Contains(playerID))
{
m_CurrentPlayerIDs.Remove(playerID);
playerStateChanged?.Invoke(playerID, false);
}
else
{
Utils.Log($"{k_DebugPrepend}Trying to remove a player ID [{playerID}] that doesn't exist", 1);
}
}
///
/// Called whenever there is a problem with connecting to game or lobby.
///
/// Failure message.
public virtual void ConnectionFailed(string reason)
{
connectionFailedAction?.Invoke(reason);
m_ConnectionState.Value = AuthenticationManager.IsAuthenticated() ? ConnectionState.Authenticated : ConnectionState.None;
}
///
/// Called whenever there is an update to connection status.
///
/// Status update message.
public virtual void ConnectionUpdated(string update)
{
connectionUpdated?.Invoke(update);
}
///
/// Joins a random lobby. If no lobbies exist, it will create a new one.
///
public virtual async void QuickJoinLobby()
{
Utils.Log($"{k_DebugPrepend}Joining Lobby by Quick Join.");
if (await AbleToConnect())
{
ConnectToLobby(await m_LobbyManager.QuickJoinLobby());
}
}
///
/// Called when trying to join a Lobby by Room Code.
///
/// Lobby to join.
public virtual async void JoinLobbyByCode(string code)
{
Utils.Log($"{k_DebugPrepend}Joining Lobby by room code: {code}.");
if (await AbleToConnect())
{
ConnectToLobby(await m_LobbyManager.JoinLobby(roomCode: code));
}
}
///
/// Called when trying to join a specific Lobby.
///
/// Lobby to join.
public virtual async void JoinLobbySpecific(Lobby lobby)
{
Utils.Log($"{k_DebugPrepend}Joining specific Lobby: {lobby.Name}.");
if (await AbleToConnect())
{
ConnectToLobby(await m_LobbyManager.JoinLobby(lobby: lobby));
}
}
///
/// Creates a new Lobby.
///
/// Name of the lobby.
/// Whether or not the lobby is private.
/// Maximum allowed players.
public virtual async void CreateNewLobby(string roomName = null, bool isPrivate = false, int playerCount = maxPlayers)
{
Utils.Log($"{k_DebugPrepend}Creating New Lobby: {roomName}.");
if (await AbleToConnect())
{
ConnectToLobby(await m_LobbyManager.CreateLobby(roomName, isPrivate, playerCount));
}
}
///
/// Checks if a we are currently able to connect to a lobby.
/// If already connected it will disconnect in attempt to "Hot Join" a new lobby.
///
/// Whether or not we are able to connect.
protected virtual async Task AbleToConnect()
{
// If in the process of trying to connect, send failure message and return false.
if (m_ConnectionState.Value == ConnectionState.Connecting)
{
string failureMessage = "Connection attempt still in progress.";
Utils.Log($"{k_DebugPrepend}{failureMessage}", 1);
ConnectionFailed(failureMessage);
return false;
}
// If already connected to a lobby, disconnect in attempt to "Hot Join".
if (Connected.Value || m_ConnectionState.Value == ConnectionState.Connected)
{
Utils.Log($"{k_DebugPrepend}Already Connected to a Lobby. Disconnecting.", 0);
await DisconnectAsync();
// Small wait while everything finishes disconnecting.
// This isn't technically needed, but makes the flow feel better.
await Task.Delay(100);
}
m_ConnectionState.Value = ConnectionState.Connecting;
return true;
}
///
/// Connect to a lobby.
///
/// Lobby to connect to.
protected virtual void ConnectToLobby(Lobby lobby)
{
// Send failure message if we can't connect.
if (lobby == null || !ConnectedToLobby())
{
FailedToConnect();
}
}
///
/// Checks if we successfully connected to a Lobby.
/// If is enabled, join networked game here.
///
/// Whether or not we connected to a lobby and / or networked game.
protected virtual bool ConnectedToLobby()
{
bool connected;
if (autoConnectOnLobbyJoin)
{
ConnectedRoomRegion = m_LobbyManager.connectedLobby.Data[LobbyManager.k_RegionKeyIdentifier].Value;
ConnectedRoomCode = m_LobbyManager.connectedLobby.LobbyCode;
ConnectedRoomName.Value = m_LobbyManager.connectedLobby.Name;
if (m_LobbyManager.connectedLobby.HostId == AuthenicationId)
{
connected = NetworkManager.Singleton.StartHost();
}
else
{
connected = NetworkManager.Singleton.StartClient();
}
}
else
{
connected = true;
//Players are connected to the lobby, but have not started a Networked Game session.
}
if (connected)
{
Utils.Log($"{k_DebugPrepend}Connected to game session. Lobby: {m_LobbyManager.connectedLobby.Name}.");
m_ConnectionState.Value = ConnectionState.Connected;
SubscribeToLobbyEvents();
}
else
{
Utils.Log($"{k_DebugPrepend}Failed to connect to lobby {m_LobbyManager.connectedLobby.Name}.");
m_LobbyManager.OnLobbyFailed?.Invoke($"Failed to connect to lobby {m_LobbyManager.connectedLobby.Name}.");
}
return connected;
}
///
/// Subscribe to lobby update events. This needed to be informed of Lobby changes (name, privacy, etc...).
///
/// See .
protected virtual async void SubscribeToLobbyEvents()
{
var callbacks = new LobbyEventCallbacks();
callbacks.LobbyChanged += OnLobbyChanged;
callbacks.LobbyEventConnectionStateChanged += OnLobbyEventConnectionStateChanged;
try
{
await LobbyService.Instance.SubscribeToLobbyEventsAsync(m_LobbyManager.connectedLobby.Id, callbacks);
}
catch (LobbyServiceException ex)
{
switch (ex.Reason)
{
case LobbyExceptionReason.AlreadySubscribedToLobby: Utils.Log($"{k_DebugPrepend}Already subscribed to lobby[{m_LobbyManager.connectedLobby.Id}]. We did not need to try and subscribe again. Exception Message: {ex.Message}", 1); break;
case LobbyExceptionReason.SubscriptionToLobbyLostWhileBusy: Utils.Log($"{k_DebugPrepend}Subscription to lobby events was lost while it was busy trying to subscribe. Exception Message: {ex.Message}", 2); throw;
case LobbyExceptionReason.LobbyEventServiceConnectionError: Utils.Log($"{k_DebugPrepend}Failed to connect to lobby events. Exception Message: {ex.Message}", 2); throw;
default: throw;
}
}
}
///
/// Callabacks for anytime the lobby event connection state has changed.
///
///
private void OnLobbyEventConnectionStateChanged(LobbyEventConnectionState state)
{
switch (state)
{
case LobbyEventConnectionState.Unsubscribed: Utils.Log($"{k_DebugPrepend}Lobby event now Unsubscribed"); break;
case LobbyEventConnectionState.Subscribing: Utils.Log($"{k_DebugPrepend}Attempting to subscribe to lobby events"); break;
case LobbyEventConnectionState.Subscribed: Utils.Log($"{k_DebugPrepend}Subscribing to lobby events now"); break;
case LobbyEventConnectionState.Unsynced:
m_LobbyManager.ReconnectToLobby();
Utils.Log($"{k_DebugPrepend}Lobby Events now unsynced.\n\n{state}", 1);
break;
case LobbyEventConnectionState.Error: Utils.Log($"{k_DebugPrepend}Lobby event error.\n\n{state}", 2); break;
}
}
///
/// Callback for anytime a lobby is updated via .
///
///
protected virtual void OnLobbyChanged(ILobbyChanges changes)
{
// Check for lobby deletion.
if (!changes.LobbyDeleted)
{
changes.ApplyToLobby(m_LobbyManager.connectedLobby);
// Update values based on lobby changes.
if (changes.Name.Changed)
{
ConnectedRoomName.Value = m_LobbyManager.connectedLobby.Name;
}
}
}
///
/// Generic failure message.
///
protected virtual void FailedToConnect(string reason = null)
{
string failureMessage = "Failed to connect to lobby.";
if (reason != null)
{
failureMessage = $"{reason}";
}
Utils.Log($"{k_DebugPrepend}{failureMessage}", 1);
}
///
/// Cancel current matchmaking.
/// Called from the Lobby UI.
///
public virtual async void CancelMatchmaking()
{
if (IsAuthenticated())
{
m_ConnectionState.Value = ConnectionState.Authenticated;
}
await m_LobbyManager.RemovePlayerFromLobby(AuthenicationId);
}
///
/// High Level Disconnect call.
///
public virtual async void Disconnect()
{
await DisconnectAsync();
}
///
/// Awaitable Disconnect call, used for Hot Joining.
///
///
public virtual async Task DisconnectAsync()
{
bool fullyDisconnected = await m_LobbyManager.RemovePlayerFromLobby(AuthenicationId);
m_Connected.Value = false;
NetworkManager.Shutdown();
if (IsAuthenticated())
{
m_ConnectionState.Value = ConnectionState.Authenticated;
}
else
{
m_ConnectionState.Value = ConnectionState.None;
}
Utils.Log($"{k_DebugPrepend}Disconnected from Game.");
return fullyDisconnected;
}
}
}