using LobbyRelaySample.relay; using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace LobbyRelaySample { /// /// Sets up and runs the entire sample. /// public class GameManager : MonoBehaviour, IReceiveMessages { #region UI elements that observe the local state. These should be assigned the observers in the scene during Start. [SerializeField] private List m_GameStateObservers = new List(); [SerializeField] private List m_LocalLobbyObservers = new List(); [SerializeField] private List m_LocalUserObservers = new List(); [SerializeField] private List m_LobbyServiceObservers = new List(); #endregion private LocalGameState m_localGameState = new LocalGameState(); private LobbyUser m_localUser; private LocalLobby m_localLobby; private LobbyServiceData m_lobbyServiceData = new LobbyServiceData(); private LobbyContentHeartbeat m_lobbyContentHeartbeat = new LobbyContentHeartbeat(); private RelayUtpSetup m_relaySetup; private RelayUtpClient m_relayClient; private vivox.VivoxSetup m_vivoxSetup = new vivox.VivoxSetup(); [SerializeField] private List m_vivoxUserHandlers; /// Rather than a setter, this is usable in-editor. It won't accept an enum, however. public void SetLobbyColorFilter(int color) { m_lobbyColorFilter = (LobbyColor)color; } private LobbyColor m_lobbyColorFilter; #region Setup private 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 Locator.Get.Provide(new Auth.Identity(OnAuthSignIn)); Application.wantsToQuit += OnWantToQuit; } 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(); } private void BeginObservers() { foreach (var gameStateObs in m_GameStateObservers) gameStateObs.BeginObserving(m_localGameState); 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 /// /// Primarily used for UI elements to communicate state changes, this will receive messages from arbitrary providers for user interactions. /// public void OnReceiveMessage(MessageType type, object msg) { if (type == MessageType.CreateLobbyRequest) { LocalLobby.LobbyData createLobbyData = (LocalLobby.LobbyData)msg; LobbyAsyncRequests.Instance.CreateLobbyAsync(createLobbyData.LobbyName, createLobbyData.MaxPlayerCount, createLobbyData.Private, m_localUser, (r) => { lobby.ToLocalLobby.Convert(r, m_localLobby); OnCreatedLobby(); }, OnFailedJoin); } else if (type == MessageType.JoinLobbyRequest) { LocalLobby.LobbyData lobbyInfo = (LocalLobby.LobbyData)msg; LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode, m_localUser, (r) => { lobby.ToLocalLobby.Convert(r, m_localLobby); OnJoinedLobby(); }, OnFailedJoin); } else if (type == MessageType.QueryLobbies) { m_lobbyServiceData.State = LobbyQueryState.Fetching; LobbyAsyncRequests.Instance.RetrieveLobbyListAsync( qr => { if (qr != null) OnLobbiesQueried(lobby.ToLocalLobby.Convert(qr)); }, er => { OnLobbyQueryFailed(); }, m_lobbyColorFilter); } else if (type == MessageType.QuickJoin) { LobbyAsyncRequests.Instance.QuickJoinLobbyAsync(m_localUser, m_lobbyColorFilter, (r) => { lobby.ToLocalLobby.Convert(r, m_localLobby); OnJoinedLobby(); }, OnFailedJoin); } else if (type == MessageType.RenameRequest) { string name = (string)msg; if (string.IsNullOrWhiteSpace(name)) { Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Empty Name not allowed."); // Lobby error type, then HTTP error type. return; } m_localUser.DisplayName = (string)msg; } else if (type == MessageType.ClientUserApproved) { ConfirmApproval(); } else if (type == MessageType.UserSetEmote) { EmoteType emote = (EmoteType)msg; m_localUser.Emote = emote; } else if (type == MessageType.LobbyUserStatus) { m_localUser.UserStatus = (UserStatus)msg; } else if (type == MessageType.StartCountdown) { m_localLobby.State = LobbyState.CountDown; } else if (type == MessageType.CancelCountdown) { m_localLobby.State = LobbyState.Lobby; } else if (type == MessageType.CompleteCountdown) { if (m_relayClient is RelayUtpHost) (m_relayClient as RelayUtpHost).SendInGameState(); } else if (type == MessageType.ChangeGameState) { SetGameState((GameState)msg); } else if (type == MessageType.ConfirmInGameState) { m_localUser.UserStatus = UserStatus.InGame; m_localLobby.State = LobbyState.InGame; } else if (type == MessageType.EndGame) { m_localLobby.State = LobbyState.Lobby; SetUserLobbyState(); } } private void SetGameState(GameState state) { bool isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) && m_localGameState.State == GameState.Lobby; m_localGameState.State = state; if (isLeavingLobby) OnLeftLobby(); } private void OnLobbiesQueried(IEnumerable lobbies) { var newLobbyDict = new Dictionary(); foreach (var lobby in lobbies) newLobbyDict.Add(lobby.LobbyID, lobby); m_lobbyServiceData.State = LobbyQueryState.Fetched; m_lobbyServiceData.CurrentLobbies = newLobbyDict; } private void OnLobbyQueryFailed() { m_lobbyServiceData.State = LobbyQueryState.Error; } private void OnCreatedLobby() { m_localUser.IsHost = true; OnJoinedLobby(); } private void OnJoinedLobby() { LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID); m_lobbyContentHeartbeat.BeginTracking(m_localLobby, m_localUser); SetUserLobbyState(); // The host has the opportunity to reject incoming players, but to do so the player needs to connect to Relay without having game logic available. // In particular, we should prevent players from joining voice chat until they are approved. OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Connecting); if (m_localUser.IsHost) { StartRelayConnection(); StartVivoxJoin(); } else { StartRelayConnection(); } } private void OnLeftLobby() { m_localUser.ResetState(); LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby.LobbyID, ResetLocalLobby); m_lobbyContentHeartbeat.EndTracking(); LobbyAsyncRequests.Instance.EndTracking(); m_vivoxSetup.LeaveLobbyChannel(); if (m_relaySetup != null) { Component.Destroy(m_relaySetup); m_relaySetup = null; } if (m_relayClient != null) { Component.Destroy(m_relayClient); m_relayClient = null; } } /// /// Back to Join menu if we fail to join for whatever reason. /// private void OnFailedJoin() { SetGameState(GameState.JoinMenu); } private void StartVivoxLogin() { m_vivoxSetup.Initialize(m_vivoxUserHandlers, OnVivoxLoginComplete); void OnVivoxLoginComplete(bool didSucceed) { if (!didSucceed) { Debug.LogError("Vivox login failed! Retrying in 5s..."); StartCoroutine(RetryConnection(StartVivoxLogin, m_localLobby.LobbyID)); return; } } } private void StartVivoxJoin() { m_vivoxSetup.JoinLobbyChannel(m_localLobby.LobbyID, OnVivoxJoinComplete); void OnVivoxJoinComplete(bool didSucceed) { if (!didSucceed) { Debug.LogError("Vivox connection failed! Retrying in 5s..."); StartCoroutine(RetryConnection(StartVivoxJoin, m_localLobby.LobbyID)); return; } } } private void StartRelayConnection() { if (m_localUser.IsHost) m_relaySetup = gameObject.AddComponent(); else m_relaySetup = gameObject.AddComponent(); m_relaySetup.BeginRelayJoin(m_localLobby, m_localUser, OnRelayConnected); void OnRelayConnected(bool didSucceed, RelayUtpClient client) { Component.Destroy(m_relaySetup); m_relaySetup = null; if (!didSucceed) { Debug.LogError("Relay connection failed! Retrying in 5s..."); StartCoroutine(RetryConnection(StartRelayConnection, m_localLobby.LobbyID)); return; } m_relayClient = client; if (m_localUser.IsHost) CompleteRelayConnection(); else Debug.Log("Client is now waiting for approval..."); } } private IEnumerator RetryConnection(Action doConnection, string lobbyId) { yield return new WaitForSeconds(5); if (m_localLobby != null && m_localLobby.LobbyID == lobbyId && !string.IsNullOrEmpty(lobbyId)) // Ensure we didn't leave the lobby during this waiting period. doConnection?.Invoke(); } private void ConfirmApproval() { if (!m_localUser.IsHost && m_localUser.IsApproved) { CompleteRelayConnection(); StartVivoxJoin(); } } private void CompleteRelayConnection() { OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby); } private void SetUserLobbyState() { SetGameState(GameState.Lobby); OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby); } private void ResetLocalLobby() { m_localLobby.CopyObserved(new LocalLobby.LobbyData(), new Dictionary()); 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 /// /// In builds, if we are in a lobby and try to send a Leave request on application quit, it won't go through if we're quitting on the same frame. /// So, we need to delay just briefly to let the request happen (though we don't need to wait for the result). /// private IEnumerator LeaveBeforeQuit() { ForceLeaveAttempt(); yield return null; Application.Quit(); } private bool OnWantToQuit() { bool canQuit = string.IsNullOrEmpty(m_localLobby?.LobbyID); StartCoroutine(LeaveBeforeQuit()); return canQuit; } private void OnDestroy() { ForceLeaveAttempt(); } private void ForceLeaveAttempt() { Locator.Get.Messenger.Unsubscribe(this); if (!string.IsNullOrEmpty(m_localLobby?.LobbyID)) { LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby?.LobbyID, null); m_localLobby = null; } } #endregion } }