Random object spawning with chances

I have class that have obstacle prefab and its spawn chance:

[System.Serializable]
public class ObstaclePrefabs
{
    [SerializeField] private GameObject obstaclePrefab;
    [SerializeField] private float spawnChanсe;
}

In another class I have an array obstaclePrefabs

 [SerializeField] private ObstaclePrefabs[] obstaclePrefabs;

In inspector it looks like this:
image

How can I Instantiate() random prefab from obstaclePrefabs with the chances set in spawnChanсe

1 Like

So mind you… you’re going to want to separate your concepts of instantiate and concepts of picking random. These are 2 different operations. Instantiating a prefab is fairly trivial so I’m not even going to cover that here.

As for picking a random entry you’ll want to calculate a random value over some range and depending on where in that range that value is is associated with one of the values. So like in your image above you could theoretically generate a value between 0 and 1, if it’s less than or equal to 0.8 it goes to plant01 and if it’s between 0.8 and 1 it’s plant02.

BUT, I would like to mention this is really annoying to do because you’re now having to adjust your odds to make sure that they all sum up to 1 (for 100%) because you’re relying on this hard coded percentage.

Have you ever noticed though that when you watch sports or hear horse race numbers things are not percentages? It’s not like “Daddy’s Lucky Horse” has a 50% chance of winning does it? Or some sports player is the 90% ball kicker. Really there are stats that “rank” them and those rankings can then be compared to one another to calculate the chance on the fly no matter who they’re pitted against.

So that’s how I do my PickRandom (and so do a lot of people):

        public static T PickRandom<T>(this IEnumerable<T> lst, System.Func<T, float> weightPredicate, IRandom rng = null)
        {
            var arr = (lst is IList<T>) ? lst as IList<T> : lst.ToList();
            if (arr.Count == 0) return default(T);

            using (var weights = com.spacepuppy.Collections.TempCollection.GetList<float>())
            {
                int i;
                float w;
                double total = 0f;
                for (i = 0; i < arr.Count; i++)
                {
                    w = weightPredicate(arr[i]);
                    if (float.IsPositiveInfinity(w)) return arr[i];
                    else if (w >= 0f && !float.IsNaN(w)) total += w;
                    weights.Add(w);
                }

                if (rng == null) rng = RandomUtil.Standard;
                double r = rng.NextDouble();
                double s = 0f;

                for (i = 0; i < weights.Count; i++)
                {
                    w = weights[i];
                    if (float.IsNaN(w) || w <= 0f) continue;

                    s += w / total;
                    if (s > r)
                    {
                        return arr[i];
                    }
                }

                //should only get here if last element had a zero weight, and the r was large
                i = arr.Count - 1;
                while (i > 0 && weights[i] <= 0f) i--;
                return arr[i];
            }
        }

github source

So this method accepts a collection of options, and a delegate to retrieve the “weight” (rank) of that element, as well as an optional IRandom (this is specific to my code… you can just replace this IRandom with unity ‘Random’ logic). It sums up those weights and calculates each elements odds based on it and then picks one.

It would get used something like this for your code:

var choice = obstaclePrefabs.PickRandom(o => o.spawnChance);
var prefab = choice?.obstaclePrefab;

The nice thing here though is that ‘spawnChance’ doesn’t have to be 0.8 and 0.2. They could be 4 and 1 for example (which is the same). You can just put “weight” values that are saying how more likely one is than the other like ranks. The plant01 is 4 times as likely than the standard rank of 1. Where as plant02 has the standard rank of 1.

And here I’ve removed the logic that has dependencies on my personal code:

        public static T PickRandom<T>(this IEnumerable<T> lst, System.Func<T, float> weightPredicate)
        {
            var arr = (lst is IList<T>) ? lst as IList<T> : lst.ToList();
            if (arr.Count == 0) return default(T);

            var weights = new List<float>(); //may want to come up with a way to recycle this
			{
                int i;
                float w;
                double total = 0f;
                for (i = 0; i < arr.Count; i++)
                {
                    w = weightPredicate(arr[i]);
                    if (float.IsPositiveInfinity(w)) return arr[i];
                    else if (w >= 0f && !float.IsNaN(w)) total += w;
                    weights.Add(w);
                }

                float r = Random.value;
                double s = 0f;

                for (i = 0; i < weights.Count; i++)
                {
                    w = weights[i];
                    if (float.IsNaN(w) || w <= 0f) continue;

                    s += w / total;
                    if (s > r)
                    {
                        return arr[i];
                    }
                }

                //should only get here if last element had a zero weight, and the r was large
                i = arr.Count - 1;
                while (i > 0 && weights[i] <= 0f) i--;
                return arr[i];
            }
        }
1 Like

There are a couple of ways to interpret your question but I believe I know what you mean. You want to instantiate only one of these right? So there is 80% chance of the first one and a 20% chance of the second.

If so then you are generating a number with the probability that it comes up 0 or 1 (corresponding to the element) based upon the spawn chance. In other words if you had an array with 100 elements, 80 of them would have a value of 0 and 20 of them would have a value of 1. The odds correspond to the chance that is set.

Not sure how small or large a chance you need, but I find I can get by with integer counts almost always, and I use this GameObjectCollection for pretty much 100% of my chance-of-spawning needs:

In your case above, you would insert four (4) copies of plant01 and one (1) copy of plant02 into the GameObjectCollection instance, giving you a 0.8 chance of plant01, 0.2 chance of plant02.

That POCO has a pick random and pick shuffled baked into it. Obviously it maintains shuffle state.

I have no idea what motivates folks to overcomplicate solutions particularly when you haven’t indicated any unusual requirements to something that has a standard solution (in math, Unity, C# and software in general) :smile:

A simple internet search reveals two very easy to implement/adapt solutions. The website/messages even explains “why” it works.

Note this is handled with about 10 lines of C# (you will want to return an object rather than a string).

I won’t bother to post the thread that contained the other example. It is for the most part done the same way but if the one above works for you it seems clear and concise.

2 Likes

lets call the .2 chance the “rare obstacle” and the 0.8 chance thing the “common obstacle”

I’d probably make a list of 10 items- 8 of the common obsticals and 2 of the rare, then shuffle it. Either that or I would pick a random number x between 6-10 or something. Then spawn x common obstacles and then one rare obstacle and then start the process again. These methods are “faking” the odds but they guarantee that you’ll always have a certain even distribution of your two obstacles.

The problem with actually evaluating the real random chance is that it can create scenarios that people don’t expect or don’t “feel” right. Like for instance you could go through an entire level and the rare obstacle may never happen to show up. Likewise you could go through the level and the rare obstacle might just happen to appear 5 times in a row.

“Random” is really really really really hard:

I’m curious do you see an issue with the code (from the link I provided) or some advantage to creating extra elements?

Wouldn’t a solution that doesn’t require anything more than the two elements (in this case) be at least slightly “better”?

It is completely unaware of the most powerful feature of Unity: the editor and asset management.

I don’t want to be the one updating code when people add stuff… looks like OP had the same idea because he was using public fields and the inspector.

That’s why I wrote GameObjectCollection (GOC).

You can hack GameObjectCollection to support putting in an odds field, such as your link shows, but what happens if someone puts in odds that don’t add up to 1.0 ? Is that an error? Is that allowed? What happens in that case? Do you possibly get TWO items if the chance is more than 1.0? Do you get zero items if the chances add up to less than 1.0? What about ten items with 0.1 each? Due to floating point imprecision, that doesn’t necessarily add up to 1.0!!

Much like our conversation the other day about librarifying stuff, I try to maintain simplicity and a reduced bug surface, while providing equivalent or nearly-equivalent functionality. Where you draw that line is of course subjective.

It is a simple matter to verify in the editor that the objects exist and that the values add up. I do this sort of thing all the time.

And your reply the other day was completely off the mark BTW. I suggested an “alarm clock” as an example and you suggested there were billions of variables. An alarm clock does not know why you need the alarm to ring at 8am or noon or once per hour. It doesn’t need to know any object that had additional restrictions would check the conditions when it was called.

Let me go find “you aren’t going to need it” articles. You are planning for a future that the OP may never encounter. In any case you aren’t being asked to modify any code let the OP make the choice based upon his needs and preferences.

1 Like

I believe OP wanted to instantiate random prefabs, not random fights

1 Like

We sample the microphone (always present in VR) and use that to get a random number.

This one was really helpful and easy to understand, thanks!

My realisation:

    GameObject RandomObstacle()
    {
        float cumulative = 0f;
        for (int i = 0; i <= obstaclePrefabs.Length; i++)
        {
            cumulative += obstaclePrefabs[i].spawnChanсe;
            if (UnityEngine.Random.Range(0f, 1f) < cumulative)
            {
                return obstaclePrefabs[i].obstaclePrefab;
            }
        }
        return null;
    }

Great, easy to understand tends to mean easy to maintain.

You do have a couple of issues in your interpretation however. Most notable is that you are obtaining new random values in the for loop. You will notice upon review that the reference code does not do that.

Yours also has the possibility of returning a null which is generally speaking a problem… like you have to check for it and what do you do in such a case?

The reference code will return the last element so it is never null.

And I would stick to using doubles as per the ref code simply because it works and you gain nothing by making it float.

1 Like

Is there any issue with that?

It shouldn’t if the sum of all my chances equals 1. However, I added return null only because of this error:

error CS0161: ‘TilesGenerator.RandomObstacle()’: not all code paths return a value

What @tleylan means, is that by “rolling” your chances at every loop iteration, you are displacing the probabilities instead of keeping them steady for a single “dice roll”.

My grain of salt would be this:

  • First, just sum all our chances, so you can know the range and use your total chances to normalize them to 0…1
  • Second, roll the dice just once (1 call to Random)
GameObject RandomObstacle()
{
    float totalChances = 0;
    for (int i = 0; i <= obstaclePrefabs.Length; i++)
        totalChances += obstaclePrefabs[i].spawnChanсe;
    float random = UnityEngine.Random.Range(0f, 1f); 
    float cumulative = 0f;
    for (int i = 0; i <= obstaclePrefabs.Length; i++)
    {
       // add to the cumulative when divided by totalChances, 
       // so it gets normalized for you to the 0..1 range 
       // (this means even if you accidentally configure chances that
       // sum more than 1, you still get proper ratios)
        cumulative += obstaclePrefabs[i].spawnChanсe / totalChances; 

        // dont roll each time, just use the original random value
        if (random < cumulative)
        {
            return obstaclePrefabs[i].obstaclePrefab;
        }
    }
    return null;
}
1 Like

And here is my version. With a little different take on the normalization. Also return last element instead of null. Written in .NET 8 so not directly working in Unity

ChanceItem RandomChance()
{
    float random = Random.Shared.NextSingle() * totalChance;
    float cumulative = 0f;
    foreach (var item in chanceItems)
    {
        cumulative += item.Chance;
        if (random < cumulative)
        {
            return item;
        }
    }
    return chanceItems[^1];
}
record struct ChanceItem(int Id, float Chance);

Tested with:

var chanceItems = new[] { new ChanceItem(1, 0.25f), new ChanceItem(2, 0.30f), new ChanceItem(3, 0.45f) };
var totalChance = chanceItems.Sum(ci => ci.Chance);

var result = Enumerable.Range(0, 1000).Select(i => RandomChance())
    .OrderBy(r => r.Id)
    .ToList();

foreach (var r in result.GroupBy(r => r.Id))
    Console.WriteLine($"Id: {r.Key}, Chance: {r.Count() / (float)result.Count}");

Output:

image

Then why post it?

That’s an absurd amount of Linq too.

1 Like

Nothing wrong with Linq outside of hot loops.

As has been pointed out “random” means random there is nothing more random about getting another random value. There may be a major problem if the new value obtained no longer matches a criteria. But again there is no point in getting updated random values.

And re: the error. I don’t mean to sound pedantic but we don’t add code to eliminate warnings/errors, we fix it. :slight_smile:

If it makes it through without a match (which can’t happen mathematically) then the value of the item with the greatest chance is the one to use. That will be the one used in reality, the compiler is letting you know you can’t covered all the bases from a “computer language” standpoint. It isn’t inspecting the values.

Subtle but worthwhile to consider… note how you return a GameObject (yes I know what is what you ultimately want) and how AndersMalmgren has returned the record “ChanceItem”. You should operate on ObstaclePrefabs not GameObjects. When you get the ObstaclePrefab back then you can instantiate the gameobject in it. Note that you could also do other things should you need to like log the id, blah, blah.

1 Like