- Home /
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[i]);
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.
Answer by gameplay4all · Mar 02, 2017 at 09:47 PM
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
Answer by Commoble · Mar 02, 2017 at 09:30 PM
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[i].Value) // check if we're smaller than that
{
objectToInstantiate = m_tileList[i]; // 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[i].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[i].Value)
{
objectToInstantiate= m_tileList[i];
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
}
For your first solution there's no need to sort the values in any particular order. The probabilities stay the same. You basically split the whole range into small sections according to the probabilities of each item. The first solution also works with relative probabilities as long as you either normalize the probabilities or roll a number that is in the range [0 - (sum of all probabilites]
)
The second solution makes not much sense. The probability of the first item is much higher and it's degrading for each item. So your actual numbers don't represent the actual probabilities. For example if you have 3 items in this order: 0.5, 0.5, 0.2. The first item has a probability of 50%. The second item has a probability of 25% (ins$$anonymous$$d of 50%) and the last item has a probability of 5% (ins$$anonymous$$d of 20%).
Your answer
Follow this Question
Related Questions
How to create a simple world generator in Unity3D? 1 Answer
Multiple Cars not working 1 Answer
Distribute terrain in zones 3 Answers
Randomly Select String from Array 0 Answers