350 lines
13 KiB
C#
350 lines
13 KiB
C#
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|