当前提交
96727d5b
共有 15 个文件被更改,包括 809 次插入 和 917 次删除
-
2Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs.meta
-
315Assets/Scripts/GameLobby/Game/GameManager.cs
-
118Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs
-
372Assets/Scripts/GameLobby/Lobby/LobbyManager.cs
-
150Assets/Scripts/GameLobby/Lobby/LobbyUpdater.cs
-
11Assets/Scripts/GameLobby/Auth/SubIdentity_Authentication.cs.meta
-
87Assets/Scripts/GameLobby/Auth/Identity.cs
-
66Assets/Scripts/GameLobby/Auth/SubIdentity_Authentication.cs
-
147Assets/Scripts/GameLobby/Lobby/LobbyContentUpdater.cs
-
420Assets/Scripts/GameLobby/Lobby/LobbyAsyncRequests.cs
-
11Assets/Scripts/GameLobby/Tests/Editor/AuthTests.cs.meta
-
27Assets/Scripts/GameLobby/Tests/Editor/AuthTests.cs
-
0/Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs.meta
-
0/Assets/Scripts/GameLobby/Lobby/LobbyManager.cs.meta
-
0/Assets/Scripts/GameLobby/Lobby/LobbyUpdater.cs.meta
|
|||
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; |
|||
} |
|||
} |
|||
} |
|
|||
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; |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|
|||
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(); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: d233e1d459180744bb44296ffec8e929 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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(); |
|||
} |
|||
} |
|||
} |
|
|||
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); |
|||
} |
|||
} |
|||
} |
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
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; |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: f1f15da02efb21948b4575f00bb800de |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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."); |
|||
} |
|||
} |
|||
} |
撰写
预览
正在加载...
取消
保存
Reference in new issue