498 lines
20 KiB
C#
498 lines
20 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using Unity.Services.Lobbies.Models;
|
|
using Unity.Services.Lobbies;
|
|
using UnityEngine;
|
|
using System.Threading.Tasks;
|
|
using System;
|
|
using Unity.Services.Relay;
|
|
using Unity.Netcode.Transports.UTP;
|
|
using Unity.XR.CoreUtils.Bindings.Variables;
|
|
using Unity.Services.Authentication;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
namespace XRMultiplayer
|
|
{
|
|
/// <summary>
|
|
/// This class manages the relationship between Lobby, Relay, and Unity Transport.
|
|
/// </summary>
|
|
public class LobbyManager : MonoBehaviour
|
|
{
|
|
// Constants for Lobby Data.
|
|
public const string k_JoinCodeKeyIdentifier = "j";
|
|
public const string k_RegionKeyIdentifier = "r";
|
|
public const string k_BuildIdKeyIdentifier = "b";
|
|
public const string k_SceneKeyIdentifier = "s";
|
|
public const string k_EditorKeyIdentifier = "e";
|
|
|
|
static bool s_HideEditorInLobbies;
|
|
|
|
[Tooltip("This will prevent joining into rooms that are being hosted in different scenes.\nThis should almost always be false.")]
|
|
public bool allowDifferentScenes = false;
|
|
|
|
[Tooltip("This will hide editor created rooms from external builds.\nNOTE: This will not hide editor created rooms from other editors.")]
|
|
public bool hideEditorFromLobby = false;
|
|
|
|
// Action that gets invoked when you fail to connect to a lobby. Primarily used for noting failure messages.
|
|
public Action<string> OnLobbyFailed;
|
|
|
|
// The current connected lobby.
|
|
public Lobby connectedLobby
|
|
{
|
|
get => m_ConnectedLobby;
|
|
set => m_ConnectedLobby = value;
|
|
}
|
|
Lobby m_ConnectedLobby;
|
|
|
|
// The Transport used for connection.
|
|
UnityTransport m_Transport;
|
|
|
|
// This routine keeps the lobby alive once joined (by default lobbies will close after 30 seconds of inactivity.
|
|
Coroutine m_HeartBeatRoutine;
|
|
|
|
/// <summary>
|
|
/// Subscribe to this bindable string for status updates from this class
|
|
/// </summary>
|
|
public static IReadOnlyBindableVariable<string> status
|
|
{
|
|
get => m_Status;
|
|
}
|
|
readonly static BindableVariable<string> m_Status = new("");
|
|
|
|
const string k_DebugPrepend = "<color=#EC0CFA>[Lobby Manager]</color> ";
|
|
|
|
|
|
/// <summary>
|
|
/// See <see cref="MonoBehaviour"/>.
|
|
/// </summary>
|
|
private void Awake()
|
|
{
|
|
m_Transport = FindFirstObjectByType<UnityTransport>();
|
|
|
|
if (!Application.isEditor)
|
|
{
|
|
hideEditorFromLobby = false;
|
|
}
|
|
s_HideEditorInLobbies = hideEditorFromLobby;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Quick Join Function will try and find any lobbies via QuickJoinLobbyAsync().
|
|
/// If no lobbies are found then a new lobby is created.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public async Task<Lobby> QuickJoinLobby()
|
|
{
|
|
m_Status.Value = "Checking For Existing Lobbies.";
|
|
Utils.Log($"{k_DebugPrepend}{m_Status.Value}");
|
|
Lobby lobby;
|
|
try
|
|
{
|
|
Utils.Log($"{k_DebugPrepend} Getting lobby via Quick Join");
|
|
lobby = await LobbyService.Instance.QuickJoinLobbyAsync(GetQuickJoinFilterOptions());
|
|
await SetupRelay(lobby);
|
|
ConnectedToLobby(lobby);
|
|
|
|
if (lobby != null)
|
|
{
|
|
m_ConnectedLobby = lobby;
|
|
return lobby;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
m_Status.Value = "No Available Lobbies. Creating New Lobby.";
|
|
Utils.Log($"{k_DebugPrepend}{m_Status.Value}");
|
|
}
|
|
|
|
// If no existing Lobbies, then create a new one.
|
|
lobby = await CreateLobby();
|
|
return lobby;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Joins a lobby.
|
|
/// </summary>
|
|
/// <param name="lobby">Lobby to join.</param>
|
|
/// <param name="roomCode">Lobby Code to join with.</param>
|
|
/// <returns>Returns the Lobby.</returns>
|
|
public async Task<Lobby> JoinLobby(Lobby lobby = null, string roomCode = null)
|
|
{
|
|
try
|
|
{
|
|
// If Lobby is null, then get the new lobby based on room code
|
|
lobby = await GetLobby(lobby, roomCode);
|
|
await SetupRelay(lobby);
|
|
ConnectedToLobby(lobby);
|
|
|
|
return lobby;
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
string failureMessage = "Failed to Join Lobby.";
|
|
Utils.Log($"{k_DebugPrepend}{e.Message}", 1);
|
|
|
|
if (e is LobbyServiceException)
|
|
{
|
|
string message = e.Message.ToLower();
|
|
|
|
if (message.Contains("Rate limit".ToLower()))
|
|
failureMessage = "Rate limit exceeded. Please try again later.";
|
|
else if (message.Contains("Lobby not found".ToLower()))
|
|
failureMessage = "Lobby not found. Please try a new Lobby.";
|
|
else
|
|
failureMessage = e.Message;
|
|
}
|
|
Utils.Log($"{k_DebugPrepend}{failureMessage}\n\n{e}", 1);
|
|
OnLobbyFailed?.Invoke($"{failureMessage}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function will try to create a lobby and host a networked session.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public async Task<Lobby> CreateLobby(string roomName = null, bool isPrivate = false, int playerCount = XRINetworkGameManager.maxPlayers)
|
|
{
|
|
try
|
|
{
|
|
m_Status.Value = "Creating Relay";
|
|
// Creates a new Allocation based on the defined max players above
|
|
var alloc = await RelayService.Instance.CreateAllocationAsync(XRINetworkGameManager.maxPlayers);
|
|
|
|
m_Status.Value = "Creating Join Code";
|
|
// Get a join code based on the Allocation
|
|
var joinCode = await RelayService.Instance.GetJoinCodeAsync(alloc.AllocationId);
|
|
|
|
// Creates Lobby Options Dictionary for other clients to find and join
|
|
var options = new CreateLobbyOptions
|
|
{
|
|
// Set the Data to be used for lobby filtering
|
|
Data = new Dictionary<string, DataObject>
|
|
{
|
|
{
|
|
// Set Join Code Key
|
|
k_JoinCodeKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, joinCode)
|
|
},
|
|
{
|
|
// Set Region Key
|
|
k_RegionKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, alloc.Region)
|
|
},
|
|
{
|
|
// Set Build ID Key
|
|
k_BuildIdKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, Application.version, DataObject.IndexOptions.S1)
|
|
},
|
|
{
|
|
// Set Scene Key
|
|
k_SceneKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, SceneManager.GetActiveScene().name, DataObject.IndexOptions.S2)
|
|
},
|
|
{
|
|
// Set Editor Key
|
|
k_EditorKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, hideEditorFromLobby.ToString(), DataObject.IndexOptions.S3)
|
|
},
|
|
},
|
|
IsPrivate = isPrivate,
|
|
};
|
|
|
|
|
|
m_Status.Value = "Creating Lobby";
|
|
// Creates the Lobby with the specified max players and lobby options. Currently just naming "General Lobby"
|
|
string lobbyName = string.IsNullOrEmpty(roomName) ? $"{XRINetworkGameManager.LocalPlayerName.Value}'s Room" : $"{roomName}";
|
|
|
|
// RATE LIMIT: 2 request per 6 seconds
|
|
var lobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, playerCount, options);
|
|
Utils.Log($"{k_DebugPrepend}Created Lobby with Join Code: {joinCode}, Region: {alloc.Region}, Build ID: {Application.version}, Scene: {SceneManager.GetActiveScene().name}, Editor: {hideEditorFromLobby}");
|
|
|
|
// Stop the heartbeat routine if one exists, and starts a new one. This keeps the lobby active for visibility
|
|
if (m_HeartBeatRoutine != null) StopCoroutine(m_HeartBeatRoutine);
|
|
m_HeartBeatRoutine = StartCoroutine(LobbyHeartbeatCoroutine(lobby.Id));
|
|
|
|
//Populate the transport data with the relay info for the host (IP, port, etc...)
|
|
m_Transport.SetHostRelayData(alloc.RelayServer.IpV4, (ushort)alloc.RelayServer.Port, alloc.AllocationIdBytes, alloc.Key, alloc.ConnectionData);
|
|
ConnectedToLobby(lobby);
|
|
|
|
return lobby;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
string failureMessage = "Failed to Create Lobby. Please try again.";
|
|
Utils.Log($"{k_DebugPrepend}{failureMessage}\n\n{e}", 1);
|
|
// Debug.LogWarning($"[XRMPT] {failureMessage}\n\n{e}");
|
|
OnLobbyFailed?.Invoke(failureMessage);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async Task SetupRelay(Lobby lobby)
|
|
{
|
|
m_Status.Value = "Connecting To Relay";
|
|
// Get the Join Allocation for the lobby based on the key
|
|
var alloc = await RelayService.Instance.JoinAllocationAsync(lobby.Data[k_JoinCodeKeyIdentifier].Value);
|
|
// Set the transport client data (IP, port, etc..)
|
|
m_Transport.SetClientRelayData
|
|
(
|
|
alloc.RelayServer.IpV4, (ushort)alloc.RelayServer.Port,
|
|
alloc.AllocationIdBytes, alloc.Key, alloc.ConnectionData, alloc.HostConnectionData
|
|
);
|
|
return;
|
|
}
|
|
|
|
QuickJoinLobbyOptions GetQuickJoinFilterOptions()
|
|
{
|
|
QuickJoinLobbyOptions options = new QuickJoinLobbyOptions();
|
|
|
|
// Create Filter Option to prevent showing any application versions that are not the same.
|
|
QueryFilter applicationVersionIdFilter = new QueryFilter(field: QueryFilter.FieldOptions.S1, value: Application.version, QueryFilter.OpOptions.EQ);
|
|
|
|
// Create Filter Option for different scenes.
|
|
QueryFilter sceneNameFilter = new QueryFilter(field: QueryFilter.FieldOptions.S2, value: SceneManager.GetActiveScene().name, QueryFilter.OpOptions.EQ);
|
|
|
|
// Create Filter Option for hiding editor created rooms from builds.
|
|
QueryFilter editorFilter = new QueryFilter(field: QueryFilter.FieldOptions.S3, value: hideEditorFromLobby.ToString(), QueryFilter.OpOptions.EQ);
|
|
|
|
options.Filter = new List<QueryFilter> { applicationVersionIdFilter, sceneNameFilter, editorFilter };
|
|
|
|
return options;
|
|
}
|
|
|
|
public async void ReconnectToLobby()
|
|
{
|
|
if (Application.isPlaying)
|
|
{
|
|
await LobbyService.Instance.ReconnectToLobbyAsync(m_ConnectedLobby.Id);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function will get a lobby based on the passed in parameters.
|
|
/// </summary>
|
|
/// <param name="lobby">Lobby to join.</param>
|
|
/// <param name="roomCode">Lobby Code to join with.</param>
|
|
/// <returns>Returns the Lobby.</returns>
|
|
async Task<Lobby> GetLobby(Lobby lobby = null, string roomCode = null)
|
|
{
|
|
if (roomCode != null)
|
|
{
|
|
// RATE LIMIT: 2 request per 6 seconds
|
|
Utils.Log($"{k_DebugPrepend} Getting lobby via Code: {roomCode}");
|
|
return await LobbyService.Instance.JoinLobbyByCodeAsync(roomCode);
|
|
}
|
|
else if (lobby != null)
|
|
{
|
|
// RATE LIMIT: 2 request per 6 seconds
|
|
Utils.Log($"{k_DebugPrepend} Getting lobby via Lobby Id: {lobby.Id}");
|
|
return await LobbyService.Instance.JoinLobbyByIdAsync(lobby.Id);
|
|
}
|
|
else
|
|
{
|
|
// RATE LIMIT: 1 request per 10 seconds
|
|
Utils.Log($"{k_DebugPrepend} Getting lobby via Quick Join");
|
|
return await LobbyService.Instance.QuickJoinLobbyAsync(GetQuickJoinFilterOptions());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after setting transport data with relay allocations when either Creating a new lobby or joining an existing lobby.
|
|
/// </summary>
|
|
void ConnectedToLobby(Lobby lobby)
|
|
{
|
|
m_ConnectedLobby = lobby;
|
|
m_Status.Value = "Connected To Lobby";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Heartbeat used to keep the lobby alive. By default the lobby will shut down after 30 seconds on inactivity.
|
|
/// </summary>
|
|
/// <param name="lobbyId">Id for the specific lobby to keep alive.</param>
|
|
/// <param name="waitTimeSeconds">Time to wait between pings.</param>
|
|
/// <returns></returns>
|
|
IEnumerator LobbyHeartbeatCoroutine(string lobbyId, float waitTimeSeconds = 15.0f)
|
|
{
|
|
// Setup a new wait based on wait time in seconds
|
|
var delay = new WaitForSecondsRealtime(waitTimeSeconds);
|
|
|
|
while (true)
|
|
{
|
|
// Continuously ping the lobby to keep it alive
|
|
LobbyService.Instance.SendHeartbeatPingAsync(lobbyId);
|
|
Utils.Log($"{k_DebugPrepend}Sending Heartbeat Ping for Lobby {lobbyId}");
|
|
yield return delay;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Changes the existing lobbies name.
|
|
/// </summary>
|
|
/// <param name="lobbyName">Name to change the lobby to.</param>
|
|
public async void UpdateLobbyName(string lobbyName)
|
|
{
|
|
if (m_ConnectedLobby != null)
|
|
{
|
|
try
|
|
{
|
|
UpdateLobbyOptions options = new()
|
|
{
|
|
Name = lobbyName,
|
|
HostId = AuthenticationService.Instance.PlayerId
|
|
};
|
|
|
|
await LobbyService.Instance.UpdateLobbyAsync(m_ConnectedLobby.Id, options);
|
|
XRINetworkGameManager.ConnectedRoomName.Value = lobbyName;
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
Utils.Log($"{k_DebugPrepend}{e}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Utils.Log($"{k_DebugPrepend}Connected Lobby is null");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the privacy setting for the current room.
|
|
/// </summary>
|
|
/// <param name="privateRoom">Whether or not to make the room private.</param>
|
|
public async void UpdateRoomPrivacy(bool privateRoom)
|
|
{
|
|
if (m_ConnectedLobby != null)
|
|
{
|
|
try
|
|
{
|
|
UpdateLobbyOptions options = new()
|
|
{
|
|
IsPrivate = privateRoom
|
|
};
|
|
await LobbyService.Instance.UpdateLobbyAsync(m_ConnectedLobby.Id, options);
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
Utils.Log($"{k_DebugPrepend}{e}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Utils.Log($"{k_DebugPrepend}Connected Lobby is null");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when leaving a room.
|
|
/// If Hosting, this function deletes the lobby for everyone.
|
|
/// If a client, this function removes the client from the lobby.
|
|
/// </summary>
|
|
/// <param name="playerId"></param>
|
|
/// <returns></returns>
|
|
public async Task<bool> RemovePlayerFromLobby(string playerId)
|
|
{
|
|
// Stop heartbeat if active (only runs on host)
|
|
if (m_HeartBeatRoutine != null) StopCoroutine(m_HeartBeatRoutine);
|
|
|
|
try
|
|
{
|
|
if (m_ConnectedLobby != null)
|
|
{
|
|
// Check if Lobby Host is current Player
|
|
if (m_ConnectedLobby.HostId == playerId)
|
|
{
|
|
// Delete Lobby if current owner
|
|
Utils.Log($"{k_DebugPrepend}Owner of lobby, shutting down.");
|
|
await LobbyService.Instance.DeleteLobbyAsync(m_ConnectedLobby.Id);
|
|
m_ConnectedLobby = null;
|
|
}
|
|
else
|
|
{
|
|
//Remove from lobby
|
|
await RemoveFromLobby(playerId);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Utils.Log($"{k_DebugPrepend}Error on Lobby Shutdown:\n\n {e}");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to remove the current player from the lobby
|
|
/// </summary>
|
|
async Task<bool> RemoveFromLobby(string playerId)
|
|
{
|
|
// If lobby id exists try to remove player from
|
|
if (!string.IsNullOrEmpty(m_ConnectedLobby.Id))
|
|
{
|
|
try
|
|
{
|
|
await LobbyService.Instance.RemovePlayerAsync(m_ConnectedLobby.Id, playerId);
|
|
m_ConnectedLobby = null;
|
|
Utils.Log($"{k_DebugPrepend}Successfully removed player from Lobby.");
|
|
return true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Utils.Log($"{k_DebugPrepend}Failed to remove player from lobby.\n\n{e}");
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
public static async Task<QueryResponse> GetLobbiesAsync()
|
|
{
|
|
// Use these options to apply things like filters, ordering, etc...
|
|
// Additionally you can add your own filters like below to have more control over the data.
|
|
QueryLobbiesOptions lobbyOptions = new QueryLobbiesOptions();
|
|
return await LobbyService.Instance.QueryLobbiesAsync(lobbyOptions);
|
|
}
|
|
|
|
public static bool CheckForLobbyFilter(Lobby lobby)
|
|
{
|
|
// If the lobby is not in the same scene, skip it
|
|
if (lobby.Data.TryGetValue(k_SceneKeyIdentifier, out DataObject sceneData))
|
|
{
|
|
if (sceneData.Value != SceneManager.GetActiveScene().name)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (lobby.Data.TryGetValue(k_EditorKeyIdentifier, out DataObject editorData))
|
|
{
|
|
// If the lobby is an editor lobby is set to filter return true
|
|
if (editorData.Value == "True" & !s_HideEditorInLobbies)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static bool CheckForIncompatibilityFilter(Lobby lobby)
|
|
{
|
|
if (lobby.Data.TryGetValue(k_BuildIdKeyIdentifier, out DataObject data))
|
|
{
|
|
//Filter out lobbies that are on different build versions
|
|
if (data.Value != Application.version)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static bool CanJoinLobby(Lobby lobby)
|
|
{
|
|
return (XRINetworkGameManager.Instance.lobbyManager.connectedLobby == null) ||
|
|
(XRINetworkGameManager.Instance.lobbyManager.connectedLobby != null && lobby.Id != XRINetworkGameManager.Instance.lobbyManager.connectedLobby.Id);
|
|
}
|
|
}
|
|
}
|