Generating Predictable Random Numbers in Unity

Edward Rowe
Red Blue Games
Published in
5 min readJan 6, 2020

--

It’s easy to generate random numbers in Unity. The built-in Random class allows us to quickly generate all types of pseudo-random numbers (for simplicity I’ll just call them random for this post), and it is more than enough for simple projects. But many procedural games require random number generators (RNG) to be predictable, for example to generate the same world from a single seed value. In these cases it’s important to approach random number generation deliberately. Here are the best practices we established for RNG when making Sparklite.

Generating Predictable Numbers

(If you understand the basics of pseudo-number generation you can skip this)

Pseudo-random number generators are not truly random (hence “pseudo”) and instead use an algorithm that spits out sequences of deterministic numbers. The sequences have no discernible pattern, so they seem random. The sequence of numbers is determined based on an initial starting value, called a seed. If you initialize two instances of random number generators with the same seed, they will report the same sequences of numbers. If you don’t supply a seed to a random number generator, it’s typically using the system clock as a seed value. This is how .NET’s Random class works.

Best Practices with RNG

Avoid Unity’s Random Class

Unity’s Random is based on a static class, which means all code is sharing the same RNG instance. This means it’s possible for code you call to reinitialize the Random class out from under you with a different seed.

Here’s an example. Let’s say you have two functions, OutputTwoRandoms() and DoStuff()

public void OutputTwoRandoms(int seed)
{
Debug.Log($"Two Randoms with seed: {seed}");
Random.Init(seed);
Debug.Log(Random.Range(0,10));
B.Go();
Debug.Log(Random.Range(0,10));
}
public static void DoStuff()
{
...
Random.Init(System.DateTime.Now.Millisecond);
...
}
public void Go()
{
OutputTwoRandoms(123);
OutputTwoRandoms(123);
}
// Results in:
// Two Randoms with seed: 123
// 3
// 9
// Two Randoms with seed: 123
// 3
// 5

If you run OutputTwoRandoms(123) twice (with the same seed), the first output value will always be the same, but because DoStuff re-initialized the generator using a different and unpredictable seed (DateTime.Now), the second value could be different. This is a contrived example, but it’s not hard to imagine that another user’s code such as a 3rd party plugin, could alter the Unity Random instance without its users knowing.

Another problem with sharing a static instance occurs when you have two systems that require predictable numbers asking it for random numbers over time and at unpredictable times. Because the RNG reports sequences of numbers, two systems would compete for values from the same sequence. The number that you get would depend on how many times other users asked for a random number since the last time you asked.

For example, let’s say an RNG sequence is [1, 7, 3, 4, 5]. On one session, system A asks for a number and gets 1. Then system B asks for two numbers and gets 7, then 3. Then A gets 4. So A got 1, 4 and B got 7, 3. If on another session, B only asks for one number before A asks again, A will get 1, 3 instead of 1, 4.

Our solution to this is to always use a System.Random instance when we need predictable random numbers. Because System.Random requires users to instantiate their own instances, they aren’t shared (global) by default. By creating and managing your own instance you have total control over the RNG.

Not all random numbers need to be predictable. For systems that really don’t need to be predictably random, like scatter on a shotgun blast, we might use Unity’s Random. Some reasons to use Unity’s Random over System.Random are that it has more utility functions that are useful for Unity, such as Random value in a Unit Circle, and it doesn’t create an object instance so therefore doesn’t create garbage. Since we only use it for unpredictable random numbers, it also signals to other programmers that we don’t need the numbers to be predictable.

Pass a System.Random (or seed) into Everything

Every system that requires predictable random numbers in Sparklite gets a System.Random instance passed in. This lets the caller decide how they want the system to generate numbers. If the caller doesn’t care about predictable results they can simply generate a new Random instance and pass it in. If they want predictable results, they will need to create a Random instance with a predictable seed.

You could alternatively simply pass a seed to all your systems that require randomness, which would allow them to use whatever random number generator they’d prefer.

For Unpredictable Random Numbers, Use a Random Singleton instead of new Random()

When using System.Random to generate unpredictable random numbers, you have to first create an instance of it. By default, new instances are initialized using the system clock. This means there’s a weird quirk where if you create two instances quickly, like in a for loop, they will produce the same random values.

To avoid this, we have a static singleton, which we store on a static RandomUtilities class:

public static class RandomUtilities
{
public static System.Random RNGSingleton = new System.Random();
...
}

Since it’s understood that this shared instance shouldn’t be used to generate predictable numbers, it’s ok if users reinitialize it (though they’d also have no reason to, as it shouldn’t be used to generate predictable numbers.)

We also have two signatures for each utility function. One takes a System.Random instance, and the other supplies the singleton.

public static bool RandomChance(System.Random rng, int percent)
{
return rng.Next(0, 100) < percent;
}
public static bool RandomChance(int percent)
{
return RandomChance(RandomUtilities.RNGSingleton, percent);
}

Another reason to share a single instance is to avoid creating a bunch of objects that need to be garbage collected.

Closing

Using these techniques, we were able to regenerate the same world from a single seed value. This was useful to keep save game sizes down, but it was also great for debugging. When we found a problematic world, we could get the seed that was used to generate it and reproduce it.

Thanks for reading! As always, please feel free to reach out to me here or on Twitter with questions or comments.

If you’d like to support us and our content, please consider buying Sparklite on any of these platforms: Steam (Mac and PC), Switch, PS4, XBox One

If you’re a starving indie, here are some other good ways to support us that won’t shorten your runway:

Resources and Further Reading

--

--

Co-founder and developer at Red Blue Games. Currently working on Sparklite.