Search Unity

Here's a High Performance Cross Platform Seeded Random Number Generator

Discussion in 'Scripting' started by teknic, Sep 15, 2015.

  1. teknic

    teknic

    Joined:
    Oct 18, 2012
    Posts:
    32
    Looking for a high performance cross platform seeded RNG? Here's one I made over the weekend called PiWalker. It's faster than Unity's built-in RNG with similar in distribution quality. After initialized, it creates zero GC overhead and will return the exact same result for any seed, on any platform.

    Internally it uses a hard coded 10,000 digit sequence from Pi and outputs integers in the range 0 - 1,000,000,000. Example usage is included in the file. Here's a comparison of PiWalker.Random, UnityEngine.Random, and System.Random:



    The digit sequence can be replaced with anything. I tried 10,000 digits from e and a fibonacci series, and they produced very similar results. Using PiWalker is easy:

    Code (CSharp):
    1.  
    2. // set starting seed
    3. uint seed = 0;
    4.  
    5. // get a random int or convert to float/double
    6. int r = PiWalker.Random(seed); // 314159265
    7. float f = PiWalker.Random(seed) / 1000000000f; // 0.3141593
    8. double d = PiWalker.Random(seed) / 1000000000d; // 0.314159265
    9.  
    10. // increment seed
    11. seed++;
    12. r = PiWalker.Random(seed); // 141592653
    13.  
    14. // pass any seed in range 0 - 10,000
    15. f = PiWalker.Random(743) / 1000000000f; // 0.7130996
    16.  
    17. // set precision
    18. PiWalker.SetPrecision(4);
    19. r = PiWalker.Random(743); // 7130
    20. f = PiWalker.Random(743) / 10000f; // 0.713
    21. f = PiWalker.Random(743) / 1000f; // 7.13
    22. f = PiWalker.Random(743) / 10f; // 713
    23.  
    24. // restore default precision (9)
    25. PiWalker.SetPrecision(0, true);
    26. d = PiWalker.Random(6789) / 1000000000d; // 0.790237421
    27.  
    Enjoy :)
     

    Attached Files:

    mbartosik likes this.
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Shouldn't the seed set the beginning of a determinate sequence? Not return the same number every time.
    How do I get 2 or more random numbers one after the other?

    Also, if I want to seed, why isn't there a object identity for the random (like System.Random) so I can declare various sequences for uses in different determinant places that are indeterminant when overlapped. I.E. one random seeded sequence for generating a map, and another for AI related RNG decisions.
     
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Something along the lines of this:

    Code (csharp):
    1.  
    2. using System;
    3. using System.Text;
    4.  
    5. public class PiWalker
    6. {
    7.  
    8.    #region static fields
    9.  
    10.    // 10,008 digits of pi
    11.    const string piString = "31415... redacted for brevity";
    12.    static byte[] byteSequence = Encoding.ASCII.GetBytes(piString);
    13.  
    14.    private static PiWalker _global;
    15.    public static PiWalker Global
    16.    {
    17.      get
    18.      {
    19.        if (_global == null) _global = new PiWalker();
    20.        return _global;
    21.      }
    22.    }
    23.  
    24.    #endregion
    25.  
    26.    #region Fields
    27.  
    28.    private int _seed;
    29.    private int _precision;
    30.  
    31.    #endregion
    32.  
    33.    #region CONSTRUCTOR
    34.  
    35.    public PiWalker()
    36.    {
    37.      _seed = 0;
    38.      _precision = 9;
    39.    }
    40.  
    41.    public PiWalker(uint seed)
    42.    {
    43.      _precision = 9;
    44.      this.Seed = seed;
    45.    }
    46.  
    47.    public PiWalker(uint seed, int precision)
    48.    {
    49.      this.Precision = precision;
    50.      this.Seed = seed;
    51.    }
    52.  
    53.    #endregion
    54.  
    55.    #region Properties
    56.  
    57.    public uint Seed
    58.    {
    59.      get { return (uint)_seed; }
    60.      set
    61.      {
    62.        _seed = (int)value % (byteSequence.Length - _precision);
    63.      }
    64.    }
    65.  
    66.    public int Precision
    67.    {
    68.      get { return _precision; }
    69.      set
    70.      {
    71.        _precision = UnityEngine.Mathf.Clamp(value, 1, 9);
    72.        _seed = _seed % (byteSequence.Length - _precision);
    73.      }
    74.    }
    75.  
    76.    #endregion
    77.  
    78.    #region Methods
    79.  
    80.    public int Next()
    81.    {
    82.      int randomInt = 0;
    83.  
    84.      for (int i = 0; i < _precision; i++)
    85.      {
    86.        randomInt += (int)(byteSequence[i + _seed] - 48) * (int)Math.Pow(10, _precision - i - 1);
    87.      }
    88.  
    89.      _seed = randomInt % (byteSequence.Length - _precision); //set seed to next value so next value is a new value
    90.      return _seed;
    91.    }
    92.  
    93.    public double NextDouble()
    94.    {
    95.      return (double)this.Next() / Math.Pow(10, _precision);
    96.    }
    97.  
    98.    #endregion
    99.  
    100. }
    101.  
    Oh and note, the seed needs to be clamped to less than the byteSequence.Length - precision, since the for loop on a seed equal to the byteSequence.Length will result in an index outside of the range.
     
    Last edited: Sep 15, 2015
  4. teknic

    teknic

    Joined:
    Oct 18, 2012
    Posts:
    32
    To get different random values, pass different seeds. The simplest way to do this is to increment the seed. You could add a Next() method which does this if you like. You could also overload the Random method to pull system time or some other source for use as a seed.

    Multiple sequences could be created by using multiple seed generators. These could be simple algorithms:

    Code (CSharp):
    1. uint seed = 0;
    2. seed += 5;
    3. int r = PiWalker.Random(seed);
    or collections:

    Code (CSharp):
    1. uint[] mapSeeds = {3, 7, 2, 8, 4, 6, 1, 9};
    2. uint[] npcSeeds = {34, 76, 63, 97 ,13, 47, 83};
    3.  
    4. int mapRand1 = PiWalker.Random(mapSeeds[0]);
    5. int mapRand2 = PiWalker.Random(mapSeeds[1]);
    or a secondary RNG:

    Code (CSharp):
    1. int rand = PiWalker.Random(UnityEngine.Random.Range(100, 200));

    ...
     
    mbartosik likes this.
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Doesn't that make this NOT an RNG? Especially since you need random seeds every step?

    So yeah, I did add a 'Next' method. See my follow up post (minor edits as I mixed up some typos typing this in the browser).
     
  6. teknic

    teknic

    Joined:
    Oct 18, 2012
    Posts:
    32
    RNG = Random Number Gatherer? ;P

    One use for PiWalker is the generation of identical procedural content across multiple platforms. For example, if you need to randomize the layout of a level and synchronize content across PC, iOS, and PS4, a deterministic RNG is critical. I researched a number of solutions to this problem, many of them computational, and ultimately settled on a simple hard-coded solution for GUARANTEED cross platform output and speed.

    The quality of PiWalker is similar to UnityEngine.Random, but it is faster and more flexible, and can easily be reconfigured with alternate sequences.
     
    mbartosik likes this.
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Actually a RNG is defined as a "computational or physical device designed to generate a sequence of number or symbols that lack any pattern".
    https://en.wikipedia.org/wiki/Random_number_generation

    Your class only supports generating a SINGLE number... and there is a very simple pattern in determining what that number will be. It'll be the 'seed' digit of pi, followed by the next 'precision' digits of pi.

    If I put in for instance 2, with precision 4, I know I'll get 4159.

    We could rename the function "GetDigitsOfPi(int position, int precision)", and effectively use the same exact code... and it's the same thing.

    Now, once we actually recycle that digit as the next seed value, then we'll start getting more unpredictable (while remaining determinant). But you don't offer that system in your class... and you actually post a very weird way of seeding the values. 1) you expect me to maintain that seed myself, meaning anywhere in my code that I access the PiWalker, I have to store the last value of my sequence for use in the next location I call it. And 2) you have strange arrays of seeds. Seeds are usually a single 'start value' to generate a sequence off of. Think like in minecraft they allow you to type in a word of your choice from which a world is built. If you type that word in again, the same world is produced. All they do is generate an integer off the word you typed in (ascii?), and use that as the FIRST value.

    The naming comes from crystal growth, where you have a small shard of a crystal, that when introduced to the desired solutionm kick starts the crystal growth.

    https://en.wikipedia.org/wiki/Seed_crystal

    I mean think about it, if I have a random map. My map could easily need thousands of random values to put it all together. I'm not sure how this helps:

    Code (csharp):
    1.  
    2.  
    3. uint[] mapSeeds = {3, 7, 2, 8, 4, 6, 1, 9};
    4. uint[] npcSeeds = {34, 76, 63, 97 ,13, 47, 83};
    5. int mapRand1 = PiWalker.Random(mapSeeds[0]);
    6. int mapRand2 = PiWalker.Random(mapSeeds[1]);
    7.  
    Where did those values come from? Am I supposed to create my own random sequence hardcoded, and then create new values from that? But PiWalker.Random(3) is always going to return the same thing... why not just store that as entry in in 'mapSeeds'?


    Or are those the seeds for 8 different maps? And I just write the next value back into the array every time I call Random. Like this:

    Code (csharp):
    1.  
    2. uint[] mapSeeds = {3, 7, 2, 8, 4, 6, 1, 9};
    3. mapSeeds[0] = PiWalker.Random(mapSeeds[0]);
    4. int mapRand1 = mapSeeds[0];
    5.  
    Object identity would be a lot easier for that though. I can create 8 different random generators and use them:

    Code (csharp):
    1.  
    2. var mapR = new RNG(3);
    3. var v1 = mapR.Next();
    4. var v2 = mapR.Next();
    5. var v3 = mapR.Next();
    6.  
    7. var npcR = new Rng(34);
    8. var n1 = npcR.Next();
    9. var n2 = npcR.Next();
    10. ...
    11.  

    If you look over the code I offered you. Which is just a modification of your code. It turns what is a 'GetDigitsOfPi' tool. To an actual RNG that has the ability to be seeded and sequenced without having to manually manage the sequence link.
     
    Last edited: Sep 18, 2015