7
\$\begingroup\$

My question is pretty general. What is the simplest way to code an RNG system or algorithm that easily accommodates additions and changes? For example, let's say I have an RNG system that will spawn an item based on the random value that is rolled. Then I guess some basic, slapped-together solution could look like

if (randomNumber > 0 && randomNumber <= 0.2)
{
    // spawn something
}

if (randomNumber > 0.2 && randomNumber <= 0.4)
{
    // spawn some other thing
}

or for the chance to spawn multiple items, maybe

if (randomNumber < 0.4)
{
    // spawn something
}

if (randomNumber < 0.2)
{
    // spawn some other thing 
}

Now, let's say I add lots and lots of possible items that can spawn in the game. It would be a hassle to go back and modify all these values while maintaining their relative rarities, etc. How do I make a more flexible RNG system?

\$\endgroup\$
  • 4
    \$\begingroup\$ Take a look at data driven programming techniques. You shouldn't have hard-coded values like that in your code. \$\endgroup\$ – MichaelHouse Jun 29 '17 at 18:27
  • \$\begingroup\$ You could build brackets of rarity, that give some X amount of weight to the items. And then from there give everything else a base value and have it choose from there. Just make certain brackets larger based on the rarity so they're less commonly found. You'll also want more randomness than a basic rand. Something more data driven as Byte said would be good \$\endgroup\$ – n_plum Jun 29 '17 at 18:28
  • \$\begingroup\$ Not a full answer, but you probably want to use a simple lookup table. Crucially, you want to follow @Byte56's suggestion and initialise these tables from text files, not directly in the code. Even better, add functionality to reload the values from the files while playing. Not having to recompile your code to change a couple of variables is great. \$\endgroup\$ – Miles Rout Jul 4 '17 at 21:23
7
\$\begingroup\$

You can use an algorithm which I call Distributed Probability RNG.

It looks like this:

public class Item {}

public class ExampleBucket
{
    private List<Item> bucket = new List<Item>();   

    public void Add (Item item, int count)
    {
        for (int i = 0; i < count; i++)
        {
            bucket.Add(item);
        }
    }

    public Item GetRandom (Random rng)
    {
        var index = rng.Next(0, bucket.Count);

        return bucket[index];
    }
}

You can initialize like this:

private void InitBucket()
{
    var itemA = new Item();
    var itemB = new Item();
    var itemC = new Item();

    var bucket = new ExampleBucket();

    bucket.Add(itemA, 1);
    bucket.Add(itemB, 2);
    bucket.Add(itemC, 3);
}

You can get items by calling GetRandom(Random) on your ExampleBucket instance.


How does it work?

We have a list named bucket in ExampleBucket class. First off it is empty. By calling Add(Item, int) method, you add a specific amount of Items to the bucket list.

At the end of our InitBucket() method, the bucket list will look like this:

itemA // 1 x itemA
itemB // 2 x itemB
itemB
itemC // 3 x itemC
itemC
itemC

Then we simply generate a random number between 0 and bucket.Count, and return the item that corresponds to that index in the bucket list.

The probability of getting a specific item from a list is Item Count / Total Count. The more items you have of a specific type in the list, the more chance that item gets selected.

So in this case;

Probability of getting itemA = 1 / (1+2+3)
Probability of getting itemB = 2 / (1+2+3)
Probability of getting itemC = 3 / (1+2+3)

When you run GetRandom() for 100000 times (the test is here in Fiddle) you can see the values are pretty close to perfect results:

A count = 16690 | Expected = 16666
B count = 33519 | Expected = 33333
C count = 49791 | Expected = 50000

What I like most in this algorithm is, you don't need to specify a total item count. You can just add any item at any time you want and don't worry about the rest.

For example, if you want to increase the chance of getting itemA from the list, you can simply call

bucket.Add (itemA, theAmountYouWant);

and you don't need to do anything else.

\$\endgroup\$
  • \$\begingroup\$ Thank you very much for the helpful response. How do I store an instance of ExampleBucket() in a variable? var bucket = new ExampleBucket(); doesn't seem to work for me. \$\endgroup\$ – embracethefuture Jun 30 '17 at 13:54
  • \$\begingroup\$ @embracethefuture What do you mean by "doesn't seem to work", are you getting an error? \$\endgroup\$ – S. Tarık Çetin Jun 30 '17 at 14:19
  • \$\begingroup\$ Oh weird, it does work. ExampleBucket bucket = new ExampleBucket() I mean... since I'm using C#. Not really sure why it wasn't working earlier. I think it's because I was trying to do it in a different script without setting up a reference. Should InitBucket() be a part of the Item class or should it exist in a MonoBehaviour script somewhere? I'm just trying to wrap my head around the architecture. \$\endgroup\$ – embracethefuture Jun 30 '17 at 15:13
  • 1
    \$\begingroup\$ @embracethefuture InitBucket() method is an example usage, that is not a part of the actual code. You should specialise it for your own codebase. But it shouldn't be inside the Item class. \$\endgroup\$ – S. Tarık Çetin Jun 30 '17 at 15:44
  • \$\begingroup\$ Right, thank you. That's what I thought. Sorry, I'm pretty new to game programming. That's why I was having the issue earlier, I don't know how to reference a non-monobehaviour subclass in my monobehaviour scripts. I can't use GetComponent, and I can't do ExampleBucket bucket = new... because my monobehaviour script doesn't know that an ExampleBucket type exists. \$\endgroup\$ – embracethefuture Jun 30 '17 at 15:58
9
\$\begingroup\$

Weighted Randoms

Rather than stuffing more copies of an item into a list, we can do this instead:

private class WeightedItem {
    public readonly int weight;
    public readonly Item drop;
    //constructor omitted
}

We then shove these into a list:

private List<WeightedItem> bucket = new List<WeightedItem>();
private totalWeight = 0;
public void Add (Item item, int count) {
    if(count <= 0) throw new Exception("Invalid random weight");
    bucket.Add(new WeightedItem(item, count));
    totalWeight += count;
}

And then we get our result like this:

public Item GetRandom (Random rng) {
    int randomVal = rng.Next(0, totalWeight);
    foreach(WeightedItem item in bucket) {
        randomVal -= item.weight;
        if(randomVal <= 0) {
            return item.drop;
        }
    }
}

This method is more flexible than just shoving items into a list because we can make the wrapper class responsible for turning the Item into an ItemStack and applying extra data (such as the stack size, or NBT data--if we think of Items in the Minecraft sense where Item is the prototypical definition and ItemStack is what actually shows up in the player's inventory: size and NBT data are just extra bits of data on top of the prototypical definition that tell us about this grouping specifically; what your project actually has and how it's represented is up to you).

What this means is that we can have multiple random genereators each with their own collection of weighted lists and any given entry in that list (say, Apples) can generate a unique quantity depending on which generator they're in. Eg. a wrapper like this:

public class RandomSizeStackDrop extends WeightedItem {
    public readonly int weight;
    public readonly Item drop;
    public WeightedItem(Item i, int w) {
        drop = i;
        weight = w;
    }

    public ItemStack GetStackFromDrop() {
        //Creates a stack with a size from 1 to 5
        return new ItemStack(drop, Random.Next(1,5));
    }
}

And rather than returning Item in the GetRandom() method, we're instead returning item.GetStackFromDrop()

So we could have a bucket for items being generated in a chest near a farm and have stacks of 1 to 5 apples show up, but in another chest, down in a mineshaft say, only generates 1 apple any time an apple shows up. Even if the probability for both is the same, the chest near the farm will generate more total apples as a result.

\$\endgroup\$
2
\$\begingroup\$

Most of your conditions are useless which makes your code longer for nothing. This is the bread and butter way of doing things.

if (randomNumber <= 0.2)
{
    // 20% chance
}else if (randomNumber <= 0.4)
{
    // 20% chance
}else{
    // 60% chance
}

If you want to make it even more convenient, you could try with weighting.

Let's assume you have a data structure that takes an Action. Here's some pseudo-code.

WeightedRandomizer wr = new WeightedRandomizer();

wr.addAction(Action.MOVE, 1); // 1 in 7 chances of happening
wr.addAction(Action.JUMP, 2); // 2 in 7 chances of happening
wr.addAction(Action.FLY, 4); // 4 in 7 chances of happening

Action action = wr.getAction();

You have a bit less control on the exact %, but it is definitely a much cleaner approach.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.