How TEngine's Procedure State Machine Manages Game Flow: Complete Technical Guide

TEngine's procedure state machine is a generic finite-state machine (FSM) that orchestrates game flow through ProcedureBase states managed by ProcedureModule, configured via the ProcedureSetting ScriptableObject, and driven by lifecycle callbacks (OnEnter, OnUpdate, OnLeave) that enable type-safe transitions between game phases.

The open-source TEngine framework (alex-rachel/tengine) provides a robust procedure state machine architecture for Unity that cleanly separates game flow configuration from execution logic. This system uses ScriptableObjects to define available procedures while a generic FSM core handles runtime state transitions, making it ideal for managing complex game loops from splash screens to gameplay scenes.

Core Architecture Components

The TEngine procedure state machine rests on three pillars that work together to create a decoupled, extensible game flow system.

ProcedureBase: The State Foundation

All game-flow states derive from ProcedureBase (UnityProject/Assets/TEngine/Runtime/Module/ProcedureModule/ProcedureBase.cs), which inherits from FsmState<IProcedureModule>. This abstract class defines the lifecycle contract that every procedure must implement:

public abstract class ProcedureBase : FsmState<IProcedureModule>
{
    protected internal override void OnInit(ProcedureOwner procedureOwner) { }
    protected internal override void OnEnter(ProcedureOwner procedureOwner) { }
    protected internal override void OnUpdate(ProcedureOwner procedureOwner,
                                              float elapseSeconds,
                                              float realElapseSeconds) { }
    protected internal override void OnLeave(ProcedureOwner procedureOwner,
                                             bool isShutdown) { }
    protected internal override void OnDestroy(ProcedureOwner procedureOwner) { }
}

The lifecycle callbacks serve distinct purposes:

  • OnInit – Executes once when the procedure is first registered with the FSM.
  • OnEnter – Called immediately when the FSM transitions into this procedure.
  • OnUpdate – Called every frame while the procedure is active, receiving elapsed time parameters.
  • OnLeave – Invoked before the FSM switches away from this procedure; the isShutdown flag indicates whether the application is quitting.
  • OnDestroy – Handles cleanup when the procedure is removed from the FSM.

ProcedureModule: The FSM Manager

ProcedureModule (ProcedureModule.cs) implements IProcedureModule and serves as the runtime engine for the procedure state machine. It maintains an internal IFsm<IProcedureModule> instance (named _procedureFsm) created via the generic Fsm<T> class located in Runtime/Module/FsmModule/Fsm.cs.

Because ProcedureModule inherits from the framework's Module base class, it initializes early in the engine startup sequence with a Priority value of -2, ensuring it shuts down after higher-priority modules but is available for game-flow management early in the lifecycle.

The module exposes a clean API for runtime control:

Method Purpose
CurrentProcedure Property returning the active ProcedureBase instance.
CurrentProcedureTime Returns elapsed seconds since entering the current state.
StartProcedure<T>() / StartProcedure(Type) Switches FSM states, triggering OnLeave on the old procedure and OnEnter on the new one.
HasProcedure<T>() / HasProcedure(Type) Verifies if a specific procedure type is registered.
GetProcedure<T>() / GetProcedure(Type) Retrieves a specific procedure instance by type.
RestartProcedure(params ProcedureBase[]) Destroys the existing FSM, recreates it with new procedure instances, and starts the first one.

ProcedureSetting: Configuration Layer

ProcedureSetting (ProcedureSetting.cs) is a ScriptableObject that bridges editor configuration with runtime instantiation. It stores two critical string arrays:

private string[] availableProcedureTypeNames = null;
private string entranceProcedureTypeName = null;

The availableProcedureTypeNames array lists all concrete ProcedureBase types available to the game, while entranceProcedureTypeName designates which procedure runs first. This separation allows designers to modify game flow in the Unity Inspector without touching code.

Initialization and Runtime Flow

Step 1: Registration via Reflection

When ProcedureSetting.StartProcedure() is called (typically from a bootstrap script), it performs reflective instantiation:

public async UniTaskVoid StartProcedure()
{
    _procedureModule = ModuleSystem.GetModule<IProcedureModule>();
    
    // Reflect and instantiate all procedure types
    foreach (var typeName in availableProcedureTypeNames)
    {
        Type procedureType = Type.GetType(typeName);
        // Instantiation logic...
    }
    
    // Start the entrance procedure
    _procedureModule.StartProcedure(_entranceProcedure.GetType());
}

This process creates instances of all listed procedures and passes them to ProcedureModule, which internally calls _fsmModule.CreateFsm(this, procedures) to build the FSM.

Step 2: State Execution Loop

Once initialized, the FSM enters its execution loop:

  1. The entrance procedure receives OnEnter.
  2. Every frame, the active procedure receives OnUpdate with elapsed time parameters.
  3. When logic dictates a transition (e.g., loading complete), the procedure calls ProcedureModule.StartProcedure<NextProcedure>().
  4. The FSM automatically invokes OnLeave on the current procedure, switches internal state, and calls OnEnter on the target procedure.

This transition mechanism is type-safe, utilizing generic constraints to prevent invalid state changes at compile time.

Implementing Custom Game Procedures

To extend the TEngine procedure state machine with custom game logic, inherit from ProcedureBase and implement the required lifecycle methods.

Creating a Custom Procedure

using TEngine;

public sealed class ProcedureBattleScene : ProcedureBase
{
    protected internal override void OnEnter(ProcedureOwner owner)
    {
        base.OnEnter(owner);
        Log.Info("Entering Battle Scene procedure.");
        // Load battle scene, initialize combat managers...
    }

    protected internal override void OnUpdate(ProcedureOwner owner,
                                              float elapseSeconds,
                                              float realElapseSeconds)
    {
        base.OnUpdate(owner, elapseSeconds, realElapseSeconds);
        
        // Check victory conditions
        if (BattleManager.Instance.IsVictory)
        {
            owner.Module.StartProcedure<ProcedureVictoryScreen>();
        }
    }

    protected internal override void OnLeave(ProcedureOwner owner, bool isShutdown)
    {
        base.OnLeave(owner, isShutdown);
        Log.Info("Leaving Battle Scene.");
        // Unload assets, save progress...
    }
}

Registration and Startup

Add ProcedureBattleScene to the Available Procedures list in your ProcedureSetting asset via the Unity Inspector. To launch the flow:

public class GameEntry : MonoBehaviour
{
    private void Awake()
    {
        // Assumes ProcedureModule initialized by ModuleSystem
        GameSettings.ProcedureSetting.StartProcedure();
    }
}

Summary

  • TEngine's procedure state machine uses a generic Fsm<T> implementation where ProcedureBase instances represent states.
  • ProcedureSetting provides ScriptableObject-driven configuration, specifying which procedures exist and which runs first, while ProcedureModule handles runtime execution.
  • State transitions are type-safe and automatically trigger OnLeave and OnEnter lifecycle callbacks, ensuring clean setup and teardown between game phases.
  • The system initializes via reflection in ProcedureSetting.StartProcedure(), creating procedure instances and launching the entrance procedure through the module's API.
  • Priority -2 ensures the procedure module initializes early but shuts down gracefully after higher-priority systems.

Frequently Asked Questions

How does ProcedureModule differ from the generic Fsm implementation?

ProcedureModule is a high-level manager that implements IProcedureModule and owns an instance of IFsm<IProcedureModule> (the generic FSM). While Fsm<T> (Fsm.cs) provides the core state machine mechanics—tracking CurrentState, managing transitions, and invoking callbacks—ProcedureModule wraps this functionality with game-specific APIs like RestartProcedure() and CurrentProcedureTime. This separation allows the generic FSM to remain reusable across different modules while the procedure module specializes it for game-flow management.

What triggers procedure transitions in TEngine?

Procedures trigger transitions by calling StartProcedure<T>() or StartProcedure(Type) on the IProcedureModule instance, accessed via ProcedureOwner.Module or ModuleSystem.GetModule<IProcedureModule>(). When invoked, the FSM automatically calls OnLeave on the current procedure (passing the isShutdown flag), switches the internal state pointer, and invokes OnEnter on the new procedure. This ensures cleanup logic always runs before initialization logic during state changes.

Why does ProcedureSetting use reflection to instantiate procedures?

ProcedureSetting uses reflection (Type.GetType()) to instantiate procedures so that the system can remain data-driven. By storing type names as strings in the ScriptableObject, designers can add, remove, or reorder procedures in the Unity Inspector without modifying bootstrap code. This approach decouples the game-flow configuration from the compilation step, allowing rapid iteration on procedural sequences like splash screens, main menus, and gameplay loops.

How does the procedure state machine handle application shutdown?

When the application shuts down, the isShutdown parameter passed to OnLeave is set to true, allowing procedures to differentiate between normal transitions and application termination. Additionally, because ProcedureModule has a priority of -2, it shuts down after higher-priority modules (like resource or audio managers), ensuring that procedures can access other systems during their cleanup phase in OnLeave or OnDestroy.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →