浏览代码

Game Manager as a Singleton

/main/staging/2021_Upgrade/Async_Refactor
当前提交
96727d5b
共有 15 个文件被更改,包括 809 次插入917 次删除
  1. 2
      Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs.meta
  2. 315
      Assets/Scripts/GameLobby/Game/GameManager.cs
  3. 118
      Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs
  4. 372
      Assets/Scripts/GameLobby/Lobby/LobbyManager.cs
  5. 150
      Assets/Scripts/GameLobby/Lobby/LobbyUpdater.cs
  6. 11
      Assets/Scripts/GameLobby/Auth/SubIdentity_Authentication.cs.meta
  7. 87
      Assets/Scripts/GameLobby/Auth/Identity.cs
  8. 66
      Assets/Scripts/GameLobby/Auth/SubIdentity_Authentication.cs
  9. 147
      Assets/Scripts/GameLobby/Lobby/LobbyContentUpdater.cs
  10. 420
      Assets/Scripts/GameLobby/Lobby/LobbyAsyncRequests.cs
  11. 11
      Assets/Scripts/GameLobby/Tests/Editor/AuthTests.cs.meta
  12. 27
      Assets/Scripts/GameLobby/Tests/Editor/AuthTests.cs
  13. 0
      /Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs.meta
  14. 0
      /Assets/Scripts/GameLobby/Lobby/LobbyManager.cs.meta
  15. 0
      /Assets/Scripts/GameLobby/Lobby/LobbyUpdater.cs.meta

2
Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs.meta


fileFormatVersion: 2
guid: b98aae4acd443d24faedf0aa20e946f3
guid: 2d4f181652f814a93be872600da7fa0b
MonoImporter:
externalObjects: {}
serializedVersion: 2

315
Assets/Scripts/GameLobby/Game/GameManager.cs


using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using ParrelSync;
#endif

/// The list is serialized, so you can navigate to the Observers via the Inspector to see who's watching.
/// </summary>
[SerializeField]
private List<LocalMenuStateObserver> m_LocalMenuStateObservers = new List<LocalMenuStateObserver>();
List<LocalMenuStateObserver> m_LocalMenuStateObservers = new List<LocalMenuStateObserver>();
private List<LocalLobbyObserver> m_LocalLobbyObservers = new List<LocalLobbyObserver>();
List<LocalLobbyObserver> m_LocalLobbyObservers = new List<LocalLobbyObserver>();
private List<LobbyUserObserver> m_LocalUserObservers = new List<LobbyUserObserver>();
List<LobbyUserObserver> m_LocalUserObservers = new List<LobbyUserObserver>();
private List<LobbyServiceDataObserver> m_LobbyServiceObservers = new List<LobbyServiceDataObserver>();
List<LobbyServiceDataObserver> m_LobbyServiceObservers = new List<LobbyServiceDataObserver>();
private LocalMenuState m_LocalMenuState = new LocalMenuState();
private LobbyUser m_localUser;
private LocalLobby m_localLobby;
private LobbyServiceData m_lobbyServiceData = new LobbyServiceData();
private LobbyContentUpdater m_LobbyContentUpdater = new LobbyContentUpdater();
public LobbyManager LobbyManager { get; private set; }
LocalMenuState m_LocalMenuState = new LocalMenuState();
LobbyUser m_LocalUser;
LocalLobby m_LocalLobby;
LobbyServiceData m_LobbyServiceData = new LobbyServiceData();
LobbyUpdater m_LobbyUpdater;
private RelayUtpSetup m_relaySetup;
private RelayUtpClient m_relayClient;
RelayUtpSetup m_RelaySetup;
RelayUtpClient m_RelayClient;
private vivox.VivoxSetup m_vivoxSetup = new vivox.VivoxSetup();
vivox.VivoxSetup m_VivoxSetup = new vivox.VivoxSetup();
private List<vivox.VivoxUserHandler> m_vivoxUserHandlers;
List<vivox.VivoxUserHandler> m_vivoxUserHandlers;
/// <summary>Rather than a setter, this is usable in-editor. It won't accept an enum, however.</summary>
public void SetLobbyColorFilter(int color)
{
m_lobbyColorFilter = (LobbyColor)color;
}
LobbyColor m_lobbyColorFilter;
private LobbyColor m_lobbyColorFilter;
#region Setup
static GameManager m_GameManagerInstance;
private void Awake()
public static GameManager Instance
// Do some arbitrary operations to instantiate singletons.
#pragma warning disable IDE0059 // Unnecessary assignment of a value
var unused = Locator.Get;
#pragma warning restore IDE0059
string serviceProfileName = "player";
#if UNITY_EDITOR
serviceProfileName = $"{serviceProfileName}_{ClonesManager.GetCurrentProject().name}";
#endif
Locator.Get.Provide(new Auth.Identity(serviceProfileName,OnAuthSignIn));
Application.wantsToQuit += OnWantToQuit;
get
{
if (m_GameManagerInstance != null)
return m_GameManagerInstance;
m_GameManagerInstance = FindObjectOfType<GameManager>();
return m_GameManagerInstance;
}
private void Start()
{
m_localLobby = new LocalLobby { State = LobbyState.Lobby };
m_localUser = new LobbyUser();
m_localUser.DisplayName = "New Player";
Locator.Get.Messenger.Subscribe(this);
BeginObservers();
}
private void OnAuthSignIn()
{
Debug.Log("Signed in.");
m_localUser.ID = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
m_localUser.DisplayName = NameGenerator.GetName(m_localUser.ID);
m_localLobby.AddPlayer(m_localUser); // The local LobbyUser object will be hooked into UI before the LocalLobby is populated during lobby join, so the LocalLobby must know about it already when that happens.
StartVivoxLogin();
}
/// <summary>
/// TODO Wire is a good update to remove the monolithic observers and move to observed values instead, on a Singleton gameManager
/// </summary>
private void BeginObservers()
/// <summary>Rather than a setter, this is usable in-editor. It won't accept an enum, however.</summary>
public void SetLobbyColorFilter(int color)
foreach (var gameStateObs in m_LocalMenuStateObservers)
gameStateObs.BeginObserving(m_LocalMenuState);
foreach (var serviceObs in m_LobbyServiceObservers)
serviceObs.BeginObserving(m_lobbyServiceData);
foreach (var lobbyObs in m_LocalLobbyObservers)
lobbyObs.BeginObserving(m_localLobby);
foreach (var userObs in m_LocalUserObservers)
userObs.BeginObserving(m_localUser);
m_lobbyColorFilter = (LobbyColor)color;
#endregion
/// <summary>
/// The Messaging System handles most of the core Lobby Service calls, and catches the callbacks from those calls.

if (type == MessageType.CreateLobbyRequest)
{
LocalLobby.LobbyData createLobbyData = (LocalLobby.LobbyData)msg;
var lobby = await LobbyAsyncRequests.Instance.CreateLobbyAsync(
var lobby = await LobbyManager.CreateLobbyAsync(
createLobbyData.Private, m_localUser);
createLobbyData.Private, m_LocalUser);
LobbyConverters.RemoteToLocal(lobby, m_localLobby);
LobbyConverters.RemoteToLocal(lobby, m_LocalLobby);
OnCreatedLobby();
}
else

else if (type == MessageType.JoinLobbyRequest)
{
LocalLobby.LobbyData lobbyInfo = (LocalLobby.LobbyData)msg;
var lobby = await LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode,
m_localUser);
var lobby = await LobbyManager.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode,
m_LocalUser);
LobbyConverters.RemoteToLocal(lobby, m_localLobby);
LobbyConverters.RemoteToLocal(lobby, m_LocalLobby);
OnJoinedLobby();
}
else

}
else if (type == MessageType.QueryLobbies)
{
m_lobbyServiceData.State = LobbyQueryState.Fetching;
var qr = await LobbyAsyncRequests.Instance.RetrieveLobbyListAsync(m_lobbyColorFilter);
m_LobbyServiceData.State = LobbyQueryState.Fetching;
var qr = await LobbyManager.RetrieveLobbyListAsync(m_lobbyColorFilter);
if (qr != null)
OnLobbiesQueried(LobbyConverters.QueryToLocalList(qr));

}
else if (type == MessageType.QuickJoin)
{
var lobby = await LobbyAsyncRequests.Instance.QuickJoinLobbyAsync(m_localUser, m_lobbyColorFilter);
var lobby = await LobbyManager.QuickJoinLobbyAsync(m_LocalUser, m_lobbyColorFilter);
LobbyConverters.RemoteToLocal(lobby, m_localLobby);
LobbyConverters.RemoteToLocal(lobby, m_LocalLobby);
OnJoinedLobby();
}
else

return;
}
m_localUser.DisplayName = (string)msg;
m_LocalUser.DisplayName = (string)msg;
}
else if (type == MessageType.ClientUserApproved)
{

{
EmoteType emote = (EmoteType)msg;
m_localUser.Emote = emote;
m_LocalUser.Emote = emote;
m_localUser.UserStatus = (UserStatus)msg;
m_LocalUser.UserStatus = (UserStatus)msg;
m_localLobby.State = LobbyState.CountDown;
m_LocalLobby.State = LobbyState.CountDown;
m_localLobby.State = LobbyState.Lobby;
m_LocalLobby.State = LobbyState.Lobby;
if (m_relayClient is RelayUtpHost)
(m_relayClient as RelayUtpHost).SendInGameState();
if (m_RelayClient is RelayUtpHost)
(m_RelayClient as RelayUtpHost).SendInGameState();
}
else if (type == MessageType.ChangeMenuState)
{

{
m_localUser.UserStatus = UserStatus.InGame;
m_localLobby.State = LobbyState.InGame;
m_LocalUser.UserStatus = UserStatus.InGame;
m_LocalLobby.State = LobbyState.InGame;
m_localLobby.State = LobbyState.Lobby;
m_LocalLobby.State = LobbyState.Lobby;
private void SetGameState(GameState state)
#region Setup
async void Awake()
{
// Do some arbitrary operations to instantiate singletons.
#pragma warning disable IDE0059 // Unnecessary assignment of a value
var unused = Locator.Get;
#pragma warning restore IDE0059
Application.wantsToQuit += OnWantToQuit;
await InitializeServices();
InitializeLobbies();
StartVivoxLogin();
Locator.Get.Messenger.Subscribe(this);
BeginObservers();
}
async Task InitializeServices()
{
string serviceProfileName = "player";
#if UNITY_EDITOR
serviceProfileName = $"{serviceProfileName}_{ClonesManager.GetCurrentProject().name}";
#endif
await Auth.Authenticate(serviceProfileName);
}
void InitializeLobbies()
{
m_LocalLobby = new LocalLobby {State = LobbyState.Lobby};
m_LocalUser = new LobbyUser();
m_LocalUser.ID = AuthenticationService.Instance.PlayerId;
m_LocalUser.DisplayName = NameGenerator.GetName(m_LocalUser.ID);
m_LocalLobby
.AddPlayer(m_LocalUser); // The local LobbyUser object will be hooked into UI before the LocalLobby is populated during lobby join, so the LocalLobby must know about it already when that happens.
LobbyManager = new LobbyManager();
m_LobbyUpdater = new LobbyUpdater(LobbyManager);
}
/// <summary>
/// TODO Wire is a good update to remove the monolithic observers and move to observed values instead, on a Singleton gameManager
/// </summary>
void BeginObservers()
{
foreach (var gameStateObs in m_LocalMenuStateObservers)
gameStateObs.BeginObserving(m_LocalMenuState);
foreach (var serviceObs in m_LobbyServiceObservers)
serviceObs.BeginObserving(m_LobbyServiceData);
foreach (var lobbyObs in m_LocalLobbyObservers)
lobbyObs.BeginObserving(m_LocalLobby);
foreach (var userObs in m_LocalUserObservers)
userObs.BeginObserving(m_LocalUser);
}
#endregion
void SetGameState(GameState state)
{
bool isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) && m_LocalMenuState.State == GameState.Lobby;
m_LocalMenuState.State = state;

private void OnLobbiesQueried(IEnumerable<LocalLobby> lobbies)
void OnLobbiesQueried(IEnumerable<LocalLobby> lobbies)
m_lobbyServiceData.State = LobbyQueryState.Fetched;
m_lobbyServiceData.CurrentLobbies = newLobbyDict;
m_LobbyServiceData.State = LobbyQueryState.Fetched;
m_LobbyServiceData.CurrentLobbies = newLobbyDict;
private void OnLobbyQueryFailed()
void OnLobbyQueryFailed()
m_lobbyServiceData.State = LobbyQueryState.Error;
m_LobbyServiceData.State = LobbyQueryState.Error;
private void OnCreatedLobby()
void OnCreatedLobby()
m_localUser.IsHost = true;
m_LocalUser.IsHost = true;
private void OnJoinedLobby()
void OnJoinedLobby()
m_LobbyContentUpdater.BeginTracking(m_localLobby, m_localUser);
m_LobbyUpdater.BeginTracking(m_LocalLobby, m_LocalUser);
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Connecting);
if (m_localUser.IsHost)
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
if (m_LocalUser.IsHost)
StartRelayConnection();
// StartRelayConnection();
StartRelayConnection();
// StartRelayConnection();
private void OnLeftLobby()
void OnLeftLobby()
m_localUser.ResetState();
m_LocalUser.ResetState();
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby.LobbyID);
LobbyManager.LeaveLobbyAsync(m_LocalLobby.LobbyID);
m_LobbyContentUpdater.EndTracking();
m_vivoxSetup.LeaveLobbyChannel();
m_LobbyUpdater.EndTracking();
m_VivoxSetup.LeaveLobbyChannel();
if (m_relaySetup != null)
if (m_RelaySetup != null)
Component.Destroy(m_relaySetup);
m_relaySetup = null;
Component.Destroy(m_RelaySetup);
m_RelaySetup = null;
if (m_relayClient != null)
if (m_RelayClient != null)
m_relayClient.Dispose();
m_RelayClient.Dispose();
StartCoroutine(FinishCleanup());
// We need to delay slightly to give the disconnect message sent during Dispose time to reach the host, so that we don't destroy the connection without it being flushed first.

Component.Destroy(m_relayClient);
m_relayClient = null;
Component.Destroy(m_RelayClient);
m_RelayClient = null;
}
}
}

/// </summary>
private void OnFailedJoin()
void OnFailedJoin()
private void StartVivoxLogin()
void StartVivoxLogin()
m_vivoxSetup.Initialize(m_vivoxUserHandlers, OnVivoxLoginComplete);
m_VivoxSetup.Initialize(m_vivoxUserHandlers, OnVivoxLoginComplete);
void OnVivoxLoginComplete(bool didSucceed)
{

StartCoroutine(RetryConnection(StartVivoxLogin, m_localLobby.LobbyID));
StartCoroutine(RetryConnection(StartVivoxLogin, m_LocalLobby.LobbyID));
private void StartVivoxJoin()
void StartVivoxJoin()
m_vivoxSetup.JoinLobbyChannel(m_localLobby.LobbyID, OnVivoxJoinComplete);
m_VivoxSetup.JoinLobbyChannel(m_LocalLobby.LobbyID, OnVivoxJoinComplete);
void OnVivoxJoinComplete(bool didSucceed)
{

StartCoroutine(RetryConnection(StartVivoxJoin, m_localLobby.LobbyID));
StartCoroutine(RetryConnection(StartVivoxJoin, m_LocalLobby.LobbyID));
private void StartRelayConnection()
void StartRelayConnection()
if (m_localUser.IsHost)
m_relaySetup = gameObject.AddComponent<RelayUtpSetupHost>();
if (m_LocalUser.IsHost)
m_RelaySetup = gameObject.AddComponent<RelayUtpSetupHost>();
m_relaySetup = gameObject.AddComponent<RelayUtpSetupClient>();
m_relaySetup.BeginRelayJoin(m_localLobby, m_localUser, OnRelayConnected);
m_RelaySetup = gameObject.AddComponent<RelayUtpSetupClient>();
m_RelaySetup.BeginRelayJoin(m_LocalLobby, m_LocalUser, OnRelayConnected);
Component.Destroy(m_relaySetup);
m_relaySetup = null;
Component.Destroy(m_RelaySetup);
m_RelaySetup = null;
StartCoroutine(RetryConnection(StartRelayConnection, m_localLobby.LobbyID));
StartCoroutine(RetryConnection(StartRelayConnection, m_LocalLobby.LobbyID));
m_relayClient = client;
if (m_localUser.IsHost)
m_RelayClient = client;
if (m_LocalUser.IsHost)
CompleteRelayConnection();
else
Debug.Log("Client is now waiting for approval...");

private IEnumerator RetryConnection(Action doConnection, string lobbyId)
IEnumerator RetryConnection(Action doConnection, string lobbyId)
if (m_localLobby != null && m_localLobby.LobbyID == lobbyId && !string.IsNullOrEmpty(lobbyId)) // Ensure we didn't leave the lobby during this waiting period.
if (m_LocalLobby != null && m_LocalLobby.LobbyID == lobbyId && !string.IsNullOrEmpty(lobbyId)) // Ensure we didn't leave the lobby during this waiting period.
private void ConfirmApproval()
void ConfirmApproval()
if (!m_localUser.IsHost && m_localUser.IsApproved)
if (!m_LocalUser.IsHost && m_LocalUser.IsApproved)
{
CompleteRelayConnection();
StartVivoxJoin();

private void CompleteRelayConnection()
void CompleteRelayConnection()
private void SetUserLobbyState()
void SetUserLobbyState()
private void ResetLocalLobby()
void ResetLocalLobby()
m_localLobby.CopyObserved(new LocalLobby.LobbyData(), new Dictionary<string, LobbyUser>());
m_localLobby.AddPlayer(m_localUser); // As before, the local player will need to be plugged into UI before the lobby join actually happens.
m_localLobby.RelayServer = null;
m_LocalLobby.CopyObserved(new LocalLobby.LobbyData(), new Dictionary<string, LobbyUser>());
m_LocalLobby.AddPlayer(m_LocalUser); // As before, the local player will need to be plugged into UI before the lobby join actually happens.
m_LocalLobby.RelayServer = null;
}
#region Teardown

/// So, we need to delay just briefly to let the request happen (though we don't need to wait for the result).
/// </summary>
private IEnumerator LeaveBeforeQuit()
IEnumerator LeaveBeforeQuit()
{
ForceLeaveAttempt();
yield return null;

private bool OnWantToQuit()
bool OnWantToQuit()
bool canQuit = string.IsNullOrEmpty(m_localLobby?.LobbyID);
bool canQuit = string.IsNullOrEmpty(m_LocalLobby?.LobbyID);
private void OnDestroy()
void OnDestroy()
m_LobbyUpdater.Dispose();
LobbyManager.Dispose();
private void ForceLeaveAttempt()
void ForceLeaveAttempt()
if (!string.IsNullOrEmpty(m_localLobby?.LobbyID))
if (!string.IsNullOrEmpty(m_LocalLobby?.LobbyID))
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby?.LobbyID);
LobbyManager.LeaveLobbyAsync(m_LocalLobby?.LobbyID);
m_localLobby = null;
m_LocalLobby = null;
}
}

118
Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs


using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Core;
using UnityEngine;
namespace LobbyRelaySample
{
public enum AuthState
{
Initialized,
Authenticating,
Authenticated,
Error,
TimedOut
}
public static class Auth
{
public static AuthState AuthenticationState { get; private set; } = AuthState.Initialized;
public static async Task<AuthState> Authenticate(string profile,int tries = 5)
{
//If we are already authenticated, just return Auth
if (AuthenticationState == AuthState.Authenticated)
{
return AuthenticationState;
}
if (AuthenticationState == AuthState.Authenticating)
{
Debug.LogWarning("Cant Authenticate if we are authenticating or authenticated");
await Authenticating();
return AuthenticationState;
}
var profileOptions = new InitializationOptions();
profileOptions.SetProfile(profile);
await UnityServices.InitializeAsync(profileOptions);
await SignInAnonymouslyAsync(tries);
Debug.Log($"Auth attempts Finished : {AuthenticationState.ToString()}");
return AuthenticationState;
}
//Awaitable task that will pass the clientID once authentication is done.
public static string ID()
{
return AuthenticationService.Instance.PlayerId;
}
//Awaitable task that will pass once authentication is done.
public static async Task<AuthState> Authenticating()
{
while (AuthenticationState == AuthState.Authenticating || AuthenticationState == AuthState.Initialized)
{
await Task.Delay(200);
}
return AuthenticationState;
}
public static bool DoneAuthenticating()
{
return AuthenticationState != AuthState.Authenticating &&
AuthenticationState != AuthState.Initialized;
}
static async Task SignInAnonymouslyAsync(int maxRetries)
{
AuthenticationState = AuthState.Authenticating;
var tries = 0;
while (AuthenticationState == AuthState.Authenticating && tries < maxRetries)
{
try
{
//To ensure staging login vs non staging
await AuthenticationService.Instance.SignInAnonymouslyAsync();
if (AuthenticationService.Instance.IsSignedIn && AuthenticationService.Instance.IsAuthorized)
{
AuthenticationState = AuthState.Authenticated;
break;
}
}
catch (AuthenticationException ex)
{
// Compare error code to AuthenticationErrorCodes
// Notify the player with the proper error message
Debug.LogError(ex);
AuthenticationState = AuthState.Error;
}
catch (RequestFailedException exception)
{
// Compare error code to CommonErrorCodes
// Notify the player with the proper error message
Debug.LogError(exception);
AuthenticationState = AuthState.Error;
}
tries++;
await Task.Delay(1000);
}
if (AuthenticationState != AuthState.Authenticated)
{
Debug.LogWarning($"Player was not signed in successfully after {tries} attempts");
AuthenticationState = AuthState.TimedOut;
}
}
public static void SignOut()
{
AuthenticationService.Instance.SignOut(false);
AuthenticationState = AuthState.Initialized;
}
}
}

372
Assets/Scripts/GameLobby/Lobby/LobbyManager.cs


using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;
namespace LobbyRelaySample
{
/// <summary>
/// An abstraction layer between the direct calls into the Lobby API and the outcomes you actually want. E.g. you can request to get a readable list of
/// current lobbies and not need to make the query call directly.
/// </summary>
public class LobbyManager: IDisposable
{
//Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
// (This assumes that the game will be actively in just one lobby at a time, though they could be in more on the service side.)
public Lobby CurrentLobby => m_currentLobby;
const int k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters.
Lobby m_currentLobby;
#region Lobby API calls are rate limited, and some other operations might want an alert when the rate limits have passed.
// Note that some APIs limit to 1 call per N seconds, while others limit to M calls per N seconds. We'll treat all APIs as though they limited to 1 call per N seconds.
// Also, this is seralized, so don't reorder the values unless you know what that will affect.
public enum RequestType
{
Query = 0,
Join,
QuickJoin,
Host
}
public RateLimiter GetRateLimit(RequestType type)
{
if (type == RequestType.Join)
return m_JoinCooldown;
else if (type == RequestType.QuickJoin)
return m_QuickJoinCooldown;
else if (type == RequestType.Host)
return m_CreateCooldown;
return m_QueryCooldown;
}
RateLimiter m_QueryCooldown = new RateLimiter(1f); // Used for both the lobby list UI and the in-lobby updating. In the latter case, updates can be cached.
RateLimiter m_CreateCooldown = new RateLimiter(3f);
RateLimiter m_JoinCooldown = new RateLimiter(3f);
RateLimiter m_QuickJoinCooldown = new RateLimiter(10f);
RateLimiter m_GetLobbyCooldown = new RateLimiter(1f);
RateLimiter m_DeleteLobbyCooldown = new RateLimiter(.2f);
RateLimiter m_UpdateLobbyCooldown = new RateLimiter(.2f);
RateLimiter m_UpdatePlayerCooldown = new RateLimiter(.2f);
RateLimiter m_LeaveLobbyOrRemovePlayer = new RateLimiter(.2f);
RateLimiter m_HeartBeatCooldown = new RateLimiter(6f);
#endregion
static Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LobbyUser user)
{
Dictionary<string, PlayerDataObject> data = new Dictionary<string, PlayerDataObject>();
var displayNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.DisplayName);
var emoteNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.Emote.ToString());
data.Add("DisplayName", displayNameObject);
data.Add("Emote", emoteNameObject);
return data;
}
/// <summary>
/// Attempt to create a new lobby and then join it.
/// </summary>
public async Task<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LobbyUser localUser)
{
if (m_CreateCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Create Lobby hit the rate limit.");
return null;
}
try
{
string uasId = AuthenticationService.Instance.PlayerId;
CreateLobbyOptions createOptions = new CreateLobbyOptions
{
IsPrivate = isPrivate,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
var lobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
return lobby;
}
catch (Exception ex)
{
Debug.LogError($"Lobby Create failed:\n{ex}");
return null;
}
}
public async Task<Lobby> GetLobbyAsync(string lobbyId)
{
await m_GetLobbyCooldown.WaitUntilCooldown();
return await LobbyService.Instance.GetLobbyAsync(lobbyId);
}
/// <summary>
/// Attempt to join an existing lobby. Either ID xor code can be null.
/// </summary>
public async Task<Lobby> JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser)
{
//Dont want to queue the join action in this case.
if (m_JoinCooldown.IsInCooldown ||
(lobbyId == null && lobbyCode == null))
{
return null;
}
await m_JoinCooldown.WaitUntilCooldown();
string uasId = AuthenticationService.Instance.PlayerId;
Lobby joinedLobby = null;
var playerData = CreateInitialPlayerData(localUser);
if (!string.IsNullOrEmpty(lobbyId))
{
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions { Player = new Player(id: uasId, data: playerData) };
joinedLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
}
else
{
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions { Player = new Player(id: uasId, data: playerData) };
joinedLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
}
return joinedLobby;
}
/// <summary>
/// Attempt to join the first lobby among the available lobbies that match the filtered limitToColor.
/// </summary>
public async Task<Lobby> QuickJoinLobbyAsync(LobbyUser localUser, LobbyColor limitToColor = LobbyColor.None)
{
//We dont want to queue a quickjoin
if (m_QuickJoinCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Quick Join Lobby hit the rate limit.");
return null;
}
await m_QuickJoinCooldown.WaitUntilCooldown();
var filters = LobbyColorToFilters(limitToColor);
string uasId = AuthenticationService.Instance.PlayerId;
var joinRequest = new QuickJoinLobbyOptions
{
Filter = filters,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
var lobby = await LobbyService.Instance.QuickJoinLobbyAsync(joinRequest);
return lobby;
}
/// <summary>
/// Used for getting the list of all active lobbies, without needing full info for each.
/// </summary>
/// <param name="onListRetrieved">If called with null, retrieval was unsuccessful. Else, this will be given a list of contents to display, as pairs of a lobby code and a display string for that lobby.</param>
public async Task<QueryResponse> RetrieveLobbyListAsync(LobbyColor limitToColor = LobbyColor.None)
{
await m_QueryCooldown.WaitUntilCooldown();
var filters = LobbyColorToFilters(limitToColor);
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions
{
Count = k_maxLobbiesToShow,
Filters = filters
};
return await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
}
List<QueryFilter> LobbyColorToFilters(LobbyColor limitToColor)
{
List<QueryFilter> filters = new List<QueryFilter>();
if (limitToColor == LobbyColor.Orange)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Orange).ToString(), QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Green)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Green).ToString(), QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Blue)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Blue).ToString(), QueryFilter.OpOptions.EQ));
return filters;
}
/// <summary>
/// Attempt to leave a lobby, and then delete it if no players remain.
/// </summary>
/// <param name="onComplete">Called once the request completes, regardless of success or failure.</param>
public async Task LeaveLobbyAsync(string lobbyId)
{
await m_LeaveLobbyOrRemovePlayer.WaitUntilCooldown();
string playerId = AuthenticationService.Instance.PlayerId;
await LobbyService.Instance.RemovePlayerAsync(lobbyId, playerId);
}
/// <param name="data">Key-value pairs, which will overwrite any existing data for these keys. Presumed to be available to all lobby members but not publicly.</param>
public async Task UpdatePlayerDataAsync(string lobbyID, Dictionary<string, string> data)
{
await m_UpdatePlayerCooldown.WaitUntilCooldown();
string playerId = AuthenticationService.Instance.PlayerId;
Dictionary<string, PlayerDataObject> dataCurr = new Dictionary<string, PlayerDataObject>();
foreach (var dataNew in data)
{
PlayerDataObject dataObj = new PlayerDataObject(visibility: PlayerDataObject.VisibilityOptions.Member, value: dataNew.Value);
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
}
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = dataCurr,
AllocationId = null,
ConnectionInfo = null
};
await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions);
}
/// <summary>
/// Lobby can be provided info about Relay (or any other remote allocation) so it can add automatic disconnect handling.
/// </summary>
public async Task UpdatePlayerRelayInfoAsync(string lobbyID, string allocationId, string connectionInfo)
{
await m_UpdatePlayerCooldown.WaitUntilCooldown();
string playerId = AuthenticationService.Instance.PlayerId;
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = new Dictionary<string, PlayerDataObject>(),
AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions);
}
/// <param name="data">Key-value pairs, which will overwrite any existing data for these keys. Presumed to be available to all lobby members but not publicly.</param>
public async Task<Lobby> UpdateLobbyDataAsync(Lobby remoteLobby, Dictionary<string, string> data)
{
await m_UpdateLobbyCooldown.WaitUntilCooldown();
Dictionary<string, DataObject> dataCurr = remoteLobby.Data ?? new Dictionary<string, DataObject>();
var shouldLock = false;
foreach (var dataNew in data)
{
// Special case: We want to be able to filter on our color data, so we need to supply an arbitrary index to retrieve later. Uses N# for numerics, instead of S# for strings.
DataObject.IndexOptions index = dataNew.Key == "Color" ? DataObject.IndexOptions.N1 : 0;
DataObject dataObj = new DataObject(DataObject.VisibilityOptions.Public, dataNew.Value, index); // Public so that when we request the list of lobbies, we can get info about them for filtering.
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
//Special Use: Get the state of the Local lobby so we can lock it from appearing in queries if it's not in the "Lobby" State
if (dataNew.Key == "State")
{
Enum.TryParse(dataNew.Value, out LobbyState lobbyState);
shouldLock = lobbyState != LobbyState.Lobby;
}
}
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = dataCurr, IsLocked = shouldLock };
var result = await LobbyService.Instance.UpdateLobbyAsync(remoteLobby.Id, updateOptions);
return result;
}
public async Task SendHeartbeatPingAsync(string remoteLobbyId)
{
if (m_HeartBeatCooldown.IsInCooldown)
return;
await m_HeartBeatCooldown.WaitUntilCooldown();
await LobbyService.Instance.SendHeartbeatPingAsync(remoteLobbyId);
}
public void Dispose()
{
throw new NotImplementedException();
}
public class RateLimiter
{
public Action<bool> onCooldownChange;
public readonly float m_CooldownSeconds;
public readonly int m_CoolDownMS;
Queue<Task> m_TaskQueue = new Queue<Task>();
Task m_DequeuedTask;
private bool m_IsInCooldown = false;
public bool IsInCooldown
{
get => m_IsInCooldown;
private set
{
if (m_IsInCooldown != value)
{
m_IsInCooldown = value;
onCooldownChange?.Invoke(m_IsInCooldown);
}
}
}
public RateLimiter(float cooldownSeconds)
{
m_CooldownSeconds = cooldownSeconds;
m_CoolDownMS = Mathf.FloorToInt(m_CooldownSeconds * 1000);
}
public async Task WaitUntilCooldown() //TODO YAGNI Handle Multiple commands? Return bool if already waiting?
{
//No Queue!
if (CanCall())
return;
while (m_IsInCooldown)
{
await Task.Delay(50);
}
}
bool CanCall()
{
if (!IsInCooldown)
{
#pragma warning disable 4014
CoolDownAsync();
#pragma warning restore 4014
return true;
}
else return false;
}
async Task CoolDownAsync()
{
if (m_IsInCooldown)
return;
IsInCooldown = true;
await Task.Delay(m_CoolDownMS);
if (m_TaskQueue.Count > 0)
{
m_DequeuedTask = m_TaskQueue.Dequeue();
await CoolDownAsync();
}
IsInCooldown = false;
}
}
}
}

150
Assets/Scripts/GameLobby/Lobby/LobbyUpdater.cs


using System;
using System.Threading.Tasks;
using LobbyRelaySample.lobby;
using Unity.Services.Lobbies.Models;
namespace LobbyRelaySample
{
/// <summary>
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
/// </summary>
public class LobbyUpdater : IReceiveMessages, IDisposable
{
LocalLobby m_LocalLobby;
LobbyUser m_LocalUser;
LobbyManager m_LobbyManager;
bool m_ShouldPushData = false;
const int
k_approvalMaxMS = 10000; // Used for determining if a user should timeout if they are unable to connect.
int m_lifetime = 0;
const int k_UpdateIntervalMS = 100;
public LobbyUpdater(LobbyManager lobbyManager)
{
m_LobbyManager = lobbyManager;
}
public void BeginTracking(LocalLobby localLobby, LobbyUser localUser)
{
m_LocalUser = localUser;
m_LocalLobby = localLobby;
m_LocalLobby.onChanged += OnLocalLobbyChanged;
m_ShouldPushData = true;
Locator.Get.Messenger.Subscribe(this);
#pragma warning disable 4014
UpdateLoopAsync();
#pragma warning restore 4014
m_lifetime = 0;
}
public void EndTracking()
{
m_ShouldPushData = false;
Locator.Get.Messenger.Unsubscribe(this);
if (m_LocalLobby != null)
m_LocalLobby.onChanged -= OnLocalLobbyChanged;
m_LocalLobby = null;
}
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.ClientUserSeekingDisapproval)
{
bool shouldDisapprove =
m_LocalLobby.State !=
LobbyState.Lobby; // By not refreshing, it's possible to have a lobby in the lobby list UI after its countdown starts and then try joining.
if (shouldDisapprove)
(msg as Action<relay.Approval>)?.Invoke(relay.Approval.GameAlreadyStarted);
}
}
/// <summary>
/// If there have been any data changes since the last update, push them to Lobby. Regardless, pull for the most recent data.
/// (Unless we're already awaiting a query, in which case continue waiting.)
/// </summary>
async Task UpdateLoopAsync()
{
while (m_LocalLobby != null)
{
if (!m_LocalUser.IsApproved && m_lifetime > k_approvalMaxMS)
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup,
"Connection attempt timed out!");
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
if (m_ShouldPushData)
await PushDataToLobby();
else
UpdateLocalLobby();
m_lifetime += k_UpdateIntervalMS;
await Task.Delay(k_UpdateIntervalMS);
}
async Task PushDataToLobby()
{
m_ShouldPushData = false;
if (m_LocalUser.IsHost)
m_LobbyManager.UpdateLobbyDataAsync(m_LobbyManager.CurrentLobby,
LobbyConverters.LocalToRemoteData(m_LocalLobby));
m_LobbyManager.UpdatePlayerDataAsync(m_LobbyManager.CurrentLobby.Id,
LobbyConverters.LocalToRemoteUserData(m_LocalUser));
}
void UpdateLocalLobby()
{
m_LocalLobby.canPullUpdate = true;
//synching our local lobby
LobbyConverters.RemoteToLocal(m_LobbyManager.CurrentLobby, m_LocalLobby);
if (!m_LocalUser.IsHost)
{
foreach (var lobbyUser in m_LocalLobby.LobbyUsers)
{
if (lobbyUser.Value.IsHost)
return;
}
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup,
"Host left the lobby! Disconnecting...");
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
}
}
void OnLocalLobbyChanged(LocalLobby localLobby)
{
if (string.IsNullOrEmpty(localLobby.LobbyID)
) // When the player leaves, their LocalLobby is cleared out.
{
EndTracking();
return;
}
if (localLobby.canPullUpdate)
{
localLobby.canPullUpdate = false;
return;
}
m_ShouldPushData = true;
}
public void Dispose()
{
EndTracking();
}
}
}

11
Assets/Scripts/GameLobby/Auth/SubIdentity_Authentication.cs.meta


fileFormatVersion: 2
guid: d233e1d459180744bb44296ffec8e929
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

87
Assets/Scripts/GameLobby/Auth/Identity.cs


using System;
using System.Collections.Generic;
namespace LobbyRelaySample.Auth
{
/// <summary>
/// Represents some provider of credentials.
/// Each provider will have its own identity needs, so we'll allow each to define whatever parameters it needs.
/// Anything that accesses the contents should know what it's looking for.
/// </summary>
public class SubIdentity : Observed<SubIdentity>
{
protected Dictionary<string, string> m_contents = new Dictionary<string, string>();
public string GetContent(string key)
{
if (!m_contents.ContainsKey(key))
m_contents.Add(key, null); // Not alerting observers via OnChanged until the value is actually present (especially since this could be called by an observer, which would be cyclical).
return m_contents[key];
}
public void SetContent(string key, string value)
{
if (!m_contents.ContainsKey(key))
m_contents.Add(key, value);
else
m_contents[key] = value;
OnChanged(this);
}
public override void CopyObserved(SubIdentity oldObserved)
{
m_contents = oldObserved.m_contents;
}
}
public enum IIdentityType { Local = 0, Auth }
public interface IIdentity : IProvidable<IIdentity>
{
SubIdentity GetSubIdentity(IIdentityType identityType);
}
public class IdentityNoop : IIdentity
{
public SubIdentity GetSubIdentity(IIdentityType identityType) { return null; }
public void OnReProvided(IIdentity other) { }
}
/// <summary>
/// Our internal representation of the local player's credentials, wrapping the data required for interfacing with the identities of that player in the services.
/// (In use here, it just wraps Auth, but it can be used to combine multiple sets of credentials into one concept of a player.)
/// (This callback pattern also allows for Unit Testing, something UnityServices does support easily.)
/// </summary>
public class Identity : IIdentity, IDisposable
{
private Dictionary<IIdentityType, SubIdentity> m_subIdentities = new Dictionary<IIdentityType, SubIdentity>();
public Identity(string profileName, Action callbackOnAuthLogin)
{
m_subIdentities.Add(IIdentityType.Local, new SubIdentity());
m_subIdentities.Add(IIdentityType.Auth, new SubIdentity_Authentication(profileName, callbackOnAuthLogin));
}
public SubIdentity GetSubIdentity(IIdentityType identityType)
{
return m_subIdentities[identityType];
}
public void OnReProvided(IIdentity prev)
{
if (prev is Identity)
{
Identity prevIdentity = prev as Identity;
foreach (var entry in prevIdentity.m_subIdentities)
m_subIdentities.Add(entry.Key, entry.Value);
}
}
public void Dispose()
{
foreach (var sub in m_subIdentities)
if (sub.Value is IDisposable)
(sub.Value as IDisposable).Dispose();
}
}
}

66
Assets/Scripts/GameLobby/Auth/SubIdentity_Authentication.cs


using System;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Core;
namespace LobbyRelaySample.Auth
{
/// <summary>
/// The Authentication package will sign in asynchronously and anonymously. When complete, we will need to store the generated ID.
/// </summary>
public class SubIdentity_Authentication : SubIdentity, IDisposable
{
private bool m_hasDisposed = false;
/// <summary>
/// This will kick off a login.
/// </summary>
public SubIdentity_Authentication(string profileName = "default", Action onSigninComplete = null)
{
#pragma warning disable 4014
DoSignIn(profileName, onSigninComplete);
#pragma warning restore 4014
}
~SubIdentity_Authentication()
{
Dispose();
}
public void Dispose()
{
if (!m_hasDisposed)
{
AuthenticationService.Instance.SignedIn -= OnSignInChange;
AuthenticationService.Instance.SignedOut -= OnSignInChange;
m_hasDisposed = true;
}
}
private async Task DoSignIn(string profileName, Action onSigninComplete)
{
var serviceProfile = new InitializationOptions();
serviceProfile.SetProfile(profileName);
await UnityServices.InitializeAsync(serviceProfile);
AuthenticationService.Instance.SignedIn += OnSignInChange;
AuthenticationService.Instance.SignedOut += OnSignInChange;
try
{ if (!AuthenticationService.Instance.IsSignedIn)
await AuthenticationService.Instance.SignInAnonymouslyAsync(); // Don't sign out later, since that changes the anonymous token, which would prevent the player from exiting lobbies they're already in.
onSigninComplete?.Invoke();
}
catch
{ UnityEngine.Debug.LogError("Failed to login. Did you remember to set your Project ID under Services > General Settings?");
throw;
}
// Note: If for some reason your login state gets weird, you can comment out the previous block and instead call AuthenticationService.Instance.SignOut().
// Then, running Play mode will fail to actually function and instead will log out of your previous anonymous account.
// When you revert that change and run Play mode again, you should be logged in as a new anonymous account with a new default name.
}
private void OnSignInChange()
{
SetContent("id", AuthenticationService.Instance.PlayerId);
}
}
}

147
Assets/Scripts/GameLobby/Lobby/LobbyContentUpdater.cs


using System;
using System.Threading.Tasks;
using LobbyRelaySample.lobby;
using Unity.Services.Lobbies.Models;
namespace LobbyRelaySample
{
/// <summary>
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
/// </summary>
public class LobbyContentUpdater : IReceiveMessages
{
private LocalLobby m_LocalLobby;
private LobbyUser m_LocalUser;
private bool m_ShouldPushData = false;
private const float k_approvalMaxTime = 10; // Used for determining if a user should timeout if they are unable to connect.
private float m_lifetime = 0;
const int k_UpdateIntervalMS = 1500;
public void BeginTracking(LocalLobby localLobby, LobbyUser localUser)
{
m_LocalUser = localUser;
m_LocalLobby = localLobby;
m_LocalLobby.onChanged += OnLocalLobbyChanged;
m_ShouldPushData = true;
Locator.Get.Messenger.Subscribe(this);
#pragma warning disable 4014
UpdateLoopAsync();
#pragma warning restore 4014
m_lifetime = 0;
}
public void EndTracking()
{
m_ShouldPushData = false;
Locator.Get.Messenger.Unsubscribe(this);
if (m_LocalLobby != null)
m_LocalLobby.onChanged -= OnLocalLobbyChanged;
m_LocalLobby = null;
}
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.ClientUserSeekingDisapproval)
{
bool shouldDisapprove = m_LocalLobby.State != LobbyState.Lobby; // By not refreshing, it's possible to have a lobby in the lobby list UI after its countdown starts and then try joining.
if (shouldDisapprove)
(msg as Action<relay.Approval>)?.Invoke(relay.Approval.GameAlreadyStarted);
}
}
void OnLocalLobbyChanged(LocalLobby changed)
{
if (string.IsNullOrEmpty(changed.LobbyID)) // When the player leaves, their LocalLobby is cleared out but maintained.
{
EndTracking();
return;
}
if (changed.canPullUpdate)
{
changed.canPullUpdate = false;
return;
}
m_ShouldPushData = true;
}
/// <summary>
/// If there have been any data changes since the last update, push them to Lobby. Regardless, pull for the most recent data.
/// (Unless we're already awaiting a query, in which case continue waiting.)
/// </summary>
private async Task UpdateLoopAsync()
{
while (m_LocalLobby != null)
{
if (!m_LocalUser.IsApproved && m_lifetime > k_approvalMaxTime)
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Connection attempt timed out!");
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
if (m_ShouldPushData)
PushDataToLobby();
else
UpdateLocalLobby();
void PushDataToLobby()
{
m_ShouldPushData = false;
if (m_LocalUser.IsHost)
{
DoLobbyDataPush();
}
DoPlayerDataPush();
}
void DoLobbyDataPush()
{
#pragma warning disable 4014
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(LobbyConverters.LocalToRemoteData(m_LocalLobby));
#pragma warning restore 4014
}
void DoPlayerDataPush()
{
#pragma warning disable 4014
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(LobbyConverters.LocalToRemoteUserData(m_LocalUser));
#pragma warning restore 4014
}
await Task.Delay(k_UpdateIntervalMS);
}
}
void UpdateLocalLobby()
{
var remoteLobby = LobbyAsyncRequests.Instance.CurrentLobby;
if (remoteLobby == null)
return;
m_LocalLobby.canPullUpdate = true;
//synching our local lobby
LobbyConverters.RemoteToLocal(remoteLobby, m_LocalLobby);
//Dont push data this tick, since we "pulled"s
if (!m_LocalUser.IsHost)
{
foreach (var lobbyUser in m_LocalLobby.LobbyUsers)
{
if (lobbyUser.Value.IsHost)
return;
}
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Host left the lobby! Disconnecting...");
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
}
}
}

420
Assets/Scripts/GameLobby/Lobby/LobbyAsyncRequests.cs


using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;
namespace LobbyRelaySample
{
/// <summary>
/// An abstraction layer between the direct calls into the Lobby API and the outcomes you actually want. E.g. you can request to get a readable list of
/// current lobbies and not need to make the query call directly.
/// </summary>
public class LobbyAsyncRequests : IDisposable
{
// Just doing a singleton since static access is all that's really necessary but we also need to be able to subscribe to the slow update loop.
private static LobbyAsyncRequests s_instance;
private const int k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters.
public static LobbyAsyncRequests Instance
{
get
{
if (s_instance == null)
s_instance = new LobbyAsyncRequests();
return s_instance;
}
}
public Lobby CurrentLobby => m_RemoteLobby;
//Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
// (This assumes that the player will be actively in just one lobby at a time, though they could passively be in more.)
Lobby m_RemoteLobby;
#region Lobby API calls are rate limited, and some other operations might want an alert when the rate limits have passed.
// Note that some APIs limit to 1 call per N seconds, while others limit to M calls per N seconds. We'll treat all APIs as though they limited to 1 call per N seconds.
// Also, this is seralized, so don't reorder the values unless you know what that will affect.
public enum RequestType
{
Query = 0,
Join,
QuickJoin,
Host
}
public RateLimitCooldown GetRateLimit(RequestType type)
{
if (type == RequestType.Join)
return m_rateLimitJoin;
else if (type == RequestType.QuickJoin)
return m_rateLimitQuickJoin;
else if (type == RequestType.Host)
return m_rateLimitHost;
return m_rateLimitQuery;
}
private RateLimitCooldown m_rateLimitQuery = new RateLimitCooldown(1.5f); // Used for both the lobby list UI and the in-lobby updating. In the latter case, updates can be cached.
private RateLimitCooldown m_rateLimitJoin = new RateLimitCooldown(3f);
private RateLimitCooldown m_rateLimitHost = new RateLimitCooldown(3f);
private RateLimitCooldown m_rateLimitQuickJoin = new RateLimitCooldown(10f);
#endregion
private static Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LobbyUser player)
{
Dictionary<string, PlayerDataObject> data = new Dictionary<string, PlayerDataObject>();
PlayerDataObject dataObjName = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, player.DisplayName);
data.Add("DisplayName", dataObjName);
return data;
}
//TODO Back to Polling i Guess
/// <summary>
/// Attempt to create a new lobby and then join it.
/// </summary>
public async Task<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LobbyUser localUser)
{
if (m_rateLimitHost.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Create Lobby hit the rate limit.");
return null;
}
try
{
string uasId = AuthenticationService.Instance.PlayerId;
CreateLobbyOptions createOptions = new CreateLobbyOptions
{
IsPrivate = isPrivate,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
var lobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
#pragma warning disable 4014
LobbyHeartBeatLoop();
#pragma warning restore 4014
JoinLobby(lobby);
return lobby;
}
catch (Exception ex)
{
Debug.LogError($"Lobby Create failed:\n{ex}");
return null;
}
}
async Task<Lobby> GetLobbyAsync(string lobbyId)
{
await m_rateLimitQuery.WaitUntilCooldown();
return await LobbyService.Instance.GetLobbyAsync(lobbyId);
}
/// <summary>
/// Attempt to join an existing lobby. Either ID xor code can be null.
/// </summary>
public async Task<Lobby> JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser)
{
if (m_rateLimitJoin.IsInCooldown ||
(lobbyId == null && lobbyCode == null))
{
return null;
}
string uasId = AuthenticationService.Instance.PlayerId;
Lobby joinedLobby = null;
var playerData = CreateInitialPlayerData(localUser);
if (!string.IsNullOrEmpty(lobbyId))
{
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions { Player = new Player(id: uasId, data: playerData) };
joinedLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
}
else
{
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions { Player = new Player(id: uasId, data: playerData) };
joinedLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
}
JoinLobby(joinedLobby);
return joinedLobby;
}
/// <summary>
/// Attempt to join the first lobby among the available lobbies that match the filtered limitToColor.
/// </summary>
public async Task<Lobby> QuickJoinLobbyAsync(LobbyUser localUser, LobbyColor limitToColor = LobbyColor.None)
{
if (m_rateLimitQuickJoin.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Quick Join Lobby hit the rate limit.");
return null;
}
var filters = LobbyColorToFilters(limitToColor);
string uasId = AuthenticationService.Instance.PlayerId;
var joinRequest = new QuickJoinLobbyOptions
{
Filter = filters,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
var lobby = await LobbyService.Instance.QuickJoinLobbyAsync(joinRequest);
JoinLobby(lobby);
return lobby;
}
void JoinLobby(Lobby response)
{
m_RemoteLobby = response;
#pragma warning disable 4014
GetLobbyLoop();
#pragma warning restore 4014
}
/// <summary>
/// Used for getting the list of all active lobbies, without needing full info for each.
/// </summary>
/// <param name="onListRetrieved">If called with null, retrieval was unsuccessful. Else, this will be given a list of contents to display, as pairs of a lobby code and a display string for that lobby.</param>
public async Task<QueryResponse> RetrieveLobbyListAsync(LobbyColor limitToColor = LobbyColor.None)
{
await m_rateLimitQuery.WaitUntilCooldown();
var filters = LobbyColorToFilters(limitToColor);
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions
{
Count = k_maxLobbiesToShow,
Filters = filters
};
return await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
}
private List<QueryFilter> LobbyColorToFilters(LobbyColor limitToColor)
{
List<QueryFilter> filters = new List<QueryFilter>();
if (limitToColor == LobbyColor.Orange)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Orange).ToString(), QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Green)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Green).ToString(), QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Blue)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Blue).ToString(), QueryFilter.OpOptions.EQ));
return filters;
}
/// <summary>
/// Attempt to leave a lobby, and then delete it if no players remain.
/// </summary>
/// <param name="onComplete">Called once the request completes, regardless of success or failure.</param>
public async Task LeaveLobbyAsync(string lobbyId)
{
m_RemoteLobby = null;
string uasId = AuthenticationService.Instance.PlayerId;
await LobbyService.Instance.RemovePlayerAsync(lobbyId, uasId);
}
/// <param name="data">Key-value pairs, which will overwrite any existing data for these keys. Presumed to be available to all lobby members but not publicly.</param>
public async Task UpdatePlayerDataAsync(Dictionary<string, string> data)
{
await m_rateLimitQuery.WaitUntilCooldown();
string playerId = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
Dictionary<string, PlayerDataObject> dataCurr = new Dictionary<string, PlayerDataObject>();
foreach (var dataNew in data)
{
PlayerDataObject dataObj = new PlayerDataObject(visibility: PlayerDataObject.VisibilityOptions.Member, value: dataNew.Value);
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
}
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = dataCurr,
AllocationId = null,
ConnectionInfo = null
};
await LobbyService.Instance.UpdatePlayerAsync(m_RemoteLobby.Id, playerId, updateOptions);
}
/// <summary>
/// Lobby can be provided info about Relay (or any other remote allocation) so it can add automatic disconnect handling.
/// </summary>
public async Task UpdatePlayerRelayInfoAsync(string allocationId, string connectionInfo)
{
await m_rateLimitQuery.WaitUntilCooldown();
await AwaitRemoteLobby();
string playerId = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = new Dictionary<string, PlayerDataObject>(),
AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
await LobbyService.Instance.UpdatePlayerAsync(m_RemoteLobby.Id, playerId, updateOptions);
}
/// <param name="data">Key-value pairs, which will overwrite any existing data for these keys. Presumed to be available to all lobby members but not publicly.</param>
public async Task UpdateLobbyDataAsync(Dictionary<string, string> data)
{
await m_rateLimitQuery.WaitUntilCooldown();
Dictionary<string, DataObject> dataCurr = m_RemoteLobby.Data ?? new Dictionary<string, DataObject>();
var shouldLock = false;
foreach (var dataNew in data)
{
// Special case: We want to be able to filter on our color data, so we need to supply an arbitrary index to retrieve later. Uses N# for numerics, instead of S# for strings.
DataObject.IndexOptions index = dataNew.Key == "Color" ? DataObject.IndexOptions.N1 : 0;
DataObject dataObj = new DataObject(DataObject.VisibilityOptions.Public, dataNew.Value, index); // Public so that when we request the list of lobbies, we can get info about them for filtering.
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
//Special Use: Get the state of the Local lobby so we can lock it from appearing in queries if it's not in the "Lobby" State
if (dataNew.Key == "State")
{
Enum.TryParse(dataNew.Value, out LobbyState lobbyState);
shouldLock = lobbyState != LobbyState.Lobby;
}
}
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = dataCurr, IsLocked = shouldLock };
var result = await LobbyService.Instance.UpdateLobbyAsync(m_RemoteLobby.Id, updateOptions);
if (result != null)
m_RemoteLobby = result;
}
#region LobbyLoops
const int m_LobbyUpdateTime = 1000;
async Task GetLobbyLoop()
{
//In this sample we only use the loop internally, when we've joined. Only after we have joined or created a lobby can we poll for updates.
//Since you only need the ID, there might be a use case to get the lobby before joining it, since you can get the ID's from Querying
while (m_RemoteLobby != null)
{
m_RemoteLobby = await GetLobbyAsync(m_RemoteLobby.Id);
await Task.Delay(m_LobbyUpdateTime);
}
}
const int k_heartbeatPeriodMS = 8000; // The heartbeat must be rate-limited to 5 calls per 30 seconds. We'll aim for longer in case periods don't align.
/// <summary>
/// Lobby requires a periodic ping to detect rooms that are still active, in order to mitigate "zombie" lobbies.
/// </summary>
async Task LobbyHeartBeatLoop()
{
while (m_RemoteLobby!=null)
{
#pragma warning disable 4014
LobbyService.Instance.SendHeartbeatPingAsync(m_RemoteLobby.Id);
#pragma warning restore 4014
await Task.Delay(k_heartbeatPeriodMS);
}
}
#endregion
async Task AwaitRemoteLobby()
{
while (m_RemoteLobby == null)
await Task.Delay(100);
}
public void Dispose()
{
if (m_RemoteLobby == null)
return;
#pragma warning disable 4014
LeaveLobbyAsync(m_RemoteLobby.Id);
#pragma warning restore 4014
}
public class RateLimitCooldown
{
public Action<bool> onCooldownChange;
public readonly float m_CooldownSeconds;
public readonly int m_CoolDownMS;
Queue<Task> m_TaskQueue = new Queue<Task>();
Task m_DequeuedTask;
private bool m_IsInCooldown = false;
public bool IsInCooldown
{
get => m_IsInCooldown;
private set
{
if (m_IsInCooldown != value)
{
m_IsInCooldown = value;
onCooldownChange?.Invoke(m_IsInCooldown);
}
}
}
public RateLimitCooldown(float cooldownSeconds)
{
m_CooldownSeconds = cooldownSeconds;
m_CoolDownMS = Mathf.FloorToInt(m_CooldownSeconds * 1000);
}
public async Task WaitUntilCooldown() //TODO YAGNI Handle Multiple commands? Return bool if already waiting?
{
//No Queue!
if (CanCall())
return;
while (m_IsInCooldown)
{
await Task.Delay(100);
}
}
bool CanCall()
{
if (!IsInCooldown)
{
#pragma warning disable 4014
CoolDownAsync();
#pragma warning restore 4014
return true;
}
else return false;
}
async Task CoolDownAsync()
{
if (m_IsInCooldown)
return;
IsInCooldown = true;
await Task.Delay(m_CoolDownMS);
if (m_TaskQueue.Count > 0)
{
m_DequeuedTask = m_TaskQueue.Dequeue();
await CoolDownAsync();
}
IsInCooldown = false;
}
}
}
}

11
Assets/Scripts/GameLobby/Tests/Editor/AuthTests.cs.meta


fileFormatVersion: 2
guid: f1f15da02efb21948b4575f00bb800de
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

27
Assets/Scripts/GameLobby/Tests/Editor/AuthTests.cs


using LobbyRelaySample.Auth;
using NUnit.Framework;
namespace Test
{
public class AuthTests
{
/// <summary>
/// Ensure that any changes to a flavor of SubIdentity are automatically updated.
/// </summary>
[Test]
public void IdentityBasicSubidentity()
{
string value = null;
int count = 0;
SubIdentity testIdentity = new SubIdentity();
testIdentity.onChanged += (si) => { value = si.GetContent("key1"); count++; };
testIdentity.SetContent("key1", "newValue1");
Assert.AreEqual(1, count, "Content changed once.");
Assert.AreEqual("newValue1", value, "Should not have to do anything to receive updated content from a set.");
testIdentity.SetContent("key2", "newValue2");
Assert.AreEqual(2, count, "Content changed twice.");
Assert.AreEqual("newValue1", value, "Contents should not affect different keys.");
}
}
}

/Assets/Scripts/GameLobby/Auth/Identity.cs.meta → /Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs.meta

/Assets/Scripts/GameLobby/Lobby/LobbyAsyncRequests.cs.meta → /Assets/Scripts/GameLobby/Lobby/LobbyManager.cs.meta

/Assets/Scripts/GameLobby/Lobby/LobbyContentUpdater.cs.meta → /Assets/Scripts/GameLobby/Lobby/LobbyUpdater.cs.meta

正在加载...
取消
保存