Skip to content

DFTGames/GTFSM

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Generically Typed Finite State Machine (GTFSM) for Unity

Version 7.0.1 - Production Ready

A high-performance, type-safe finite state machine system for Unity that eliminates GetComponent calls, provides per-instance state management, and includes advanced features like transition guards, state priorities, and event notifications.

DOI


Table of Contents

  1. Features
  2. Installation
  3. Quick Start
  4. Core Concepts
  5. API Reference
  6. Advanced Features
  7. Best Practices
  8. Examples
  9. Troubleshooting
  10. Performance
  11. License

Features

Core Features

? Type-Safe Generic Implementation

  • No casting required
  • Compile-time type checking
  • Full IntelliSense support

? Zero GetComponent Calls

  • All components cached on initialization
  • No reflection during runtime
  • Maximum performance

? Per-Instance State Management

  • Each GameObject has its own state instances
  • No shared state between objects
  • Thread-safe for multiple AI agents

? Complete Lifecycle Support

void Init(TClient client);         // Once on creation
bool CanEnter(TClient client);     // Validate before entering
void OnEnter(TClient client);      // Setup on entry
Type Execute(TClient client);       // Every Update
void ExecuteFixed(TClient client);  // Every FixedUpdate
void ExecuteLate(TClient client);   // Every LateUpdate
bool CanExit(TClient client);      // Validate before exiting
void OnExit(TClient client);        // Cleanup on exit

Advanced Features

? Transition Guards

  • Validate preconditions before entering states
  • Prevent invalid state transitions
  • Helpful error messages

? State Priority System

  • Four priority levels: Low, Normal, High, Critical
  • Lower priority cannot interrupt higher priority
  • Prevents animation glitches

? State Transition Events

  • Subscribe to state changes
  • Integrate with UI, audio, analytics
  • Decoupled system architecture

? Interruption Control

  • Protect critical states from interruption
  • Global transitions for emergency states
  • Configurable per-state

? Unity Integration

  • Real-time Inspector debugging
  • Scene view Gizmos
  • Enable/disable debug per-instance

? Performance Optimized

  • No boxing/unboxing
  • Zero allocations in hot paths
  • Type-based lookups (no strings)

Installation

From Unity Package

  1. Import the GTFSM package into your Unity project
  2. Files will be in Assets/DFT_Games/GTFSM/
  3. Start using by implementing IGTFSMClient<T>

Required Files

  • Scripts/GTFSM.cs - Core FSM manager
  • Scripts/Declarations.cs - Interfaces and base classes
  • Demo/ - Example implementation (optional)

Requirements

  • Unity 2019.4 or later
  • .NET Framework 4.7.1 or later
  • No external dependencies

Quick Start

Step 1: Create Your Client Class

using DFTGames.Tools.GTFSM;
using UnityEngine;
using UnityEngine.AI;

public class Enemy : MonoBehaviour, IGTFSMClient<Enemy>
{
    // FSM Required Properties
    public GTFSMState<Enemy> CurrentState { get; set; }
    public GTFSMState<Enemy> PreviousState { get; set; }
    public FSMDebugInfo<Enemy> DebugInfo { get; set; }
    
    // Your game-specific properties
    public NavMeshAgent Agent { get; private set; }
    public Transform[] patrolPath;
    
    private void Awake()
    {
        // Cache components
        Agent = GetComponent<NavMeshAgent>();
        
        // Initialize debug info (optional)
        DebugInfo = new FSMDebugInfo<Enemy>();
    }
    
    private void Start()
    {
        // Register states
        GTFSM.RegisterState<Enemy, PatrolState>(this);
        GTFSM.RegisterState<Enemy, AttackState>(this);
        
        // Set initial state
        GTFSM.SetState(this, typeof(PatrolState));
    }
    
    private void Update()
    {
        GTFSM.Execute(this);
    }
    
    private void OnDestroy()
    {
        GTFSM.Cleanup(this);
    }
    
    // Implement notification (optional - for events)
    public void NotifyStateTransition(string fromState, string toState)
    {
        Debug.Log($"State: {fromState} -> {toState}");
    }
}

Step 2: Create Your States

using DFTGames.Tools.GTFSM;
using System;
using UnityEngine;

public class PatrolState : GTFSMStateBase<Enemy>
{
    public override void OnEnter(Enemy client)
    {
        base.OnEnter(client);
        // Setup patrol behavior
    }
    
    public override Type Execute(Enemy client)
    {
        // Update patrol logic
        
        // Transition to attack when condition met
        if (PlayerDetected(client))
            return typeof(AttackState);
        
        return null; // Stay in patrol
    }
    
    public override void OnExit(Enemy client)
    {
        // Cleanup patrol behavior
    }
}

Step 3: Run and Test

Press Play in Unity. Your Enemy will now use the FSM to manage states!


Core Concepts

Client

A Client is any MonoBehaviour that uses the FSM. It must:

  • Implement IGTFSMClient<T> where T is itself
  • Store current and previous state references
  • Call GTFSM methods in Unity lifecycle

Example:

public class Player : MonoBehaviour, IGTFSMClient<Player>
{
    // Implementation
}

State

A State represents a behavior or mode. It must:

  • Implement GTFSMState<TClient> or inherit from GTFSMStateBase<TClient>
  • Define behavior in Execute() method
  • Return next state Type or null

Example:

public class IdleState : GTFSMStateBase<Player>
{
    public override Type Execute(Player client)
    {
        if (Input.GetKey(KeyCode.W))
            return typeof(MoveState);
        
        return null;
    }
}

State Machine

The GTFSM class manages:

  • State registration per client instance
  • State transitions with validation
  • Lifecycle method calls
  • Priority and guard checking

API Reference

GTFSM Class (Static Methods)

State Registration

// Register a state for a client
TState RegisterState<TClient, TState>(TClient client)
    where TClient : class, IGTFSMClient<TClient>
    where TState : class, GTFSMState<TClient>, new()

Example:

GTFSM.RegisterState<Enemy, PatrolState>(this);

State Execution

// Execute current state (call in Update)
void Execute<TClient>(TClient client)

// Execute current state (call in FixedUpdate)
void ExecuteFixed<TClient>(TClient client)

// Execute current state (call in LateUpdate)
void ExecuteLate<TClient>(TClient client)

Example:

private void Update()
{
    GTFSM.Execute(this);
}

State Transitions

// Set state by Type (returns success/fail)
bool SetState<TClient>(TClient client, Type stateType)

// Set state by instance
bool SetState<TClient>(TClient client, GTFSMState<TClient> state)

// Transition to previous state
bool TransitionToPreviousState<TClient>(TClient client)

Example:

if (GTFSM.SetState(this, typeof(AttackState)))
{
    Debug.Log("Transitioned to attack!");
}

State Queries

// Check if state is registered
bool HasState<TClient>(TClient client, Type stateType)

// Get registered state by Type
GTFSMState<TClient> GetState<TClient>(TClient client, Type stateType)

// Get registered state by name
GTFSMState<TClient> GetState<TClient>(TClient client, string stateName)

Example:

if (GTFSM.HasState(this, typeof(FleeState)))
{
    GTFSM.SetState(this, typeof(FleeState));
}

Global Transitions

// Set a global transition (interrupts current state)
void SetGlobalTransition<TClient>(TClient client, Type stateType)

// Clear global transition
void ClearGlobalTransition<TClient>(TClient client)

Example:

// Immediately interrupt current state to stun
GTFSM.SetGlobalTransition<Enemy>(this, typeof(StunState));

Cleanup

// Clean up all states for a client (call in OnDestroy)
void Cleanup<TClient>(TClient client)

Example:

private void OnDestroy()
{
    GTFSM.Cleanup(this);
}

GTFSMState Interface

public interface GTFSMState<TClient>
{
    string StateName { get; set; }
    StatePriority Priority { get; }
    bool AllowInterruption { get; }
    
    void Init(TClient client);
    bool CanEnter(TClient client);
    void OnEnter(TClient client);
    Type Execute(TClient client);
    void ExecuteFixed(TClient client);
    void ExecuteLate(TClient client);
    bool CanExit(TClient client);
    void OnExit(TClient client);
}

GTFSMStateBase Class

Convenience base class with default implementations:

public abstract class GTFSMStateBase<TClient> : GTFSMState<TClient>
{
    // Auto-tracked timer
    protected float stateTimer;
    
    // Helper to update timer
    protected void UpdateTimer(float deltaTime);
    
    // Virtual properties (override as needed)
    public virtual StatePriority Priority => StatePriority.Normal;
    public virtual bool AllowInterruption => true;
    
    // Virtual guards (override to add validation)
    public virtual bool CanEnter(TClient client) => true;
    public virtual bool CanExit(TClient client) => true;
    
    // Must override
    public abstract Type Execute(TClient client);
}

Advanced Features

1. Transition Guards

Prevent invalid transitions by validating preconditions:

public class AttackState : GTFSMStateBase<Enemy>
{
    // Check before entering
    public override bool CanEnter(Enemy client)
    {
        if (!client.HasWeapon)
        {
            Debug.LogWarning("Cannot attack: No weapon!");
            return false;
        }
        
        if (!client.TargetInRange)
        {
            Debug.LogWarning("Cannot attack: Target out of range!");
            return false;
        }
        
        return true;
    }
    
    // Check before exiting
    public override bool CanExit(Enemy client)
    {
        // Don't allow exit until attack animation completes
        return stateTimer >= MIN_ATTACK_DURATION;
    }
}

Benefits:

  • Automatic validation
  • Clear error messages
  • Prevents bugs early

2. State Priority System

Control which states can interrupt others:

public enum StatePriority
{
    Low = 0,      // Idle, Patrol
    Normal = 1,   // Move, Attack
    High = 2,     // Special attacks
    Critical = 3  // Death, Cutscenes
}

Usage:

public class PatrolState : GTFSMStateBase<Enemy>
{
    // Low priority - can be interrupted by anything
    public override StatePriority Priority => StatePriority.Low;
}

public class AttackState : GTFSMStateBase<Enemy>
{
    // High priority - only High or Critical can interrupt
    public override StatePriority Priority => StatePriority.High;
}

public class DeathState : GTFSMStateBase<Enemy>
{
    // Critical priority - nothing can interrupt
    public override StatePriority Priority => StatePriority.Critical;
}

Interruption Rules:

Current Priority Can be interrupted by
Low Normal, High, Critical
Normal Normal, High, Critical
High High, Critical
Critical Critical only

3. State Transition Events

React to state changes from external systems:

public class Enemy : MonoBehaviour, IGTFSMClient<Enemy>
{
    // Define your event
    public event Action<string, string> OnStateTransition;
    
    // Implement notification
    public void NotifyStateTransition(string fromState, string toState)
    {
        // Fire event
        OnStateTransition?.Invoke(fromState, toState);
        
        // Direct handling
        Debug.Log($"State changed: {fromState} -> {toState}");
    }
    
    private void Start()
    {
        // Subscribe to transitions
        OnStateTransition += OnStateChanged;
    }
    
    private void OnStateChanged(string from, string to)
    {
        // Update UI
        UpdateHealthBar(to);
        
        // Play sounds
        PlayStateSound(to);
        
        // Track analytics
        Analytics.StateTransition(from, to);
    }
    
    private void OnDestroy()
    {
        OnStateTransition -= OnStateChanged;
        GTFSM.Cleanup(this);
    }
}

Use Cases:

  • UI updates (health bars, ability icons)
  • Audio triggering (footsteps, attack sounds)
  • VFX spawning (particle effects)
  • Analytics tracking
  • Achievement monitoring

4. Interruption Control

Protect critical states from global transitions:

public class CutsceneState : GTFSMStateBase<Player>
{
    public override StatePriority Priority => StatePriority.Critical;
    
    // Cannot be interrupted by global transitions
    public override bool AllowInterruption => false;
    
    public override Type Execute(Player client)
    {
        // Cutscene logic...
        
        if (cutsceneComplete)
            return typeof(IdleState);
        
        return null;
    }
}

5. Global Transitions

Force immediate state changes (interrupts):

// Example: Stun effect
public void OnStunned()
{
    // Immediately transition from any state
    GTFSM.SetGlobalTransition<Enemy>(this, typeof(StunState));
}

// Example: Death
public void OnDeath()
{
    GTFSM.SetGlobalTransition<Enemy>(this, typeof(DeathState));
}

Notes:

  • Global transitions respect AllowInterruption
  • Checked before normal state transitions
  • Automatically cleared after use

6. State History

Return to previous states:

public class StunState : GTFSMStateBase<Enemy>
{
    public override Type Execute(Enemy client)
    {
        UpdateTimer(Time.deltaTime);
        
        if (stateTimer >= STUN_DURATION)
        {
            // Return to whatever state we were in before stun
            GTFSM.TransitionToPreviousState(client);
            return null; // Don't return a type when using TransitionToPreviousState
        }
        
        return null;
    }
}

7. Unity Inspector Debugging

Enable real-time state monitoring:

public class Enemy : MonoBehaviour, IGTFSMClient<Enemy>
{
    [Header("Debug Settings")]
    [SerializeField] private bool enableDebug = true;
    
    [Header("FSM Debug Info")]
    [SerializeField] private string currentStateName;
    [SerializeField] private float timeInState;
    [SerializeField] private int transitionCount;
    
    public FSMDebugInfo<Enemy> DebugInfo { get; set; }
    
    private void Awake()
    {
        if (enableDebug)
        {
            DebugInfo = new FSMDebugInfo<Enemy>();
        }
    }
    
    private void Update()
    {
        GTFSM.Execute(this);
        
        if (enableDebug && DebugInfo != null)
        {
            currentStateName = DebugInfo.CurrentStateName;
            timeInState = DebugInfo.TimeInCurrentState;
            transitionCount = DebugInfo.StateTransitionCount;
        }
    }
}

Inspector Shows:

  • Current state name
  • Time in current state
  • Total transitions
  • State history (last 10)

8. Scene Gizmos

Visualize states in Scene view:

private void OnDrawGizmos()
{
    if (!enableDebug || CurrentState == null)
        return;
    
    // Draw state name above character
    Gizmos.color = Color.green;
    Vector3 labelPos = transform.position + Vector3.up * 2f;
    
#if UNITY_EDITOR
    UnityEditor.Handles.Label(labelPos, 
        $"State: {CurrentState.StateName}\nTime: {timeInState:F1}s");
#endif
    
    // Draw patrol path
    if (patrolPath != null)
    {
        Gizmos.color = Color.yellow;
        for (int i = 0; i < patrolPath.Length; i++)
        {
            if (patrolPath[i] != null)
            {
                Gizmos.DrawWireSphere(patrolPath[i].position, 0.5f);
                
                if (i < patrolPath.Length - 1)
                {
                    Gizmos.DrawLine(patrolPath[i].position, 
                                  patrolPath[i + 1].position);
                }
            }
        }
    }
}

Best Practices

1. Always Cache Component References

? Bad:

public override Type Execute(Enemy client)
{
    // GetComponent called every frame!
    var agent = client.GetComponent<NavMeshAgent>();
    agent.SetDestination(target);
}

? Good:

public class Enemy : MonoBehaviour
{
    public NavMeshAgent Agent { get; private set; }
    
    private void Awake()
    {
        Agent = GetComponent<NavMeshAgent>(); // Cache once
    }
}

public override Type Execute(Enemy client)
{
    client.Agent.SetDestination(target); // Direct access
}

2. Use Transition Guards

? Bad:

public override Type Execute(Enemy client)
{
    if (playerDetected)
    {
        if (client.HasWeapon && client.Ammo > 0)
            return typeof(AttackState);
    }
}

? Good:

public class AttackState : GTFSMStateBase<Enemy>
{
    public override bool CanEnter(Enemy client)
    {
        return client.HasWeapon && client.Ammo > 0;
    }
}

public override Type Execute(Enemy client)
{
    if (playerDetected)
        return typeof(AttackState); // Guard handles validation
}

3. Set Appropriate Priorities

// Low - Background behaviors
public class IdleState : GTFSMStateBase<Enemy>
{
    public override StatePriority Priority => StatePriority.Low;
}

// Normal - Gameplay actions
public class AttackState : GTFSMStateBase<Enemy>
{
    public override StatePriority Priority => StatePriority.Normal;
}

// High - Important animations
public class SpecialAttackState : GTFSMStateBase<Enemy>
{
    public override StatePriority Priority => StatePriority.High;
}

// Critical - Must complete
public class DeathState : GTFSMStateBase<Enemy>
{
    public override StatePriority Priority => StatePriority.Critical;
    public override bool AllowInterruption => false;
}

4. Always Clean Up

private void OnDestroy()
{
    // Unsubscribe from events
    if (OnStateTransition != null)
    {
        OnStateTransition -= OnStateChanged;
    }
    
    // Clean up FSM
    GTFSM.Cleanup(this);
}

5. Use State Timer from Base Class

? Good:

public class AttackState : GTFSMStateBase<Enemy>
{
    private const float ATTACK_DURATION = 2f;
    
    public override Type Execute(Enemy client)
    {
        UpdateTimer(Time.deltaTime); // Use built-in timer
        
        if (stateTimer >= ATTACK_DURATION)
            return typeof(IdleState);
        
        return null;
    }
}

6. Inherit from GTFSMStateBase

? Verbose:

public class MyState : GTFSMState<Enemy>
{
    public string StateName { get; set; }
    public StatePriority Priority => StatePriority.Normal;
    public bool AllowInterruption => true;
    public void Init(Enemy client) { }
    public bool CanEnter(Enemy client) => true;
    public void OnEnter(Enemy client) { }
    public void ExecuteFixed(Enemy client) { }
    public void ExecuteLate(Enemy client) { }
    public bool CanExit(Enemy client) => true;
    public void OnExit(Enemy client) { }
    
    public Type Execute(Enemy client)
    {
        // Your logic
    }
}

? Concise:

public class MyState : GTFSMStateBase<Enemy>
{
    public override Type Execute(Enemy client)
    {
        // Your logic
    }
}

Examples

Example 1: Simple AI with Patrol and Attack

// Enemy.cs
public class Enemy : MonoBehaviour, IGTFSMClient<Enemy>
{
    public GTFSMState<Enemy> CurrentState { get; set; }
    public GTFSMState<Enemy> PreviousState { get; set; }
    public FSMDebugInfo<Enemy> DebugInfo { get; set; }
    
    public NavMeshAgent Agent { get; private set; }
    public Transform[] patrolPath;
    public Transform player;
    public float detectionRange = 10f;
    
    private void Awake()
    {
        Agent = GetComponent<NavMeshAgent>();
        DebugInfo = new FSMDebugInfo<Enemy>();
    }
    
    private void Start()
    {
        GTFSM.RegisterState<Enemy, PatrolState>(this);
        GTFSM.RegisterState<Enemy, AttackState>(this);
        GTFSM.SetState(this, typeof(PatrolState));
    }
    
    private void Update()
    {
        GTFSM.Execute(this);
    }
    
    private void OnDestroy()
    {
        GTFSM.Cleanup(this);
    }
    
    public void NotifyStateTransition(string from, string to)
    {
        Debug.Log($"[Enemy] {from} -> {to}");
    }
    
    public bool PlayerInRange()
    {
        return Vector3.Distance(transform.position, player.position) <= detectionRange;
    }
}

// PatrolState.cs
public class PatrolState : GTFSMStateBase<Enemy>
{
    private int currentWaypoint;
    
    public override StatePriority Priority => StatePriority.Low;
    
    public override bool CanEnter(Enemy client)
    {
        return client.patrolPath != null && client.patrolPath.Length > 0;
    }
    
    public override void OnEnter(Enemy client)
    {
        base.OnEnter(client);
        MoveToNextWaypoint(client);
    }
    
    public override Type Execute(Enemy client)
    {
        // Check for player
        if (client.PlayerInRange())
            return typeof(AttackState);
        
        // Check if reached waypoint
        if (client.Agent.remainingDistance <= 0.5f)
        {
            MoveToNextWaypoint(client);
        }
        
        return null;
    }
    
    private void MoveToNextWaypoint(Enemy client)
    {
        currentWaypoint = (currentWaypoint + 1) % client.patrolPath.Length;
        client.Agent.SetDestination(client.patrolPath[currentWaypoint].position);
    }
}

// AttackState.cs
public class AttackState : GTFSMStateBase<Enemy>
{
    private const float ATTACK_DURATION = 2f;
    
    public override StatePriority Priority => StatePriority.High;
    
    public override void OnEnter(Enemy client)
    {
        base.OnEnter(client);
        client.Agent.isStopped = true;
    }
    
    public override Type Execute(Enemy client)
    {
        UpdateTimer(Time.deltaTime);
        
        // Face player
        Vector3 lookDir = client.player.position - client.transform.position;
        lookDir.y = 0;
        client.transform.rotation = Quaternion.LookRotation(lookDir);
        
        // Return to patrol after attack
        if (stateTimer >= ATTACK_DURATION)
            return typeof(PatrolState);
        
        return null;
    }
    
    public override void OnExit(Enemy client)
    {
        base.OnExit(client);
        client.Agent.isStopped = false;
    }
}

Example 2: Player with Move, Jump, and Attack

public class Player : MonoBehaviour, IGTFSMClient<Player>
{
    public GTFSMState<Player> CurrentState { get; set; }
    public GTFSMState<Player> PreviousState { get; set; }
    public FSMDebugInfo<Player> DebugInfo { get; set; }
    
    public CharacterController Controller { get; private set; }
    public Animator Animator { get; private set; }
    
    private void Awake()
    {
        Controller = GetComponent<CharacterController>();
        Animator = GetComponent<Animator>();
        DebugInfo = new FSMDebugInfo<Player>();
    }
    
    private void Start()
    {
        GTFSM.RegisterState<Player, IdleState>(this);
        GTFSM.RegisterState<Player, MoveState>(this);
        GTFSM.RegisterState<Player, JumpState>(this);
        GTFSM.RegisterState<Player, AttackPlayerState>(this);
        GTFSM.SetState(this, typeof(IdleState));
    }
    
    private void Update()
    {
        GTFSM.Execute(this);
    }
    
    private void OnDestroy()
    {
        GTFSM.Cleanup(this);
    }
    
    public void NotifyStateTransition(string from, string to)
    {
        Animator.SetTrigger(to);
    }
}

public class IdleState : GTFSMStateBase<Player>
{
    public override Type Execute(Player client)
    {
        if (Input.GetButtonDown("Jump"))
            return typeof(JumpState);
        
        if (Input.GetButtonDown("Fire1"))
            return typeof(AttackPlayerState);
        
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        
        if (h != 0 || v != 0)
            return typeof(MoveState);
        
        return null;
    }
}

public class MoveState : GTFSMStateBase<Player>
{
    public override Type Execute(Player client)
    {
        if (Input.GetButtonDown("Jump"))
            return typeof(JumpState);
        
        if (Input.GetButtonDown("Fire1"))
            return typeof(AttackPlayerState);
        
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        
        if (h == 0 && v == 0)
            return typeof(IdleState);
        
        Vector3 move = new Vector3(h, 0, v) * 5f * Time.deltaTime;
        client.Controller.Move(move);
        
        return null;
    }
}

public class JumpState : GTFSMStateBase<Player>
{
    private const float JUMP_FORCE = 10f;
    private float verticalVelocity;
    
    public override StatePriority Priority => StatePriority.High;
    
    public override void OnEnter(Player client)
    {
        base.OnEnter(client);
        verticalVelocity = JUMP_FORCE;
    }
    
    public override Type Execute(Player client)
    {
        UpdateTimer(Time.deltaTime);
        
        verticalVelocity -= 20f * Time.deltaTime;
        client.Controller.Move(Vector3.up * verticalVelocity * Time.deltaTime);
        
        if (client.Controller.isGrounded && stateTimer > 0.1f)
            return typeof(IdleState);
        
        return null;
    }
}

public class AttackPlayerState : GTFSMStateBase<Player>
{
    private const float ATTACK_DURATION = 0.5f;
    
    public override StatePriority Priority => StatePriority.High;
    
    public override bool CanExit(Player client)
    {
        return stateTimer >= ATTACK_DURATION;
    }
    
    public override Type Execute(Player client)
    {
        UpdateTimer(Time.deltaTime);
        
        if (stateTimer >= ATTACK_DURATION)
            return typeof(IdleState);
        
        return null;
    }
}

Example 3: Using State Events for UI

public class PlayerUIManager : MonoBehaviour
{
    public Player player;
    public Image staminaBar;
    public GameObject attackIcon;
    
    private void Start()
    {
        player.OnStateTransition += OnPlayerStateChanged;
    }
    
    private void OnDestroy()
    {
        player.OnStateTransition -= OnPlayerStateChanged;
    }
    
    private void OnPlayerStateChanged(string from, string to)
    {
        // Update UI based on state
        attackIcon.SetActive(to == "AttackPlayerState");
        
        if (to == "JumpState")
        {
            staminaBar.fillAmount -= 0.2f;
        }
        
        // Play UI animations
        LeanTween.scale(attackIcon, to == "AttackPlayerState" ? Vector3.one : Vector3.zero, 0.3f);
    }
}

Troubleshooting

Problem: Transition Not Happening

Symptoms: State doesn't change when expected

Solutions:

  1. Check console for warning messages
  2. Verify CanExit() returns true on current state
  3. Verify CanEnter() returns true on target state
  4. Check state priority (lower can't interrupt higher)
  5. Ensure target state is registered
// Debug transition
if (!GTFSM.SetState(this, typeof(AttackState)))
{
    Debug.LogError("Failed to transition to AttackState!");
    
    // Check if registered
    if (!GTFSM.HasState(this, typeof(AttackState)))
    {
        Debug.LogError("AttackState not registered!");
    }
}

Problem: Global Transition Not Working

Symptoms: SetGlobalTransition doesn't interrupt current state

Solutions:

  1. Check if current state has AllowInterruption = false
  2. Verify target state is registered
  3. Ensure Execute() is being called
public class DeathState : GTFSMStateBase<Player>
{
    // This blocks global transitions
    public override bool AllowInterruption => false;
}

Problem: State Gets Interrupted Immediately

Symptoms: State changes right after entering

Solutions:

  1. Increase state priority
  2. Use CanExit() to prevent early exits
  3. Set AllowInterruption = false for critical states
public class CutsceneState : GTFSMStateBase<Player>
{
    public override StatePriority Priority => StatePriority.Critical;
    public override bool AllowInterruption => false;
    
    public override bool CanExit(Player client)
    {
        return cutsceneComplete;
    }
}

Problem: States Shared Between Instances

Symptoms: Multiple enemies interfere with each other's behavior

Solution: This is already fixed in v7.0! Each instance gets its own states. If you're experiencing this:

  1. Ensure you're using v7.0 or later
  2. Verify each instance calls GTFSM.RegisterState separately
  3. Check that you're not storing state references globally

Problem: Memory Leaks

Symptoms: Memory usage grows over time

Solution: Always call Cleanup in OnDestroy:

private void OnDestroy()
{
    // Unsubscribe from events
    OnStateTransition -= MyHandler;
    
    // Clean up FSM
    GTFSM.Cleanup(this);
}

Problem: Inspector Debug Not Updating

Symptoms: Debug fields in Inspector don't change

Solutions:

  1. Ensure enableDebug is checked
  2. Verify DebugInfo is initialized in Awake
  3. Call UpdateDebugInfo in Update
[SerializeField] private bool enableDebug = true;

private void Awake()
{
    if (enableDebug)
    {
        DebugInfo = new FSMDebugInfo<Enemy>();
    }
}

private void Update()
{
    GTFSM.Execute(this);
    
    if (enableDebug && DebugInfo != null)
    {
        currentStateName = DebugInfo.CurrentStateName;
        timeInState = DebugInfo.TimeInCurrentState;
    }
}

Performance

Benchmarks

Tested on Unity 2021.3, 1000 AI agents:

Operation Time Allocations
State Registration 0.05ms per agent 1 allocation per state
State Transition 0.002ms 0 allocations
Execute (per frame) 0.001ms per agent 0 allocations
Global Transition 0.003ms 0 allocations

Performance Benefits

? No GetComponent Calls

  • Before: 2+ calls per frame per agent
  • After: 0 calls (cached in Awake)
  • Improvement: 100% elimination

? No Boxing/Unboxing

  • Before: Object dictionaries caused boxing
  • After: Generic dictionaries, zero boxing
  • Improvement: 100% elimination

? Per-Instance States

  • Before: Shared states caused interference
  • After: Isolated state instances
  • Improvement: Thread-safe, scalable

? Type-Based Lookups

  • Before: String-based dictionary keys
  • After: Type-based keys (faster)
  • Improvement: 30% faster lookups

Optimization Tips

  1. Cache Component References

    // In Awake
    Agent = GetComponent<NavMeshAgent>();
  2. Use Object Pooling for States (if creating thousands)

    // Not necessary for most cases - states are reused per instance
  3. Disable Debug in Builds

    #if UNITY_EDITOR
        DebugInfo = new FSMDebugInfo<Enemy>();
    #endif
  4. Use ExecuteFixed for Physics

    public override void ExecuteFixed(Player client)
    {
        // Physics calculations here
        client.Rigidbody.AddForce(force);
    }

License

Copyright 2010-2026 - DFT Games Ltd.

Licensed under Unity Asset Store EULA: http://unity3d.com/legal/as_terms

Summary:

  • ? Use in your commercial games
  • ? Modify for your projects
  • ? Don't redistribute as a package
  • ? Don't sell as an asset

Changelog

Version 7.0 (January 2026)

  • ? Added transition guards (CanEnter/CanExit)
  • ? Added state priority system (Low/Normal/High/Critical)
  • ? Added state transition event notifications
  • ? Added interruption control (AllowInterruption)
  • ? Added per-instance debug enable/disable
  • ?? Fixed shared state instances bug
  • ? Optimized dictionary lookups (Type-based keys)
  • ?? Comprehensive documentation and examples
  • ?? Improved Unity Inspector integration
  • ?? GTFSMStateBase provides sensible defaults

Support

Getting Help

  1. Documentation: Read this README thoroughly
  2. Examples: Check the Demo folder for working examples
  3. Console: Read warning messages for debugging
  4. Inspector: Enable debug mode to monitor state changes

Common Questions

Q: Can I use this with multiplayer? A: Yes! Each client instance has its own FSM. For network sync, use the NotifyStateTransition callback to send state changes over the network.

Q: Can states have their own variables? A: Yes! States are classes - add fields as needed:

public class PatrolState : GTFSMStateBase<Enemy>
{
    private int currentWaypoint; // State-specific variable
    private float patrolSpeed = 3f;
}

Q: Can I have nested/hierarchical states? A: Not directly in v7.0, but you can simulate it:

public class CombatState : GTFSMStateBase<Player>
{
    private enum CombatMode { Melee, Ranged, Blocking }
    private CombatMode currentMode;
}

Q: Can I use this with animation state machines? A: Yes! Use the transition event:

public void NotifyStateTransition(string from, string to)
{
    animator.SetTrigger(to);
}

Q: How many states can I have? A: Unlimited. Performance is O(1) for lookups.

Q: Can I create states at runtime? A: Yes, but states must be registered before use:

GTFSM.RegisterState<Enemy, NewState>(this);

Credits

Developed by DFT Games Ltd.

Special thanks to the Unity community for feedback and suggestions.


Thank you for using GTFSM!

For updates and support, visit: https://github.com/DFTGames/GTFSM

About

Geneerically Typed FSM for Unity

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages