Determinstic Seeding (Global Seed)

New in 1.4.0 - make enemy population and placement reproducible across runs.

This page explains how to enable and use the Global Seed to get deterministic, debuggable spawn behavior in the Runtime Spawner. With a fixed seed and the same content, the spawner will produce the same spawn choices (per-entry RNG), positions, and group sizes every run- great for QA repros, speedruns, and perf testing.


What the seed controls

  • A single Global Seed drives a fast, reproducible PCG32 RNG for spawns.

  • For each spawn request, the spawner derives a stable sub-stream using:

    • the Global Seed

    • the SpawnEntry’s stable identity

    • context hints (e.g., SpawnContext.SourceName such as a wave/table name, or region name)

  • The derived RNG is used for:

    • Placement sampling (global/region/wave)

    • Group size & grouping order (leader + members)

    • Member offsets around the leader

Result: given the same seed, scene, and timing, spawn locations and groups repeat exactly.


Enabling determinism

In the Inspector

  1. Select your RuntimeSpawner.

  2. Enable Use Deterministic Seed.

  3. Set Global Seed (any int).

Call this before any spawns start (i.e., before StartSpawners() or before waves fire).

using MegaCrush.Spawner;
using UnityEngine;

public class GameBootstrap : MonoBehaviour
{
    [SerializeField] private RuntimeSpawner spawner;
    [SerializeField] private int missionSeed = 123456; // set from save/CLI/dev console

    void Awake()
    {
        if (!spawner) spawner = FindAnyObjectByType<RuntimeSpawner>();

        // Locks the package into deterministic mode and broadcasts the change.
        spawner.SetGlobalSeed(missionSeed);

        // Optionally start the system afterward
        spawner.Init();
        spawner.StartSpawners();
    }
}

You can listen to RuntimeSpawner.onSeedChanged if you mirror the seed to UI/telemetry:

private void OnEnable()  => RuntimeSpawner.onSeedChanged += OnSeedChanged;
private void OnDisable() => RuntimeSpawner.onSeedChanged -= OnSeedChanged;

private void OnSeedChanged(int seed)
{
    Debug.Log($"Spawner seed now {seed}");
    // Update HUD, write to a run log, etc.
}

Picking good seeds

A few simple patterns:

1) Per-run random → saved for repro

int seed = System.Environment.TickCount;        // or RandomNumberGenerator for crypto
spawner.SetGlobalSeed(seed);
// Save 'seed' to your run history so QA can replay exactly the same mission.

2) Campaign + node → stable across sessions

int seed = Hash32(playerCampaignSeed, currentNodeId);
spawner.SetGlobalSeed(seed);

// Example non-cryptographic hash
static int Hash32(int a, int b)
{
    unchecked { uint h = 2166136261; h = (h ^ (uint)a) * 16777619; h = (h ^ (uint)b) * 16777619; return (int)h; }
}

3) Command line / dev console

-game_seed=987654

Parse once, call SetGlobalSeed(value) in your bootstrap.


Best practices

  • Call early. Set the seed before any spawns can occur (including auto-start or WaveTrigger.StartAutomatically).

  • Use the same seed everywhere (if your own systems also randomize). For true full-game determinism, route your other RNG-using systems off the same root seed.

  • Log the seed in your run summary so QA can replay bugs easily.

  • Don’t mix UnityEngine.Random into spawn decisions you expect to be deterministic. The spawner already supplies a per-request RNG internally; if you extend spawns, use that RNG rather than global state.


Scope & limitations

Determinism covers:

  • Entry placement sampling (global/region/wave)

  • Group size and per-member offsets

  • Spawner-internal randomization that uses the derived PCG32 stream

Not guaranteed deterministic:

  • Wave entry selection order inside WaveSpawnLoop if you rely on UnityEngine.Random in your own code paths.

    • If you need fully deterministic wave composition, either pre-author explicit sequences, or adapt your selection to route through the same seed (e.g., add a small utility that uses SpawnerSeedRouter for wave picking).

  • External nondeterminism: physics timing, asynchronous content loads, time-based triggers, or anything randomized outside the spawner using other RNGs.

In short: the spawner side is deterministic; the rest of your game must also avoid nondeterministic influences if you want a perfectly repeatable run.


Frequently asked

Q: Can I change the seed mid-mission? A: You can call SetGlobalSeed(...) at any time; it will affect subsequent spawn requests. For reproducible runs, set it once at mission start.

Q: Does the seed affect waves? A: Yes - spawn placement and group behavior within a wave are deterministic. If you also want deterministic which SpawnEntry a wave picks when randomizing, drive that selection from the same seed (see limitation above).

Q: How does this interact with pooled objects? A: Pooling is unaffected. The RNG decides where/what/how many; the pool just supplies instances.

Q: Do I need to enable anything else? A: No. Turn on Use Deterministic Seed and set a seed. Everything else is built in.


Example: QA repro flow

  1. On mission start, generate/log a seed:

    int seed = System.Environment.TickCount;
    spawner.SetGlobalSeed(seed);
    Debug.Log($"RUN SEED: {seed}");
  2. If a bug is found, re-launch with -game_seed=<value> or paste the seed in your dev console and rerun.

  3. The same enemies, placements, and groupings will appear at the same times/places (subject to the limitations above).


API recap

  • Inspector

    • Use Deterministic Seed (bool)

    • Global Seed (int)

  • Runtime

    • void SetGlobalSeed(int seed)

    • int GlobalSeed { get; }

    • bool UseDeterminism { get; }

    • static event Action<int> onSeedChanged

That’s it - flip the switch, set a seed, and enjoy reproducible spawns.

Last updated