Percentage based chance in C#?

I’m attempting to create a random 2D tile-based terrain generator in C#.
What i want it to do is have a list of tiles, each tile having a float value representing it’s percentage to spawn (so, a float value from 0 to 1).

Currently I make the list of percentage manually, which also means I can sort them.
In this case I sorted the array holding the percentages from highest at the back (the lowest index) and lowest at the front (highest index).

My current code works good if the array contains 2 items, with different percentages.
If I add more values to the list or make the percentages on one or more equal it will also fail.

These are all my variables related to the generation :

    [SerializeField] private List<GameObject> m_tileList = new List<GameObject>();
    private GameObject[,] m_worldTiles;
    private const float M_TILESIZE = 2.0f;

    private List<KeyValuePair<string, float>> m_chanceList = new List<KeyValuePair<string, float>> // These should together become 1, and none of them can be the same amount??
    {
        MakePair("Grass", 0.5f),
        MakePair("Dirt_Road", 0.5f),
    };

And this is the function which should generate the tiles :

    public void GenerateWorld(int xSize, int ySize)
    {
        m_worldTiles = new GameObject[xSize, ySize];

        for (int x = 0; x < xSize; x++)
        {
            for (int y = 0; y < ySize; y++)
            {
                for (int i = 0; i < m_chanceList.Count; i++)
                {
                    float val = Random.value; // First I generate a random percentage (0 - 1).

                    try // The reason I use a try/catch here is because if the chance list has no next element this will crash.
                    {
                        if (m_chanceList[i + 1].Value < val) // Here I check the next element and see if the percentage that we generated randomly is less than the next one, cause that means we should be selecting the i tile in the list. I believe this is the reason it only works with two and why it doesn't work when the chance list has two or more equal values.
                        {
                            // And then we just do the spawning of the tile.
                            GameObject instantiated = GameObject.Instantiate(m_tileList*);*

instantiated.transform.position = new Vector3(x * M_TILESIZE, y * M_TILESIZE, 0);

m_worldTiles[x, y] = instantiated;
}
}
catch
{

}

}

}
}

}
Sorry if this makes no sense, comment if it doesn’t and I’ll try explaining again.

Here’s another explanation of what I want to achieve :
What I want to do is have a list full of percentages, and that percentage representing a tile.
So, if I have a value in the list it would be ****
So if the list looks something like this :
- Grass Tile, 0.5
- Dirt Tile, 0.5
- Stone Tile, 0.2
- Gold Tile, 0.0001
It should have a 50% chance to spawn a grass tile, 50% chance to spawn a dirt tile, 20% chance to spawn a stone tile, and a 0.01% chance of spawning a gold tile.

You can make it easier to maintain by sorting the list at the start of the game via List.Sort and by normalizing the values to make the sum of the list 100. So then you don’t have to sort the list and small diversions such as 50%, 50%, 1% which would add up to 101% but the normalized values (49,5% and 0,99%) would still be accurate. (To normalize use percentage = 100/sum*value)

Hope this points you in the right direction to create a maintainable system.

-Gameplay4all

P.S. Custom inspectors can also be used here

There’s a few different ways to do this.

  1. If you want to set the exact probability of each outcome, all the individual probabilities must add up to 100% (i.e. 1F); they can add up to a number less than 100% if you handle what happens if the loop doesn’t choose any objects after it’s over, but adding up to a number larger than 100% doesn’t make sense for this solution. If they add up to 100% and order them from smallest probability to largest, then you can roll randomly once before you start the loop, and your loop looks like this:

    float val = Random.value; // random value in the range [0,1]
    // warning: Unity’s built-in RNG can give exactly 1, unlike most other RNGs

    GameObject objectToInstantiate == null;

    for (int i=0; i<m_chanceList.Count; i++)
    {
    // we have to use <= here instead of < because if the RNG rolls exactly 1,
    // then it’ll screw this up if we use <
    if (val <= m_chanceList*.Value) // check if we’re smaller than that*

  • {*
    objectToInstantiate = m_tileList*; // use this object if we rolled under it*
    * break; // and stop the loop*
    * }*
    * else*
    * {*
    * // otherwise, subtract this object’s value and try the next one*
    val -= m_chanceList*.Value;
    _ }
    }*
    This works fine for small sets of values, but if you have a large list that you’re continually adding to, it’s a pain to maintain.
    2) Another solution, then, is to roll randomly for EACH tile, and define each tile’s value as the probability of using that tile if every roll before that failed. In this case, the values don’t have to add up to anything, and, the order of the values doesn’t have to be in numerical order, but they do have to be in the order that you want to roll for them in.
    GameObject objectToInstantiate == null;_

for (int i=0; i<m_chanceList.Count; i++)
{
* float val = Random.value;*

* // if we roll below this object’s value, use it*
if (val <= m_chanceList*.Value)
_ {_
objectToInstantiate= m_tileList;
_ break; // and stop the loop*
* }
}*_

// you can either set the last object’s probability as 1F,
* //giving it a 100% chance to be used if all else fails,*
// or you can do this after the loop:

if (objectToInstantiate == null)
{
* objectToInstantiate = someDefaultObject; // can be m_chanceList[m_chanceList.Count-1] to always use the last object*
}