using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using BracerLib.StateManagement.Interfaces; using UnityEngine; using Debug = UnityEngine.Debug; namespace BracerLib.StateManagement { public delegate IEnumerator GameStateEventHandler(GameState state); [RequireComponent(typeof(StateMap))] [DefaultExecutionOrder(-999)] public class GameStateController : MonoBehaviour { /// /// Priority-specific holder of events that need to trigger. /// private class GameStatePriorityEventHolder { /// /// Event that houses all registered delegates to be invoked. /// public event GameStateEventHandler GameStateEventTrigger; /// /// Priority reference of this particular event holder. /// public GameStatePriority Priority { get; private set; } /// /// The list of delegates that can be invoked and yielded piece-meal rather than all at once via the assigned event. /// public IEnumerable InvocationList => GameStateEventTrigger?.GetInvocationList() ?? Array.Empty(); public GameStatePriorityEventHolder(GameStatePriority priority) => Priority = priority; ~GameStatePriorityEventHolder() => Clear(); /// /// Get rid of all the attached delegates before clearing this out so no (less?) memory leaks /// public void Clear() { var invocations = InvocationList; foreach (var i in invocations) GameStateEventTrigger -= (GameStateEventHandler)i; } } /// /// Tracks priority events for a given state. /// private class GameStateEvent : IGameStateTrigger { /// /// Pre-allocation capacity for required states. /// private const int MAXSTATECAPACITY = 2; public IDictionary PrioritizedHandlers; private bool isCompleted; private IList requiredStates; private IGameStateTrigger[] cachedRequiredStates; /// /// this event represents. /// public GameState State { get; } /// /// The s required before this state can be triggered. /// public IGameStateTrigger[] RequiredStates => cachedRequiredStates ??= requiredStates.ToArray(); /// /// is being processed. Can only be set true if all required /// triggers have been completed. If triggered prematurely, set into a queue state to be polled /// by until it can be triggered. /// public bool IsTriggered { get; private set; } /// /// Check to see if has been completed. Will rely on Coroutines or async behavior. /// public bool IsComplete => IsTriggered && isCompleted; public GameStateEvent(GameState state) { State = state; requiredStates = new List(MAXSTATECAPACITY); PrioritizedHandlers = new Dictionary(); var priorities = Enum.GetValues(typeof(GameStatePriority)); foreach (GameStatePriority p in priorities) PrioritizedHandlers.Add(p, new GameStatePriorityEventHolder(p)); } ~GameStateEvent() => Clear(); /// /// Add a state requirement for this event to follow. /// /// A state that must be complete before these events can be invoked. public void AddRequiredState(IGameStateTrigger gameGameState) { if (!requiredStates.Contains(gameGameState)) requiredStates.Add(gameGameState); } public void Reset() => IsTriggered = false; public void Clear() { Reset(); requiredStates.Clear(); foreach (var pair in PrioritizedHandlers) pair.Value.Clear(); } /// /// Upon being triggered, go through and wait for each registered coroutine to run and complete. /// public IEnumerator WaitForGameStateEvents() { if (IsTriggered || isCompleted) yield break; IsTriggered = true; yield return null; foreach (var p in PriorityStates) { var invocations = PrioritizedHandlers[p].InvocationList; foreach (var @delegate in invocations) { var handler = (GameStateEventHandler)@delegate; // TODO: Figure out a way to get status or result from this yield return handler.Invoke(State); } } isCompleted = true; } private bool AllRequiredStatesTriggered() { var result = true; foreach (var s in requiredStates) result &= s.IsTriggered; return result; } private bool AllRequiredStatesCompleted() { var result = true; foreach (var s in requiredStates) result &= s.IsComplete; return result; } } private static GameStateController INSTANCE; private static IDictionary STATE_EVENTS = new Dictionary(); public static GameStateController Instance { get { if (INSTANCE == null) #if UNITY_6000_0_OR_NEWER INSTANCE = FindAnyObjectByType(); #else INSTANCE = FindObjectOfType(); #endif return INSTANCE; } } public static GameStatePriority[] PriorityStates { get; } = (GameStatePriority[])Enum.GetValues(typeof(GameStatePriority)); /// /// Attempts to run the available action if a has been triggered, meaning all its /// dependencies have been triggered as well. /// /// The target state. /// The handler or delegate to run. /// The priority that the invocation should occur with. /// A status the user can act upon, if needed. public static GameStateInvocationStatus RunOrDefer(GameState state, GameStateEventHandler handler, GameStatePriority priority = GameStatePriority.Medium) { var triggered = IsGameStateTriggered(state) || IsGameStateComplete(state); if (triggered) { Instance.StartCoroutine(handler(state)); return GameStateInvocationStatus.Success; } if (Instance == null && STATE_EVENTS.Count == 0) return GameStateInvocationStatus.Failure; if (!STATE_EVENTS.TryGetValue(state, out var gameStateEvent)) { Debug.LogError($"GameState invocation failure: Could not defer invocation of ${handler.Method.Name}."); return GameStateInvocationStatus.Failure; } STATE_EVENTS[state].PrioritizedHandlers[priority].GameStateEventTrigger += handler; return GameStateInvocationStatus.Deferred; } public static bool IsGameStateTriggered(GameState state) => STATE_EVENTS.TryGetValue(state, out var stateEvent) && stateEvent.IsTriggered; public static bool IsGameStateComplete(GameState state) => STATE_EVENTS.TryGetValue(state, out var stateEvent) && stateEvent.IsComplete; private static void ResetStateFlags() { foreach (var entry in STATE_EVENTS) entry.Value.Reset(); } private StateMap stateMap; private bool isTriggered; private bool isCompleted; private Coroutine initCoroutine; private YieldInstruction frameWait; private Stopwatch stateStopwatch; private Stopwatch overallStopwatch; /// /// Bootstrap all state dependencies upon awake so they can be registered first /// private void Awake() { INSTANCE = this; stateStopwatch = new Stopwatch(); overallStopwatch = new Stopwatch(); if (STATE_EVENTS.Count > 0) { Debug.LogError("No GameStates recognized. Exiting state registration. You've more than likely done something very wrong."); return; } frameWait = new WaitForEndOfFrame(); stateMap = GetComponent(); foreach (var v in stateMap.GetGameStateMap()) STATE_EVENTS.Add(v, new GameStateEvent(v)); // Add state dependency map. Left-hand GameState requires right-hand GameState to be finished before proceeding // Supports multiple-required states. Read as "LeftHandState requires RightHandState." // New states can be added in this chain, but they must be linked from one end to the other // e.g. (GameState.B, GameState.A), -> GameState.B requires GameState.A // (GameState.C, GameState.B) -> GameState.C requires GameState.B foreach (var t in stateMap.GetGameStateRequirements()) STATE_EVENTS[t.Item1].AddRequiredState(STATE_EVENTS[t.Item2]); } private void Start() => TriggerInitialization(); private void OnDestroy() { INSTANCE = null; STATE_EVENTS.Clear(); foreach (var entry in STATE_EVENTS) entry.Value.Clear(); } private void TriggerInitialization() { if (initCoroutine != null) return; initCoroutine = StartCoroutine(PerformInitialization()); } /// /// Run events attached to all states running up the chain of requirements for . /// private IEnumerator PerformInitialization() { ResetStateFlags(); // Allow all other objects in scene to wake up yield return new WaitForSeconds(0.5f); var readyState = stateMap.GetReadyState(); IGameStateTriggerInvoker targetEvent; while ((targetEvent = NextAvailableGameStateEvent(readyState)) != null) { Debug.Log($"Running GameState: {targetEvent.State}"); stateStopwatch.Restart(); yield return StartCoroutine(targetEvent.WaitForGameStateEvents()); stateStopwatch.Stop(); yield return frameWait; var span = TimeSpan.FromMilliseconds(stateStopwatch.ElapsedMilliseconds); Debug.Log($"{targetEvent.State}: {span.Minutes:D2} minutes, {span.Seconds:D2} seconds, {span.Milliseconds:D3} ms"); } var overallSpan = TimeSpan.FromMilliseconds(stateStopwatch.ElapsedMilliseconds); Debug.Log($"Finish initializing all GameStates\n{overallSpan.Minutes:D2} minutes, {overallSpan.Seconds:D2} seconds, {overallSpan.Milliseconds:D3} ms"); } /// /// Recursively traverse the required that each event may have until it reaches /// the state that needs to be run next. /// /// State to start traversal from. /// The earliest event whose events have not yet triggered. private IGameStateTrigger NextAvailableGameStateEvent(GameState state) { var currentEvent = STATE_EVENTS[state]; var states = currentEvent.RequiredStates; var index = 0; while (index < states.Length) { var target = NextAvailableGameStateEvent(states[index++].State); if (target != null && !target.IsTriggered) return target; } return !currentEvent.IsTriggered ? currentEvent : null; } } }