Seeding a random number generator

Importance of the seed

All random number generators are called pseudo generators which have an internal state. This state is generated upon construction and updated with every generated value. This internal state determines the next output of the RNG (and additional initialization parameters, which are not allowed to change during operation).

The generation is as follows:

  • Get internal state
  • Do some calculations on this state
  • Return the result and store an updated state
  • Repeat for the next value

The internal state is seeded by a user supplied number. If the same seed is used, the random number generator outputs the same set of random numbers in the same order.

Strategies

The strategies on working with seeds depend on the use case:

  • Random events, for example: damage generation, dice rolls, everything where the user experience should feel random every time
  • Stable generation, for example: map generators, procedural generation that should produce the same result with a maybe user supplied seed

Random Events

To make random numbers feel random, a seed has to be provided that is different every time the RNG is initialized. Either when it is instanced on-demand or on game start. For this case the global game time can be used, or the system time.

var rng = new GRandom(DateTimeOffset.Now.ToUnixTimeSeconds());

Inside of burst the result of DateTimeOffset.Now.ToUnixTimeSeconds() needs to be supplied via parameters or other variables.

Random number generators can be used inside the Job System of DOTS. In this case every RNG needs to be initialized in each job so that they do not produce the same sequence of random numbers for each index or invocation.

public struct Example : IJobParallelFor {
    public long timestamp;

    public void Execute(int index) {
        var rng = GRandom.CreateFrom(timestamp, index);
    }
}

or

public struct Example : IJobEntity {
    public double globalTime; // SystemAPI.Time.ElapsedTime

    void Execute([ChunkIndexInQuery] int chunkIndexInQuery, [EntityIndexInChunk] int entityIndexInChunk)
    {
        var random = GRandom.CreateForEcs(chunkIndexInQuery, entityIndexInChunk, globalTime);
    }
}

A custom initialisation from a custom seed source is also possible. In that case the timestamp can be replaced with the custom source.

Stable Generation

Stable generation needs the same sequence of random numbers everytime when the same seed is applied. The seed is usually something that is user visible and/or saved between games. This use case is much more difficult.

In this case the seed can be directly supplied to the RNG:

var rng = new GRandom(seed);

The seed shouldn’t be zero and should provide some lower bits for the RNG to initialize correctly.

When using the unity Job System of DOTS or ECS, every thread will need its own random number generator. In this case it might be necessary to pre-generate threads for each needed thread.

A low-key solution would be to use an RNG with a higher period (and a static seed) to generate random numbers, which in turn are then used for each job as seed. This solution is easy, but doesn’t prevent that two jobs are started with the same seed (and produce the same number sequence).

A more complicated solution is to jump the RNG, so each job/iteration is guaranteed to have a non-overlapping sequnce of random numbers (currently not supported, maybe 1.1.0)