using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; /// <summary> /// singleton for a single source of truth game state and flow management /// </summary> public class GameManager : MonoBehaviour { /// <summary> /// enum for available menus in the game, for use with <c>ShowMenu()</c> /// </summary> public enum DisplayState { UnassociatedState, // initial state, then we transition to Nothing to initialise the ui Nothing, GameView, LeaderboardView, AccountView } /// <summary> /// singleton pattern: define instance field for accessing the singleton elsewhere /// </summary> public static GameManager Instance; /// <summary> /// the current display state of the game /// </summary> [SerializeField] private DisplayState state = DisplayState.UnassociatedState; /// <summary> /// the game object for the ui /// </summary> [SerializeField] private GameObject uiGameObject; // /// <summary> // /// callback functions to be invoked when the display state changes // /// </summary> // private readonly List<Action<DisplayState>> _onDisplayStateChange = new(); /// <summary> /// callback functions to be invoked when the local player data is updated /// </summary> private readonly List<Action<LocalPlayerData>> _onLocalPlayerDataUpdateCallbacks = new(); /// <summary> /// the local player data object for storing player data /// </summary> private LocalPlayerData _data; /// <summary> /// the visual element for the ui /// </summary> private VisualElement _ui; /// <summary> /// backend object for handling communication with the firebase backend /// </summary> public Backend Backend; /// <summary> /// enforces singleton behaviour; sets doesn't destroy on load and checks for multiple instances /// </summary> private void Awake() { // check if instance hasn't been set yet if (Instance == null) { Debug.Log("awake as singleton instance, setting self as the forever-alive instance"); Instance = this; DontDestroyOnLoad(gameObject); } // check if instance is already set and it's not this instance else if (Instance != null && Instance != this) { Debug.Log("awake as non-singleton instance, destroying self"); Destroy(gameObject); } if (uiGameObject == null) throw new NullReferenceException("a reference UI GameObject is not set in the inspector"); _ui = uiGameObject.GetComponent<UIDocument>()?.rootVisualElement; if (_ui == null) throw new NullReferenceException("could not grab the UIDocument in the reference UI GameObject"); } /// <summary> /// called before the first frame update /// </summary> private void Start() { // transition to the initial state SetDisplayState(DisplayState.Nothing); // initialise the backend Backend = new Backend(); Backend.Initialise(status => { Debug.Log("initialised backend, setting connection status text"); _ui.Q<Label>("ConnectionStatusText").text = status switch { Backend.FirebaseConnectionStatus.Connected => "Status: Connected", Backend.FirebaseConnectionStatus.Updating => "Status: Updating... (Retrying in a bit!)", Backend.FirebaseConnectionStatus.NotConnected => "Status: Disconnected", Backend.FirebaseConnectionStatus.UpdateRequired => "Status: Disconnected (Device Component Update Required)", Backend.FirebaseConnectionStatus.ExternalError => "Status: Disconnected (External/Device Error)", Backend.FirebaseConnectionStatus.InternalError => "Status: Disconnected (Internal Error)", _ => "Status: Disconnected (unknown fcs state, this is unreachable and a bug)" }; if (status == Backend.FirebaseConnectionStatus.Connected) return; // if we're not connected, hide any online 'features' _ui.Q<Button>("LeaderboardButton").style.display = DisplayStyle.None; _ui.Q<Button>("AccountButton").style.display = DisplayStyle.None; }); // load the local player data and refresh the ui _data = new LocalPlayerData(); _data.LoadFromTheWorld(data => { foreach (var callback in _onLocalPlayerDataUpdateCallbacks) callback(data); }); // register a callback to refresh the ui when the player signs in Backend.RegisterOnSignInCallback(_ => { Debug.Log("post-auth callback, refreshing player data from the world"); _data.LoadFromTheWorld(data => { Debug.Log("firing lpdata update callbacks"); foreach (var callback in _onLocalPlayerDataUpdateCallbacks) callback(data); }); }); } /// <summary> /// called when the game object is disabled /// </summary> private void OnDestroy() { Backend.Cleanup(); } // /// <summary> // /// function to register a callback for when the display state changes // /// </summary> // /// <param name="callback">callback function that takes in a <c>DisplayState</c> enum</param> // public void RegisterOnDisplayStateChange(Action<DisplayState> callback) // { // _onDisplayStateChange.Add(callback); // } /// <summary> /// function to register a callback for when the local player data is updated /// </summary> /// <param name="callback">callback function that takes in a <c>LocalPlayerData</c> object</param> public void RegisterOnLocalPlayerDataUpdate(Action<LocalPlayerData> callback) { _onLocalPlayerDataUpdateCallbacks.Add(callback); Debug.Log( $"registering on lpdata update callback, there are now {_onLocalPlayerDataUpdateCallbacks.Count} callbacks"); } /// <summary> /// function to show a menu based on the enum passed, /// and any other necessary actions /// </summary> /// <param name="newDisplayState">the game menu to show</param> public void SetDisplayState(DisplayState newDisplayState) { var currentDisplayState = state; // if the new state is the same as the current state, do nothing if (currentDisplayState == newDisplayState) { Debug.Log($"staying at {currentDisplayState} (illogical transition)"); return; } Debug.Log($"switching from {currentDisplayState} to {newDisplayState}"); var gameView = _ui.Q<VisualElement>("GameView"); var leaderboardView = _ui.Q<VisualElement>("LeaderboardView"); var accountView = _ui.Q<VisualElement>("AccountView"); switch (newDisplayState) { case DisplayState.Nothing: gameView.style.display = DisplayStyle.None; leaderboardView.style.display = DisplayStyle.None; accountView.style.display = DisplayStyle.None; break; case DisplayState.GameView: gameView.style.display = DisplayStyle.Flex; leaderboardView.style.display = DisplayStyle.None; accountView.style.display = DisplayStyle.None; break; case DisplayState.LeaderboardView: gameView.style.display = DisplayStyle.None; leaderboardView.style.display = DisplayStyle.Flex; accountView.style.display = DisplayStyle.None; break; case DisplayState.AccountView: gameView.style.display = DisplayStyle.None; leaderboardView.style.display = DisplayStyle.None; accountView.style.display = DisplayStyle.Flex; break; case DisplayState.UnassociatedState: default: throw new ArgumentOutOfRangeException(nameof(newDisplayState), newDisplayState, null); } } }