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
|
|
|
|
}
|