
In game development, you often need to tie something to randomness: Unity has its own Random, while there is also System.Random. Long ago, one of our projects left the impression that both could behave differently (even though they should have a uniform distribution).
At that time, we didn't delve into the details — it was sufficient to know that switching to System.Random resolved all issues. Now we've decided to look into it more closely and conduct a small investigation: how 'biased' or predictable are RNGs, and which one to choose. Especially since I've often heard conflicting opinions about their 'fairness' — let's try to figure out how real results relate to the claims.
A brief primer or what RNG actually is
If you're already familiar with random number generators, you can skip directly to the 'Testing' section.
Random numbers (RNs) are a sequence of numbers generated through some random (chaotic) process, an entropy source. In other words, it's a sequence whose elements are not interconnected by any mathematical law — they lack a cause-and-effect relationship.
What creates RNs is called a random number generator (RNG). It seems elementary, but when moving from theory to practice, implementing a software algorithm to generate such a sequence is not that simple.
The reason lies in the lack of that very chaos in modern consumer electronics. Without it, random numbers cease to be random, and their generator turns into a simple function of predetermined arguments. This poses a serious problem for many IT specialties (for example, in cryptography), while for others, there's a quite acceptable solution.
You need to write an algorithm that returns, if not truly random numbers, then as close to them as possible — so-called pseudorandom numbers (PRNs). The algorithm in this case is called a pseudorandom number generator (PRNG).
There are several options for creating a PRNG, but the following will be relevant for all of them:
- The necessity of preliminary initialization.
A pseudo-random number generator (PRNG) lacks a source of entropy, so it requires an initial state before use. This is set as a number (or vector) and is called a seed (seed, random seed). Often, a CPU cycle counter or a numeric representation of the system time is used as the seed.
- Reproducibility of the sequence.
A PRNG is completely deterministic, so the seed specified during initialization uniquely determines the entire future sequence of numbers. This means that the same PRNG initialized with the same seed (at different times, in different programs, on different devices) will generate the same sequence.
It is also necessary to know the probability distribution that characterizes the PRNG—what numbers it will generate and with what probability. Most often, this is either a normal distribution or a uniform distribution.

Normal distribution (left) and uniform distribution (right)
Suppose we have a fair 24-sided die. If we roll it, the probability of rolling a one is 1/24 (as is the probability of rolling any other number). If we roll it many times and record the results, we will notice that all sides show up with roughly the same frequency. Essentially, this die can be considered a random number generator with a uniform distribution.
And what if we roll 10 such dice at once and count the total points? Will uniformity be preserved? No. Most often, the sum will be close to 125 points, which is some average value. As a result, even before rolling, we can roughly estimate the future outcome.
The reason is that there are the most combinations to achieve the average sum of points. The further away from it, the fewer combinations there are—and thus, the lower the probability of occurrence. If this data were visualized, it would loosely resemble the shape of a bell. Therefore, with some exaggeration, a system of 10 dice can be called a random number generator with a normal distribution.
Another example, this time in a plane—shooting at a target. The shooter will be a PRNG generating a pair of numbers (x, y), which is displayed on a graph.

Agree that the left option is more closely related to real life — it's a random number generator with a normal distribution. But if you need to spread stars across a dark sky, the right option, obtained using a uniformly distributed random number generator, is a better fit. In general, choose the generator based on the task at hand.
Now let's talk about the entropy of the random number sequence. For example, consider a sequence that starts like this:
89, 93, 33, 32, 82, 21, 4, 42, 11, 8, 60, 95, 53, 30, 42, 19, 34, 35, 62, 23, 44, 38, 74, 36, 52, 18, 58, 79, 65, 45, 99, 90, 82, 20, 41, 13, 88, 76, 82, 24, 5, 54, 72, 19, 80, 2, 74, 36, 71, 9, …
How random do these numbers seem at first glance? Let's start by checking the distribution.

It looks close to uniform, but if we read the sequence in pairs and interpret them as coordinates on a plane, we get this:

Patterns become distinctly visible. And since the data in the sequence is ordered in a particular way (meaning it has low entropy), this can lead to that very 'bias.' As a minimum, such a PRNG is not well suited for generating coordinates on a plane.
Another sequence:
42, 72, 17, 0, 30, 0, 15, 9, 47, 19, 35, 86, 40, 54, 97, 42, 69, 19, 20, 88, 4, 3, 67, 27, 42, 56, 17, 14, 20, 40, 80, 97, 1, 31, 69, 13, 88, 89, 76, 9, 4, 85, 17, 88, 70, 10, 42, 98, 96, 53, …
It seems that everything is fine even on the plane:

Let's look in 3D (reading three numbers at a time):

And again patterns emerge. It is no longer possible to build a visualization in four dimensions. But patterns can exist in this dimension and in higher ones.
In cryptography, where PRNGs are subject to the most stringent requirements, such a situation is categorically unacceptable. Therefore, special algorithms have been developed to assess their quality, which we will not cover here. This is a vast topic and warrants its own article.
Testing
If we do not know something for certain, how do we work with it? Is it worth crossing the road if you do not know which traffic signal permits it? The consequences can vary.
The same applies to the infamous randomness in Unity. It's good if the documentation reveals the necessary details, but the story mentioned at the beginning of the article happened precisely due to the lack of the desired specifics.
If you don't understand how the tool works, you won't be able to apply it correctly. In general, it's time to test and conduct an experiment to finally ensure at least regarding the distribution.
The solution was simple and effective - to gather statistics, obtain objective data, and look at the results.
Research subject
In Unity, there are several ways to generate random numbers - we tested five.
- System.Random.Next(). Generates integer values within a specified range.
- System.Random.NextDouble(). Generates double precision numbers in the range of [0; 1).
- UnityEngine.Random.Range(). Generates single precision numbers (float) within a specified range.
- UnityEngine.Random.value. Generates single precision numbers (float) in the range of [0; 1).
- Unity.Mathematics.Random.NextFloat(). Part of the new Unity.Mathematics library. Generates single precision numbers (float) within a specified range.
Almost everywhere in the documentation, uniform distribution was indicated, except for UnityEngine.Random.value (where the distribution is not specified, but uniform was also expected by analogy with UnityEngine.Random.Range()) and Unity.Mathematics.Random.NextFloat() (where the xorshift algorithm is used, so again we have to wait for uniform distribution).
By default, the expected results were taken to be those specified in the documentation.
Methodology
We wrote a small application that generated sequences of random numbers using each of the presented methods and saved the results for further processing.
The length of each sequence is 100,000 numbers.
The range of random numbers is [0, 100).
Data were collected from several target platforms:
- Windows
— Unity v2018.3.14f1, Editor mode, Mono, .NET Standard 2.0 - macOS
— Unity v2018.3.14f1, Editor mode, Mono, .NET Standard 2.0
— Unity v5.6.4p4, Editor mode, Mono, .NET Standard 2.0 - Android
— Unity v2018.3.14f1, build for device, Mono, .NET Standard 2.0 - iOS
— Unity v2018.3.14f1, build for device, il2cpp, .NET Standard 2.0
Implementation
We have several different ways to generate random numbers. For each of them, we will write a separate wrapper class that should provide:
- The ability to set the range of values [min/max). This will be set via the constructor.
- A method that returns the random number. We will choose float as the type, as it is more general.
- The name of the generation method for labeling the results. For convenience, we will return the full name of the class + the name of the method used for generating the random number.
First, we will declare an abstraction that will be represented by the IRandomGenerator interface:
namespace RandomDistribution
{
public interface IRandomGenerator
{
string Name { get; }
float Generate();
}
}Implementation of System.Random.Next()
This method allows you to specify a range of values, but it returns integers, while we need floats. You can either interpret integers as floats or expand the range of values by several orders of magnitude, compensating during each random number generation. It will look something like fixed-point with a specified precision order. We will use this option, as it is closer to the actual float value.
using System;
namespace RandomDistribution
{
public class SystemIntegerRandomGenerator : IRandomGenerator
{
private const int DefaultFactor = 100000;
private readonly Random _generator = new Random();
private readonly int _min;
private readonly int _max;
private readonly int _factor;
public string Name => "System.Random.Next()";
public SystemIntegerRandomGenerator(float min, float max, int factor = DefaultFactor)
{
_min = (int)min * factor;
_max = (int)max * factor;
_factor = factor;
}
public float Generate() => (float)_generator.Next(_min, _max) / _factor;
}
}Implementation of System.Random.NextDouble()
Here we have a fixed range of values [0; 1). To project it onto the range specified in the constructor, we use simple arithmetic: X * (max − min) + min.
using System;
namespace RandomDistribution
{
public class SystemDoubleRandomGenerator : IRandomGenerator
{
private readonly Random _generator = new Random();
private readonly double _factor;
private readonly float _min;
public string Name => "System.Random.NextDouble()";
public SystemDoubleRandomGenerator(float min, float max)
{
_factor = max - min;
_min = min;
}
public float Generate() => (float)(_generator.NextDouble() * _factor) + _min;
}
}Implementation of UnityEngine.Random.Range()
This method of the static class UnityEngine.Random allows you to specify a range of values and returns a float type random number. No additional conversions will be necessary.
using UnityEngine;
namespace RandomDistribution
{
public class UnityRandomRangeGenerator : IRandomGenerator
{
private readonly float _min;
private readonly float _max;
public string Name => "UnityEngine.Random.Range()";
public UnityRandomRangeGenerator(float min, float max)
{
_min = min;
_max = max;
}
public float Generate() => Random.Range(_min, _max);
}
}Implementation of UnityEngine.Random.value
The value property of the static class UnityEngine.Random returns a float type random number from a fixed range of values [0; 1). We will project it onto the specified range in the same way as in the System.Random.NextDouble() implementation.
using UnityEngine;
namespace RandomDistribution
{
public class UnityRandomValueGenerator : IRandomGenerator
{
private readonly float _factor;
private readonly float _min;
public string Name => "UnityEngine.Random.value";
public UnityRandomValueGenerator(float min, float max)
{
_factor = max - min;
_min = min;
}
public float Generate() => (float)(Random.value * _factor) + _min;
}
}Implementation of Unity.Mathematics.Random.NextFloat()
The NextFloat() method of the Unity.Mathematics.Random class returns a float type random number and allows you to specify a range of values. The nuance is that each instance of Unity.Mathematics.Random must be initialized with a seed — this avoids generating repeating sequences.
using Unity.Mathematics;
namespace RandomDistribution
{
public class UnityMathematicsRandomValueGenerator : IRandomGenerator
{
private Random _generator;
private readonly float _min;
private readonly float _max;
public string Name => "Unity.Mathematics.Random.NextFloat()";
public UnityMathematicsRandomValueGenerator(float min, float max)
{
_min = min;
_max = max;
_generator = new Random();
_generator.InitState(unchecked((uint)System.DateTime.Now.Ticks));
}
public float Generate() => _generator.NextFloat(_min, _max);
}
}Implementation of MainController
Several implementations of IRandomGenerator are ready. Next, we need to generate sequences and save the resulting dataset for processing. For this, let's create a scene in Unity along with a small script, MainController, which will handle all the necessary tasks while also managing interactions with the UI.
We will set the dataset size and the range of random number values, as well as have a method that returns an array of configured and ready-to-use generators.
namespace RandomDistribution
{
public class MainController : MonoBehaviour
{
private const int DefaultDatasetSize = 100000;
public float MinValue = 0f;
public float MaxValue = 100f;
...
private IRandomGenerator[] CreateRandomGenerators()
{
return new IRandomGenerator[]
{
new SystemIntegerRandomGenerator(MinValue, MaxValue),
new SystemDoubleRandomGenerator(MinValue, MaxValue),
new UnityRandomRangeGenerator(MinValue, MaxValue),
new UnityRandomValueGenerator(MinValue, MaxValue),
new UnityMathematicsRandomValueGenerator(MinValue, MaxValue)
};
}
...
}
}Now let's form the dataset. In this case, data generation will be combined with writing results to a text stream (in csv format). Each IRandomGenerator will have its own separate column, with the first row containing the generator's Name.
namespace RandomDistribution
{
public class MainController : MonoBehaviour
{
...
private void GenerateCsvDataSet(TextWriter writer, int dataSetSize, params IRandomGenerator[] generators)
{
const char separator = ',';
int lastIdx = generators.Length - 1;
// write header
for (int j = 0; j <= lastIdx; j++)
{
writer.Write(generators[j].Name);
if (j != lastIdx)
writer.Write(separator);
}
writer.WriteLine();
// write data
for (int i = 0; i <= dataSetSize; i++)
{
for (int j = 0; j <= lastIdx; j++)
{
writer.Write(generators[j].Generate());
if (j != lastIdx)
writer.Write(separator);
}
if (i != dataSetSize)
writer.WriteLine();
}
}
...
}
}It remains to call the GenerateCsvDataSet method and save the result to a file, or directly send the data over the network from the source device to the receiver. server.
namespace RandomDistribution
{
public class MainController : MonoBehaviour
{
...
public void GenerateCsvDataSet(string path, int dataSetSize, params IRandomGenerator[] generators)
{
using (var writer = File.CreateText(path))
{
GenerateCsvDataSet(writer, dataSetSize, generators);
}
}
public string GenerateCsvDataSet(int dataSetSize, params IRandomGenerator[] generators)
{
using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture))
{
GenerateCsvDataSet(writer, dataSetSize, generators);
return writer.ToString();
}
}
...
}
}The project source files are located at .
Results
No miracle happened. What was expected is exactly what we got — in all cases, a uniform distribution with no hint of conspiracies. There's no point in attaching separate graphs for the platforms — they all show roughly the same results.
The reality is this:

Visualization of sequences on the plane from all five generation methods:

And visualization in 3D. I will leave only the result of System.Random.Next() to avoid creating a bunch of identical content.

The story told in the introduction about the normal distribution of UnityEngine.Random did not repeat: either it was originally erroneous, or something has changed in the engine since then. However, now we are sure.
Source: habr.com
