com.alicizax.unity.framework/Runtime/FSM/HighPerfFSM.cs

415 lines
13 KiB
C#
Raw Normal View History

2025-09-05 19:46:30 +08:00
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>(T bb) where T : class, IMemory;
public delegate int StateUpdate<T>(T bb) where T : class, IMemory; // -1 = stay
public delegate void StateExit<T>(T bb) where T : class, IMemory;
public delegate bool Condition<T>(T bb) where T : class, IMemory;
// ===================== StateFunc =====================
public sealed class StateFunc<T> where T : class, IMemory
{
public StateEnter<T> OnEnter;
public StateUpdate<T> OnUpdate;
public StateExit<T> OnExit;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static StateFunc<T> Make(StateEnter<T> enter = null, StateUpdate<T> update = null, StateExit<T> exit = null)
{
return new StateFunc<T>
{
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<T> where T : class, IMemory
{
public int From;
public int To;
public Condition<T> Cond; // may be null
public int Priority; // smaller first
public float Timeout; // >0 enables timeout
public Transition(int from, int to, Condition<T> 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<T> where T : class, IMemory
{
public readonly StateFunc<T>[] Funcs; // [state] -> funcs
public readonly Transition<T>[] Transitions; // flat transitions list
public readonly int StateCount; // >0
public readonly int DefaultState; // initial state
public FsmConfig(StateFunc<T>[] func, Transition<T>[] 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<T> : IFsmRunner where T : class, IMemory
{
private static int _nextId;
public static Fsm<T> Rent(FsmConfig<T> cfg, T blackboard, UnityEngine.Object owner = null, Func<int, string> stateNameGetter = null)
{
Fsm<T> fsm = MemoryPool.Acquire<Fsm<T>>();
fsm.Init(cfg, blackboard, owner, stateNameGetter);
return fsm;
}
public static void Return(Fsm<T> fsm)
{
if (fsm == null) return;
MemoryPool.Release(fsm);
}
// ---- Instance ----
private StateFunc<T>[] _funcs;
private Transition<T>[] _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<int, string> _stateNameGetter;
public T Blackboard
{
get => _bb;
}
public Fsm()
{
}
private void Init(FsmConfig<T> cfg, T blackboard, UnityEngine.Object owner, Func<int, string> stateNameGetter)
{
Id = Interlocked.Increment(ref _nextId);
DisplayName = typeof(T).Name;
_funcs = cfg.Funcs;
_trans = cfg.Transitions ?? Array.Empty<Transition<T>>();
_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>[] 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;
2025-09-10 16:06:07 +08:00
bool flag = false;
2025-09-05 19:46:30 +08:00
if (!Initialized)
{
Reset(Current);
2025-09-10 16:06:07 +08:00
flag = true;
2025-09-05 19:46:30 +08:00
}
2025-09-10 16:06:07 +08:00
if (!flag)
2025-09-05 19:46:30 +08:00
{
2025-09-10 16:06:07 +08:00
TimeInState += deltaTime;
// 1) state internal update (may suggest next state)
int suggested = _funcs[Current].OnUpdate(_bb);
if (suggested >= 0 && suggested != Current)
2025-09-05 19:46:30 +08:00
{
2025-09-10 16:06:07 +08:00
ChangeState(suggested);
}
else
{
// 2) transitions (timeout first, then condition)
ref readonly TransitionIndex ti = ref _index[Current];
for (int i = 0; i < ti.Length; i++)
2025-09-05 19:46:30 +08:00
{
2025-09-10 16:06:07 +08:00
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;
}
2025-09-05 19:46:30 +08:00
}
}
}
2025-09-10 16:06:07 +08:00
2025-09-05 19:46:30 +08:00
#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<UnityEngine.Object> Owner; // may be null
public Func<object> BlackboardGetter; // snapshot getter (only queried when painting)
public Func<int, string> StateNameGetter; // may be null
}
private static readonly Dictionary<int, Entry> _entries = new Dictionary<int, Entry>(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<int>.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<int>.Release(toRemove);
}
internal static void Register<T>(in Fsm<T> 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<object> bbGetter, Func<int, string> stateNameGetter)
{
if (!_entries.TryGetValue(id, out var e))
return;
e.Owner = owner != null ? new WeakReference<UnityEngine.Object>(owner) : null;
e.BlackboardGetter = bbGetter;
e.StateNameGetter = stateNameGetter;
}
internal static void Track<T>(in Fsm<T> 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<int, Entry> Entries => _entries;
// --- tiny List pool ---
private static class ListPool<TItem>
{
private static readonly Stack<List<TItem>> _pool = new Stack<List<TItem>>(8);
public static List<TItem> Get() => _pool.Count > 0 ? _pool.Pop() : new List<TItem>(16);
public static void Release(List<TItem> list)
{
list.Clear();
_pool.Push(list);
}
}
}
#endif
}