Weighted Rarity Table
In this challenge, I have to create a system where some enemies spawn more often than others and the same for the powerups where things like ammo would spawn more often and things like the triple-shot would spawn less often.
Create a balanced spawning system between enemies and pickups.
Pickups like health should be rare, where ammo is frequent.
In this article, I won’t go into deep detail about how I completed the challenge and implemented it in my project. But instead, discuss the challenge at hand and explain how I came to a solution and explain the solution in a generalized demo that can be understood and applied in a myriad of ways.
I’ve done some research and something that came up often is a weighted system. Basically how it works is if you have three objects, A, B, and C.
A has a value of 60
B has a value of 30
C has a value of 10
The combined total of A, B and C is 100. A has the biggest value and is the “heaviest”, where C has the smallest value and is the “lightest”. Lighter numbers are rarer compared to heavier numbers. It might not make any sense yet so let me explain.
So our combined numbers make a total of 100, you then draw a random number between 0 and 100. Let’s say that number 49, you then check is 49 bigger than 60 (A)? No, 49 is not bigger than 60 so then A is the object you get. If you draw a new number but this time it’s 72. Is 72 bigger than 60 (A)? Yes, it is. What you then do is subtract 60 from 72 and get 12. Now at this point, A is no longer in the picture, and is 12 bigger than 30 (B)? No, it’s not and therefore B is your object. Lastly, let’s draw a new number. This time we get 98. Is 98 bigger than 60(A)? Yes, it is, but let’s subtract 60 from 98. Our new value is 38. Is 38 bigger than 30 (B)? Yes, it is, but again subtract 30 from 38. Our value is 8. Is 8 bigger than 10 ( C)? No, it’s not and finally, C is our object.
The math might be a bit hard to follow but that’s how we weigh objects and define a rarity. In theory, the math is simple and well, I’ve had to calculate these numbers myself. So let’s replicate this in code.
For the sake of this article, let’s imagine we have a chest in a dungeon and when you open it, you could get one of four weapons.
- Battle Axe- 40
- Long Sword- 25
- Great Sword- 20
- Katana- 15
I created a script called WeightedLootTable and loaded it up with my IDE of choice. Next, I created my tables that would specify the objects to give, their weight and ID. For this article, I have an empty scene with a text element on the screen that would display what item has been won. I also decided to use a string array that would have the name of the objects to give away, but if you want to give actual objects you could use a GameObject array. Next, I created an int array that would store the weights of the objects and lastly I created a second int array. This might not be necessary but it’s handy if your objects in the array are not sorted so this way you can have them stored however you wanted, but still give the correct reward.
Two important things to keep in mind, firstly the weights should always be from heaviest to lightest and that you should not have two rewards with the same weight. I’ve tested it and causes some inconsistencies.
Let’s calculate the weights and give a reward. Ideally, you would have two functions in your script. One that would give the reward, and one to calculate the reward. The function that would be responsible to give the reward would first run the function to calculate the reward to give and then give the reward. But we’ll get to the first function a bit later since it’s not our focus here.
The first thing we need to do is to create a global int variable to store the total calculated weight, I named mine _weightedTotal. The next variable we need to add is another int for the reward to give, again I named mine _rewardToGive. You’ll see why they’re important in a moment. I created a private void function called SelectNewReward() and the first thing we do is clear the _weightedTotal. This is important in case you add or remove rewards from the array. Then we need to total the weights of the rewards in a foreach loop. Once that’s done we create a local variable called randomNumber and that equals Random.Range(0, _weightedTotal) to get a number between 0 and 100.
Next, we create another local variable called i and equal that to 0. Once that’s done it’s time to compare the number we got from the Random.Rage to our weights. For this we use a foreach loop and in that loop, we compare a local var called weight in _lootTable. Then we check if our randomNumber is less or equal to to the weight. and if that is true we will give the corresponding reward and return. If that if statement is not true, then we add 1 to i and minus the weight from the randomNumber and continue to check again if randomNumber is less or equals to the weight. And this will loop through until we have found our reward.
At this point we can now work on our function that would give the reward. I’ve created an if statement in Update() to check if we press spacebar and then to select a reward and display it on the screen as text. But you’re able to use this however you like.
The Full Script
When I first started working on my balanced spawning challenge I’ve probably spent like a week figuring out how this system works and why is the _rewardID is all mixed up and not in order like 1, 2, 3 etc.
I hope you understood what I was explaining. I found other articles on Medium and on the Unity Forums but most of them were vague and didn’t fully understand where the _rewardToGive = _rewardID[i] was coming from so I’ve done lots of tests and rewrites to understand this system. But once you understand how it works it’s not that hard.