Search Unity

Randomly pick a set amount of objects with each object having a chance of being picked

Discussion in 'Scripting' started by DroidifyDevs, Jul 30, 2017.

  1. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Hello!

    So basically I want to make a system where a set number of objects are picked, and each object has a chance of being picked. Currently I've been going with an approach of 2 arrays, 1 filled with objects to pick, and another filled with their respective pick chances. However, it has a fatal flaw, and that is that modifying other objects' percentages modifies the final percent chance to be spawned. It's very hard to explain in words, so here's the code:
    //in this particular example, it randomly spawns objects along the ground to use in a primitive procedural generation system for mobile
    Code (CSharp):
    1. using System.Collections;
    2. using System;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class TilePopulator : MonoBehaviour {
    7.     public float TileWidth;
    8.     public GameObject[] PrefabObjects;
    9.     public float[] SpawnPercentChances;
    10.     //these floats and bool used for calculation of percent chance of appearing during diagnostics / design stage
    11.     //they have nothing to do with spawning
    12.     public bool CalculateChance;
    13.     public int DiagnosticIndex;
    14.     float Object1TotalSpawned;
    15.     public float TotalObjectsSpawned;
    16.     public float MaxAmount;
    17.     public float MinXRange;
    18.     public float MaxXRange;
    19.     public float MinZRange;
    20.     public float MaxZRange;
    21.     // Use this for initialization
    22.     void Start ()
    23.     {
    24.         TileWidth = transform.GetComponent<Renderer>().bounds.x;
    25.         if (PrefabObjects.Length != SpawnPercentChances.Length)
    26.             Debug.Log("<color=red><b>CRITICAL ERROR: PrefabObjects.Length MUST = SpawnChances.Length. You can't spawn an object without a spawn chance!</b></color>");
    27.         PopulateTile();
    28.     }
    29.    
    30.     //Random.Range includes SMALLEST number, but NOT LARGEST number for **INTEGERS ONLY**. For example, range 0-3 will give 0, 1, 2, but NEVER 3
    31.     //With floats, Random.Range is INCLUSIVE of **BOTH** ranges. I know... WTF?
    32.     void PopulateTile()
    33.     {
    34.         System.Random rnd = new System.Random();
    35.         int OverflowProtector = 0;
    36.         int RandomNumber = 0;
    37.         //RandomNumber++;
    38.         for (int i = 0; i < MaxAmount; i++)
    39.         {
    40.             //IMPORTANT: Put code INSIDE this if statement to protect against an accidental infinite loop. NEVER remove OverflowProtector++
    41.             if (OverflowProtector < 10000000)
    42.             {
    43.                 OverflowProtector++;
    44.                 RandomNumber = rnd.Next(0, PrefabObjects.Length);
    45.                 //Debug.Log("RandomNumber is " + RandomNumber);
    46.                 if (CalculateChance)
    47.                     MaxAmount++;
    48.                 if (SpawnPercentChances[RandomNumber] >= UnityEngine.Random.Range(0f, 100f))
    49.                 {
    50.                     TotalObjectsSpawned++;
    51.                     //for percentage calculation only during diagnostics / design
    52.                     if (CalculateChance && RandomNumber == DiagnosticIndex)
    53.                         CalculatePercentSpawnChance(DiagnosticIndex);
    54.                     //these ranges are for finding the acceptable spawn position ranges. Don't want to spawn something off of the play area!
    55.                     MinXRange = transform.position.x - TileWidth + PrefabObjects[RandomNumber].GetComponent<Renderer>().bounds.extents.x;
    56.                     MaxXRange = transform.position.x + TileWidth - PrefabObjects[RandomNumber].GetComponent<Renderer>().bounds.extents.x;
    57.                     MinZRange = transform.position.z - TileWidth + PrefabObjects[RandomNumber].GetComponent<Renderer>().bounds.extents.z;
    58.                     MaxZRange = transform.position.z + TileWidth - PrefabObjects[RandomNumber].GetComponent<Renderer>().bounds.extents.z;
    59.                     var spawnobj = Instantiate(PrefabObjects[RandomNumber], new Vector3(UnityEngine.Random.Range(MinXRange, MaxXRange), 0.5f,
    60.                         UnityEngine.Random.Range(MinZRange, MaxZRange)), Quaternion.identity);
    61.                     //Debug.Log("Spawned: " + PrefabObjects[RandomNumber].name);
    62.                 }
    63.             }
    64.             else
    65.                 Debug.Log("<color=red><b>CRITICAL ERROR: Custom overflow protection triggered in PopulateTile()</b></color>");
    66.         }
    67.     }
    68.  
    69.     //just spawn lots of objects and count them to get a percentage value. Primitive but works.
    70.     void CalculatePercentSpawnChance(int ObjectIndex)
    71.     {
    72.         Object1TotalSpawned++;
    73.         if(TotalObjectsSpawned >= 5000)
    74.         {
    75.             MaxAmount = 0;
    76.             CalculateChance = false;
    77.             Debug.Log("<color=green>Percentage of " + PrefabObjects[ObjectIndex].name + " being spawned is " + (Object1TotalSpawned * 100 / TotalObjectsSpawned) + "</color>");
    78.         }
    79.     }
    80. }
    So my question is: How to make a proper object-picking system where each object has a percentage value that specifies its likeliness to be picked? My system does accomplish this goal, but the flaws I listed make it seem like something a noob would code, and I'm trying to advance past that.

    Anything that points me into the right direction will be appreciated :)
     
    Last edited: Jul 30, 2017
  2. Deleted User

    Deleted User

    Guest

    i used, like a raffle, and each outcome has x number of raffle tickets ... seen Morrowind/Oblivion/Skyrim do random loot like that...

    lol, i basically add an entry to a list for each raffle ticket it has ... hmm that seems dumb now i think about it (kinda?).... buuut .. really the most the list will be in my case that ive used it so far is like 100entries .. the random number picks an index...

    hmm thered be better ways to do it i guess, but yeah something with like a raffle? i guess? maybe? LOL
     
  3. cstooch

    cstooch

    Joined:
    Apr 16, 2014
    Posts:
    354
    I like the lotto ticket idea, so +1 for that.

    One other idea is maybe you could have rarity groups, for lack of a better term here... so say you'd have like 5 different arrays.. each with a different % chance (example: 2%, 10%, 18%, 30%, 40%) ... the combination of %s adds up to 100. These numbers don't ever change. You'd assign your objects to specific rarity group arrays. Your first random check would be to see what rarity you spawned. Then, once you figure that out, you select something randomly from that rarity group array, with each item in the array having an equal chance of being picked (pick a random index between 0 and array size -1 or whatever).
     
    DroidifyDevs likes this.
  4. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    That actually reminds me of how many games work. For example, the Walking Dead on Andorid and iOS. They have chance boxes that can yield a 1-5 star character, where it's easy to get 1-3 stars and practically impossible to draw a 5-star. Once you draw a 5-star character, it doesn't really matter which exact one you draw. So basically, I think the logic is that they first draw the amount of stars, then just pick a random character.

    For example, this is what I've come up with:
    Code (CSharp):
    1. void PopulateTileV2()
    2.     {
    3.         int RandomNumber;
    4.         for(int i=0; i <= MaxAmount; i++)
    5.         {
    6.             RandomNumber = UnityEngine.Random.Range(0, 101);
    7.             foreach(float f in PercentChancesCategories)
    8.             {
    9.                 if (f > RandomNumber)
    10.                 {
    11.                     //Debug.Log("Picked! " + f);
    12.                     TotalObjectsSpawned++;
    13.                 }
    14.             }
    15.         }
    16.     }
    Where PercentChancesCategories is an array that has categories of chances (90%, 10% etc...), MaxAmount is total amount of objects to pick. For example, if I know that f = 10, then I can pick randomly from an array of objects that have a 10% chance of being spawned. That way, the AMOUNT of total objects does not affect their chances of being picked, because once you pick a 10% chance object list, you can pick randomly from that 10% chance list, regardless of how many objects are there.

    Anyways, huge thanks for that idea!!
     
    cstooch likes this.
  5. cstooch

    cstooch

    Joined:
    Apr 16, 2014
    Posts:
    354
    Yep, exactly what I was thinking.. good stuff! It's a pretty common thing in RPGs too. Glad that worked out for you.
     
    Last edited: Jul 31, 2017
  6. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Only downside is that I do get a lot of arrays and if statements, for example:
    Code (CSharp):
    1. void PopulateTileV2()
    2.     {
    3.         System.Random rand = new System.Random();
    4.         int RandomNumber;
    5.         for(int i=0; i <= MaxAmount; i++)
    6.         {
    7.             RandomNumber = UnityEngine.Random.Range(0, 101);
    8.             foreach(float f in PercentChancesCategories)
    9.             {
    10.                 if (f > RandomNumber)
    11.                 {
    12.                     //Debug.Log("Picked! " + f);
    13.                     TotalObjectsSpawned++;
    14.                     if (CalculateChance && f == DiagnosticIndex)
    15.                         Object1TotalSpawned++;
    16.                     if (f == 10)
    17.                         Instantiate(TenPercentObjects[rand.Next(TenPercentObjects.Length)], transform.position, Quaternion.identity);
    18.                     if (f == 90)
    19.                         Instantiate(NinetyPercentObjects[rand.Next(NinetyPercentObjects.Length)], transform.position, Quaternion.identity);
    20.                     //etc...
    21.                 }
    22.             }
    23.         }
    24.         if(CalculateChance)
    25.             CalculatePercentSpawnChanceV2();
    26.     }
    I really try to avoid multiple if statements in a row, but I guess here it's necessary.
     
  7. cstooch

    cstooch

    Joined:
    Apr 16, 2014
    Posts:
    354
    I'm just thinking out loud here, but in my head i'd say you could have two different collections (you could do it with one, but it might make more sense to have two.. I don't know.. either way could be implemented just fine) where the first collection is going to have some sort of key to the second one. The first one just has the number ranges between 0 and 100 where that group will be picked from. Think of this as your "rarity groups". So for example, your epic items maybe get picked if your random number is between 0 and 2. The second collection has the key back to the first collection so that you know how to limit this collection based on the first number that was randomly generated. The 2nd collection has everything in it indicating the object and the "rarity group" it belongs to.

    Then the 2nd pass through, you might put these in some temporary list or something for simplicity's sake, then just randomly pick one.

    Hopefully you get what I mean, if not, I can elaborate more, but you should be able to do this a lot more dynamically. I initially said you'd have an array/list/whatever for each "rarity group", but I actually think it's probably better to have it all in one collection instead so that you can avoid the problem you pointed out.
     
    Last edited: Jul 31, 2017
  8. cstooch

    cstooch

    Joined:
    Apr 16, 2014
    Posts:
    354
    I think the only reason I'd suggest two collections (of some sort) above is that it would be easier to maintain, should you decide to add a new level of rarity. You wouldn't have to go update all the items to indicate the random range they are for. You just need to go update the other collection to add in your new ranges + key value, and/or possibly change some ranges.
     
  9. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    That would be great if Unity would show Dictionaries in the Inspector. Since it can't, I'd have to hard-code all the objects, or port them from arrays to dictionaries. I LOVE using dictionaries, but unfortunately you can't drag-drop prefabs into them and that limits their use :(