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

> Learn how TEngine's procedure state machine orchestrates game flow using ProcedureBase states and lifecycle callbacks for seamless transitions between game phases. A complete technical guide.

- Repository: [ALEX/tengine](https://github.com/alex-rachel/tengine)
- Tags: deep-dive
- Published: 2026-02-24

---

**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`](https://github.com/alex-rachel/tengine/blob/main/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:

```csharp
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`](https://github.com/alex-rachel/tengine/blob/main/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`](https://github.com/alex-rachel/tengine/blob/main/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`](https://github.com/alex-rachel/tengine/blob/main/ProcedureSetting.cs)) is a ScriptableObject that bridges editor configuration with runtime instantiation. It stores two critical string arrays:

```csharp
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:

```csharp
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

```csharp
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:

```csharp
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<T> 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`](https://github.com/alex-rachel/tengine/blob/main/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`.