4
\$\begingroup\$

I have an array of gameObjects in a spawner. I want to randomly spawn these gameObjects based on a weighted chance. I want to be able to manually set in the Inspector a percentage chance that an object from the array will spawn.

So when I drag my gameobjects into the array, how do I also manually set a percentage chance that it will be selected?

\$\endgroup\$
2

1 Answer 1

8
\$\begingroup\$

Here's one way to do it, using a custom struct to store object-weight pairs:

public class Spawner : MonoBehaviour
{
   // Here's where we define our weighted object structure,
   // and flag it Serializable so it can be edited in the Inspector.
   [System.Serializable]
   public struct Spawnable
   {
       public GameObject gameObject;
       public float weight;
   }

   // Now expose an array of these to be populated in the Inspector.
   public Spawnable[] spawnList;

   // Track the total weight used in the whole array.
   float _totalSpawnWeight;

   // Update the total weight when the user modifies Inspector properties,
   // and on initialization at runtime.
   void OnValidate()
   {
      _totalSpawnWeight = 0f;
      foreach(var spawnable in spawnList)
          _totalSpawnWeight += spawnable.weight;
   }

   // As Problematic points out below, OnValidate isn't called
   // in a built executable. But in that case we don't need to react
   // to a user fiddling with the Inspector mid-game, so it suffices
   // to run this code once during Awake:
   void Awake()
   {
       OnValidate();
   }

   // Spawn an item randomly, according to the relative weights.
   public void Spawn()
   {
      // Generate a random position in the list.
      float pick = Random.value * _totalSpawnWeight;
      int chosenIndex = 0;
      float cumulativeWeight = spawnList[0].weight;

      // Step through the list until we've accumulated more weight than this.
      // The length check is for safety in case rounding errors accumulate.
      while(pick > cumulativeWeight && chosenIndex < spawnList.length - 1)
      {
         chosenIndex++;
         cumulativeWeight += spawnList[chosenIndex].weight;
      }

      // Spawn the chosen item.
      Instantiate(spawnList[chosenIndex].gameObject, transform.position, transform.rotation);
   }
}

This shows up in the Inspector like so: Inspector example showing structure fields

You can make this look neater by creating a custom PropertyDrawer or Editor, but out of the box this gets the core functionality.

Note that your weights don't have to sum to 1 or 100 or any other particular value. They're taken as portions relative to the total. As long as they're non-negative they should "just work."

\$\endgroup\$
4
  • \$\begingroup\$ +1 I like that OnValidate method, that's useful. \$\endgroup\$
    – House
    Commented Apr 9, 2016 at 22:05
  • \$\begingroup\$ This is great. Thank you so much. I agree with Byte56, the OnValidate IS useful! I think it was a case of overthinking to the point that I dumbed out. DMGregory, you are a superstar. \$\endgroup\$ Commented Apr 9, 2016 at 22:09
  • 1
    \$\begingroup\$ Note that OnValidate is an editor-only method, and won't run in a build player. \$\endgroup\$ Commented Apr 10, 2016 at 3:51
  • 2
    \$\begingroup\$ @Problematic thanks for confirming. I've added a fix for built executables. Still keeping OnValidate so the script responds correctly to mid-play tweaking in the Editor, but also calling it manually in Awake for the built player. \$\endgroup\$
    – DMGregory
    Commented Apr 10, 2016 at 4:02

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .