Random
Seeded, deterministic random numbers with named streams and snapshot/restore for save-correct runs.
Reproducible randomness: same seed, same rolls — on every platform, across save and load.
| Interface | IRandomService |
| Off switch | NullRandomService |
| Assembly | CommonGameSystem.Core |
| Startup | Registered automatically at boot — no setup needed |
What it does
The Random service replaces Unity's single global Random state with independent, reproducible random sources. Deterministic means: give it a fixed seed, and your loot tables, enemy encounters, and procedural generation reproduce exactly on every replay — and the sequence is identical on every platform. That is ideal for bug reports ("seed 12345, third room") and essential for save/load correctness.
Each game system can draw from its own named stream — an independent random sequence identified by a string such as "loot" or "vfx". Streams are isolated, so adding one random call in one system never shifts the sequence of another. A cosmetic screen-shake roll can never change what loot drops.
The full state can be captured to a snapshot and restored later: reload a save mid-run, and the next roll is exactly where you left off — no save-scumming a better drop by reloading.
Quick start
Resolve the service and cache it in a MonoBehaviour marked [DefaultExecutionOrder(100)] so it runs after the framework has started:
var rng = ServiceLocator.Resolve<IRandomService>();
// or the short alias: var rng = SL.Resolve<IRandomService>();
For a fully deterministic run, replace the service with one built from an explicit seed:
ServiceLocator.Replace<IRandomService>(new RandomService(new RandomOptions(seed: 12345)));
The service itself is the default (master) stream — all of its draw methods are ready to call immediately.
API reference
Core draw methods
Each call advances the stream state.
ulong NextULong()— raw 64-bit value.uint NextUInt()— raw 32-bit value.int NextInt(int maxExclusive)— unbiased integer in[0, maxExclusive).int NextInt(int minInclusive, int maxExclusive)— unbiased integer in the given range.float NextFloat()— uniform value in[0, 1); it never returns exactly 1.0.float NextFloat(float min, float max)— uniform value in the given range.double NextDouble()— uniform value in[0, 1).bool NextBool()— fair coin flip.bool Chance(float probability)—truewith the given probability. A probability of exactly 0 or 1 returns immediately without drawing.
Convenience methods
int NextIndex(int count)— uniform index in[0, count); returns -1 whencountis zero or negative.int NextWeightedIndex(IReadOnlyList<float> weights)— pick an index in proportion to its weight; returns -1 for degenerate input (empty list, all-zero weights).T Pick<T>(IReadOnlyList<T> items)— a uniformly chosen element from the list.void Shuffle<T>(IList<T> items)— shuffle the list in place; every ordering is equally likely.
Named streams (isolation)
IRandomStream GetStream(string name)— get or create an independent stream. The same name returns the same stream instance for the whole session.IRandomStreamoffers the same draw methods listed above.ulong Seed { get; }— the current master seed.void Reseed(ulong seed)— reset to a new seed. This drops all named streams.
Save and restore
RandomSnapshot Capture()— serialize the full state: the master seed, the default stream, and every named stream.void Restore(RandomSnapshot snapshot)— resume from a snapshot, for example when loading a save mid-run.
RandomSnapshot is a plain serializable struct, so it drops straight into your save data class (see the save-integration example below). Its IsValid property tells you whether a loaded snapshot carries a usable payload.
Examples
Everyday draws
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class LootDropper : MonoBehaviour
{
private IRandomService _rng;
void Awake()
{
_rng = ServiceLocator.Resolve<IRandomService>();
}
public void RollDrop()
{
// Deterministic damage roll: the same seed gives the same sequence on all platforms.
int dmg = _rng.NextInt(8, 13);
Debug.Log($"Damage: {dmg}");
// A named stream keeps cosmetic effects isolated from gameplay rolls.
var vfx = _rng.GetStream("vfx");
float shake = vfx.NextFloat(0f, 0.5f);
Debug.Log($"Screen shake: {shake}");
// Weighted loot table.
float[] weights = { 30f, 50f, 20f }; // common, rare, epic
int rarity = _rng.NextWeightedIndex(weights);
Debug.Log($"Rarity: {rarity}");
}
}
Snapshot / restore with the Save service
The service does not write the snapshot anywhere itself — you route it through the Save / Load service alongside the rest of your save data. Embed RandomSnapshot as a field of your save class:
using CommonGameSystem.Core;
using UnityEngine;
[System.Serializable]
public class RunSaveData
{
public int stage;
public RandomSnapshot rng; // the full random state rides inside your save data
}
[DefaultExecutionOrder(100)]
public class RunSaveManager : MonoBehaviour
{
private IRandomService _rng;
private ISaveService _save;
void Awake()
{
_rng = ServiceLocator.Resolve<IRandomService>();
_save = ServiceLocator.Resolve<ISaveService>();
}
public void SaveRun(int currentStage)
{
var data = new RunSaveData
{
stage = currentStage,
rng = _rng.Capture() // freeze the dice exactly where they are
};
_save.Save("run-autosave", data);
}
public void LoadRun()
{
var result = _save.Load<RunSaveData>("run-autosave");
if (result.Status != SaveStatus.Ok)
return; // no save yet, or the file is unreadable — start fresh
if (result.Value.rng.IsValid)
_rng.Restore(result.Value.rng); // the next roll continues the saved sequence
// ... restore the rest of your run state from result.Value ...
}
}
After Restore, the very next NextInt (on the master stream or any named stream) returns exactly what it would have returned had the game never quit. Reloading the save cannot reroll the outcome.
Turning it off
ServiceLocator.Replace<IRandomService>(new NullRandomService());
This mutes the random service. Every draw returns a fixed, safe default (0, 0f, false, -1, or default(T)) instead of a random value. Useful for suppressing randomness in tests or in a deterministic sandbox mode. Programming errors still throw: a null stream name or a call after Dispose raises an exception, just like the real service.
Common pitfalls
- Main thread only. All draws and stream access must happen on the main thread. If a worker thread needs random values, draw them (or capture a snapshot) on the main thread first and hand the results to the worker.
- Do not hold named streams across
Reseed().Reseed()drops every named stream from the service. A stream you cached before the reseed keeps working on its own, but the service no longer tracks it — and it no longer appears in snapshots. CallGetStream()again after a reseed to get a stream derived from the new seed. Chance(0f)andChance(1f)skip the draw. These two cases return immediately and do not advance the stream state. If your code depends on exact draw counts for determinism, account for this.- Saving the snapshot is your job.
Capture()returns a plain value; the service does not write it anywhere. Route it through the Save / Load service with the rest of your save data, and callRestorewhen loading — as shown above. - Bad input returns safe values instead of throwing. An empty list, a zero-width range, all-zero weights, or a negative count returns 0, -1, or
default(T), optionally with a logged warning (controlled byRandomOptions.WarnOnDegenerateInput). The service never crashes on degenerate data.
Related pages
- Save / Load — persisting the snapshot with the rest of your save data
- Service Locator — resolving and replacing services