Weighted Selections in Unity3D

Weighted Selections : 

When using randomly generated content, there's many ways you can get random selections,
the easiest being the Unity Random class. However there are cases when that doesnt work well.

For example, if I have a list of monsters for a dungeon that looks like this

MONSTER_1
MONSTER_2
BOSS_MONSTER

If you use the Random class to pick from that list, you would have a boss about 1/3 of the time.
Most players wouldn't want to fight a boss every 3rd enemy or so.  Therese probably many ways
to solve this, but my favourite way is to use a weighted selection. 

What is a weighted selection?

The easiest way to think of a weighted selection is pulling names from a hat. Everyone has their
names on a slip of paper, and you pull out one. However not everyone  has their names in equal
times. It could be set up like this :

MONSTER_1 x 5
MONSTER_2 X 4
BOSS_MONSTER x 1

So in this example, you would see about 5 1's, 4 2's and 1 Boss in a selection of 10, approximately.
This is great, because now its easy to balance how often one shows up vs the others. 

This sounds great! How do I use it in my game?

It's fairly simple to implement. This isnt the only way to implement it, but its the way Ive been using it. 


 public string GetWeightedSelection(List<KeyValuePair<string, int>> kvps)
    {
        string selectedKey = ""; //This is our selected item

        int totalWeight = 0; // This is our total weight, and is set to 0

        foreach(KeyValuePair<string,int> kvp in kvps)
        {
            totalWeight += kvp.Value; // Add up the weight for each item
        }
        
        int roll = Random.Range(0, totalWeight); // Roll a random number from 0 to total weight
        int sum = 0; // Sum all the weights together as you iterate over them
        for(int i=0; i<kvps.Count; i++)
        {
sum += kvps[i].Value; if (sum >roll) { selectedKey = kvps[i].Key; //If the added weight is now GREATER than what we rolled, we found our item, break break; } } if(selectedKey == "") { Debug.LogError("We tried to get a weighted selection, but didnt find one"); //Something went badly wrong. } return selectedKey; }

So what we get is a pretty simple, and easy to use generic method. I store all my objects in dictionarys so I might have a dictionarty
of enemy groups, and want a weighted enemy selection from it, so to use this method, Id have a method like this somewhere :


 public EnemyData GetWeightedEnemy(string groupID)
    {

        List<KeyValuePair<string, int>> kvps = new List<KeyValuePair<string, int>>();

        foreach(EnemyTable eT in enemyTable[groupID])
        {
            kvps.Add(new KeyValuePair<string, int>(eT.enemyID, eT.weight));
        }
        return enemyData[GetWeightedSelection(kvps)];
    }

And that would build the key value pair, and send it to the weighted selection method, and return an EnemyData Object.

 

As an added note, I like to use the plugin GoogleFU for my Unity Data. So all my data looks like this : 

All the enemyTable objects are stored in a Dictionary of Lists sorted by group. Getting them from the group is as easy as calling enemyTable["simpleGroup"].

Each EnemyID in the table maps to enemyIDs in an enemyDataSheet 


It may seem a little complicated, but in the end this allows me to map an enemy group to my level, and define what those enemies are, without even opening
Unity. I can do all my design from a spreadsheet. This even allows for things like spreadsheet formulas, like enemies should always have double the health of
the one above it, or even just have a script that prints an error if 2 enemies can drop some unique item.  Google sheets is a super powerful tool. 

You could even take this to the next level by having the enemies have a weighted selection for their rewardItem, instead of just a set item. 

Its such a useful and  expansive system, I hope you guys have fun with it.

 

Category: Blog