The foundation of Make It Random is the concept of a random engine, an object providing the basic services of a pseudo-random number generator (PRNG), responsible for generating a deterministic sequence of seemingly random bits initialized by a seed value.
This topic contains the following sections:
All random engine classes implement IBitGenerator and IRandom. The core of every random engine is the pair of functions Next32 and Next64. These two functions will generate 32 or 64 bits of random data, based on the engine's current state, and will update that state so that the next call to either of these functions will generate a new collection of 32 or 64 bits.
Aside from the caveats that PRNGs are entirely deterministic, and that some PRNGs are better at hiding the dependence of later values on earlier values, all bits should seem to be entirely independent of all other bits, either bits from earliers calls or the same call to Next32 or Next64. True and false bit values should occur with exactly equal probability.
All other random data produced by the extension methods of Make It Random are generated entirely through the data returned by these two methods.
PRNGs typically support the concept of a Seed, a value that is used to initialize the state of the random engine. If a particular type of PRNG is initialized with a particular seed value, it is guaranteed to produce exactly the same sequence of bits every time. Seeds can thus be incredibly valuable when game behavior is needed that appears random, but can reproduced on demand (such as in a replay).
Random engines in Make It Random all share a uniform interface for specifying seeds, despite often having substantially different internal state structures. This is accomplished through the help of the RandomStateGenerator class. This class can accept seed data input in a variety of formats, and will then hash that data to produce a sequence of bits, similar to random engines themselves, which can be used to initialized the internal state of a random engine.
IRandom provides a few different overloads of the Seed function to reseed a random engine, taking a few common values as input. And in case none of those types are sufficient, an already-initialized instance of RandomStateGenerator, another instance of IRandom, or any other object implementing IBitGenerator can be supplied as input to reseed the engine. Concrete random engine types such as XorShift128Plus provide a similar set of static Create(...) functions to specify the seed value directly when creating a new instance of a random engine.
The Seed functions completely replace the prior state of the random engine. If instead you want the new state to be influenced by both the current state and the value of a new seed, you can use one of the MergeSeed functions instead.
Whenever a parameterless seed-related or creation function is called, unpredictable seed data is generated by the default constructor RandomStateGenerator. This data includes variable inputs such as the current time, the amount of time that has passed since the system was started, and the process id, as well as a global value which is altered every time this constructor is called. This helps to ensure that a random engine does not by default produce the exact same sequence of values every time the program is run, and likewise that multiple independent random engines created in quick succession of each other do not all produce the same sequence of values. If you instead want such cases to always produce the same sequence, be sure to use an explicit seed value, even if it is nothing more than an arbitrarily chosen hard-coded integer.
At any given moment, an instance of a random engine will have an internal state value which entirely determines what values the following calls to Next32 or Next64 will produce. Different types of random engines will have different internal structures for this state, but at the very least, this state can always be represented as an opaque byte array.
As such, IRandom provides the methods SaveState and RestoreState(Byte), allowing you to easily serialize and deserialize random engines. This is similar to using seeds, but is more powerful in that it can capture and restore a random engine's state at any point in a random engine's deterministic sequence, whereas seeds can only be used to restore a random engine's state at the beginning of some particular sequence.
Concrete implementations of IRandom may also include overloads of SaveState(...) and RestoreState(...), as well as CreateWithState(...) functions, that expose the internal data structure of the random engine, avoiding the need to work with opaque byte arrays when the specific type of the random engine is known and fixed.
Do not mistake any concrete implementation's of Create(...) that takes a byte array or a parameter that matches the implementations internal state data structure as a creation function that will restore that state. These functions always take seed values, and are free to do any deterministic hashing that they want on the seeds before assigning the results to the internal state. If you have explicit state data, always ensure that you use CreateWithState(...) to create and initialize a random engine with that state.
Some random engines are mathematically structured such that there is an efficient way to skip over a vast number of values in the deterministic sequence without having to make that same number of explicitly calls to Next32 or Next64 (or the related function Step which updates the internal state but returns no value). Instead, they update the step in a way that is equivalent to making all those calls, but typically has a time complexity of O(log n) where n is the number of skipped values.
A common use case is in parallel computation, where one wishes to use just a single seed to initialize a random engine, and then to split the usage of that random engine across multiple threads, processes, or machines. In this case, each parallel execution can take a copy of the originally seeded engine and jump ahead a different number of intervals from any other parallel execution. If the intervals are large enough, this guarantees that each parallel execution will be pulling from an entirely different section of the overall sequence of random bits, and will never run long enough to overlap with any other parallel execution.
The XorShift128+ engine is both fast and small while still exhibiting high quality randomness, and is the recommended engine to use in most cases.
Similar to XorShift128+, the XorShift1024* engine trades a little bit of performance and a notable increase in state size in order to obtain a much larger period.
Also designed by Sebastiano Vigna, this engine uses a 128-byte (1024-bit) state, has a period of 21024 - 1, and performs very well on randomness tests such as BigCrush.
Because of its large period, XorShift1024* is recommended over XorShift128+ in situations where a vast possibility space is required. For example, shuffling a standard deck of 52 cards requires a period at least as large as 2226 if you wish to allow all possible permutations. Engines with a smaller period such as XorShift128+ are simply incapable of generating the vast majority of possible permutations in such cases. For most games this is unnecessary, but some types of games might legitimately call for this extra detail, such as gambling games.
Similar to XorShift128+, the XoroShiro128+ engine utilizes operations that are potentially faster on certain CPU architectures.
Designed by David Blackman and Sebastiano Vigna, this engine uses a 16-byte (128-bit) state, has a period of 2128 - 1, and performs very well on randomness tests such as BigCrush.
XoroShiro128+ is based on the bitwise rotation operation. On CPUs that support this operation natively, this engine can have a measurable speed impovement over XorShift128+. Unfortunately, my tests have shown that limitations of either C# itself or the compiler technology used by Unity prevents such optimizations from being realized. But if PRNG speed is a critical factor for your game, you may wish to revisit this generator as Unity Technologies updates their compiler.
The XSAdd engine is both fast and small while still exhibiting decent quality randomness, and is optimized for 32-bit CPU architectures.
Designed by Mutsuo Saito and Makoto Matsumoto, extending designs by George Marsaglia, this engine uses an 16-byte (128-bit) state, has a period of 2128 - 1, and performs moderately well on randomness tests such as BigCrush.
Because it is implemented entirely in terms of 32-bit operations, it can often outperform other engines that are otherwise faster on 64-bit CPUs. Consider using this engine if PRNG speed or power consumption are critical and you're targeting a 32-bit architecture.
The SplitMix64 engine is fairly fast and very small while still exhibiting high quality randomness.
Implemented by Sebastiano Vigna, based on the design of java.util.SplittableRandom, this engine uses an 8-byte (64-bit) state, has a period of 264, and performs very well on randomness tests such as BigCrush.
SplitMix64's smaller state size makes it ideal in situations where a large number of engines need to exist simultaneously or be stored, and space is at a premium. For cases in which L1 cache usage needs to be optimized, this could occur at numbers as low as a thousand random engine instances.
This is a wrapper around Unity's built-in random number generator, UnityEngine.Random
If for some reason you wish to use Unity's PRNG but want to have access to the extension methods of Make It Random, you can use this wrapper.
Note that because Experilous.MakeItRandomUnityRandom will share the same state, and pulling a random number from one instance or from UnityEngine.Random itself will alter the next number that will be generated by every other instance.is inherently a single-instance global, all instances of
This is a wrapper around .NET's built-in random number generator, SystemRandom
If for some reason you wish to use .NET's PRNG but want to have access to the extension methods of Make It Random, you can use this wrapper.
Although a static interface or a singleton instance of a random engine is a less flexible design than the instantiable model used by Make It Random, it can nonetheless be convenient at times, especially for quick experiments and prototypes. Make It Random therefore provides such a global instance through the static property shared.
This instance is guaranteed to be created upon use, and unless explicitly changed, will be an instance returned by a call to CreateStandard, which will currently produce an instance of XorShift128Plus, seeded with unpredictable data from a default-initialized instance of RandomStateGenerator.
The above selection of random engines should offer more than enough variety for most cases. But there's always the possibility that you have a reason to use a different implementation than one provided by Make It Random. If that's the case, simply implement IRandom and all the advanced functionality provided by Make It Random extension methods will immediately apply.
And although it is not necessary, it is prudent to also provide custom implementations for the following:
Note that some methods not in the first list above have a default implementation that throws NotSupportedException if you do not provide an implementation of your own.