Quick answer: Log the RNG seed at the start of every session and include it in crash reports. Use separate RNG streams for gameplay and cosmetics so visual changes don’t break replay determinism. When a bug can’t be reproduced with the same seed, look for hidden sources of non-determinism: hash map ordering, thread scheduling, floating-point inconsistencies, or calls to the system RNG instead of your seeded generator.
Randomness is essential to making games feel alive, but it is the enemy of reproducible bugs. A player reports that enemies spawned inside a wall, but you run the same level fifty times and never see it. The bug is real — it just requires a specific sequence of random numbers to trigger. Without the seed that produced that sequence, you are debugging blind. Here’s how to build systems that make random bugs reproducible.
Seed Logging: The Foundation of Random Bug Debugging
The single most impactful thing you can do for random-related debugging is log the seed. Every time a game session starts, record the seed that initialized the random number generator. Store it in the save file, print it to the developer console, and — most importantly — include it in crash reports and bug telemetry.
When a player submits a bug report, the seed gives you the ability to recreate the exact sequence of random events that led to the problem. Without it, you are guessing. With it, you can replay the scenario deterministically.
// C# (Unity) - Seed logging at session start
public class GameSession : MonoBehaviour
{
public int Seed { get; private set; }
void Awake()
{
Seed = System.Environment.TickCount;
Random.InitState(Seed);
Debug.Log($"Session seed: {Seed}");
// Include in crash reports
CrashReporter.SetMetadata("rng_seed", Seed.ToString());
}
}
# GDScript (Godot) - Seed logging at session start
var session_seed: int
func _ready():
session_seed = Time.get_unix_time_from_system()
seed(session_seed)
print("Session seed: ", session_seed)
Make the seed visible during development builds. Display it on a debug overlay, include it in screenshot metadata, and make it easy to paste into a “replay with seed” field. The easier it is to capture and reuse seeds, the more likely your team will actually use them when investigating bugs.
Isolating RNG Streams
A single global RNG is simple but fragile. Every call to random() advances the sequence by one step. If a particle system requests a random number for a visual effect, it shifts the entire sequence, and every subsequent gameplay-affecting random call returns a different value. Adding a new particle effect to a level can change enemy spawn positions, loot drops, and AI decisions — all because the visual system consumed an extra random number.
The solution is to use separate RNG instances for different systems. At minimum, separate gameplay-critical randomness from cosmetic randomness. A more robust approach uses dedicated streams for each domain.
// C++ (Unreal) - Separate RNG streams
class FGameRNG
{
public:
FRandomStream WorldGen; // Level layout, spawns
FRandomStream LootDrops; // Item drops, rewards
FRandomStream AIBehavior; // Enemy decisions, patrol routes
FRandomStream VFX; // Particles, screen shake
void InitializeAll(int32 BaseSeed)
{
WorldGen.Initialize(BaseSeed);
LootDrops.Initialize(BaseSeed + 1);
AIBehavior.Initialize(BaseSeed + 2);
VFX.Initialize(BaseSeed + 3);
}
};
With isolated streams, you can change visual effects without affecting gameplay determinism. You can also debug individual systems in isolation — if loot drops are wrong, you only need to trace the LootDrops stream, ignoring everything else. And when you add new cosmetic features, you know they won’t change the behavior of existing gameplay systems.
Finding Hidden Sources of Non-Determinism
You log the seed, you replay with the same seed, and the bug does not reproduce. This means something besides your seeded RNG is introducing randomness. Here are the most common hidden sources.
Hash map iteration order. In many languages, hash maps (dictionaries) do not guarantee iteration order. If you iterate over a dictionary to process game entities, the order may differ between runs even with the same seed. Use sorted collections or arrays with stable ordering for anything that affects gameplay.
Floating-point non-determinism. IEEE 754 floating-point math can produce different results on different CPU architectures, different compiler optimization levels, and even different execution orders of the same operations. This is particularly dangerous in physics calculations. If two players on different hardware run the same seed, they may get slightly different physics results that compound over time into completely different game states.
Thread scheduling. If multiple threads consume random numbers or modify shared game state, the order in which threads execute is determined by the OS scheduler and varies between runs. Lock-free gameplay code is fast but non-deterministic. For reproducibility, process all gameplay logic on a single thread or use deterministic task scheduling.
System calls. Calls to DateTime.Now, Time.realtimeSinceStartup, or hardware timers inject real-world non-determinism. Use your game’s simulation clock (fixed timestep) for all gameplay logic. Reserve real-time clocks for UI display and network timestamps only.
Building a Replay System for Bug Reproduction
A seed alone is not enough for full reproduction. The seed determines the random sequence, but player input determines which random calls are made and in what order. A complete replay system records both the seed and the input sequence, then replays them together.
// Minimal replay recording structure
struct ReplayFrame
{
int FrameNumber;
float InputX;
float InputY;
bool JumpPressed;
bool AttackPressed;
// ... all input axes and buttons
};
struct ReplayData
{
int Seed;
int GameVersion;
List<ReplayFrame> Frames;
};
During replay, feed the recorded inputs into the game instead of reading from the controller. If your game is fully deterministic (same seed + same inputs = same output), the replay will reproduce the exact sequence of events, including the bug. If the replay diverges from the recording, you have found a source of non-determinism that needs to be eliminated.
Replay systems have a maintenance cost: any change to the input handling or game simulation can invalidate old replays. Version your replay format and include the game build number. When investigating a bug, replay against the same build the player was running. Cross-version replay is a nice-to-have but requires careful versioning of the simulation logic.
Practical Debugging Workflow
When a random-seeming bug is reported, follow this sequence. First, check the bug report for a seed value. If present, replay with that seed and the reported inputs. If the bug reproduces, you have a deterministic test case and can debug normally.
If the bug does not reproduce with the seed, start eliminating non-determinism sources. Add checksums at key points in the simulation: after each frame, hash the positions of all entities and the RNG state. Compare these checksums between the original run and your replay. The first frame where the checksums diverge is where non-determinism entered the simulation. From there, narrow down which system produced a different result and why.
For bugs that only occur rarely, set up a headless test that runs thousands of sessions with random seeds and checks for invariant violations. If enemies should never spawn inside walls, assert that after every spawn. Run overnight. The test will eventually hit a seed that triggers the bug, giving you a reproducible case to debug.
Log the seed, isolate the streams, record the inputs, checksum the state.