Files

350 lines
13 KiB
C#
Raw Permalink Normal View History

2026-06-02 18:57:47 -04:00
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
{
/// <summary>
/// Priority-specific holder of <see cref="GameStateEventHandler"/> events that need to trigger.
/// </summary>
private class GameStatePriorityEventHolder
{
/// <summary>
/// Event that houses all registered delegates to be invoked.
/// </summary>
public event GameStateEventHandler GameStateEventTrigger;
/// <summary>
/// Priority reference of this particular event holder.
/// </summary>
public GameStatePriority Priority { get; private set; }
/// <summary>
/// The list of delegates that can be invoked and yielded piece-meal rather than all at once via the assigned event.
/// </summary>
public IEnumerable<Delegate> InvocationList => GameStateEventTrigger?.GetInvocationList() ?? Array.Empty<Delegate>();
public GameStatePriorityEventHolder(GameStatePriority priority) => Priority = priority;
~GameStatePriorityEventHolder() => Clear();
/// <summary>
/// Get rid of all the attached delegates before clearing this out so no (less?) memory leaks
/// </summary>
public void Clear()
{
var invocations = InvocationList;
foreach (var i in invocations)
GameStateEventTrigger -= (GameStateEventHandler)i;
}
}
/// <summary>
/// Tracks priority events for a given state.
/// </summary>
private class GameStateEvent : IGameStateTrigger
{
/// <summary>
/// Pre-allocation capacity for required states.
/// </summary>
private const int MAXSTATECAPACITY = 2;
public IDictionary<GameStatePriority, GameStatePriorityEventHolder> PrioritizedHandlers;
private bool isCompleted;
private IList<IGameStateTrigger> requiredStates;
private IGameStateTrigger[] cachedRequiredStates;
/// <summary>
/// <see cref="GameState"/> this event represents.
/// </summary>
public GameState State { get; }
/// <summary>
/// The <see cref="GameState"/>s required before this state can be triggered.
/// </summary>
public IGameStateTrigger[] RequiredStates => cachedRequiredStates ??= requiredStates.ToArray();
/// <summary>
/// <see cref="GameState"/> 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 <see cref="GameStateController"/> until it can be triggered.
/// </summary>
public bool IsTriggered { get; private set; }
/// <summary>
/// Check to see if <see cref="GameState"/> has been completed. Will rely on Coroutines or async behavior.
/// </summary>
public bool IsComplete => IsTriggered && isCompleted;
public GameStateEvent(GameState state)
{
State = state;
requiredStates = new List<IGameStateTrigger>(MAXSTATECAPACITY);
PrioritizedHandlers = new Dictionary<GameStatePriority, GameStatePriorityEventHolder>();
var priorities = Enum.GetValues(typeof(GameStatePriority));
foreach (GameStatePriority p in priorities)
PrioritizedHandlers.Add(p, new GameStatePriorityEventHolder(p));
}
~GameStateEvent() => Clear();
/// <summary>
/// Add a state requirement for this event to follow.
/// </summary>
/// <param name="gameGameState">A state that must be complete before these events can be invoked.</param>
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();
}
/// <summary>
/// Upon being triggered, go through and wait for each registered coroutine to run and complete.
/// </summary>
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<GameState, GameStateEvent> STATE_EVENTS = new Dictionary<GameState, GameStateEvent>();
public static GameStateController Instance
{
get
{
if (INSTANCE == null)
#if UNITY_6000_0_OR_NEWER
INSTANCE = FindAnyObjectByType<GameStateController>();
#else
INSTANCE = FindObjectOfType<GameStateController>();
#endif
return INSTANCE;
}
}
public static GameStatePriority[] PriorityStates { get; } = (GameStatePriority[])Enum.GetValues(typeof(GameStatePriority));
/// <summary>
/// Attempts to run the available action if a <see cref="GameState"/> has been triggered, meaning all its
/// dependencies have been triggered as well.
/// </summary>
/// <param name="state">The target <see cref="GameState"/> state.</param>
/// <param name="handler">The <see cref="GameStateEventHandler"/> handler or delegate to run.</param>
/// <param name="priority">The <see cref="GameStatePriority"/> priority that the invocation should occur with.</param>
/// <returns>A <see cref="GameStateInvocationStatus"/> status the user can act upon, if needed.</returns>
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;
/// <summary>
/// Bootstrap all state dependencies upon awake so they can be registered first
/// </summary>
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<StateMap>();
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());
}
/// <summary>
/// Run events attached to all states running up the chain of requirements for <see cref="GameStateEvent"/>.
/// </summary>
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");
}
/// <summary>
/// Recursively traverse the required <see cref="GameStateEvent"/> that each event may have until it reaches
/// the state that needs to be run next.
/// </summary>
/// <param name="state"><see cref="GameState"/> State to start traversal from.</param>
/// <returns>The earliest <see cref="GameStateEvent"/> event whose events have not yet triggered.</returns>
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;
}
}
}