Initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
using BracerLib.Utility;
|
||||
|
||||
namespace BracerLib.StateManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// Representation of a GameState enum that can be expanded without issue
|
||||
/// and into other classes of a base GameState.
|
||||
/// </summary>
|
||||
public class GameState : Enumeration
|
||||
{
|
||||
public GameState(int id, string name) : base(id, name) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c91277dea27634a4a849d3f5a0013533
|
||||
@@ -0,0 +1,349 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e7c95ba8e0d6d24b8d1492525d538ae
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace BracerLib.StateManagement
|
||||
{
|
||||
public enum GameStateInvocationStatus
|
||||
{
|
||||
Failure,
|
||||
Deferred,
|
||||
Success
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21f5fc392c0ba374b9eb8f0950b6a799
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace BracerLib.StateManagement
|
||||
{
|
||||
public enum GameStatePriority
|
||||
{
|
||||
Highest,
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
Lowest
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 140bdf34ce09b7b47b9a681fb52bf8b6
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79e978ffe1b3a3249a3a451d36aeb3c0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BracerLib.StateManagement.Interfaces
|
||||
{
|
||||
public interface IGameStateIdentifier
|
||||
{
|
||||
GameState State { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b426d591d7141ce9762bc4a762a865d
|
||||
timeCreated: 1697030279
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace BracerLib.StateManagement.Interfaces
|
||||
{
|
||||
public interface IGameStateTrigger : IGameStateTriggerStatus, IGameStateTriggerInvoker { }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8400e946acf14bfd89ee59f69c4a44ae
|
||||
timeCreated: 1697030243
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace BracerLib.StateManagement.Interfaces
|
||||
{
|
||||
public interface IGameStateTriggerInvoker : IGameStateIdentifier
|
||||
{
|
||||
IEnumerator WaitForGameStateEvents();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90d8238af53645aca1a06d611b4c15c4
|
||||
timeCreated: 1697030264
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace BracerLib.StateManagement.Interfaces
|
||||
{
|
||||
public interface IGameStateTriggerStatus
|
||||
{
|
||||
bool IsTriggered { get; }
|
||||
bool IsComplete { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42365a62862043f693a71a439c00a693
|
||||
timeCreated: 1697030254
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BracerLib.StateManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// Base StateMap component that can be extended and used for start up or other state loads.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-1000)]
|
||||
public class StateMap : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Method to send back the number of states to be utilized.
|
||||
/// </summary>
|
||||
public virtual GameState[] GetGameStateMap() => Array.Empty<GameState>();
|
||||
|
||||
/// <summary>
|
||||
/// The running requirement set for all <see cref="GameState"/> state entries.
|
||||
/// </summary>
|
||||
public virtual Tuple<GameState, GameState>[] GetGameStateRequirements() => Array.Empty<Tuple<GameState, GameState>>();
|
||||
|
||||
/// <summary>
|
||||
/// What is considered the last game ready state to run before starting the game.
|
||||
/// </summary>
|
||||
public virtual GameState GetReadyState() => null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3f8dd6850d6fa144b4aabaceaadfeac
|
||||
Reference in New Issue
Block a user