Initial commit
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
using System.Threading.Tasks;
|
||||
using Unity.Services.Authentication;
|
||||
using Unity.Services.Core;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
// Unity 6 Only
|
||||
#if HAS_MPPM
|
||||
using Unity.Multiplayer.Playmode;
|
||||
using UnityEngine.XR.Interaction.Toolkit.UI;
|
||||
#endif
|
||||
|
||||
#if HAS_PARRELSYNC
|
||||
using ParrelSync;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class AuthenticationManager : MonoBehaviour
|
||||
{
|
||||
const string k_DebugPrepend = "<color=#938FFF>[Authentication Manager]</color> ";
|
||||
|
||||
/// <summary>
|
||||
/// The argument ID to search for in the command line args.
|
||||
/// </summary>
|
||||
const string k_playerArgID = "PlayerArg";
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the AuthenticationManager should use command line args to determine the player ID when launching a build.
|
||||
/// </summary>
|
||||
[SerializeField] bool m_UseCommandLineArgs = true;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Simple Authentication function. This uses bare bones authentication and anonymous sign in.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<bool> Authenticate()
|
||||
{
|
||||
// Check if UGS has not been initialized yet, and initialize.
|
||||
if (UnityServices.State == ServicesInitializationState.Uninitialized)
|
||||
{
|
||||
var options = new InitializationOptions();
|
||||
string playerId = "Player";
|
||||
// Check for editor clones (MPPM or ParrelSync).
|
||||
// This allows for multiple instances of the editor to connect to UGS.
|
||||
#if UNITY_EDITOR
|
||||
playerId = "Editor";
|
||||
|
||||
#if HAS_MPPM
|
||||
//Check for MPPM
|
||||
playerId += CheckMPPM();
|
||||
#elif HAS_PARRELSYNC
|
||||
// Check for ParrelSync
|
||||
playerId += CheckParrelSync();
|
||||
#endif
|
||||
#endif
|
||||
// Check for command line args in builds
|
||||
if (!Application.isEditor && m_UseCommandLineArgs)
|
||||
{
|
||||
playerId += GetPlayerIDArg();
|
||||
}
|
||||
|
||||
options.SetProfile(playerId);
|
||||
Utils.Log($"{k_DebugPrepend}Signing in with profile {playerId}");
|
||||
|
||||
// Initialize UGS using any options defined
|
||||
await UnityServices.InitializeAsync(options);
|
||||
}
|
||||
|
||||
// If not already signed on then do so.
|
||||
if (!AuthenticationService.Instance.IsAuthorized)
|
||||
{
|
||||
// Signing in anonymously for simplicity sake.
|
||||
await AuthenticationService.Instance.SignInAnonymouslyAsync();
|
||||
}
|
||||
|
||||
// Cache PlayerId.
|
||||
XRINetworkGameManager.AuthenicationId = AuthenticationService.Instance.PlayerId;
|
||||
return UnityServices.State == ServicesInitializationState.Initialized;
|
||||
}
|
||||
|
||||
public static bool IsAuthenticated()
|
||||
{
|
||||
try
|
||||
{
|
||||
return AuthenticationService.Instance.IsSignedIn;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Checking for AuthenticationService.Instance before initialized.{e}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string GetPlayerIDArg()
|
||||
{
|
||||
string playerID = "";
|
||||
string[] args = System.Environment.GetCommandLineArgs();
|
||||
foreach (string arg in args)
|
||||
{
|
||||
arg.ToLower();
|
||||
if (arg.ToLower().Contains(k_playerArgID.ToLower()))
|
||||
{
|
||||
var splitArgs = arg.Split(':');
|
||||
if (splitArgs.Length > 0)
|
||||
{
|
||||
playerID += splitArgs[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return playerID;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
#if HAS_MPPM
|
||||
string CheckMPPM()
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}MPPM Found");
|
||||
string mppmString = "";
|
||||
if(CurrentPlayer.ReadOnlyTags().Length > 0)
|
||||
{
|
||||
mppmString += CurrentPlayer.ReadOnlyTags()[0];
|
||||
|
||||
// Force input module to disable mouse and touch input to suppress MPPM startup errors.
|
||||
var inputModule = FindFirstObjectByType<XRUIInputModule>();
|
||||
inputModule.enableMouseInput = false;
|
||||
inputModule.enableTouchInput = false;
|
||||
}
|
||||
|
||||
return mppmString;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAS_PARRELSYNC
|
||||
string CheckParrelSync()
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}ParrelSync Found");
|
||||
string pSyncString = "";
|
||||
if (ClonesManager.IsClone()) pSyncString += ClonesManager.GetArgument();
|
||||
return pSyncString;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a23511cff0c072e4fb042675bd9c0fb4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,497 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4fc9ddf205bd8784da3ae5681e15741d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,79 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the network functionality for VR multiplayer.
|
||||
/// </summary>
|
||||
public class NetworkManagerVRMultiplayer : NetworkManager
|
||||
{
|
||||
[SerializeField, Tooltip("Set this to control how much logging is generated")]
|
||||
LogLevel m_LogLevel;
|
||||
|
||||
[SerializeField, Tooltip("This should almost always be set to true")]
|
||||
bool m_RunInBackground = true;
|
||||
|
||||
[SerializeField]
|
||||
NetworkConfig m_NetworkConfig;
|
||||
|
||||
///<inheritdoc/>
|
||||
void Awake()
|
||||
{
|
||||
LogLevel = m_LogLevel;
|
||||
RunInBackground = m_RunInBackground;
|
||||
NetworkConfig = m_NetworkConfig;
|
||||
Utils.s_LogLevel = LogLevel;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[CustomEditor(typeof(NetworkManagerVRMultiplayer))]
|
||||
class VRMutliplayerTemplateNetworkManagerEditor : Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// This function is called when the inspector is drawn.
|
||||
/// </summary>
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
base.OnInspectorGUI();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
switch (XRINetworkGameManager.CurrentConnectionState.Value)
|
||||
{
|
||||
case XRINetworkGameManager.ConnectionState.None:
|
||||
GUILayout.Box("Authenticating");
|
||||
break;
|
||||
case XRINetworkGameManager.ConnectionState.Authenticating:
|
||||
GUILayout.Box("Authenticating");
|
||||
break;
|
||||
case XRINetworkGameManager.ConnectionState.Authenticated:
|
||||
if (GUILayout.Button("Connect"))
|
||||
{
|
||||
XRINetworkGameManager.Instance.QuickJoinLobby();
|
||||
}
|
||||
break;
|
||||
case XRINetworkGameManager.ConnectionState.Connecting:
|
||||
GUILayout.Box("Connecting");
|
||||
break;
|
||||
case XRINetworkGameManager.ConnectionState.Connected:
|
||||
if (GUILayout.Button("Disconnect"))
|
||||
{
|
||||
XRINetworkGameManager.Instance.Disconnect();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Box("Game not running.");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ccc99996a191dc34aab62c66e4aa42b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,534 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Unity.Netcode;
|
||||
using Unity.Services.Vivox;
|
||||
using Unity.XR.CoreUtils.Bindings.Variables;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Android;
|
||||
using System.Collections;
|
||||
|
||||
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the Vivox Voice Chat functionality in the VR Multiplayer template.
|
||||
/// </summary>
|
||||
public class VoiceChatManager : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// String used to notify the player that they need to enable microphone permissions.
|
||||
/// </summary>
|
||||
const string k_MicrophonePersmissionDialogue = "Microphone Permissions Required.";
|
||||
|
||||
/// <summary>
|
||||
public static BindableVariable<bool> s_HasMicrophonePermission = new(false);
|
||||
/// <summary>
|
||||
/// Dictionary of all the <see cref="XRINetworkPlayer"/>'s in the voice chat.
|
||||
/// </summary>
|
||||
public static Dictionary<string, XRINetworkPlayer> m_PlayersDictionary = new();
|
||||
|
||||
/// <summary>
|
||||
/// This is the bindable variable for subscribing to the local player muting themselves.
|
||||
/// </summary>
|
||||
public IReadOnlyBindableVariable<bool> selfMuted
|
||||
{
|
||||
get => m_SelfMuted;
|
||||
}
|
||||
readonly BindableVariable<bool> m_SelfMuted = new(false);
|
||||
|
||||
/// <summary>
|
||||
/// This is the bindable variable for subscribing to the connection status of the voice chat service.
|
||||
/// </summary>
|
||||
public IReadOnlyBindableVariable<string> connectionStatus
|
||||
{
|
||||
get => m_ConnectionStatus;
|
||||
}
|
||||
readonly BindableVariable<string> m_ConnectionStatus = new();
|
||||
|
||||
/// <summary>
|
||||
/// The chat capability of the channel, by default it should Audio Only.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("The chat capability of the channel, by default it should Audio Only")] ChatCapability m_ChatCapability = ChatCapability.AudioOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Update frequency for audio callbacks.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("Update frequency for audio callbacks")] ParticipantPropertyUpdateFrequency m_UpdateFrequency = ParticipantPropertyUpdateFrequency.TenPerSecond;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum distance from the listener that a speaker can be heard.
|
||||
/// </summary>
|
||||
public int AudibleDistance
|
||||
{
|
||||
get => m_AudibleDistance;
|
||||
set => m_AudibleDistance = value;
|
||||
}
|
||||
[Header("Voice Chat Properties")]
|
||||
[SerializeField, Tooltip("The maximum distance from the listener that a speaker can be heard.")]
|
||||
int m_AudibleDistance = 32;
|
||||
|
||||
/// <summary>
|
||||
/// The distance from the listener within which a speaker’s voice is heard at its original volume, and beyond which the speaker's voice begins to fade.
|
||||
/// </summary>
|
||||
public int ConversationalDistance
|
||||
{
|
||||
get => m_ConversationalDistance;
|
||||
set => m_ConversationalDistance = value;
|
||||
}
|
||||
[SerializeField, Tooltip("The distance from the listener within which a speaker’s voice is heard at its original volume, and beyond which the speaker's voice begins to fade.")]
|
||||
int m_ConversationalDistance = 7;
|
||||
|
||||
/// <summary>
|
||||
/// The strength of the audio fade effect as the speaker moves away from the listener past the conversational distance.
|
||||
/// </summary>
|
||||
public float AudioFadeIntensity
|
||||
{
|
||||
get => m_AudioFadeIntensity;
|
||||
set => m_AudioFadeIntensity = value;
|
||||
}
|
||||
[SerializeField, Tooltip("The strength of the audio fade effect as the speaker moves away from the listener past the conversational distance.")]
|
||||
float m_AudioFadeIntensity = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The model that determines the distance falloff of the voice chat.
|
||||
/// </summary>
|
||||
/// The strength of the audio fade effect as the speaker moves away from the listener past the conversational distance.
|
||||
/// </summary>
|
||||
public AudioFadeModel AudioFadeModel
|
||||
{
|
||||
get => m_AudioFadeModel;
|
||||
set => m_AudioFadeModel = value;
|
||||
}
|
||||
[SerializeField, Tooltip("The model that determines the distance falloff of the voice chat.")]
|
||||
AudioFadeModel m_AudioFadeModel = AudioFadeModel.LinearByDistance;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum and maximum volume for the voice output.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("The minimum and maximum volume for the voice output.")] Vector2 m_MinMaxVoiceOutputVolume = new Vector2(-10.0f, 10.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The minimum and maximum volume for the voice input.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("The minimum and maximum volume for the voice input.")] Vector2 m_MinMaxVoiceInputVolume = new Vector2(-10.0f, 10.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The local participant in the voice chat.
|
||||
/// </summary>
|
||||
VivoxParticipant m_LocalParticpant;
|
||||
|
||||
/// <summary>
|
||||
/// The current lobby id the player is connected to.
|
||||
/// </summary>
|
||||
string m_CurrentLobbyId;
|
||||
|
||||
/// <summary>
|
||||
/// If the player is connected to a room.
|
||||
/// </summary>
|
||||
bool m_ConnectedToRoom;
|
||||
|
||||
/// <summary>
|
||||
/// If the voice chat service is initialized.
|
||||
/// </summary>
|
||||
bool m_IsInitialized;
|
||||
const string k_DebugPrepend = "<color=#0CFAFA>[Voice Chat Manager]</color> ";
|
||||
///<inheritdoc/>
|
||||
private void Awake()
|
||||
{
|
||||
m_ConnectedToRoom = false;
|
||||
|
||||
XRINetworkGameManager.CurrentConnectionState.Subscribe(ConnectionStateUpdated);
|
||||
XRINetworkGameManager.Connected.Subscribe(ConnectedToGame);
|
||||
}
|
||||
|
||||
///<inheritdoc/>
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (VivoxService.Instance != null)
|
||||
{
|
||||
VivoxService.Instance.LoggedIn -= LocalUserLoggedIn;
|
||||
UnbindParticipantEvents();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for when the local player connection state is updated.
|
||||
/// </summary>
|
||||
/// <param name="connected">Wether or not a player is connected.</param>
|
||||
void ConnectedToGame(bool connected)
|
||||
{
|
||||
if (!m_IsInitialized) return;
|
||||
|
||||
if (connected)
|
||||
{
|
||||
Login(XRINetworkGameManager.AuthenicationId, XRINetworkGameManager.Instance.lobbyManager.connectedLobby.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogOut();
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectionStateUpdated(XRINetworkGameManager.ConnectionState connectionState)
|
||||
{
|
||||
if (!m_IsInitialized && connectionState == XRINetworkGameManager.ConnectionState.Authenticated)
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Initializing Voice Chat");
|
||||
m_ConnectionStatus.Value = "Initializing Voice Service";
|
||||
m_IsInitialized = true;
|
||||
EnableVoiceChat();
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
|
||||
{
|
||||
StartCoroutine(ShowPermissionsAfterDelay());
|
||||
}
|
||||
else
|
||||
{
|
||||
MicrophonePermissionGranted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator ShowPermissionsAfterDelay(float delay = 1.0f)
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Requesting Microphone Permissions");
|
||||
PlayerHudNotification.Instance.ShowText("Requesting Microphone Permissions", 3.0f);
|
||||
yield return new WaitForSeconds(delay);
|
||||
PermissionCallbacks permissionCallbacks = new();
|
||||
permissionCallbacks.PermissionDenied += PermissionDeniedCallback;
|
||||
permissionCallbacks.PermissionGranted += PermissionGrantedCallback;
|
||||
Permission.RequestUserPermission(Permission.Microphone, permissionCallbacks);
|
||||
}
|
||||
|
||||
void PermissionGrantedCallback(string permissionName)
|
||||
{
|
||||
if (permissionName == Permission.Microphone)
|
||||
{
|
||||
MicrophonePermissionGranted();
|
||||
}
|
||||
}
|
||||
|
||||
void PermissionDeniedCallback(string permissionName)
|
||||
{
|
||||
if (permissionName == Permission.Microphone)
|
||||
{
|
||||
PlayerHudNotification.Instance.ShowText("Microphone Permissions Denied", 3.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void MicrophonePermissionGranted()
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Microphone Permissions Granted");
|
||||
s_HasMicrophonePermission.Value = true;
|
||||
PlayerHudNotification.Instance.ShowText("Microphone Permissions Granted", 3.0f);
|
||||
}
|
||||
|
||||
public async void EnableVoiceChat()
|
||||
{
|
||||
try
|
||||
{
|
||||
await VivoxService.Instance.InitializeAsync();
|
||||
m_ConnectionStatus.Value = "Voice Service Initialized";
|
||||
VivoxService.Instance.LoggedIn += LocalUserLoggedIn;
|
||||
BindToParticipantEvents();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EditorGUI.hyperLinkClicked += HyperlinkClicked;
|
||||
Utils.Log($"{k_DebugPrepend}Vivox Initialization Failed. Please check the Vivox Service Window <a data=\"OpenVivoxSettings\"><b>Project Settings > Services > Vivox</b></a>\n\n{e}", 2);
|
||||
#else
|
||||
Utils.Log($"{k_DebugPrepend}Vivox Initialization Failed.\n\n{e}", 2);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void HyperlinkClicked(EditorWindow window, HyperLinkClickedEventArgs args)
|
||||
{
|
||||
if(args.hyperLinkData.ContainsValue("OpenVivoxSettings"))
|
||||
{
|
||||
SettingsService.OpenProjectSettings("Project/Services/Vivox");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
public async void Login(string displayName, string roomCode)
|
||||
{
|
||||
m_CurrentLobbyId = roomCode;
|
||||
|
||||
LoginOptions loginOptions = new()
|
||||
{
|
||||
DisplayName = displayName,
|
||||
ParticipantUpdateFrequency = m_UpdateFrequency
|
||||
};
|
||||
|
||||
if (VivoxService.Instance.IsLoggedIn)
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Logging out of Voice Chat");
|
||||
m_ConnectionStatus.Value = "Logging out of Voice Chat";
|
||||
await VivoxService.Instance.LogoutAsync();
|
||||
}
|
||||
|
||||
if (!VivoxService.Instance.IsLoggedIn)
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Logging In to room {roomCode} as {displayName}");
|
||||
m_ConnectionStatus.Value = "Logging In To Voice Service";
|
||||
await VivoxService.Instance.LoginAsync(loginOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Attempting to login to voice chat while already logged in.", 1);
|
||||
}
|
||||
}
|
||||
|
||||
void LocalUserLoggedIn()
|
||||
{
|
||||
if (VivoxService.Instance.IsLoggedIn)
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Local User Logged In to Voice Chat.");
|
||||
m_ConnectionStatus.Value = "Joining Voice Channel";
|
||||
ConnectToVoiceChannel();
|
||||
}
|
||||
}
|
||||
|
||||
public async void ConnectToVoiceChannel()
|
||||
{
|
||||
if (NetworkManager.Singleton.IsConnectedClient & !m_ConnectedToRoom)
|
||||
{
|
||||
Channel3DProperties properties = new(AudibleDistance, ConversationalDistance, AudioFadeIntensity, AudioFadeModel);
|
||||
Utils.Log($"{k_DebugPrepend}Joining Voice Channel: {m_CurrentLobbyId}, properties: {properties}");
|
||||
await VivoxService.Instance.JoinPositionalChannelAsync(m_CurrentLobbyId, m_ChatCapability, properties);
|
||||
|
||||
// Once connecting, make sure we are still in the game session, if not, disconnect from the voice chat.
|
||||
if (!NetworkManager.Singleton.IsConnectedClient)
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Failed to join Voice Chat, Player is not connected to a game", 1);
|
||||
}
|
||||
}
|
||||
|
||||
void BindToParticipantEvents()
|
||||
{
|
||||
VivoxService.Instance.ParticipantAddedToChannel += OnParticipantAdded;
|
||||
VivoxService.Instance.ParticipantRemovedFromChannel += OnParticipantRemoved;
|
||||
}
|
||||
|
||||
void UnbindParticipantEvents()
|
||||
{
|
||||
VivoxService.Instance.ParticipantAddedToChannel -= OnParticipantAdded;
|
||||
VivoxService.Instance.ParticipantRemovedFromChannel -= OnParticipantRemoved;
|
||||
}
|
||||
|
||||
async void DisconnectAsync()
|
||||
{
|
||||
m_ConnectionStatus.Value = "Leaving current channel";
|
||||
await VivoxService.Instance.LeaveAllChannelsAsync();
|
||||
}
|
||||
|
||||
[ContextMenu("Reconnect")]
|
||||
public void Reconnect()
|
||||
{
|
||||
ReconnectAsync();
|
||||
}
|
||||
|
||||
async void ReconnectAsync()
|
||||
{
|
||||
m_ConnectionStatus.Value = "Leaving current channel";
|
||||
await VivoxService.Instance.LeaveAllChannelsAsync();
|
||||
|
||||
if (VivoxService.Instance.IsLoggedIn)
|
||||
{
|
||||
ConnectToVoiceChannel();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ConnectionStatus.Value = "Reconnecting to Voice Chat";
|
||||
Login(XRINetworkGameManager.AuthenicationId, XRINetworkGameManager.Instance.lobbyManager.connectedLobby.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogOut()
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Logging out of Voice Chat.");
|
||||
if (VivoxService.Instance.IsLoggedIn && m_ConnectedToRoom)
|
||||
{
|
||||
m_ConnectedToRoom = false;
|
||||
VivoxService.Instance.LeaveAllChannelsAsync();
|
||||
VivoxService.Instance.LogoutAsync();
|
||||
}
|
||||
|
||||
m_PlayersDictionary.Clear();
|
||||
}
|
||||
|
||||
public void Set3DAudio(Transform localPlayerHeadTransform)
|
||||
{
|
||||
if (VivoxService.Instance.IsLoggedIn && VivoxService.Instance.ActiveChannels.Count > 0 && VivoxService.Instance.TransmittingChannels[0] == m_CurrentLobbyId)
|
||||
{
|
||||
VivoxService.Instance.Set3DPosition(localPlayerHeadTransform.position,
|
||||
localPlayerHeadTransform.position,
|
||||
localPlayerHeadTransform.forward,
|
||||
localPlayerHeadTransform.up,
|
||||
m_CurrentLobbyId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void ToggleSelfMute(bool setManual = false, bool mutedOverrideValue = false)
|
||||
{
|
||||
if (Permission.HasUserAuthorizedPermission(Permission.Microphone))
|
||||
{
|
||||
m_SelfMuted.Value = setManual ? mutedOverrideValue : !m_SelfMuted.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_SelfMuted.Value = false;
|
||||
}
|
||||
|
||||
if (VivoxService.Instance.IsLoggedIn)
|
||||
{
|
||||
if (m_SelfMuted.Value)
|
||||
{
|
||||
VivoxService.Instance.MuteInputDevice();
|
||||
}
|
||||
else
|
||||
{
|
||||
VivoxService.Instance.UnmuteInputDevice();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OfflinePlayerAvatar.muted = m_SelfMuted.Value;
|
||||
}
|
||||
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
|
||||
{
|
||||
PlayerHudNotification.Instance.ShowText(k_MicrophonePersmissionDialogue, 3.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetInputVolume(float volume)
|
||||
{
|
||||
volume = Mathf.Clamp(volume, m_MinMaxVoiceInputVolume.x, m_MinMaxVoiceInputVolume.y);
|
||||
|
||||
VivoxService.Instance.SetInputDeviceVolume((int)volume);
|
||||
|
||||
// Since the slider goes to .001 percent, add a buffer to mute the mic
|
||||
if (volume <= (m_MinMaxVoiceInputVolume.x + .05f))
|
||||
{
|
||||
ToggleSelfMute(true, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToggleSelfMute(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOutputVolume(float volume)
|
||||
{
|
||||
volume = Mathf.Clamp(volume, m_MinMaxVoiceOutputVolume.x, m_MinMaxVoiceOutputVolume.y);
|
||||
VivoxService.Instance.SetOutputDeviceVolume((int)volume);
|
||||
}
|
||||
|
||||
void OnParticipantAdded(VivoxParticipant participant)
|
||||
{
|
||||
if (participant.IsSelf)
|
||||
{
|
||||
m_ConnectedToRoom = true;
|
||||
m_LocalParticpant = participant;
|
||||
m_SelfMuted.Value = false;
|
||||
XRINetworkPlayer.LocalPlayer.SetVoiceId(m_LocalParticpant.PlayerId);
|
||||
Utils.Log($"{k_DebugPrepend}Joined Voice Channel: {m_CurrentLobbyId}");
|
||||
m_ConnectionStatus.Value = "Joined Voice Channel";
|
||||
PlayerHudNotification.Instance.ShowText("Joined Voice Chat", 3.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Non-Local Player Joined Voice Channel: {participant.PlayerId}");
|
||||
foreach (XRINetworkPlayer player in FindObjectsByType<XRINetworkPlayer>(FindObjectsSortMode.None))
|
||||
{
|
||||
if (player.playerVoiceId == participant.PlayerId)
|
||||
{
|
||||
player.SetupVoicePlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnParticipantRemoved(VivoxParticipant participant)
|
||||
{
|
||||
RemoveVivoxPlayer(participant.PlayerId);
|
||||
if (participant.IsSelf)
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Left Voice Channel: {m_CurrentLobbyId}");
|
||||
m_ConnectionStatus.Value = "Left Voice Channel";
|
||||
m_ConnectedToRoom = false;
|
||||
m_LocalParticpant = null;
|
||||
PlayerHudNotification.Instance.ShowText("Voice Chat Disconnected", 3.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public VivoxParticipant GetVivoxParticipantById(string participantPlayerId)
|
||||
{
|
||||
foreach (var participant in VivoxService.Instance.ActiveChannels[m_CurrentLobbyId])
|
||||
{
|
||||
if (participantPlayerId == participant.PlayerId)
|
||||
return participant;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Gets called as soon as participant ID is synced
|
||||
public static void AddNewVivoxPlayer(string participantID, XRINetworkPlayer networkPlayer)
|
||||
{
|
||||
if (!m_PlayersDictionary.ContainsKey(participantID))
|
||||
{
|
||||
m_PlayersDictionary.Add(participantID, networkPlayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Attempting to load multiple players with same id {participantID}", 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveVivoxPlayer(string participantID)
|
||||
{
|
||||
if (participantID == XRINetworkPlayer.LocalPlayer.playerVoiceId)
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Local Player Left Voice Chat.");
|
||||
return;
|
||||
}
|
||||
if (m_PlayersDictionary.ContainsKey(participantID))
|
||||
{
|
||||
m_PlayersDictionary.Remove(participantID);
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Disconnect")]
|
||||
public void Disconnect()
|
||||
{
|
||||
DisconnectAsync();
|
||||
}
|
||||
|
||||
[ContextMenu("Debug Particpants")]
|
||||
void DebugParticipants()
|
||||
{
|
||||
StringBuilder output = new StringBuilder();
|
||||
output.Append($"[Room Type: Positional\n[Room Code: {m_CurrentLobbyId}]");
|
||||
foreach (var participant in VivoxService.Instance.ActiveChannels[m_CurrentLobbyId])
|
||||
{
|
||||
output.Append($"\n[ParticipantID: {participant.PlayerId}]\n[AudioEnergy: {participant.AudioEnergy}]");
|
||||
}
|
||||
Utils.Log($"{k_DebugPrepend}{output}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef0c7cbc48b68dd40923508f2547cc98
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,667 @@
|
||||
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
|
||||
/// <summary>
|
||||
/// Workaround for a bug introduced in NGO 1.9.1.
|
||||
/// </summary>
|
||||
/// <remarks> Delete this class once the bug is fixed in NGO.</remarks>
|
||||
class ForceByteSerialization : NetworkBehaviour
|
||||
{
|
||||
NetworkVariable<byte> m_ForceByteSerialization;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Manages the high level connection for a networked game session.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(LobbyManager)), RequireComponent(typeof(AuthenticationManager))]
|
||||
public class XRINetworkGameManager : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines the current state of the networked game connection.
|
||||
/// </summary>
|
||||
///<remarks>
|
||||
/// None: No connection state.
|
||||
/// Authenticating: Currently authenticating.
|
||||
/// Authenticated: Authenticated.
|
||||
/// Connecting: Currently connecting to a lobby.
|
||||
/// Connected: Connected to a lobby.
|
||||
/// </remarks>
|
||||
public enum ConnectionState
|
||||
{
|
||||
None,
|
||||
Authenticating,
|
||||
Authenticated,
|
||||
Connecting,
|
||||
Connected
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Max amount of players allowed when creating a new room.
|
||||
/// </summary>
|
||||
public const int maxPlayers = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton Reference for access to this manager.
|
||||
/// </summary>
|
||||
public static XRINetworkGameManager Instance => s_Instance;
|
||||
static XRINetworkGameManager s_Instance;
|
||||
|
||||
/// <summary>
|
||||
/// OwnerClientId that gets set for the local player when connecting to a game.
|
||||
/// </summary>
|
||||
public static ulong LocalId;
|
||||
|
||||
/// <summary>
|
||||
/// Authentication Id that gets passed once Authenticated.
|
||||
/// </summary>
|
||||
public static string AuthenicationId;
|
||||
|
||||
/// <summary>
|
||||
/// Internal Room Code set by Lobby.
|
||||
/// </summary>
|
||||
public static string ConnectedRoomCode;
|
||||
|
||||
/// <summary>
|
||||
/// Current connected region set by Lobby and Relay.
|
||||
/// </summary>
|
||||
public static string ConnectedRoomRegion;
|
||||
|
||||
/// <summary>
|
||||
/// Bindable Variable that gets updated when changing the the currently connected room.
|
||||
/// </summary>
|
||||
public static BindableVariable<string> ConnectedRoomName = new("");
|
||||
|
||||
/// <summary>
|
||||
/// Bindable Variable that gets updated when the local player changes name.
|
||||
/// </summary>
|
||||
public static BindableVariable<string> LocalPlayerName = new("Player");
|
||||
|
||||
/// <summary>
|
||||
/// Bindable Variable that gets updated when the local player changes color.
|
||||
/// </summary>
|
||||
public static BindableVariable<Color> LocalPlayerColor = new(Color.white);
|
||||
|
||||
/// <summary>
|
||||
/// Bindable Variable that gets updated when a player connects or disconnects from a networked game.
|
||||
/// </summary>
|
||||
public static IReadOnlyBindableVariable<bool> Connected
|
||||
{
|
||||
get => m_Connected;
|
||||
}
|
||||
static BindableVariable<bool> m_Connected = new BindableVariable<bool>(false);
|
||||
|
||||
/// <summary>
|
||||
/// Bindable Variable that gets updated throughout the authentication and connection process.
|
||||
/// See <see cref="ConnectionState"/>
|
||||
/// </summary>
|
||||
public static IReadOnlyBindableVariable<ConnectionState> CurrentConnectionState
|
||||
{
|
||||
get => m_ConnectionState;
|
||||
}
|
||||
static BindableEnum<ConnectionState> m_ConnectionState = new BindableEnum<ConnectionState>(ConnectionState.None);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool autoConnectOnLobbyJoin { get => m_AutoConnectOnLobbyJoin; }
|
||||
[SerializeField] bool m_AutoConnectOnLobbyJoin = true;
|
||||
|
||||
/// <summary>
|
||||
/// Flag for updating positional voice chat.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will be removed in the future with the Vivox v16 update.
|
||||
/// </remarks>
|
||||
public bool positionalVoiceChat = false;
|
||||
|
||||
/// <summary>
|
||||
/// Action for when a player connects or disconnects.
|
||||
/// </summary>
|
||||
public Action<ulong, bool> playerStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Action for when connection status is updated.
|
||||
/// </summary>
|
||||
public Action<string> connectionUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Action for when connection fails.
|
||||
/// </summary>
|
||||
public Action<string> connectionFailedAction;
|
||||
|
||||
/// <summary>
|
||||
/// Lobby Manager handles the Lobby and Relay work between players.
|
||||
/// </summary>
|
||||
public LobbyManager lobbyManager => m_LobbyManager;
|
||||
LobbyManager m_LobbyManager;
|
||||
|
||||
/// <summary>
|
||||
/// Lobby Manager handles the Lobby and Relay work between players.
|
||||
/// </summary>
|
||||
public AuthenticationManager authenticationManager => m_AuthenticationManager;
|
||||
AuthenticationManager m_AuthenticationManager;
|
||||
|
||||
/// <summary>
|
||||
/// List that handles all current players by ID.
|
||||
/// Useful for getting specific players.
|
||||
/// See <see cref="GetPlayerByID"/>
|
||||
/// </summary>
|
||||
readonly List<ulong> m_CurrentPlayerIDs = new();
|
||||
|
||||
/// <summary>
|
||||
/// Flagged whenever the application is in the process of shutting down.
|
||||
/// </summary>
|
||||
bool m_IsShuttingDown = false;
|
||||
|
||||
const string k_DebugPrepend = "<color=#FAC00C>[Network Game Manager]</color> ";
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
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 <b>Settings -> Project Settings -> Services</b> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected virtual void Start()
|
||||
{
|
||||
NetworkManager.Singleton.OnClientStopped += OnLocalClientStopped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
public override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
|
||||
ShutDown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
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<bool> Authenticate()
|
||||
{
|
||||
return await m_AuthenticationManager.Authenticate();
|
||||
}
|
||||
public bool IsAuthenticated()
|
||||
{
|
||||
return AuthenticationManager.IsAuthenticated();
|
||||
}
|
||||
/// <summary>
|
||||
/// Called from XRINetworkPlayer once they have spawned.
|
||||
/// </summary>
|
||||
/// <param name="localPlayerId">Sets based on <see cref="NetworkObject.OwnerClientId"/> from the local player</param>
|
||||
public virtual void LocalPlayerConnected(ulong localPlayerId)
|
||||
{
|
||||
m_Connected.Value = true;
|
||||
LocalId = localPlayerId;
|
||||
PlayerHudNotification.Instance.ShowText($"<b>Status:</b> Connected");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when disconnected from any networked game.
|
||||
/// </summary>
|
||||
/// <param name="id">
|
||||
/// Local player id.
|
||||
/// </param>
|
||||
protected virtual void OnLocalClientStopped(bool id)
|
||||
{
|
||||
m_Connected.Value = false;
|
||||
m_CurrentPlayerIDs.Clear();
|
||||
PlayerHudNotification.Instance.ShowText($"<b>Status:</b> Disconnected");
|
||||
// Check if authenticated on disconnect.
|
||||
if (IsAuthenticated())
|
||||
{
|
||||
m_ConnectionState.Value = ConnectionState.Authenticated;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ConnectionState.Value = ConnectionState.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all <see cref="XRINetworkPlayer"/>'s existing in the scene and gets the <see cref="XRINetworkPlayer"/>
|
||||
/// based on <see cref="NetworkObject.OwnerClientId"/> for that player.
|
||||
/// </summary>
|
||||
/// <param name="id">
|
||||
/// <see cref="NetworkObject.OwnerClientId"/> of the player.
|
||||
/// </param>
|
||||
/// <param name="player">
|
||||
/// Out <see cref="XRINetworkPlayer"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Returns true based on whether or not a player with that Id exists.
|
||||
/// </returns>
|
||||
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<XRINetworkPlayer>(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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function will set the player ID in the list <see cref="m_CurrentPlayerIDs"/> and
|
||||
/// invokes the callback <see cref="playerStateChanged"/>.
|
||||
/// </summary>
|
||||
/// <param name="playerID"><see cref="NetworkObject.OwnerClientId"/> of the joined player.</param>
|
||||
/// <remarks>Called from <see cref="XRINetworkPlayer.CompleteSetup"/>.</remarks>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from <see cref="XRINetworkPlayer.OnDestroy"/>.
|
||||
/// </summary>
|
||||
/// <param name="playerID"><see cref="NetworkObject.OwnerClientId"/> of the player who left.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever there is a problem with connecting to game or lobby.
|
||||
/// </summary>
|
||||
/// <param name="reason">Failure message.</param>
|
||||
public virtual void ConnectionFailed(string reason)
|
||||
{
|
||||
connectionFailedAction?.Invoke(reason);
|
||||
m_ConnectionState.Value = AuthenticationManager.IsAuthenticated() ? ConnectionState.Authenticated : ConnectionState.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever there is an update to connection status.
|
||||
/// </summary>
|
||||
/// <param name="update">Status update message.</param>
|
||||
public virtual void ConnectionUpdated(string update)
|
||||
{
|
||||
connectionUpdated?.Invoke(update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Joins a random lobby. If no lobbies exist, it will create a new one.
|
||||
/// </summary>
|
||||
public virtual async void QuickJoinLobby()
|
||||
{
|
||||
Utils.Log($"{k_DebugPrepend}Joining Lobby by Quick Join.");
|
||||
if (await AbleToConnect())
|
||||
{
|
||||
ConnectToLobby(await m_LobbyManager.QuickJoinLobby());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when trying to join a Lobby by Room Code.
|
||||
/// </summary>
|
||||
/// <param name="lobby">Lobby to join.</param>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when trying to join a specific Lobby.
|
||||
/// </summary>
|
||||
/// <param name="lobby">Lobby to join.</param>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Lobby.
|
||||
/// </summary>
|
||||
/// <param name="roomName">Name of the lobby.</param>
|
||||
/// <param name="isPrivate">Whether or not the lobby is private.</param>
|
||||
/// <param name="playerCount">Maximum allowed players.</param>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>Whether or not we are able to connect.</returns>
|
||||
protected virtual async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to a lobby.
|
||||
/// </summary>
|
||||
/// <param name="lobby">Lobby to connect to.</param>
|
||||
protected virtual void ConnectToLobby(Lobby lobby)
|
||||
{
|
||||
// Send failure message if we can't connect.
|
||||
if (lobby == null || !ConnectedToLobby())
|
||||
{
|
||||
FailedToConnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if we successfully connected to a Lobby.
|
||||
/// If <see cref="autoConnectOnLobbyJoin"/> is enabled, join networked game here.
|
||||
/// </summary>
|
||||
/// <returns>Whether or not we connected to a lobby and / or networked game.</returns>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to lobby update events. This needed to be informed of Lobby changes (name, privacy, etc...).
|
||||
/// </summary>
|
||||
/// <remarks>See <see cref="OnLobbyChanged(ILobbyChanges)"/>.</remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callabacks for anytime the lobby event connection state has changed.
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for anytime a lobby is updated via <see cref="LobbyService.Instance.SubscribeToLobbyEventsAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="changes"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generic failure message.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel current matchmaking.
|
||||
/// Called from the Lobby UI.
|
||||
/// </summary>
|
||||
public virtual async void CancelMatchmaking()
|
||||
{
|
||||
if (IsAuthenticated())
|
||||
{
|
||||
m_ConnectionState.Value = ConnectionState.Authenticated;
|
||||
}
|
||||
await m_LobbyManager.RemovePlayerFromLobby(AuthenicationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// High Level Disconnect call.
|
||||
/// </summary>
|
||||
public virtual async void Disconnect()
|
||||
{
|
||||
await DisconnectAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaitable Disconnect call, used for Hot Joining.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad97d272b98331644876b2289d56dd4a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user