The problem with UnityEngine.Random

Oliver Oliver • Published 3 years ago Updated 3 years ago


When it comes to game dev, random number generation is a subject that comes up a lot. With the advent of Minecraft, No Man's Sky, and other games with procedurally-generated environments, unique variation in your game is an attractive feature which increases replayability and enhances player experiences.

Unity provides a random number generator (RNG), and you may have already interacted with its API in the form of Random.Range. There is, however, one major flaw with its implementation: it's a singleton. Why is that a problem, you ask?

Consider this example. Imagine you want to generate objects at random positions in your world. You might do something like this:

using System.Collections;
using UnityEngine;

public class ExampleBehaviour : MonoBehaviour
{
    [SerializeField] private GameObject _spherePrefab;

    private void Awake()
    {
        StartCoroutine(GenerateRandomSpheres());
    }

    private IEnumerator GenerateRandomSpheres()
    {
        const float range = 5f;
        
        for (int i = 0; i < 100; i++)
        {
            var x = <mark>Random.Range</mark>(-range, range);
            var y = <mark>Random.Range</mark>(-range, range);
            var pos = new Vector2(x, y);
            Instantiate(_spherePrefab, pos, Quaternion.identity);

            yield return null;
        }
    }
}

We have a coroutine GenerateRandomSpheres which loops 100 times, calling Random.Range to give us random X and Y coordinates between -5 and 5. This would spawn in the sphere prefab 100 times at random locations:

My scene looks like it has acne.

Running it again would yield a slightly different distribution of objects:

Sure, it's random, but it's randomly different!

This is fine in many cases. However, games such as Minecraft allow the player to specify a “seed”. A seed lets you initialise an RNG so that it produces the same sequence every time, and this is why are you able to share “seeds” with your friends so they can explore the same random world as you. This is interesting, because it means these RNGs are not pure “random”, they are what is known as pseudorandom. They are 100% deterministic based on their initial condition: the seed. UnityEngine.Random allows you specify a seed with the InitState method, so let's alter the coroutine so that it calls this method with some made up seed like 1234567890.

using System.Collections;
using UnityEngine;

public class ExampleBehaviour : MonoBehaviour
{
    [SerializeField] private GameObject _spherePrefab;

    private void Awake()
    {
        StartCoroutine(GenerateRandomSpheres());
    }

    private IEnumerator GenerateRandomSpheres()
    {
        const float range = 5f;
        <mark>Random.InitState(1234567890);</mark>
        
        for (int i = 0; i < 100; i++)
        {
            var x = Random.Range(-range, range);
            var y = Random.Range(-range, range);
            var pos = new Vector2(x, y);
            Instantiate(_spherePrefab, pos, Quaternion.identity);

            yield return null;
        }
    }
}

Playing the scene again, we see this:

If you ran this code, you'd see the exact same distribution!

Stop the scene, and play it again. What do we see?

Déjà vu

Would you look at that? It's the exact same distribution from the last time. This is the deterministic behaviour of the pseudorandomness showing its true colours. We could run this once, twice, or a hundred times - but it doesn't matter. The random sequence is purely determined by its initial state. Change the seed? You change the sequence.

So where does it fall apart?

We have our coroutine to generate a collection of random spheres. Now suppose you wish to introduce random cubes too.

We'll create a second coroutine called GenerateRandomCubes, but this time we will have it use a different seed like 987654321 (truly revolutionary, right?). It will initialise its own state, while letting the original GenerateRandomSpheres coroutine initialise with its state.

We will add the code to do this but we won't actually call Instantiate, because otherwise the scene will be a mess of cubes and spheres and it will be difficult to keep track. The important thing is we are still calling Random.Range, the positions of the potential cubes are still being calculated.

using System.Collections;
using UnityEngine;

public class ExampleBehaviour : MonoBehaviour
{
    [SerializeField] private GameObject _spherePrefab;

    private void Awake()
    {
        StartCoroutine(GenerateRandomSpheres());
        <mark>StartCoroutine(GenerateRandomCubes());</mark>
    }

    private IEnumerator GenerateRandomSpheres()
    {
        const float range = 5f;
        Random.InitState(1234567890);

        for (int i = 0; i < 100; i++)
        {
            var x = Random.Range(-range, range);
            var y = Random.Range(-range, range);
            var pos = new Vector2(x, y);
            Instantiate(_spherePrefab, pos, Quaternion.identity);

            yield return null;
        }
    }

    private IEnumerator GenerateRandomCubes()
    {
        const float range = 5f;
        Random.InitState(<mark>987654321</mark>);

        for (int i = 0; i < 100; i++)
        {
            var x = Random.Range(-range, range);
            var y = Random.Range(-range, range);
            var pos = new Vector2(x, y);

            yield return null;
        }
    }
}

If we play the scene, we see this:

Level Design: 💯

Can you spot the problem? It might take a second.

One of these things is not like the other...

Adding in cube generation broke our expected sequence! Comment out the call StartCoroutine(GenerateRandomCubes()); and you will see, it reverts back to the first pattern we expected. How could this be? Didn't our coroutines initialise their own random state? They are working with different seeds, right?

Right?!

Possibly the best scene in any show ever.

As I mentioned earlier, Unity's RNG is completely static. It is a singleton. In the name of simplicity, Unity has sacrificed something critically important, which is encapsulated state; we only have one Random to work with throughout the entire lifetime of our game. This means adding any new random sequences to our code breaks existing ones.

So how can we work around it?

The Solution, or: How I Learned to Stop Using Unity's RNG and Love .NET

.NET has had its own implementation of an RNG ever since it was first released. System.Random gives us exactly what we need: encapsulated state.

If you've ever had to import both the System namespace and the UnityEngine namespace and been hit with the following error…

error CS0104: 'Random' is an ambiguous reference between 'UnityEngine.Random' and 'System.Random'

... you likely resolved it by either fully qualifying as UnityEngine.Random or by adding a using alias as using Random = UnityEngine.Random; - well now it's time to do a 180. From now on, you should use System.Random whenever possible.

System.Random works on an instance-basis. The constructor accepts a 32-bit integer as its seed, so let's go back to our original code where we simply only generate spheres, and change our call to InitState to instead an instantiation of System.Random. Don't forget to add the using alias so that the compiler knows which type you mean:

using System.Collections;
using UnityEngine;
<mark>using Random = System.Random;</mark>

public class ExampleBehaviour : MonoBehaviour
{
    [SerializeField] private GameObject _spherePrefab;

    private void Awake()
    {
        StartCoroutine(GenerateRandomSpheres());
    }

    private IEnumerator GenerateRandomSpheres()
    {
        const float range = 5f;
        <mark>var random = new Random(1234567890);</mark>

        for (int i = 0; i < 100; i++)
        {
            var x = Random.Range(-range, range);
            var y = Random.Range(-range, range);
            var pos = new Vector2(x, y);
            Instantiate(_spherePrefab, pos, Quaternion.identity);
            
            yield return null;
        }
    }
}

Of course, this won't compile. Random.Range is now undefined because the Random it's referencing is now .NET, which does not have a static Range method. Instead, we'll ultimately need to call NextDouble on the instance we just created. There are two problems though: This method returns a double, and it also only returns a value from 0 - 1. What we need is for it to return a float between any two values we want so that we can simulate the behaviour of UnityEngine.Random.Range.

To accomplish this, we can write a quick extension method. Create a new class in a separate file called RandomExtensions, and feel free to copy/paste this code into it:

using Random = System.Random;

public static class RandomExtensions
{
    public static float NextSingle(this Random random, float min = 0f, float max = 1f)
    {
        // thank you to Zenvin for this genius solution
        return Mathf.Lerp(min, max, (float) random.NextDouble());
    }
}

If you haven't worked with extension methods before, this syntax might seem a little strange. The keyword this on the parameter is rather unique but it lets us do something very useful, which is access it on an instance of that parameter type. With this we can replace our call to Random.Range with a call to the NextSingle extension method on the random object.

using System.Collections;
using UnityEngine;
using Random = System.Random;

public class ExampleBehaviour : MonoBehaviour
{
    [SerializeField] private GameObject _spherePrefab;

    private void Awake()
    {
        StartCoroutine(GenerateRandomSpheres());
    }

    private IEnumerator GenerateRandomSpheres()
    {
        const float range = 5f;
        var random = new Random(1234567890);

        for (int i = 0; i < 100; i++)
        {
            var x = <mark>random.NextSingle</mark>(-range, range);
            var y = <mark>random.NextSingle</mark>(-range, range);
            var pos = new Vector2(x, y);
            Instantiate(_spherePrefab, pos, Quaternion.identity);

            yield return null;
        }
    }
}

This method also serves as a replacement for Random.value. If we simply pass no arguments and call it as random.NextSingle(), they will use their default values giving us a random float between 0 and 1.

If we play the scene, we'll see a new random distribution of objects:

It's beautiful

You might be wondering why the distribution of objects is different from before, even though we are using the same seed. This is because Unity's Random doesn't wrap the .NET Random, it instead calls native code and generates it that way. But this is irrelevant, what matters is consistency. As long as we always use the same mechanism for RNG from the get-go, it will work out. Play the scene again, and you will see the same distribution of objects!

It's beautiful: Round 2

Now, let's bring back our GenerateRandomCubes coroutine. In this method, we will create a separate Random instance, and use the second seed from last time 987654321. Again, we will omit the call to Instantiate purely to keep the scene clean, but we will still calculate random positions for where the cubes would go.

using System.Collections;
using UnityEngine;
using Random = System.Random;

public class ExampleBehaviour : MonoBehaviour
{
    [SerializeField] private GameObject _spherePrefab;

    private void Awake()
    {
        StartCoroutine(GenerateRandomSpheres());
        <mark>StartCoroutine(GenerateRandomCubes());</mark>
    }

    private IEnumerator GenerateRandomSpheres()
    {
        const float range = 5f;
        var random = new Random(1234567890);

        for (int i = 0; i < 100; i++)
        {
            var x = random.NextSingle(-range, range);
            var y = random.NextSingle(-range, range);
            var pos = new Vector2(x, y);
            Instantiate(_spherePrefab, pos, Quaternion.identity);

            yield return null;
        }
    }

<mark>    private IEnumerator GenerateRandomCubes()</mark>
<mark>    {</mark>
<mark>        const float range = 5f;</mark>
<mark>        var random = new Random(987654321);</mark>

<mark>        for (int i = 0; i < 100; i++)</mark>
<mark>        {</mark>
<mark>            var x = random.NextSingle(-range, range);</mark>
<mark>            var y = random.NextSingle(-range, range);</mark>
<mark>            var pos = new Vector2(x, y);</mark>

<mark>            yield return null;</mark>
<mark>        }</mark>
<mark>    }</mark>
}

Play the scene, and drumroll please…

It's the same distribution!

It worked! The two coroutines declare their own independent instances of System.Random, with their own seeds, which means each instance will generate its own independent sequence of random numbers. Perfect 👌

Substituting everything in UnityEngine.Random

Unity's RNG does offer some very useful members related to game dev such as insideUnitCircle, onUnitSphere, and the like. We can replicate these using the .NET RNG, by expanding on our RandomExtensions class.

The first thing we will need is a method that, strangely, does not actually replace anything that Unity's RNG has to offer. There is no onUnitCircle that this method is mimicking. However, it will become invaluable for you I'm sure. NextOnUnitCircle will return a random point which lies on the radius of the unit circle. It is essentially the Vector2 equivalent of onUnitSphere.

We need to generate a random angle (a degree value between 0-360, and convert it to radians), which will serve as the direction that our vector is pointing, and then perform some trigonometry to have a point which lies on the unit circle.

public static Vector2 NextOnUnitCircle(this Random random)
{
    // credit to Pulni for offering this solution over my last one!
    var angle = random.NextSingle(0, 360f) * Mathf.Deg2Rad;
    var x = Mathf.Cos(angle);
    var y = Mathf.Sin(angle);
    return new Vector2(x, y);
}

If you are curious about how this method works, then here is a handy illustration which might make it more apparent:

θ is our angle we generated. The rest is Math™

Now we can get to copying over the Unity values. We'll start with onUnitSphere, so let us define an extension method called NextOnUnitSphere which achieves the same effect.

The math for this is largely similar to the 2D version (NextOnUnitCircle), though admittedly a little more involved. Trigonometry is our best friend here:

public static Vector3 NextOnUnitSphere(this Random random)
{
    // credit to Pulni again, for offering this solution over my last one here too
    var angle = random.NextSingle(0, 360f) * Mathf.Deg2Rad;
    var z = random.NextSingle(-1f, 1f);
    var mp = Mathf.Sqrt(1 - z * z);
    var x = mp * Mathf.Cos(angle);
    var y = mp * Mathf.Sin(angle);
    return new Vector3(x, y, z);
}

Now, insideUnitSphere. This one is extremely simple to recreate now that we have NextOnUnitSphere. Since that will return a Vector3 whose magnitude is 1, all we have to do is scale that result by a random float between 0 - 1, to get position inside that sphere!

public static Vector3 NextInsideUnitSphere(this Random random)
{
    return random.NextOnUnitSphere() * random.NextSingle();
}

insideUnitCircle can do almost the same thing. We'll call the new NextOnUnitCircle we wrote, before scaling it by a random float.

public static Vector2 NextInsideUnitCircle(this Random random)
{
    return random.NextOnUnitCircle() * random.NextSingle();
}

We can substitute UnityEngine.Random.rotation for a method which generates 3 random values between 0 - 360, and calling Quaternion.Euler to wrap it as a quaternion.

public static Quaternion NextRotation(this Random random)
{
    var x = random.NextSingle(0f, 360f);
    var y = random.NextSingle(0f, 360f);
    var z = random.NextSingle(0f, 360f);
    return Quaternion.Euler(x, y, z);
}

UnityEngine.Random.rotationUniform involves Math™, and I'm not going to go into detail about how this method works, primarily because I'm not entirely sure myself (Quaternions - not even once). However we can replicate its functionality like so:

public static Quaternion NextRotationUniform(this Random random)
{
    // special thanks to NovHak on the Unity forums

    float normal, w, x, y, z;

    do
    {
        w = random.NextSingle(-1f, 1f);
        x = random.NextSingle(-1f, 1f);
        y = random.NextSingle(-1f, 1f);
        z = random.NextSingle(-1f, 1f);
        normal = w * w + x * x + y * y + z * z;
    }
    while (normal > 1f || normal == 0f);

    normal = Mathf.Sqrt(normal);
    return new Quaternion(x / normal, y / normal, z / normal, w / normal);
}

The final method we need to replace is ColorHSV. This one is actually easy to replace since the source for this is method is publicly available. All we have to do is port it to a set of extension methods which perform the same logic:

public static Color NextColorHsv(this Random random)
{
    return random.NextColorHsv(0f, 1f, 0f, 1f, 0f, 1f, 1f, 1f);
}

public static Color NextColorHsv(this Random random, float minHue, float maxHue)
{
    return random.NextColorHsv(minHue, maxHue, 0f, 1f, 0f, 1f, 1f, 1f);
}

public static Color NextColorHsv(
    this Random random,
    float minHue,
    float maxHue,
    float minSaturation,
    float maxSaturation)
{
    return random.NextColorHsv(minHue, maxHue, minSaturation, maxSaturation, 0f, 1f, 1f, 1f);
}

public static Color NextColorHsv(
    this Random random,
    float minHue,
    float maxHue,
    float minSaturation,
    float maxSaturation,
    float minValue,
    float maxValue)
{
    return random.NextColorHsv(minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, 1f, 1f);
}

public static Color NextColorHsv(
    this Random random,
    float minHue,
    float maxHue,
    float minSaturation,
    float maxSaturation,
    float minValue,
    float maxValue,
    float minAlpha,
    float maxAlpha)
{
    var h = Mathf.Lerp(minHue, maxHue, random.NextSingle());
    var s = Mathf.Lerp(minSaturation, maxSaturation, random.NextSingle());
    var v = Mathf.Lerp(minValue, maxValue, random.NextSingle());
    var color = Color.HSVToRGB(h, s, v, true);
    color.a = Mathf.Lerp(minAlpha, maxAlpha, random.NextSingle());
    return color;
}

If you'd like to download the full source code for this RandomExtensions class, you can do so here!