Creational Patterns

July 1, 2026 · View on GitHub

Creational patterns in FronkonGames.GameWork.Foundation for object construction, global access, and service registration. All implementations are generic, subclass or implement the interfaces with your game types.

PatternFolderRole
BuilderBuilderFluent step-by-step product construction
FactoryFactoryCreate products by key from registered creators
Service LocatorServiceLocatorThread-safe registry with dependency checks
SingletonSingletonLazy, thread-safe single instances

Builder

Why use it: Construct complex objects step by step with readable, self-documenting code. Optional fields, defaults, and validation can live in fluent methods instead of constructors with many overloads. Use when a type has many configurable properties, spells, quests, dialogue lines, procedural room configs, and you want Create().WithX().WithY().Build() instead of a 12-parameter constructor.

Builder.cs

TypeDescription
IBuilder<TBuilder, TProduct>Builder contract
Builder<TBuilder, TProduct>CRTP base with Create() and Build()

Subclass with fluent methods that return this. The curiously recurring template pattern (TBuilder : Builder<TBuilder, TProduct>) enables chaining without casts.

using FronkonGames.GameWork.Foundation;

public class FireballSpell
{
  public float Damage;
  public float Speed;
  public UnityEngine.Color Color;
}

public class FireballBuilder : Builder<FireballBuilder, FireballSpell>
{
  private readonly FireballSpell spell = new();

  public FireballBuilder WithDamage(float damage)
  {
    spell.Damage = damage;
    return this;
  }

  public FireballBuilder WithSpeed(float speed)
  {
    spell.Speed = speed;
    return this;
  }

  public FireballBuilder WithColor(UnityEngine.Color color)
  {
    spell.Color = color;
    return this;
  }

  public override FireballSpell Build() => spell;
}

// Scene: SpellCaster
FireballSpell fireball = FireballBuilder.Create()
  .WithDamage(25.0f)
  .WithSpeed(12.0f)
  .WithColor(UnityEngine.Color.red)
  .Build();

Factory

Why use it: Hide concrete types behind a key and a shared interface. Callers ask for EnemyType.Grunt or "bolt" without knowing which class gets instantiated. Centralizes creation logic, register new types in one place when content expands. Ideal for wave spawners, loot tables, ability systems, and any code that creates objects from data (JSON, ScriptableObjects, network messages).

Factory.cs

TypeDescription
Factory<TKey, TProduct>Key → parameterless creator
Factory<TKey, TProduct, TParam>Key → creator with one parameter
IFactory<...>Factory contracts

Register creators in the constructor with Register(key, creator). Unregistered keys throw KeyNotFoundException.

using FronkonGames.GameWork.Foundation;

public enum EnemyType { Grunt, Archer, Boss }

public interface IEnemy { void SpawnAt(UnityEngine.Vector3 position); }

public class GruntEnemy : IEnemy
{
  public void SpawnAt(UnityEngine.Vector3 position) { /* ... */ }
}

public class EnemyFactory : Factory<EnemyType, IEnemy>
{
  public EnemyFactory()
  {
    Register(EnemyType.Grunt, () => new GruntEnemy());
    Register(EnemyType.Archer, () => new ArcherEnemy());
    Register(EnemyType.Boss, () => new BossEnemy());
  }
}

// Scene: WaveSpawner
var factory = new EnemyFactory();
IEnemy enemy = factory.Create(EnemyType.Grunt);
enemy.SpawnAt(spawnPoint.position);

Parameterized factory:

public class ProjectileFactory : Factory<string, Projectile, float>
{
  public ProjectileFactory()
  {
    Register("arrow", speed => new ArrowProjectile(speed));
    Register("bolt", speed => new BoltProjectile(speed));
  }
}

Projectile bolt = new ProjectileFactory().Create("bolt", speed: 20.0f);

Service Locator

Why use it: Register services once at bootstrap and retrieve them anywhere by type, with enforced dependency order. A service cannot register until its dependencies are already initialized, which catches setup mistakes early. Use for cross-scene systems (audio, save, analytics) where you need a single registry instead of passing references through every constructor. Prefer dependency injection for larger projects; Service Locator fits rapid prototyping and small-to-medium Unity games.

ServiceLocator.cs, Service.cs, ScriptableService.cs

TypeDescription
IServiceLocatorRegister, get, unregister services
ServiceLocatorThread-safe ConcurrentDictionary implementation
IServiceLifecycle: OnRegister, OnUnregister, GetDependencies
ServicePlain C# service base
ScriptableServiceScriptableObject service base
ServiceStatusNotInitialized, Initialized, Failed

Registration rules:

  • Service must be NotInitialized before registering.
  • All dependencies must already be registered and Initialized.
  • Duplicate registration logs a warning and is ignored.
  • OnRegister sets status to Initialized; OnUnregister resets it.
using FronkonGames.GameWork.Foundation;
using System;
using System.Collections.Generic;

public class AudioService : Service { /* PlaySfx, PlayMusic */ }

public class SaveService : Service
{
  public override List<Type> GetDependencies() => new() { typeof(AudioService) };
}

// Scene: Bootstrap (early execution order)
var locator = new ServiceLocator();
locator.Register(new AudioService());
locator.Register(new SaveService());

AudioService audio = (AudioService)locator.Get<AudioService>();
audio.PlaySfx("menu_open");

locator.UnregisterAll();

For ScriptableService, create assets in the project and register them the same way, useful when service config lives in the Inspector.


Singleton

Why use it: Guarantee a single, lazily created instance with thread-safe access. Useful when exactly one object should exist, game session state, input routing, global config. The four variants cover plain C#, scene-bound MonoBehaviour, persistent across loads, and ScriptableObject assets. Use sparingly: global access is convenient but makes testing and dependencies harder to trace.

Four lazy, thread-safe variants for different Unity lifetimes.

TypeLifetimeSource
Singleton<T>Plain C# classSingleton.cs
MonoBehaviourSingleton<T>Scene-bound MonoBehaviourMonoBehaviourSingleton.cs
PersistentMonoBehaviourSingleton<T>DontDestroyOnLoadPersistentMonoBehaviourSingleton.cs
ScriptableObjectSingleton<T>Resources assetScriptableObjectSingleton.cs

Singleton

using FronkonGames.GameWork.Foundation;

public sealed class GameRules : Singleton<GameRules>
{
  public float GravityMultiplier = 1.0f;

  private GameRules() { } // required: private ctor
}

float gravity = GameRules.Instance.GravityMultiplier;

MonoBehaviourSingleton

Finds an existing component or creates a new GameObject. Not persistent across scenes. Call base.OnDestroy() when overriding OnDestroy.

using FronkonGames.GameWork.Foundation;
using UnityEngine;

public class InputRouter : MonoBehaviourSingleton<InputRouter>
{
  public Vector2 MoveAxis { get; private set; }

  void Update() => MoveAxis = /* read input */;
}

// First access creates or finds the instance, avoid calling Instance every frame in hot paths
Vector2 move = InputRouter.Instance.MoveAxis;

PersistentMonoBehaviourSingleton

Same as above, but survives scene loads via DontDestroyOnLoad.

public class GameSession : PersistentMonoBehaviourSingleton<GameSession>
{
  public int CurrentLevel { get; set; }
}

ScriptableObjectSingleton

Asset must live in a Resources folder and be named exactly like the type (GameBalance.asset for GameBalance).

using UnityEngine;

[CreateAssetMenu(fileName = "GameBalance", menuName = "Game/Balance")]
public class GameBalance : ScriptableObjectSingleton<GameBalance>
{
  public float PlayerSpeed = 6.0f;
}

float speed = GameBalance.Instance.PlayerSpeed;

Tests