Table of Contents

Singletons Guide

Singletons are world-level resources not tied to any entity. This guide covers when and how to use them.

What are Singletons?

Singletons are global data within a world:

  • Not attached to entities - Exist at the world level
  • One per type - Only one instance of each type per world
  • World-isolated - Different worlds have different singletons

Common uses:

  • Game time / delta time
  • Input state
  • Configuration settings
  • Random number generators
  • Asset references

Basic Usage

Setting a Singleton

world.SetSingleton(new GameTime
{
    DeltaTime = 0.016f,
    TotalTime = 0f,
    FrameCount = 0
});

Getting a Singleton

// Get by reference (zero-copy)
ref var time = ref world.GetSingleton<GameTime>();
Console.WriteLine($"Delta: {time.DeltaTime}");

// Modify directly
time.TotalTime += time.DeltaTime;
time.FrameCount++;

Read-Only Access

ref readonly var time = ref world.GetSingleton<GameTime>();
float delta = time.DeltaTime;
// time.DeltaTime = 0;  // Compile error - readonly

Checking Existence

if (world.HasSingleton<GameConfig>())
{
    ref var config = ref world.GetSingleton<GameConfig>();
    // Use config...
}

Try-Get Pattern

if (world.TryGetSingleton<InputState>(out var input))
{
    // Use input (this is a copy, not a reference)
    Console.WriteLine($"Move: ({input.MoveX}, {input.MoveY})");
}

Removing Singletons

bool removed = world.RemoveSingleton<DebugConfig>();

Common Singleton Types

Game Time

public struct GameTime
{
    public float DeltaTime;
    public float TotalTime;
    public long FrameCount;
    public float TimeScale;
}

// TimeSystem updates it each frame
public class TimeSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        ref var time = ref World.GetSingleton<GameTime>();
        time.DeltaTime = deltaTime * time.TimeScale;
        time.TotalTime += time.DeltaTime;
        time.FrameCount++;
    }
}

Input State

public struct InputState
{
    public float MoveX;
    public float MoveY;
    public bool Jump;
    public bool Attack;
    public float MouseX;
    public float MouseY;
}

// InputSystem reads raw input and populates singleton
public class InputSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        World.SetSingleton(new InputState
        {
            MoveX = GetAxis("Horizontal"),
            MoveY = GetAxis("Vertical"),
            Jump = IsKeyPressed(Keys.Space),
            Attack = IsMouseButtonPressed(0)
        });
    }
}

// Other systems read it
public class PlayerControlSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        ref readonly var input = ref World.GetSingleton<InputState>();

        foreach (var entity in World.Query<Velocity>().With<Player>())
        {
            ref var vel = ref World.Get<Velocity>(entity);
            vel.X = input.MoveX * 5f;
            vel.Y = input.MoveY * 5f;
        }
    }
}

Game Configuration

public struct GameConfig
{
    public int Difficulty;
    public float MasterVolume;
    public float MusicVolume;
    public float SfxVolume;
    public bool Fullscreen;
    public int ResolutionWidth;
    public int ResolutionHeight;
}

// Set during game initialization
world.SetSingleton(new GameConfig
{
    Difficulty = 1,
    MasterVolume = 0.8f,
    MusicVolume = 0.7f,
    SfxVolume = 1.0f,
    Fullscreen = true,
    ResolutionWidth = 1920,
    ResolutionHeight = 1080
});

Random Number Generator

public struct GameRandom
{
    public int Seed;
    public Random Instance;  // Note: Reference type inside struct
}

// Initialize once
world.SetSingleton(new GameRandom
{
    Seed = 12345,
    Instance = new Random(12345)
});

// Use in systems
public class SpawnSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        ref var rng = ref World.GetSingleton<GameRandom>();

        foreach (var entity in World.Query<Spawner>())
        {
            float x = rng.Instance.NextSingle() * 100f;
            float y = rng.Instance.NextSingle() * 100f;
            // Spawn at random position...
        }
    }
}

Camera Data

public struct CameraData
{
    public float X;
    public float Y;
    public float Zoom;
    public float Rotation;
    public Entity Target;
}

// CameraSystem updates singleton
public class CameraSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        ref var cam = ref World.GetSingleton<CameraData>();

        if (cam.Target.IsValid && World.IsAlive(cam.Target))
        {
            ref readonly var targetPos = ref World.Get<Position>(cam.Target);
            cam.X = Lerp(cam.X, targetPos.X, deltaTime * 5f);
            cam.Y = Lerp(cam.Y, targetPos.Y, deltaTime * 5f);
        }
    }
}

// RenderSystem reads it
public class RenderSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        ref readonly var cam = ref World.GetSingleton<CameraData>();

        // Apply camera transform
        BeginCamera(cam.X, cam.Y, cam.Zoom, cam.Rotation);

        foreach (var entity in World.Query<Position, Sprite>())
        {
            // Render entities...
        }

        EndCamera();
    }
}

Singletons vs Components

Use Singleton Use Component
One instance in the world Multiple instances (per entity)
Global state Entity-specific state
Systems share data Entity owns data
Time, input, config Position, health, velocity

Patterns

Lazy Initialization

public class AudioSystem : SystemBase
{
    protected override void OnInitialize()
    {
        if (!World.HasSingleton<AudioState>())
        {
            World.SetSingleton(new AudioState { MasterVolume = 1.0f });
        }
    }
}

Default Values

public class PhysicsSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        // Use TryGet with defaults
        if (!World.TryGetSingleton<PhysicsConfig>(out var config))
        {
            config = new PhysicsConfig { Gravity = -9.81f };
        }

        foreach (var entity in World.Query<Velocity>().Without<Static>())
        {
            ref var vel = ref World.Get<Velocity>(entity);
            vel.Y += config.Gravity * deltaTime;
        }
    }
}

System Communication

// Combat system writes damage results
public class CombatSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        int totalDamage = 0;
        // Calculate damage...

        World.SetSingleton(new CombatResults
        {
            TotalDamageDealt = totalDamage,
            EntitiesKilled = killCount
        });
    }
}

// UI system reads results
public class UISystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        if (World.TryGetSingleton<CombatResults>(out var results))
        {
            DisplayDamageNumbers(results.TotalDamageDealt);
        }
    }
}

Best Practices

Do: Use Structs

// ✅ Good: Value type
public struct GameTime
{
    public float DeltaTime;
    public float TotalTime;
}

Do: Keep Singletons Focused

// ✅ Good: Separate concerns
public struct InputState { /* input only */ }
public struct GameTime { /* time only */ }
public struct AudioConfig { /* audio only */ }

// ❌ Bad: Kitchen sink singleton
public struct GameGlobals
{
    public float DeltaTime;
    public float MoveX, MoveY;
    public float MasterVolume;
    public int Score;
    // ... everything else
}

Don't: Use Singletons for Entity-Specific Data

// ❌ Bad: Should be a component on an entity
public struct PlayerHealth
{
    public int Current;
    public int Max;
}

// ✅ Good: Use components
public struct Health : IComponent
{
    public int Current;
    public int Max;
}

Next Steps