A Finite State of One: Rethinking the Singleton Pattern with FSMs

A Finite State of One: Rethinking the Singleton Pattern with FSMs

Leader posted 4 min read

A Finite State of One: Rethinking the Singleton Pattern with FSMs

The Singleton pattern is a cornerstone of game development and application architecture, ensuring a class has only one instance and providing a global point of access. Yet, its implementation in C# can often feel like wrestling with unnecessary boilerplate—or worse, a forced, inflexible use of generics that clutters your codebase.

What if your Singleton could be inherently self-regulating, decoupling the enforcement logic from the class itself, all while giving you granular control over performance?

We recently discovered an incredibly intuitive and versatile approach that achieves just this, leveraging the power of a modern Finite State Machine (FSM) framework, specifically TheSingularityWorkshop.FSM_API, to abstract and enforce the Singleton constraint.


The Pain Points of Traditional Singletons

Most C# Singletons fall into two camps, both of which introduce either bloat or manual effort:

  1. The Boilerplate (Non-Generic): Requires manual static field, private constructor, and thread-safety logic for every class.
  2. The Generic (Templated): Reduces boilerplate but introduces a base class dependency, which can be seen as "bloat" and often complicates inheritance hierarchies.

Here is a quick look at the typical C# Unity implementation:

public class ClassicInputManager : MonoBehaviour
{
    private static ClassicInputManager _instance;

    public static ClassicInputManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<ClassicInputManager>();
                if (_instance == null)
                {
                    Debug.LogError("ClassicInputManager not found in scene!");
                }
            }
            return _instance;
        }
    }

    // Other class members...
}

This is fragile: what if two instances are accidentally placed in the scene? You have to add clumsy boilerplate to Awake() or Start() to destroy duplicates, and the instance check runs on every single call to Instance.


The FSM-Driven Singleton: Self-Healing and Performant

The FSM_Singleton is an abstraction layer that wraps your target class (the actual Singleton, like a BuildersInputManager) and delegates the enforcement of uniqueness to an FSM instance.

By creating an FSM instance upon the target's initialization, we gain immediate access to the FSM API's core benefits: robust lifecycle management and customizable processing rates.

In the example below, the BuildersInputManager simply creates an FSM_Singleton in its Awake() method:

public class BuildersInputManager : MonoBehaviour, IStateContext
{
    // ...
    void Awake()
    {
        // Establish this as a singleton, checking for duplicates every 10 update calls
        new FSM_Singleton(this, 10, "Update"); 
    }
    // ...
}

The magic lies within the FSM_Singleton's constructor and the single "Operating" state of its FSM definition:

public class FSM_Singleton : IStateContext
{
    public FSM_Singleton(IStateContext stateContext, int processRate, string processGroup)
    {
        // 1. Create FSM Definition (if it doesn't exist)
        if(!FSM_API.Interaction.Exists("BuildersInputManager", processGroup))
        {
            FSM_API.Create.CreateFiniteStateMachine("BuildersInputManager", processRate, processGroup)
                .State("Operating", null, OnUpdateOperating, null)
                .BuildDefinition();
        }
        // 2. Create FSM Instance for the target context
        Status = FSM_API.Create.CreateInstance("BuildersInputManager", this, processGroup);
        // ...
    }

    private void OnUpdateOperating(IStateContext context)
    {
        if(context is FSM_Singleton singleton)
        {
            Enforce(singleton); // Check and remove duplicates
        }
    }

    private void Enforce(FSM_Singleton singleton)
    {
        var bucket = FSM_API.Internal.GetBucket(stateContext.Name, singleton.ProcessGroup);
        if (bucket.Instances.Count > 1)
        {
            RemoveRedundant(singleton, bucket);
        }
    }
    
    private static void RemoveRedundant(FSM_Singleton singleton, FSM_API.Internal.FsmBucket bucket)
    {
        for (int i = bucket.Instances.Count - 1; i >= 0; i--)
        {
            var instanceHandle = bucket.Instances[i];
            if (instanceHandle.Id != singleton.Status.Id)
            {
                Debug.LogWarning($"Destroyed duplicate instance with ID: {instanceHandle.Id}");
                FSM_API.Interaction.DestroyInstance(instanceHandle);
            }
        }
    }
    // ...
}

Versatility and Performance: Decoupling Enforcement

This mechanism showcases the ingenuity of decoupled architecture. Unlike traditional Singleton implementations, where duplicate checks must occur on every static property access or during immediate object instantiation, the FSM_Singleton establishes a self-healing contract. It initiates a constant, scheduled validation loop to ensure the existence of exactly one instance.

In the typical and expected scenario, the OnUpdateOperating logic performs a lightning-fast API call to retrieve the instance count, instantly confirming a count of one, and then terminates its execution for that cycle. This high-performance check effectively minimizes frame-time overhead.

Crucially, in the rare event that a duplicate is accidentally created, the enforcement logic will instantly eradicate it (DestroyInstance). This limits the duplicate's lifespan to the number of frames defined by the FSM's ProcessRate. By setting ProcessRate = 10 (as in the example), we drastically reduce overhead because the resource-intensive cleanup iteration runs only periodically, yet the detection and destruction of a redundant instance is still handled automatically and asynchronously.

Furthermore, the FSM framework allows for assigning this check to a specific Processing Group, enabling advanced scheduling tactics such as running the validation only during periods of available frame time, thus prioritizing critical game logic.


Analysis: FSM vs. Classic Singleton

Feature Classic C# Singleton (Standard Boilerplate) FSM_Singleton (TheSingularityWorkshop.FSM_API)
Enforcement Logic Scattered throughout the target class (Awake/Start for cleanup, static property for creation/access). Centralized and abstracted in the dedicated FSM_Singleton class.
Duplicate Handling Must be manually coded for destruction; highly prone to developer error and race conditions. Self-Healing via periodic FSM state updates (OnUpdateOperating), automatically detecting and destroying duplicates.
Performance Enforcement or creation checks typically run every frame or on every public static Instance access, leading to constant minor overhead. Highly Configurable via ProcessRate (e.g., run every 10th frame, or only on event), significantly minimizing the update overhead.
Code Style Requires repetitive boilerplate (static field, private constructor, lock) or forces generic base class inheritance, potentially complicating the object hierarchy. Clean pattern implementation without unnecessary generics, relying on standard interfaces (IStateContext) and a clean API wrapper.

The FSM-based approach successfully elevates the Singleton pattern beyond a rigid architectural constraint, transforming it into a flexible, self-regulating service. This decoupled solution expertly manages its own lifecycle and performance with exceptional granularity, representing a potent abstraction that simplifies the codebase and entirely eliminates the common implementation headaches associated with managing instance uniqueness.

Resources & Code:

The FSM Package (Unity Asset Store):
https://assetstore.unity.com/packages/slug/332450

NuGet Package (Non-Unity Core):
https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API

GitHub Repository:
https://github.com/TrentBest/FSM_API

Support Our Work:

Patreon Page:
https://www.patreon.com/c/TheSingularityWorkshop

Support Us (PayPal Donation):
https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J

We'd love to hear your thoughts! Please Like this post, Love the code, and Share your feedback in the comments.

1 Comment

1 vote

More Posts

Finite State Machines - the Universal Unit of Work

The Singularity Workshop - Nov 4

Caution: Mind Blowing Finite State Machine contained within (Nothing but theory)

The Singularity Workshop - Nov 17

The Deterministic Engine: Why The Singularity Workshop’s FSM_API Redefines C# State Management

The Singularity Workshop - Nov 19

The Sentient Primitive: Waking Up Your Data with FSMs

The Singularity Workshop - Nov 29

The Grand Finale: Orchestrating Complex Motion with Multi-Context FSMs

The Singularity Workshop - Oct 15
chevron_left