Skip to content

Instantly share code, notes, and snippets.

@KervenGame
Forked from macklinb/Unity-XORShift.md
Last active February 8, 2023 13:26
Show Gist options
  • Save KervenGame/b0aa191d6c6d6dc9b1c1a936384e77d5 to your computer and use it in GitHub Desktop.
Save KervenGame/b0aa191d6c6d6dc9b1c1a936384e77d5 to your computer and use it in GitHub Desktop.

Revisions

  1. KervenGame revised this gist Jul 3, 2022. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions Unity-XORShift.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    Unity随机数算法,使用的是Xorshift128
    This is a partial C# implementation of `UnityEngine.Random` with (almost) 1-to-1 parity.

    Unity uses [Xorshift](https://en.wikipedia.org/wiki/Xorshift) for psuedorandom number generation. In particular Xorshift128, which uses a state consisting of four unsigned 32-bit integer values. The state is initialized in `UnityEngine.Random.InitState` using a signed 32-bit integer seed, which is shuffled around with a technique similar to the way a [Mersenne Twister](https://en.wikipedia.org/wiki/Mersenne_Twister#Initialization) is initialized.
  2. @macklinb macklinb renamed this gist Feb 25, 2021. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. @macklinb macklinb created this gist Feb 25, 2021.
    111 changes: 111 additions & 0 deletions Tests.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,111 @@
    using UnityEngine;
    using System.Runtime.InteropServices;
    using Random = UnityEngine.Random;

    public class Tests : MonoBehaviour
    {
    System.Text.StringBuilder sb = new System.Text.StringBuilder();

    // Start is called before the first frame update
    void Start()
    {
    UnityEngine.Random.InitState(1234);
    XORShift128.InitSeed(1234);

    // Print some ranged integers
    // 0 to max
    IntegerTests(0, int.MaxValue);
    // 0 to min
    IntegerTests(0, int.MinValue);
    // Min to max
    IntegerTests(int.MinValue, int.MaxValue);
    // Max to min
    IntegerTests(int.MaxValue, int.MinValue);
    // Min to min (error)
    IntegerTests(int.MinValue, int.MinValue);

    // Print some floating points
    for (int i = 0; i < 5; i++) AppendTestFloat(Random.value, XORShift128.NextFloat());

    // Print some ranged floating points
    // 0 to 100
    FloatingPointTests(0f, 100f);
    // 0 to 100000
    FloatingPointTests(0f, 100000f);
    // 0 to max (not accurate)
    FloatingPointTests(0f, float.MaxValue);
    // 0 to min (not accurate)
    FloatingPointTests(0f, float.MinValue);
    // Min to max
    FloatingPointTests(float.MinValue, float.MaxValue);
    // Max to min
    FloatingPointTests(float.MaxValue, float.MinValue);
    // Min to min
    FloatingPointTests(float.MinValue, float.MinValue);

    Debug.Log(sb.ToString());
    }

    void IntegerTests(int min, int max)
    {
    sb.AppendLine("--- " + min + " to " + max + " ---");

    for (int i = 0; i < 5; i++)
    {
    int ur = UnityEngine.Random.Range(min, max);
    int xr = XORShift128.NextIntRange(min, max);
    AppendTestInt(ur, xr);
    }
    }

    void FloatingPointTests(float min, float max)
    {
    sb.AppendLine("--- " + min + " to " + max + " ---");
    for (int i = 0; i < 5; i++)
    {
    float ur = UnityEngine.Random.Range(min, max);
    float xr = XORShift128.NextFloatRange(min, max);
    AppendTestFloat(ur, xr);
    }
    }

    void AppendTest(string a, string b)
    {
    var state = (RandomStateWrapper)UnityEngine.Random.state;
    sb.AppendLine(state.v3.ToString().PadRight(15) + XORShift128.w.ToString().PadRight(15) + a.PadRight(15) + b.PadRight(15));
    }

    void AppendTestInt(int a, int b)
    {
    AppendTest(a.ToString(), b.ToString());
    }

    void AppendTestFloat(float a, float b)
    {
    AppendTest(a.ToString(), b.ToString());
    }
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct RandomStateWrapper
    {
    [FieldOffset(0)] public Random.State state;
    [FieldOffset(0)] public uint v0;
    [FieldOffset(4)] public uint v1;
    [FieldOffset(8)] public uint v2;
    [FieldOffset(12)] public uint v3;

    public static implicit operator RandomStateWrapper(Random.State aState)
    {
    return new RandomStateWrapper { state = aState };
    }
    public static implicit operator Random.State(RandomStateWrapper aState)
    {
    return aState.state;
    }

    public override string ToString()
    {
    return v0 + ", " + v1 + ", " + v2 + ", " + v3;
    }
    }
    151 changes: 151 additions & 0 deletions Unity-XORShift.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,151 @@
    This is a partial C# implementation of `UnityEngine.Random` with (almost) 1-to-1 parity.

    Unity uses [Xorshift](https://en.wikipedia.org/wiki/Xorshift) for psuedorandom number generation. In particular Xorshift128, which uses a state consisting of four unsigned 32-bit integer values. The state is initialized in `UnityEngine.Random.InitState` using a signed 32-bit integer seed, which is shuffled around with a technique similar to the way a [Mersenne Twister](https://en.wikipedia.org/wiki/Mersenne_Twister#Initialization) is initialized.

    This has been tested as far back as Unity 4.7.0f1, and as recent as Unity 2020.1.17f1.

    ### Notes ###
    - Huge thanks to MoatShrimp for figuring out how Unity initializes the Xorshift state parameters in InitState, and floating point generation.
    - **C#** - As below. Values may differ in .NET 5.0, but thankfully Unity doesn't yet support that.
    - **JavaScript** - Check out MoatShrimp's JS implemention (and neat Undermine loot lookup tool) here:
    - http://moatshrimp.com/
    - **Lua** - I've translated this into Lua for use on [MediaWiki](https://www.mediawiki.org/wiki/MediaWiki) wikis with the [Scribunto](https://www.mediawiki.org/wiki/Extension:Scribunto) extension installed. Note that this only works if Lua was compiled with `LUA_NUMBER = double`, which should be the case for most wikis. In my case the initial goal was to evaluate random loot lists in Pillars of Eternity, for use on the wiki.
    - [Module:Lootlist](https://pillarsofeternity.gamepedia.com/Module:Lootlist) (might have to scroll down a bit)
    - If you translate this into other languages, or have any improvements/insights, feel free to comment below!
    - If you do translate this, be mindful of:
    - How your language handles numbers. Most programming languages that only define one type for numbers will use a double-precision floating point value to represent all numbers.
    - Integer overflow, and casting behaviours (particularly from uint to int)
    - Differences in the `%` operator (modulo vs remainder)

    ### Unknowns ###
    - How Unity initializes `UnityEngine.Random` when a seed is not provided. Probably seeded based on time, perhaps hourly? Need to do further testiong.
    - Floating point RPG is slightly inaccurate. Unity's implementation is likely done differently, though their exact method is unknown.

    ## Tests ##
    The following tests were performed using the C# implementation in Unity, initialized with: `UnityEngine.Random.InitState(1234)` and `XORShift128.InitSeed(1234)`, generated sequentially (**the state isn't reset between runs**).

    `UnityEngine.Random.State.s3` and `XORShift128.w` are the last state parameter in Xorshift, the actual number that was generated in Xorshift. This value is a `uint` and is used as a basis for all other Random functions.

    ### Integer ####
    Uses `UnityEngine.Random.Range(min, max)` and `XORShift128.NextIntRange(min, max)`

    0 to `int.MaxValue`

    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 3463400838 | 1315917191 | 1315917191
    | 3496203776 | 1348720129 | 1348720129
    | 3452947669 | 1305464022 | 1305464022
    | 1278673611 | 1278673611 | 1278673611
    | 4169168310 | 2021684663 | 2021684663

    0 to `int.MinValue` (-2,147,483,648)
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 916287344 | -916287344 | -916287344
    | 2240259090 | -92775442 | -92775442
    | 1901252403 | -1901252403 | -1901252403
    | 2323917162 | -176433514 | -176433514
    | 1472147877 | -1472147877 | -1472147877

    `int.MinValue` to `int.MaxValue`
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 4020283508 | 1872799860 | 1872799860
    | 141347300 | -2006136348 | -2006136348
    | 2735243002 | 587759354 | 587759354
    | 227819815 | -1919663833 | -1919663833
    | 3885870057 | 1738386409 | 1738386409

    `int.MaxValue` to `int.MinValue`
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 2312142103 | -164658456 | -164658456
    | 1775189369 | 372294278 | 372294278
    | 3338523678 | -1191040031 | -1191040031
    | 3426086347 | -1278602700 | -1278602700
    | 3322349983 | -1174866336 | -1174866336

    `int.MinValue` to `int.MinValue` (error is caught and a suitable value is returned before a value is even generated, hence the non-changing state value)
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 3322349983 | -2147483648 | -2147483648
    | 3322349983 | -2147483648 | -2147483648
    | 3322349983 | -2147483648 | -2147483648
    | 3322349983 | -2147483648 | -2147483648
    | 3322349983 | -2147483648 | -2147483648

    ### Floating point ####
    Uses `UnityEngine.Random.value` and `XORShift128.NextFloat()`

    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 3593715923 | 0.4043221 | 0.404322
    | 4266042159 | 0.551855 | 0.551855
    | 2642301593 | 0.9868958 | 0.9868957
    | 1674312536 | 0.593608 | 0.5936079
    | 733387434 | 0.426595 | 0.426595

    Uses `UnityEngine.Random.Range(min, max)` and `XORShift128.NextFloatRange(min, max)`
    0.0 to 100.0
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 3760331560 | 73.35463 | 73.35463
    | 2410116911 | 69.16753 | 69.16753
    | 3012185272 | 91.95337 | 91.95337
    | 745993129 | 7.068896 | 7.068908
    | 3699201620 | 2.080286 | 2.080297

    0.0 to 100000.0
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 1758944507 | 31747.49 | 31747.5
    | 2312712917 | 30313.62 | 30313.62
    | 313095164 | 67614.8 | 67614.8
    | 609241099 | 37280.14 | 37280.14
    | 4138924286 | 60177.63 | 60177.64

    0.0 to `float.MaxValue` (3.402823E+38)
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 3065852775 | 1.775827E+38 | 1.775827E+38
    | 4023550175 | 1.209507E+38 | 1.209507E+38
    | 1231330622 | 7.280383E+37 | 7.280387E+37
    | 678488548 | 4.010639E+37 | 4.010643E+37
    | 1997128070 | 3.143466E+38 | 3.143466E+38

    0.0 to `float.MinValue` (-3.402823E+38)
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 212231104 | -2.382252E+38 | -2.382252E+38
    | 1632010759 | -1.528401E+38 | -1.528402E+38
    | 3470161922 | -1.104089E+38 | -1.104089E+38
    | 4159346095 | -5.693158E+37 | -5.693162E+37
    | 3362607345 | -4.967007E+37 | -4.967012E+37

    `float.MinValue` to `float.MaxValue`
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 2641274177 | -2.480102E+38 | -2.480101E+38
    | 3758475398 | 3.095331E+38 | 3.095331E+38
    | 1138851524 | -1.78091E+38 | -1.780909E+38
    | 3785966737 | 1.208649E+38 | 1.208649E+38
    | 151368008 | 3.100158E+38 | 3.100158E+38

    `float.MaxValue` to `float.MinValue`
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 3347712278 | -2.869245E+38 | -2.869245E+38
    | 2413123709 | 1.13493E+38 | 1.13493E+38
    | 619907290 | 2.713464E+38 | 2.713463E+38
    | 5796605 | 1.299941E+38 | 1.299941E+38
    | 2534213977 | -2.709683E+38 | -2.709683E+38

    `float.MinValue` to `float.MinValue`
    | `s3` / `w` | Unity | XORShift
    | --- | --- | ---
    | 2990456181 | -3.402823E+38 | -3.402823E+38
    | 238536240 | -3.402823E+38 | -3.402823E+38
    | 3443233425 | -3.402823E+38 | -3.402823E+38
    | 847449518 | -3.402823E+38 | -3.402823E+38
    | 1964100510 | -3.402823E+38 | -3.402823E+38
    108 changes: 108 additions & 0 deletions XORShift128.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,108 @@
    public static class XORShift128
    {
    public static uint x = 0, y = 0, z = 0, w = 0;

    const uint MT19937 = 1812433253;

    // Initialize Xorshift using a signed integer seed, calculating the state values using the initialization method from Mersenne Twister (MT19937)
    // https://en.wikipedia.org/wiki/Mersenne_Twister#Initialization
    public static void InitSeed(int seed)
    {
    x = (uint)seed;
    y = (uint)(MT19937 * x + 1);
    z = (uint)(MT19937 * y + 1);
    w = (uint)(MT19937 * z + 1);
    }

    // Explicitly set the state parameters
    public static void InitState(uint x, uint y, uint z, uint w)
    {
    XORShift128.x = x;
    XORShift128.y = y;
    XORShift128.z = z;
    XORShift128.w = w;
    }

    // XORShift, returns an unsigned 32-bit integer
    public static uint XORShift()
    {
    uint t = x ^ (x << 11);
    x = y; y = z; z = w;
    return w = w ^ (w >> 19) ^ t ^ (t >> 8);
    }

    // UInt32 / uint

    // UnityEngine.Random doesn't have any uint functions so these functions behave exactly like int Random.Range

    // Alias of base Next/XORShift
    public static uint NextUInt()
    {
    return XORShift();
    }

    // Generate a random unsigned 32-bit integer value in the range 0 (inclusive) to max (exclusive)
    public static uint NextUIntMax(uint max)
    {
    if (max == 0) return 0;
    return XORShift() % max;
    }

    // Generate random unsigned 32-bit integer value in the range min (inclusive) to max (exclusive)
    public static uint NextUIntRange(uint min, uint max)
    {
    if (max - min == 0) return min;

    if (max < min)
    return min - XORShift() % (max + min);
    else
    return min + XORShift() % (max - min);
    }

    // Int32 / int

    // Generate a random signed 32-bit integer value in the range -2,147,483,648 (inclusive) to 2,147,483,647 (inclusive)
    public static int NextInt()
    {
    return (int)(XORShift() % int.MaxValue);
    }

    public static int NextIntMax(int max)
    {
    return NextInt() % max;
    }

    // Generate a random signed 32-bit integer value in the range min (inclusive) to max (exclusive)
    // If you only need to generate positive integers, use NextUIntRange instead
    public static int NextIntRange(int min, int max)
    {
    // If max and min are the same, just return min since it will result in a DivideByZeroException
    if (max - min == 0) return min;

    // Do operations in Int64 to prevent overflow that might be caused by any of the following operations
    // I'm sure there's a faster/better way to do this and avoid casting, but we prefer equivalence to Unity over performance
    long minLong = (long)min;
    long maxLong = (long)max;
    long r = XORShift();

    // Flip the first operator if the max is lower than the min,
    if (max < min)
    return (int)(minLong - r % (maxLong - minLong));
    else
    return (int)(minLong + r % (maxLong - minLong));
    }

    // Single / float

    // Generate a random floating point between 0.0 and 1.0 (inclusive?)
    public static float NextFloat()
    {
    return 1.0f - NextFloatRange(0.0f, 1.0f);
    }

    // Generate a random floating point between min (inclusive) and max (exclusive)
    public static float NextFloatRange(float min, float max)
    {
    return (min - max) * ((float)(XORShift() << 9) / 0xFFFFFFFF) + max;
    }
    }