using System; using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; namespace AlicizaX.Fsm { internal interface IFsmRunner : IDisposable, IMemory { int Id { get; } string DisplayName { get; } UnityEngine.Object Owner { get; } void Tick(float deltaTime); } public delegate void StateEnter(T bb) where T : class, IMemory; public delegate int StateUpdate(T bb) where T : class, IMemory; // -1 = stay public delegate void StateExit(T bb) where T : class, IMemory; public delegate bool Condition(T bb) where T : class, IMemory; // ===================== StateFunc ===================== public sealed class StateFunc where T : class, IMemory { public StateEnter OnEnter; public StateUpdate OnUpdate; public StateExit OnExit; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static StateFunc Make(StateEnter enter = null, StateUpdate update = null, StateExit exit = null) { return new StateFunc { OnEnter = enter ?? EmptyEnter, OnUpdate = update ?? Stay, OnExit = exit ?? EmptyExit, }; } private static void EmptyEnter(T bb) { } private static int Stay(T bb) => -1; private static void EmptyExit(T bb) { } } // ===================== Transition ===================== public struct Transition where T : class, IMemory { public int From; public int To; public Condition Cond; // may be null public int Priority; // smaller first public float Timeout; // >0 enables timeout public Transition(int from, int to, Condition cond, int priority = 0, float timeout = 0f) { From = from; To = to; Cond = cond; Priority = priority; Timeout = timeout; } } internal struct TransitionIndex { public int Start; // start index in _trans public int Length; // number of entries } public readonly struct FsmConfig where T : class, IMemory { public readonly StateFunc[] Funcs; // [state] -> funcs public readonly Transition[] Transitions; // flat transitions list public readonly int StateCount; // >0 public readonly int DefaultState; // initial state public FsmConfig(StateFunc[] func, Transition[] transition, int defaultState = -1) { Funcs = func; Transitions = transition; StateCount = Funcs.Length; DefaultState = defaultState; if (Funcs == null || Funcs.Length == 0) throw new ArgumentException("Funcs must not be null/empty"); if (StateCount <= 0) StateCount = Funcs.Length; if ((uint)DefaultState >= (uint)StateCount) throw new ArgumentOutOfRangeException(nameof(DefaultState)); } } public sealed class Fsm : IFsmRunner where T : class, IMemory { private static int _nextId; public static Fsm Rent(FsmConfig cfg, T blackboard, UnityEngine.Object owner = null, Func stateNameGetter = null) { Fsm fsm = MemoryPool.Acquire>(); fsm.Init(cfg, blackboard, owner, stateNameGetter); return fsm; } public static void Return(Fsm fsm) { if (fsm == null) return; MemoryPool.Release(fsm); } // ---- Instance ---- private StateFunc[] _funcs; private Transition[] _trans; private TransitionIndex[] _index; private int _stateCount; private T _bb; private bool _disposed; public int Id { get; private set; } public string DisplayName { get; private set; } public int Current { get; private set; } public float TimeInState { get; private set; } public bool Initialized { get; private set; } public UnityEngine.Object Owner { get; private set; } private Func _stateNameGetter; public T Blackboard { get => _bb; } public Fsm() { } private void Init(FsmConfig cfg, T blackboard, UnityEngine.Object owner, Func stateNameGetter) { Id = Interlocked.Increment(ref _nextId); DisplayName = typeof(T).Name; _funcs = cfg.Funcs; _trans = cfg.Transitions ?? Array.Empty>(); _index = BuildIndex(_trans, cfg.StateCount); _stateCount = cfg.StateCount; _bb = blackboard; Owner = owner; _stateNameGetter = stateNameGetter; Current = cfg.DefaultState; TimeInState = 0f; Initialized = false; _disposed = false; #if UNITY_EDITOR FSMDebugger.Register(this, typeof(T)); FSMDebugger.BindProvider(Id, owner, BlackboardSnapshot, _stateNameGetter); #endif } private object BlackboardSnapshot() => _bb; private static TransitionIndex[] BuildIndex(Transition[] t, int stateCount) { if (t.Length > 1) { Array.Sort(t, (a, b) => { int f = a.From.CompareTo(b.From); return (f != 0) ? f : a.Priority.CompareTo(b.Priority); }); } var idx = new TransitionIndex[stateCount]; int cur = 0; while (cur < t.Length) { int from = t[cur].From; int end = cur + 1; while (end < t.Length && t[end].From == from) end++; if ((uint)from < (uint)stateCount) { idx[from].Start = cur; idx[from].Length = end - cur; } cur = end; } return idx; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset(int state) { if (_disposed) return; if ((uint)state >= (uint)_stateCount) return; if (Initialized) _funcs[Current].OnExit(_bb); Current = state; TimeInState = 0f; _funcs[Current].OnEnter(_bb); Initialized = true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Tick(float deltaTime) { if (_disposed) return; bool flag = false; if (!Initialized) { Reset(Current); flag = true; } if (!flag) { TimeInState += deltaTime; // 1) state internal update (may suggest next state) int suggested = _funcs[Current].OnUpdate(_bb); if (suggested >= 0 && suggested != Current) { ChangeState(suggested); } else { // 2) transitions (timeout first, then condition) ref readonly TransitionIndex ti = ref _index[Current]; for (int i = 0; i < ti.Length; i++) { ref readonly var tr = ref _trans[ti.Start + i]; bool timeoutOk = (tr.Timeout > 0f && TimeInState >= tr.Timeout); bool condOk = (!timeoutOk && tr.Cond != null && tr.Cond(_bb)); if (timeoutOk || condOk) { ChangeState(tr.To); break; } } } } #if UNITY_EDITOR if (FSMDebugger.Enabled) FSMDebugger.Track(this); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ChangeState(int next) { if ((uint)next >= (uint)_stateCount || next == Current) return; _funcs[Current].OnExit(_bb); Current = next; TimeInState = 0f; _funcs[Current].OnEnter(_bb); } public void Dispose() { if (_disposed) return; _disposed = true; #if UNITY_EDITOR FSMDebugger.Unregister(Id); #endif } public void Clear() { _funcs = null; _trans = null; _index = null; _stateCount = 0; _stateNameGetter = null; Owner = null; Initialized = false; TimeInState = 0f; DisplayName = null; MemoryPool.Release(_bb); _bb = null; } } #if UNITY_EDITOR // ===================== FSMDebugger (Editor Only) ===================== public static class FSMDebugger { public sealed class Entry { public int Id; public string Name; // Blackboard type name public int StateIndex; public double LastSeenEditorTime; public float TimeInState; public FieldInfo[] Fields; // cached fields of blackboard public WeakReference Owner; // may be null public Func BlackboardGetter; // snapshot getter (only queried when painting) public Func StateNameGetter; // may be null } private static readonly Dictionary _entries = new Dictionary(256); private const double PRUNE_INTERVAL_SEC = 3.0; private const double STALE_SEC = 10.0; private static double _lastPruneCheck; public static bool Enabled { get; private set; } = true; static FSMDebugger() { UnityEditor.EditorApplication.update += PruneLoop; } public static void SetEnabled(bool enabled) => Enabled = enabled; private static void PruneLoop() { double now = UnityEditor.EditorApplication.timeSinceStartup; if (now - _lastPruneCheck < PRUNE_INTERVAL_SEC) return; _lastPruneCheck = now; var toRemove = ListPool.Get(); foreach (var kv in _entries) { var e = kv.Value; bool deadOwner = false; if (e.Owner != null && e.Owner.TryGetTarget(out var target)) { deadOwner = target == null; } else if (e.Owner != null) { deadOwner = true; } bool stale = (now - e.LastSeenEditorTime) > STALE_SEC; if (deadOwner || stale) toRemove.Add(kv.Key); } for (int i = 0; i < toRemove.Count; i++) _entries.Remove(toRemove[i]); ListPool.Release(toRemove); } internal static void Register(in Fsm fsm, Type blackboardType) where T : class, IMemory { var id = fsm.Id; if (_entries.ContainsKey(id)) return; var fields = blackboardType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); _entries[id] = new Entry { Id = id, Name = blackboardType.Name, StateIndex = -1, LastSeenEditorTime = UnityEditor.EditorApplication.timeSinceStartup, TimeInState = 0f, Fields = fields, Owner = null, BlackboardGetter = null, StateNameGetter = null }; } internal static void BindProvider(int id, UnityEngine.Object owner, Func bbGetter, Func stateNameGetter) { if (!_entries.TryGetValue(id, out var e)) return; e.Owner = owner != null ? new WeakReference(owner) : null; e.BlackboardGetter = bbGetter; e.StateNameGetter = stateNameGetter; } internal static void Track(in Fsm fsm) where T : class, IMemory { if (!_entries.TryGetValue(fsm.Id, out var e)) return; e.StateIndex = fsm.Current; e.TimeInState = fsm.TimeInState; e.LastSeenEditorTime = UnityEditor.EditorApplication.timeSinceStartup; } public static void Unregister(int id) => _entries.Remove(id); public static IReadOnlyDictionary Entries => _entries; // --- tiny List pool --- private static class ListPool { private static readonly Stack> _pool = new Stack>(8); public static List Get() => _pool.Count > 0 ? _pool.Pop() : new List(16); public static void Release(List list) { list.Clear(); _pool.Push(list); } } } #endif }