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 { /// /// 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. /// /// /// Manages one Lobby at a time, Only entry points to a lobby with ID is via JoinAsync, CreateAsync, and QuickJoinAsync 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; Lobby m_CurrentLobby; const int k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters. Task m_HeartBeatTask; #region Rate Limiting public enum RequestType { Query = 0, Join, QuickJoin, Host } public bool InLobby() { if (m_CurrentLobby == null) { Debug.LogError("LobbyManager not currently in a lobby. Did you CreateLobbyAsync or JoinLobbyAsync?"); return false; } return true; } 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; } // Rate Limits are posted here: https://docs.unity.com/lobby/rate-limits.html RateLimiter m_QueryCooldown = new RateLimiter(1f); 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(.3f); RateLimiter m_UpdatePlayerCooldown = new RateLimiter(.3f); RateLimiter m_LeaveLobbyOrRemovePlayer = new RateLimiter(.3f); RateLimiter m_HeartBeatCooldown = new RateLimiter(6f); #endregion Dictionary CreateInitialPlayerData(LocalPlayer user) { Dictionary data = new Dictionary(); var displayNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.DisplayName.Value); data.Add("DisplayName", displayNameObject); return data; } public async Task CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LocalPlayer localUser) { if (m_CreateCooldown.IsInCooldown) { UnityEngine.Debug.LogWarning("Create Lobby hit the rate limit."); return null; } await m_CreateCooldown.WaitUntilCooldown(); Debug.Log("Lobby - Creating"); try { string uasId = AuthenticationService.Instance.PlayerId; CreateLobbyOptions createOptions = new CreateLobbyOptions { IsPrivate = isPrivate, Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser)) }; m_CurrentLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions); StartHeartBeat(); return m_CurrentLobby; } catch (Exception ex) { Debug.LogError($"Lobby Create failed:\n{ex}"); return null; } } public async Task JoinLobbyAsync(string lobbyId, string lobbyCode, LocalPlayer localUser) { if (m_JoinCooldown.IsInCooldown || (lobbyId == null && lobbyCode == null)) { return null; } await m_JoinCooldown.WaitUntilCooldown(); Debug.Log($"{localUser.DisplayName}({localUser.ID}) Joining Lobby- {lobbyId} with {lobbyCode}"); string uasId = AuthenticationService.Instance.PlayerId; var playerData = CreateInitialPlayerData(localUser); if (!string.IsNullOrEmpty(lobbyId)) { JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions { Player = new Player(id: uasId, data: playerData) }; m_CurrentLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions); } else { JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions { Player = new Player(id: uasId, data: playerData) }; m_CurrentLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions); } return m_CurrentLobby; } public async Task QuickJoinLobbyAsync(LocalPlayer 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(); Debug.Log("Lobby - Quick Joining."); var filters = LobbyColorToFilters(limitToColor); string uasId = AuthenticationService.Instance.PlayerId; var joinRequest = new QuickJoinLobbyOptions { Filter = filters, Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser)) }; return m_CurrentLobby = await LobbyService.Instance.QuickJoinLobbyAsync(joinRequest); } public async Task RetrieveLobbyListAsync(LobbyColor limitToColor = LobbyColor.None) { await m_QueryCooldown.WaitUntilCooldown(); Debug.Log("Lobby - Retrieving List."); var filters = LobbyColorToFilters(limitToColor); QueryLobbiesOptions queryOptions = new QueryLobbiesOptions { Count = k_maxLobbiesToShow, Filters = filters }; return await LobbyService.Instance.QueryLobbiesAsync(queryOptions); } List LobbyColorToFilters(LobbyColor limitToColor) { List filters = new List(); 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; } public async Task GetLobbyAsync(string lobbyId = null) { if (!InLobby()) return null; await m_GetLobbyCooldown.WaitUntilCooldown(); lobbyId ??= m_CurrentLobby.Id; return m_CurrentLobby = await LobbyService.Instance.GetLobbyAsync(lobbyId); } public async Task LeaveLobbyAsync() { await m_LeaveLobbyOrRemovePlayer.WaitUntilCooldown(); if (!InLobby()) return; string playerId = AuthenticationService.Instance.PlayerId; Debug.Log($"{playerId} leaving Lobby {m_CurrentLobby.Id}"); await LobbyService.Instance.RemovePlayerAsync(m_CurrentLobby.Id, playerId); m_CurrentLobby = null; } public async Task UpdatePlayerDataAsync(Dictionary data) { if (!InLobby()) return null; await m_UpdatePlayerCooldown.WaitUntilCooldown(); Debug.Log("Lobby - Updating Player Data"); string playerId = AuthenticationService.Instance.PlayerId; Dictionary dataCurr = new Dictionary(); 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 }; return m_CurrentLobby = await LobbyService.Instance.UpdatePlayerAsync(m_CurrentLobby.Id, playerId, updateOptions); } public async Task UpdatePlayerRelayInfoAsync(string lobbyID, string allocationId, string connectionInfo) { if (!InLobby()) return null; await m_UpdatePlayerCooldown.WaitUntilCooldown(); Debug.Log("Lobby - Relay Info (Player)"); string playerId = AuthenticationService.Instance.PlayerId; UpdatePlayerOptions updateOptions = new UpdatePlayerOptions { Data = new Dictionary(), AllocationId = allocationId, ConnectionInfo = connectionInfo }; return m_CurrentLobby = await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions); } public async Task UpdateLobbyDataAsync(Dictionary data) { if (!InLobby()) return null; await m_UpdateLobbyCooldown.WaitUntilCooldown(); Debug.Log("Lobby - Updating Lobby Data"); Dictionary dataCurr = m_CurrentLobby.Data ?? new Dictionary(); 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 == "LocalLobbyColor" ? 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" LocalLobbyState if (dataNew.Key == "LocalLobbyState") { Enum.TryParse(dataNew.Value, out LobbyState lobbyState); shouldLock = lobbyState != LobbyState.Lobby; } } UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = dataCurr, IsLocked = shouldLock }; return m_CurrentLobby = await LobbyService.Instance.UpdateLobbyAsync(m_CurrentLobby.Id, updateOptions); } public async Task DeleteLobbyAsync() { if (!InLobby()) return; await m_DeleteLobbyCooldown.WaitUntilCooldown(); Debug.Log("Lobby - Deleting Lobby"); await LobbyService.Instance.DeleteLobbyAsync(m_CurrentLobby.Id); } public void Dispose() { m_CurrentLobby = null; m_HeartBeatTask.Dispose(); } #region HeartBeat //Since the LobbyManager maintains the "connection" to the lobby, we will continue to heartbeat until host leaves. async Task SendHeartbeatPingAsync() { if (!InLobby()) return; if (m_HeartBeatCooldown.IsInCooldown) return; await m_HeartBeatCooldown.WaitUntilCooldown(); Debug.Log("Lobby - Heartbeat"); await LobbyService.Instance.SendHeartbeatPingAsync(m_CurrentLobby.Id); } void StartHeartBeat() { #pragma warning disable 4014 m_HeartBeatTask = HeartBeatLoop(); #pragma warning restore 4014 } async Task HeartBeatLoop() { while (m_CurrentLobby != null) { await SendHeartbeatPingAsync(); await Task.Delay(8000); } } #endregion } //Manages the Cooldown for each service call. //Adds a buffer to account for ping times. public class RateLimiter { public Action onCooldownChange; public readonly float cooldownSeconds; public readonly int coolDownMS; public readonly int pingBufferMS; //(If you're still getting rate limit errors, try increasing the pingBuffer) public RateLimiter(float cooldownSeconds, int pingBuffer = 100) { this.cooldownSeconds = cooldownSeconds; pingBufferMS = pingBuffer; coolDownMS = Mathf.CeilToInt(this.cooldownSeconds * 1000) + pingBufferMS; } public async Task WaitUntilCooldown() { //No Queue! if (!m_IsInCooldown) { #pragma warning disable 4014 CooldownAsync(); #pragma warning restore 4014 return; } while (m_IsInCooldown) { await Task.Delay(10); } } async Task CooldownAsync() { IsInCooldown = true; await Task.Delay(coolDownMS); IsInCooldown = false; } bool m_IsInCooldown = false; public bool IsInCooldown { get => m_IsInCooldown; private set { if (m_IsInCooldown != value) { m_IsInCooldown = value; onCooldownChange?.Invoke(m_IsInCooldown); } } } } }