BlessRNG or check the RNG for honesty





In gamedev, you often need to tie something up on a random house: Unity has its own Random for this, and System.Random exists in parallel with it. Once upon a time on one of the projects it seemed that both could work differently (although they should have a uniform distribution).



Then they did not go into details - it was enough that the transition to System.Random fixed all the problems. Now we decided to understand in more detail and conduct a small study: how “biased” or predictable RNGs are, and which one to choose. Moreover, I have often heard conflicting opinions about their “honesty” - let’s try to figure out how the real results relate to the stated ones.



A brief educational program or RNG is actually a PRNG



If you are already familiar with random number generators, then you can immediately proceed to the "Testing" section.



Random numbers (MF) is a sequence of numbers generated using some random (chaotic) process, a source of entropy. That is, this is such a sequence, the elements of which are not connected by any mathematical law - they do not have a causal relationship.



What creates a midrange is called a random number generator (RNG). It would seem that everything is elementary, but if we go from theory to practice, then actually implementing a software algorithm for generating such a sequence is not so simple.



The reason lies in the absence of the very randomness in modern consumer electronics. Without it, random numbers cease to be random, and their generator turns into an ordinary function of deliberately determined arguments. For a number of specialties in the IT field this is a serious problem (for example, for cryptography), for the rest there is a perfectly acceptable solution.



We need to write an algorithm that would return, even if not truly random numbers, but as close as possible to them - the so-called pseudorandom numbers (PSNs). The algorithm in this case is called the pseudorandom number generator (PRNG).



There are several options for creating a PRNG, but for all, the following will be relevant:



  1. The need for pre-initialization.



    The PRNG is devoid of a source of entropy, therefore, before using it, it is necessary to indicate the initial state. It is given as a number (or vector) and is called a seed (seed, random seed). Often, a processor clock counter or the numerical equivalent of system time is used as seed.
  2. Repeatability of the sequence.



    The PRNG is completely deterministic, so the seed specified during initialization uniquely determines the entire future sequence of numbers. This means that a single PRSP, initialized with the same seed (at different times, in different programs, on different devices) will generate the same sequence.


You also need to know the probability distribution characterizing 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)



Let's say we have an honest dice with 24 faces. If you drop it, then the probability of a unit falling out will be 1/24 (as well as the probability of any other number falling out). If you make a lot of throws and record the results, you will notice that all the faces fall out at about the same frequency. In fact, this dice can be considered RNG with a uniform distribution.



And if you immediately throw 10 of these bones and count the total points? Will uniformity be maintained for her? Not. Most often, the amount will be close to 125 points, that is, to some average value. And as a result - even before making a throw, you can roughly estimate the future result.



The reason is that to get the average amount of points there is the greatest number of combinations. The farther from it, the fewer combinations - and, accordingly, the less chance of loss. If you visualize this data, it will remotely resemble the shape of a bell. Therefore, with some stretch, a system of 10 bones can be called an RNG with a normal distribution.



Another example, only already in the plane - target shooting. The shooter will be the RNG generating a pair of numbers (x, y), which is displayed on the graph.



Agree that the option on the left is closer to real life - this is an RNG with a normal distribution. But if you need to scatter stars in a dark sky, then the right option, obtained with the help of an RNG with a uniform distribution, is better. In general, choose a generator depending on the task.



Now let's talk about the entropy of the PSP sequence. For example, there is 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 are these numbers at first glance? Let's start by checking the distribution.



It looks like close to uniform, but if you read the sequence of two numbers and interpret them as coordinates on the plane, you get this:



Patterns are clearly visible. And since the data in the sequence is ordered in a certain way (that is, they have low entropy), this can lead to the very “bias”. At a minimum, such a PRNG is not very suitable 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, ...



Everything seems to be fine here even on the plane:



Let's look in volume (we read three numbers each):



And again the patterns. Build visualization in four dimensions will not work. But patterns can exist both on this dimension and on large ones.



In the same cryptography, where the most stringent requirements are imposed on the PRNG, such a situation is categorically unacceptable. Therefore, to evaluate their quality, special algorithms have been developed, which we will not touch on now. The topic is extensive and draws on a separate article.



Testing



If we don’t know something for sure, then how to work with it? Is it worth it to cross the road if you don’t know what traffic signal it allows? The consequences may be different.



The same goes for the notorious randomness in Unity. Well, if the documentation reveals the necessary details, but the story mentioned at the beginning of the article happened just because of the lack of desired specifics.



And without knowing how the tool works, you cannot apply it correctly. In general, the time has come to check and conduct an experiment to finally make sure at least at the expense of distribution.



The solution was simple and effective - to collect statistics, obtain objective data and look at the results.



Subject of study



There are several ways to generate random numbers in Unity — we tested five.



  1. System.Random.Next (). Generates integers in a given range of values.
  2. System.Random.NextDouble (). Generates double precision numbers (double) in the range from [0; one).
  3. UnityEngine.Random.Range (). Generates single precision numbers (float) in a given range of values.
  4. UnityEngine.Random.value. Generates single precision numbers (float) ranging from [0; one).
  5. Unity.Mathematics.Random.NextFloat (). Part of the new Unity.Mathematics library. Generates single precision numbers (float) in a given range of values.


Almost everywhere in the documentation, a uniform distribution was indicated, with the exception of UnityEngine.Random.value (where the distribution is not specified, but similarly to UnityEngine.Random.Range (), uniformity was also expected) and Unity.Mathematics.Random.NextFloat () (where in the basis is the xorshift algorithm, which means that again you need to wait for a uniform distribution).



By default, those expected in the documentation were taken for the expected results.



Methodology



We wrote a small application that generated sequences of random numbers in 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 was collected from several target platforms:





Implementation



We have several different ways to generate random numbers. For each of them we will write a separate wrapper class, which should provide:



  1. Ability to set the range of values ​​[min / max). It will be set through the constructor.
  2. Method that returns midrange. We will choose float as the type, as a more general one.
  3. The name of the generation method for marking the results. For convenience, we will return the full class name + the name of the method used to generate the midrange as the value.


First, declare an abstraction, which 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, and a float is needed. You can simply interpret the integer as a float, or you can expand the range of values ​​by several orders of magnitude, compensating for them each time the midrange is generated. It will turn out something like fixed-point with the given accuracy. We will use this option, since it is closer to the real 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 a fixed range of values ​​[0; one). To project it onto the one 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; } }
      
      





UnityEngine.Random.Range () implementation



This method of the UnityEngine.Random static class allows you to specify a range of values ​​and returns a midrange of type float. No additional transformations are 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); } }
      
      





UnityEngine.Random.value implementation



The value property of the static class UnityEngine.Random returns a midrange of type float from a fixed range of values ​​[0; one). We project it onto a given range in the same way as when implementing System.Random.NextDouble ().



 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; } }
      
      





Unity.Mathematics.Random.NextFloat () implementation



The NextFloat () method of the Unity.Mathematics.Random class returns a midrange of type float and allows you to specify a range of values. The only nuance is that each instance of Unity.Mathematics.Random will have to be initialized with some seed - this way we will avoid generating repeated 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); } }
      
      





MainController implementation



Several IRandomGenerator implementations are ready. Next, you need to generate sequences and save the resulting dataset for processing. To do this, create a scene in Unity and a small script MainController, which will perform all the necessary work and simultaneously be responsible for interacting with the UI.



We set the size of the dataset and the range of midrange values, and also get a method that returns an array of tuned 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) }; } ... } }
      
      





And now we are forming a dataset. In this case, the data generation will be combined with the recording of the results in a text stream (in csv format). To store the values ​​of each IRandomGenerator, its own separate column is allocated, and the first line contains the name of the generator.



 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 immediately transfer data over the network from the end device to the receiving 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 source code for the project is on GitLab .



results



No miracle happened. What they expected, they got it - in all cases a uniform distribution without a hint of conspiracies. I don’t see the point of applying separate graphics on the platforms - they all show approximately the same results.



The reality is:





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





And visualization in 3D. I’ll leave only the result of System.Random.Next () so as not to produce a bunch of the same content.





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



All Articles