© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
C. PittProcedural Generation in Godothttps://doi.org/10.1007/978-1-4842-8795-8_6

6. Creating a Seeding System

Christopher Pitt1  
(1)
Durbanville, South Africa
 

Back in Chapter 2, we learned that randomization takes a couple forms in game development. The first and most common is randomization where we don’t explicitly control the seed used. That’s what we used to randomize the behavior of the nodes in that chapter.

The second form of randomization is where we know what the seed is, and we can control it to some extent. Minecraft is a great example of this, because each level starts with an input where you can specify a seed.

Seeding can be a bit confusing, but it’s a way to start the randomization algorithm at an unguessable point. That’s because most randomization isn’t true randomization but rather pseudo-randomization.

Computers cannot produce random numbers without observing some outside stimulus; so instead, they offer a predictable kind of randomization progression.

Imagine an infinite sequence of random numbers. Seeding is a way to start at some point in the sequence and continue it:

An illustration of seeded randomization. Infinitely predictable random numbers are mentioned in addition to the seed, which is 90, and the next random number, which is 31.

Seeded randomization

If we pick a different seed, then the starting point is different, so the sequence of random numbers will be different. If someone using the same algorithm picks the seed of 90, they will get the same sequence of numbers.

Minecraft picks a random seed, seeded from details like the date and time and details of the computer’s hardware and configuration.

That seed is then used as the starting point for the pseudo-random number generation to create what appears to be a completely random world.

A New Experiment

We’ll become more familiar with these concepts as we see them in action. Let’s create a new experiment where we study the effects of pseudo-randomization and create a way for starting seeds to be derived and changed.

Let’s call it the SeedExperiment. We can add the following component tree, so we can edit the seed value and see a sample of random numbers that are generated with that seed:

A screenshot of the scene window with the seed experiment option selected. It opens the seed experiment window, where the generated random numbers are mentioned.

Setting up SeedExperiment

You can make these UI controls as small or as big as you like. We can't anchor them to the screen because their parent is a Node2D, so it has no implicit size. We can set up a couple methods to pick a new random number and to update the sample of random numbers (the labels):

This is from nodes/experiments/seed_experiment.gd
extends GameExperiment
@onready var _line_edit := $VBoxContainer/HBoxContainer/LineEdit as LineEdit
@onready var _grid_container := $VBoxContainer/GridContainer as GridContainer
func pick_random_number() -> void:
    _line_edit.text = str(randi() % 100)
func update_random_sample() -> void:
    var generator = RandomNumberGenerator.new()
    generator.seed = _line_edit.text.to_int()
    for child in _grid_container.get_children():
        child.text = str(generator.randi() % 100)

Here, we can see both popular forms of randomization. The first is in pick_random_number, where we’re not defining a seed to start from. When the game starts, the randomize() function is automatically called. This is much the same way as Minecraft does to generate the seed, which it then allows the player to edit.

The second kind of randomization happens in update_random_sample, where we define the seed for the RandomNumberGenerator so that it starts at a predictable point.

We can tie these methods together by adding some signals to the LineEdit and Button, and we can also make the randomization process happen once on load:

This is from nodes/experiments/seed_experiment.gd
func _ready() -> void:
    refresh()
func refresh() -> void:
    pick_random_number()
    update_random_sample()
func _on_button_pressed() -> void:
    refresh()
func _on_line_edit_text_changed(_new_text: String) -> void:
    update_random_sample()

When the experiment starts, it will update the LineEdit with a random number between 0 and 99. This is used to update all the labels to use this as the seed.

Press the randomize button a few times to see the different samples. Then, edit the value of the LineEdit so that you switch back and forth between two known seeds. You’ll see that each time you put in the same seed number, the same sample set of random numbers appears.

That’s the power of seeded randomization. You can share the same experiences with your friends, even in a random system, if they use the same seed as you.

Generating Easier Seeds

A random number or sequence of random characters is difficult to remember. Random number generators tend to use longer seeds so that the seed is harder to guess.

Why force your players to remember a sequence of numbers when you can show them with something much easier, like two or three words? Those are much easier to memorize.

Search on Google or GitHub for a word list file in text format. I’m using one that is about 23KB big. I forget where exactly I found it, but I used it for a game I made a couple years ago.

Once you’ve found one, put it into the experiment project so that we can load it into this experiment. Then, we can use code like this to load all the words and allow us to select the desired number of words for our seed:

A screenshot of the file system window. The resources folder's object dot text file is highlighted.

Downloading a words file

This is from nodes/experiments/seed_experiment.gd
func get_words(generator : RandomNumberGenerator, number : int = 3) -> PackedStringArray:
    var words := get_all_words()
    var size := words.size()
    var chosen := []
    for i in range(3):
        chosen.append(words[generator.randi() % size])
    return PackedStringArray(chosen)
func get_all_words() -> PackedStringArray:
    var file = File.new()
    if file.file_exists("res://resources/objects.txt"):
        file.open("res://resources/objects.txt", File.READ)
        var content = file.get_as_text()
        file.close()
        return content.split(" ", false)
    return PackedStringArray()

The RandomNumberGenerator’s seed property must be an integer, which we can get using the hash method on strings:

This is from nodes/experiments/seed_experiment.gd
func get_hash_from_words(words : PackedStringArray) -> int:
    var complete = ""
    for word in words:
        complete += word.trim_prefix(" ").trim_suffix(" ").to_lower()
    return complete.hash()

Now, all we need to do is replace the numeric seeds we were generating with these new methods:

This is from nodes/experiments/seed_experiment.gd
var generator : RandomNumberGenerator
func pick_random_words() -> void:
    _line_edit.text = " ".join(get_words(generator))
func update_random_sample() -> void:
    generator.seed = get_hash_from_words(_line_edit.text.split(" "))
    for child in _grid_container.get_children():
        child.text = str(generator.randi() % 100)
func _ready() -> void:
    generator = RandomNumberGenerator.new()
    refresh()
func refresh() -> void:
    pick_random_words()
    update_random_sample()

It seems a bit silly that we’re getting a PackedStringArray of words from get_all_words, getting a PackedStringArray from get_words, joining them together, and splitting them into another PackedStringArray to get the hash. It’s because we want to show the words to the user, and LineEdit’s text property can only be a string.

This makes the seeds a lot easier to remember and share, because it’s a small number of words to remember.

The hash method’s documentation is careful to point out that identical strings can generate identical hashes to each other, but that the reverse isn’t always true. The hash value is a 32-bit integer, which means larger values are limited to 32-bit representations. Different values can generate identical hashes, because of that loss of specificity. This is called a collision, and it’s common to talk about the possibility of collisions in cryptography.

In fact, seeded randomization has many of the same underpinnings as encryption. Reversible encryption values are only as strong as the encryption algorithm and the secrecy of the key used to seed them. Sound familiar? If someone knows the randomization algorithm and seed, they can reproduce the same sequence of random values.

Summary

In this chapter, we learned all about generating seeds so that we can control the randomization that can occur in our games. We’re going to use this knowledge in the next chapter, as we build a new game.

Take some time to think about how to integrate this code into a larger project. How would you structure this code if it wasn’t on the “play” screen? How can you call into it to get the words and use them to randomize the behavior of worlds and NPCs?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset