From MonoFSM Framework
Guides usage of the MonoFSM finite state machine framework for Unity. Covers scene editing, C# script writing for Actions/Conditions, variable timers, and FSM structure traversal/export.
How this skill is triggered — by the user, by Claude, or both
Slash command
/monofsm:MonoFSMThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
以 GameObject 層級為核心的有限狀態機框架。
SKILL.md.metareferences.metareferences/components.mdreferences/components.md.metareferences/csharp-patterns.mdreferences/csharp-patterns.md.metareferences/effect-system.mdreferences/effect-system.md.metareferences/fsm-traversal.mdreferences/fsm-traversal.md.metareferences/scene-editing.mdreferences/scene-editing.md.metareferences/serialization-migration.mdreferences/serialization-migration.md.metareferences/value-source.mdreferences/value-source.md.metareferences/var-wrapper.mdreferences/var-wrapper.md.meta以 GameObject 層級為核心的有限狀態機框架。
GameObject 層級表達式:狀態、轉換、動作和條件都是場景中的 GameObject。
[FSM Root] # MonoFSMOwner
├── [VarFolder] VariableFolder # 變數區(VariableFolder + StateMachineLogic)
│ └── f_varName # VarFloat / VarEntity 等
│ └── BoundModifier # VariableFloatBoundModifier(可選)
├── [SchemaFolder] SchemaFolder
├── [StateFolder] StateFolder # StateMachineLogic 主體
│ └── [State] StateName # GeneralState
│ ├── OnStateEnterHandler # 進入時觸發 → 子層放 Action
│ │ └── [Action] XxxAction # AbstractStateAction 實作
│ ├── [Timer] TimerName # VarFloatCountDownTimer(可選)
│ └── [Transition] => Target # TransitionBehaviour
│ └── [Condition] Name # AbstractConditionBehaviour 實作
└── Context # MonoContext
直接用 MCP 工具在 Scene 中編輯 FSM,見 references/scene-editing.md。
實作 Editor 工具(匯出、視覺化、批次修改)需要 traverse MonoFSM 階層時,見 references/fsm-traversal.md。涵蓋 StateFolder 偵測、變數/狀態/轉換/條件/動作的走訪規則,以及 AnimatorPlayAction 不繼承 AbstractStateAction 等 gotcha。實作範本:MonoFSM/1_MonoFSM_Core/Editor/PrefabExporter/FsmTextExporter.cs。
public class MyAction : AbstractStateAction
{
[Required] [SerializeField] private VarEntity _target;
protected override void OnActionExecuteImplement()
{
// 動作邏輯
}
}
public class MyCondition : AbstractConditionBehaviour
{
protected override bool IsValid => /* 條件邏輯 */;
}
AbstractDescriptionBehaviour 預設 public virtual string Description => GetType().Name;,State 樹列表上只看到類別名。自訂 Action / Condition 應該 override Description,組合關鍵欄位成一句話,方便在 Inspector / State 樹一眼看出每個節點在做什麼,不用點進去看欄位。
// HasStateTagCondition
public override string Description => $"Has Tag [{(_tag != null ? _tag.name : "?")}]";
// IsStateCondition
public override string Description => $"Is {_targetState?.name}";
要點:
?(避免 NRE,並提示尚未設定)[Action] / [Condition] 之類的 tag,父類的 DescriptionTag 會自動加上VarFloatWrapper 等 wrapper 已有 Description 屬性可以直接組合進去可參考:1_MonoFSM_Core/Runtime/1_Conditions/HasStateTagCondition.cs、IsStateCondition.cs
AbstractStateAction(掛 IActionParent、event 觸發)與 AbstractRenderBehaviour(掛 IRenderInvoker、每 render frame 觸發)都繼承 AbstractDescriptionBehaviour,但父物件契約與 Condition 系統完全不同(Action 用 _conditions[],Render 用 _conditionGroup,HasError 各自檢查父型別)。
C# 單一繼承,不要在同一個 class 上 implement IRenderBehaiour 硬兼容兩者:不管選哪個 base,另一邊的 HasError 會因父物件型別不符而報錯,掛載位置被綁死。
正解:把核心邏輯抽成一個 [Serializable] class,做兩個薄 wrapper:
[Serializable]
public class XxxWriter // 共用邏輯 + 欄位 + Description
{
public string Description => ...;
public void Write(Object byWho) { ... }
}
public class XxxAction : AbstractStateAction
{
[HideLabel] [InlineProperty] public XxxWriter _writer = new();
public override string Description => _writer.Description;
protected override void OnActionExecuteImplement() => _writer.Write(this);
}
public class XxxRender : AbstractRenderBehaviour
{
[HideLabel] [InlineProperty] public XxxWriter _writer = new();
public override string Description => _writer.Description;
public override void OnEnterRenderImplement() => _writer.Write(this);
public override void OnRenderImplement() => _writer.Write(this);
}
[HideLabel][InlineProperty] 讓 writer 欄位在 Inspector 攤平,看起來就像直接寫在 wrapper 上。
參考實作:1_MonoFSM_Core/Runtime/Action/VariableAction/(PositionToVarVector3Writer + SetVarVector3FromTargetAction / SetVarVector3FromTargetRender)
[Auto] // GetComponent<T>()
[AutoParent] // GetComponentInParent<T>()
[AutoChildren] // GetComponentsInChildren<T>()
[AutoChildren(DepthOneOnly = true)] // 僅直接子物件
[Required] // 必填欄位(Inspector 警告)
[CompRef] // 標記為組件引用
[DropDownRef] // 下拉選擇(需手動在 Inspector 設定,MCP 無法設定此類型)
[SOConfig("子資料夾名")] // ScriptableObject 欄位用,提供 Create 按鈕與路徑選擇器
[SOConfig] 的 Drawer(SOConfigAttributeDrawer)使用 IList.Add() 新增資產,因此:
List<T>,不可用 T[](原生陣列大小固定,Add() 會拋 NotSupportedException)[SerializeField] [SOConfig("StateTags")] private List<StateTag> _stateTags = new();狀態有 Priority 屬性,高優先級狀態不會被低優先級狀態打斷。
SerializeField 和 public field 以底線開頭:_myField[Range(0f, 1f)]),不用 0~100定義「誰可以對誰造成效果」的互動系統,見 references/effect-system.md。
AbstractValueSource<T> 泛型基類用於每幀計算並提供值(方向、位置、輸入等)。Variable 系統(VarFloat、VarVector3 等)的 IsValueExist 用於判斷 runtime 有效值。詳見 references/value-source.md。
需要「目標位置」時,用 TargetPositionResolver(namespace MonoValueProvider,在 Core),不要在欄位寫死 Transform。它是 [Serializable],統一解析 VarVector3 / VarTransform / VarEntity 三種來源(優先序:Vector3 > Transform > Entity,各自 IsValueExist 才採用)。常用 API:GetTargetPosition(fallback)、ResolvedTransform、HasTarget、ActiveSource、ClearPositionTarget()。用法:欄位宣告 [InlineProperty][HideLabel] public TargetPositionResolver _source = new();,取值前先判 HasTarget。位置:1_MonoFSM_Core/Runtime/0_Pattern/DataProvider/EntityProvider/ValueSource/TargetPositionResolver.cs。
VarFloatWrapper / VarIntWrapper 等 [Serializable] 包裝類,讓欄位在 Inspector 二選一:綁一個 Var 引用,或直接填常數。取值一律用 .Value,宣告預設值用 new(...)(如 private VarIntWrapper _index = new(-1)),namespace 為 MonoFSM.Variable。詳見 references/var-wrapper.md。
核心精神:用 Var 變數作為組件之間的溝通介面,而非直接呼叫 method。
當一個系統(如角色移動)需要被多個 State/Action 控制時:
VarVector3 _moveDirection、VarFloat _speedMultiplier、VarVector3 _externalForce)範例:
_moveDirection_moveDirection 設為 (0,0,0) → 角色停下_speedMultiplier 設為 0.5 → 角色減速_externalForce 設為擊退向量 → Controller 自動衰減好處:
何時該寫專用 Action:當操作涉及複雜邏輯(如計算追蹤方向、路徑規劃)而非單純設值時。
參考實作:SimpleChController(MonoFSM-Pro/Runtime/GamePlay/Source/Characters/)
Unity 回調(OnCollisionEnter、OnEnable、OnDisable、OnTriggerEnter 等)的觸發時機不可控:可能在同一幀多次觸發、可能在 Simulate 執行順序之外發生。不要在回調中直接執行邏輯或修改狀態,改為 cache 資料/flag,在 Simulate() 統一處理。
原則:
Simulate() 開頭檢查 cache,統一處理一次後清除if (_cached) return;)// 回調只 cache
private bool _cached;
private SomeData _pendingData;
private void OnXxx(...) // OnCollisionEnter / OnEnable / OnTriggerEnter 等
{
if (_cached) return;
_pendingData = ...;
_cached = true;
}
public void Simulate(float deltaTime)
{
if (_cached) { /* 處理 _pendingData */ _cached = false; }
// ... 正常邏輯 ...
}
參考實作:
SimpleFlyingCharacter(MonoFSM-Pro/Runtime/GamePlay/Source/Characters/)OnEnableInvoker(MonoFSM/1_MonoFSM_Core/Runtime/Module/)所有 raycast 應透過 IRaycastProcessor(namespace MonoFSM.PhysicsWrapper),不要直接呼叫 Physics.Raycast。讓專案可以集中替換實作(如紀錄、debug overlay、Fusion lag compensation 等)。
取得方式:
private IRaycastProcessor _raycastProcessor;
private IRaycastProcessor RaycastProcessor =>
_raycastProcessor ??= WorldUpdateSimulator
.GetWorldUpdateSimulator(gameObject)
?.GetCompCache<IRaycastProcessor>();
使用時建議保留 Physics.Raycast fallback(當 simulator 不存在於 prefab preview 或 edit-time 時仍能運作):
if (RaycastProcessor != null)
RaycastProcessor.Raycast(origin, dir, out hit, dist, mask, QueryTriggerInteraction.Ignore);
else
Physics.Raycast(origin, dir, out hit, dist, mask, QueryTriggerInteraction.Ignore);
同介面群還有 ISphereCastProcessor / ICapsuleRaycastProcessor / IBoxCastProcessor,需要 SphereCast / BoxCast 時用對應介面。
參考實作:MonoFSM/MonoFSM_Physics/Runtime/Interact/SpatialDetection/Raycast/MyRaycast.cs
介面定義:MonoFSM/MonoFSM_Physics/Runtime/Interact/SpatialDetection/Raycast/IRaycastProcessor.cs
撰寫 MonoFSM 相關 C# 程式碼時的 GC 避免技巧,見 references/csharp-patterns.md。
需要把已序列化的欄位改成不同型別(如 VarFloat 直接參照 → VarFloatWrapper)又不想掉 prefab reference 時,見 references/serialization-migration.md。涵蓋為何直接改型別一定掉 ref、legacy 欄位 + FormerlySerializedAs 接舊資料、LoadPrefabContents 批次遷移、驗證與清孤兒資料的完整 6 步流程。
npx claudepluginhub red-candle-games-co-ltd/monofsm --plugin monofsmUnity state and behavior system architecture. FSM, Hierarchical FSM, Behavior Trees, stack-based state machines, Animator-vs-code decisions, state machine testing. DECISION format: WHEN/DECISION/SCAFFOLD/GOTCHA. Based on Unity 6.3 LTS.
Implements state machines in Godot 4.3+ using enum-based, node-based, and resource-based FSM patterns with trade-offs for each complexity level.
Guides the update lifecycle system for MonoObj, covering WorldUpdateSimulator's loop architecture, Simulate/Render per-frame logic, and registration/unregistration of IUpdateSimulate, IBeforeSimulate, IAfterSimulate, IRenderSimulate implementations.