Solving Player Frustration: Techniques for Random Number Generation
If you strike up a conversation with an RPG fan, it won’t take long to hear a rant about randomized results and loot—and how frustrating they can be. Many gamers have made this irritation known, and while some developers have been creating innovative solutions, many are still forcing us through infuriating tests of perseverance.
There’s a better way. By altering how we as developers utilize random numbers and their generators, we are able to create engaging experiences that push for that “perfect” amount of difficulty without pushing players over the edge. But before we get into that, let’s go over some basics of Random Number Generators (or RNGs for short).
The Random Number Generator and Its Use
Random numbers are all around us, being used to add variation to our software. In general, the major uses of RNGs are to represent chaotic events, show volatility, or behave as an artificial limiter.
You likely interact with random numbers, or the results of their actions, every day. They’re used in scientific trials, video games, animations, art, and nearly every application on your computer. For example, an RNG is likely implemented in the basic animations on your phone.
Now that we’ve talked about what an RNG is, let’s take a look at its implementation and how it can improve our games.
The Standard Random Number Generator
Almost every programming language uses a Standard RNG in basic functions. It works by returning a random value between two numbers. Standard RNGs can be implemented in dozens of different ways across different systems, but they all have generally the same effect: return a randomized number where each value in the range has the same chance of being returned.
For games, these are commonly used to simulate rolling dice. Ideally, they should only be used in situations where each result is desired to occur an equal number of times.
If you want to experiment with rarity or different rates of randomization, this next method is more suitable for you.
Weighted Random Numbers and Rarity Slotting
This type of RNG is the basis for any RPG with item rarity. Specifically, when you need a randomized result but want some to occur with less frequency than others. In most probability classes, this is commonly represented with a bag of marbles. With weighted RNGs, your bag might have three blue marbles and one red one. Since we only want one marble, we’ll either get a red one or a blue one, but it’s much more likely for it to be blue.
Why would weighted randomization be important? Let’s use SimCity’s in-game events as an example. If each event was selected using non-weighted methods, then the potential for each event to occur is statistically the same. That makes it just as likely for you to get a proposal for a new casino as to experience an in-game earthquake. By adding weighting, we can ensure that these events happen in a proportional amount that preserves gameplay.
Its Forms and Uses
Grouping of the Same Items
In many computer science courses or books, this method is often referred to as a ‘bag’. The name is pretty on the nose, using classes or objects to create a virtual representation of a literal bag.
It works basically like this: there is a container that objects can be placed into where they are stored, a function for placing an object into the ‘bag’, and a function for randomly selecting an item from the ‘bag’. To refer back to our marble example, this means that you would treat your bag as containing one blue marble, one blue marble, one blue marble, and one red marble.
Utilizing this method of randomization, we can roughly determine the rate at which an outcome occurs to help homogenize each player’s experience. If we were to simplify results on a scale from ‘Very Bad’ to ‘Very Good’, we’ve now made it much more viable that a player will experience an unnecessary string of unwanted results (such as receiving the ‘Very Bad’ result 20 times in a row).
However, it is still statistically possible to receive a series of bad results, just increasingly less so. We’ll take a look at a method that goes slightly further to reduce unwanted results shortly.
Here’s a quick pseudocode example of what a bag class might look like:
Class Bag { //Keep an array of all the items that are in the bag Array itemsInBag; //Fill the bag with items when its created Constructor (Array startingItems) { itemsInBag = startingItems; } //add an item to the bag by passing the object (then just push it onto the array) Function addItem (Object item) { itemsInBag.push(item); } //To get a random item return, use a built-in random function to grab an item from the array Function getRandomItem () { return(itemsInBag[random(0,itemsInBag.length-1)]); } }
Rarity Slotting Implementation
Similar to the grouping implementation from before, rarity slotting is a method of standardization to determine rates (typically to make the process of game design and player reward easier to maintain).
Instead of individually determining the rate of every item in a game, you’ll create a representative rarity—where the rate of a ‘Common’ might represent a 20 in X chance of a certain outcome, whereas ‘Rare’ might represent a 1 in X chance.
This method doesn’t alter much in the actual function of the bag itself, but rather can be used to increase efficiency on the end of the developer, allowing an exponentially large number of items to be quickly assigned a statistical chance.
In addition, rarity slotting is useful in shaping the perception of a player, by easily allowing them to understand how often an event might occur without eliminating their immersion through number crunching.
Here’s a simple example of how we might add rarity slotting to our bag:
Class Bag { //Keep an array of all the items that are in the bag Array itemsInBag; //add an item to the bag by passing the object Function addItem (Object item) { //keep track of the looping related to rarity slots Int timesToAdd; //Check the rarity variable on the item //(but first create that rarity variable in the item class, //preferably with an enumerated type) Switch(item.rarity) { Case 'common': timesToAdd = 5; Case 'uncommon': timesToAdd = 3; Case 'rare': timesToAdd = 1; } //Add instances of the item to the bag according to rarity While (timesToAdd >0) { itemsInBag.push(item); timesToAdd--; } } }
Variable Rate Random Numbers
We’ve now talked about some of the most common ways to deal with randomization in games, so let’s delve into a more advanced one. The concept of using variable rates starts similarly to the bag from before: we have a set number of outcomes, and we know how often we want them to occur. The difference with this implementation is that we want to adjust the potential for outcomes as they happen.
Why would we want to do this? Take, for example, games with a collectable aspect. If you have ten possible outcomes for the item you receive, with nine being “common” and one being “rare”, then your chances are pretty straightforward: 90% of the time, a player will get the common, and 10% of the time they’ll get the rare. The issue comes when we take multiple draws into account.
Let’s look at your chances of drawing a series of common results:
- On your first draw, there’s a 90% chance of drawing a common.
- At two draws, there’s an 81% chance of having drawn all commons.
- At 10 draws, there’s still a 35% chance of all commons.
- At 20 draws, there’s a 12% chance of all commons.
While the initial ratio of 9:1 seemed to be an ideal rate at first, it only ended up representing the average result, and left 1 in 10 players spending twice as long as intended to get that rare. Furthermore, 4% of players would spend three times as long to get the rare, and an unlucky 1.5% would spend four times as long.
How Variable Rates Solve This Issue
The solution is implementing a rarity range on our objects. You do so by defining both a maximum and minimum rarity for each object (or rarity slot, if you’d like to combine it with the previous example). For example, let’s give our common item a minimum rarity value of 1, with a maximum of 9. The rare will have a minimum and maximum value of 1.
Now, with the scenario from before, we’ll have ten items, and nine of them are one instance of a common, while one of them is a rare. On the first draw, there is a 90% chance of getting the common. With variable rates now, after that common is drawn, we’re going to lower its rarity value by 1.
This makes our next draw have a total of nine items, eight of which are common, giving an 89% chance of drawing a common. After each common result, the rarity of that item drops, making it more likely to pull the rare until we cap out with two items in the bag, one common and one rare.
Whereas before there was a 35% chance of drawing 10 commons in a row, now there is only a 5% chance. For the outlier results, such as drawing 20 commons in a row, the chances are now reduced to 0.5%, and even further down the line. This creates a more consistent outcome for our players, and prevents those edge cases where a player repeatedly has a bad result.
Building a Variable Rate Class
The most basic implementation of variable rate would be to remove an item from the bag, rather than just returning it, like this:
Class Bag { //Keep an array of all the items that are in the bag Array itemsInBag; //Fill the bag with items when its created Constructor (Array startingItems) { itemsInBag = startingItems; } //add an item to the bag by passing the object (then just push it onto the array) Function addItem (Object item) { itemsInBag.push(item); } Function getRandomItem () { //pick a random item from the bag Var currentItem = itemsInBag[random(0,itemsInBag.length-1)]; //reduce the number of instances of that item if it's above the minimum If (instancesOf (currentItem, itemsInBag) > currentItem.minimumRarity) { itemsInBag.remove(currentItem); } return(currentItem); } }
While such a simple version brings with itself a few issues (such as the bag eventually reaching a state of standard randomization), it represents the minor changes that can help to stabilize the results of randomization.
Expansions on the Idea
While this covers the basic idea of variable rates, there are still quite a few things to consider for your own implementations:
-
Removing items from the bag helps to create consistent results, but eventually returns to the issues of standard randomization. How could we shape functions to allow both increases and decreases of items to prevent this?
-
What happens when we are dealing with thousands or millions of items? Utilizing a bag filled with bags could be a solution for this. For example, creating a bag for each rarity (all of the common items in one bag, rares in another) and placing each of those into slots within a large bag can provide a wide number of new possibilities for manipulation.
The Case for Less Tedious Random Numbers
Many games are still using standard random number generation to create difficulty. By doing so, a system is created where half of player experiences fall on either side of the intended. If left unchecked, this creates the potential for edge cases of repeat bad experiences to happen at an unintended amount.
By limiting the ranges of how far the results can stray, a more cohesive user experience is ensured, letting a larger number of players enjoy your game without ongoing tedium.
Wrapping Up
Random number generation is a staple of good game design. Make sure that you’re double-checking your statistics and implementing the best kind of generation for each scenario to enhance the player experience.
Do you love another method that I didn’t cover? Have questions about random number generation in your own game’s design? Leave me a comment below, and I’ll do my best to get back to you.
from Envato Tuts+ Tutorials
Comments
Post a Comment