We've all been there: a simple boolean flag here, a nested if statement there. It seems harmless at first, but soon your codebase is a tangled mess of conditional logic. Your Door
class can be opened and closed. Easy, right?
But in the wild, the simple, intuitive code you first wrote for a door quickly starts to smell. The moment we try to add a new feature, such as a lock, our elegant two-state object becomes a mess of conditional logic.
Let's look at what our intuitive code looks like to begin with:
public class Door
{
public bool IsOpen { get; set; } = false;
public void Open()
{
IsOpen = true;
Console.WriteLine("The door is now open.");
}
public void Close()
{
IsOpen = false;
Console.WriteLine("The door is now closed.");
}
}
Now, imagine we need to add a "locked" state. To accommodate this, our code is no longer simple. We have to add a new boolean flag, IsLocked
, and then we have to modify all of our existing methods to account for this new state. We also have to add new methods, such as Lock
and Unlock
.
public class Door
{
public bool IsOpen { get; set; } = false;
public bool IsLocked = false; // We added this!
public void Open()
{
if (!IsLocked) // We modified this!
{
IsOpen = true;
Console.WriteLine("The door is now open.");
}
else
{
Console.WriteLine("The door is locked and cannot be opened.");
}
}
public void Close()
{
IsOpen = false;
Console.WriteLine("The door is now closed.");
}
public void Lock() // We added this!
{
IsLocked = true;
Console.WriteLine("The door is now locked.");
}
public void Unlock() // We added this!
{
IsLocked = false;
Console.WriteLine("The door is now unlocked.");
}
}
What about a Broken state? Or a Sliding door? Or a RollUp door? We can approximate these with more booleans, but each new addition requires us to go back and add more and more if/else
statements to every method. It's not scalable, it's brittle, and it's full of boilerplate.
Here are the pros and cons of using this approach:
Pros | Cons |
---|---|
Simple and quick to implement for basic cases. | Tight Coupling: Logic is tied to a single object. |
No external dependencies or abstractions. |
Boilerplate: if/else checks for every state change. |
Difficult to Scale: Adding new features breaks existing logic. | |
Difficult to maintain: Code becomes a tangled mess. |
The Typical FSM Solution and Its Pitfalls
A typical FSM approach would have you create distinct classes for each state (e.g., DoorOpenState
, DoorClosedState
, DoorLockedState
) that all implement a common IState
interface. The Door
class would then hold a reference to its current state object. This is what you'll see in most tutorials and books.
public interface IState<T>
{
void Enter(T context);
void Update(T context);
void Exit(T context);
}
public class DoorContext
{
public bool IsOpen { get; set; } = false;
}
public class DoorClosedState : IState<DoorContext>
{
public void Enter(DoorContext context)
{
context.IsOpen = false;
Console.WriteLine("The door is now closed.");
}
public void Update(DoorContext context) { }
public void Exit(DoorContext context) { }
}
public class DoorOpenState : IState<DoorContext>
{
public void Enter(DoorContext context)
{
context.IsOpen = true;
Console.WriteLine("The door is now open.");
}
public void Update(DoorContext context) { }
public void Exit(DoorContext context) { }
}
Now, what does it take to add a "locked" state to this? It's not as simple as adding a boolean. We need to create a new state class, DoorLockedState
, and then modify our existing states and the context to allow for transitions to this new state. This still requires a fair amount of boilerplate and modification of existing code.
Here's an overview of the pros and cons of this and other FSM implementations:
Pros | Cons |
---|---|
Clean State Separation: Logic for each state is in a distinct class. | Boilerplate: Requires a lot of code to set up states, transitions, and the state machine itself. |
Enforced Transitions: The model naturally enforces valid transitions. | Framework-Dependent: Often tightly coupled to a specific game engine or framework. |
Well-Established Pattern: A common and well-understood design pattern. | Rigid Design: FSM definition is static and difficult to modify at runtime. |
Hierarchical FSMs (HFSMs): Allows for nested states, which can reduce complexity. | Hierarchical FSMs (HFSMs): Can introduce complexity and make the state machine harder to understand and debug. |
Behavior Trees: A different approach that uses a tree structure to define behavior. | Behavior Trees: Can be overkill for simple state management and can be difficult to reason about the overall state of the system. |
Hybrid Approaches: Combining FSMs with other patterns like Behavior Trees. | Hybrid Approaches: Can be overly complex and lead to "spaghetti code" if not carefully implemented. |
State Pattern: Allows a context object to change its behavior depending on its internal state. | State Pattern: Can lead to a large number of classes for each state, which can be difficult to manage. Changes to the state machine often require changing multiple classes. |
A Better Way: FSM_API and Centralized State
The FSM_API is a new approach to FSMs that is designed to be blazing-fast, software-agnostic, and fully decoupled from your application's logic. It's a declarative, data-driven approach that allows you to define your FSM once and use it everywhere.
The key concept is that the FSM_API operates on Plain Old C# Objects (POCOs) that simply implement the IStateContext
interface. This ensures a clean separation between your FSM logic and your application's data.
Here's our door, refactored with the FSM_API.
1. Define the Context
Our Door
class is now a simple POCO. All its logic is handled by the FSMs we are about to create.
public class Door : IStateContext
{
public bool IsOpen { get; set; } = false;
public bool IsLocked = false;
public string Name { get; set; } = "FrontDoor";
public bool IsValid => true; // FSM API will not operate on this if false.
}
2. Multiple FSMs on a Single Context
The true power of FSM_API is its ability to manage multiple, independent behaviors on a single context object. This allows us to separate our "open/close" logic from our "lock/unlock" logic.
First, let's create a simple FSM to handle the door's opening and closing. This is the DoorFSM
:
FSM_API.Create.CreateFiniteStateMachine("DoorFSM")
.State("Closed", onEnter: (ctx) => { ((Door)ctx).IsOpen = false; })
.State("Open", onEnter: (ctx) => { ((Door)ctx).IsOpen = true; })
.WithInitialState("Closed")
.Transition("Closed", "Open", (ctx) => !((Door)ctx).IsLocked)
.Transition("Open", "Closed")
.Transition("Closed", "Locked")
.Transition("Locked", "Closed", (ctx) => !((Door)ctx).IsLocked)
.Transition("Unlocked", "Closed")
.BuildDefinition();
Because the FSM_API allows for runtime redefinition of FSMs, we can dynamically add states and transitions to our door object without recompiling. This capability is what makes the FSM_API a powerful choice for highly configurable or live-updating systems.
Here's an overview of the pros and cons of our API:
Pros | Cons |
---|---|
Centralized State: FSM logic is separated from data. | Learning Curve: New concepts might take time to grasp, especially if you are used to traditional FSM implementations. |
Extremely Lightweight: The core FSM_API.dll is a tiny 45kb, and the full NuGet package is only 417.09kb. | Minimalism: The API is minimal by design; you will need to rely on the provided documentation and quick-start guides to get started. |
Runtime Redefinition: FSMs can be modified on the fly. | |
Lightweight & Performant: Minimal memory allocations and optimized performance. | |
Framework Agnostic: No external dependencies or frameworks required. | |
Thread-Safe: Designed for safe, deferred mutation handling. | |
Error-Tolerant: Built-in diagnostics prevent runaway logic. |
This is just a glimpse of what's possible with the FSM_API. Its features make it a robust choice for any C# application requiring sophisticated state management.
If you'd like to get started, you can find our NuGet package and all the documentation on our GitHub repository:
- NuGet Package: https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API
- GitHub Repository: https://github.com/TrentBest/FSM_API
If you feel this tool is valuable to you, please consider supporting the project. Donate via PayPal
Top comments (0)