Search code examples
godotgdscript

Does random seeding change the project globally, or at the scene/tree level?


Godot/gdscript's seed(...) is a very easy way to implement a random seed, but would you need to set a seed for all randomized scenes? In the docs, it says:

...This method should be called only once when your project starts to initialize the random seed. Calling it multiple times is unnecessary and may impact performance negatively. (see: https://docs.godotengine.org/en/stable/tutorials/math/random_number_generation.html)

Is this "use it once" advice meaning that calling seed(...) (and maybe randomize()) changes the project at a global scale, a tree scale, or just that node/ its children?

For example, lets say you have some sort of randomized world generation (parent) scene in which you instance (children) enemies that have their own randomized parameters/behaviors. In your world generation script, you call seed(...). But lets say now that you have instantiated new children scenes since then, and they each have some sort of random behavior. Would their random behavior be seeded as well now? Or would I want to keep track of the seed and use it in their _ready()? At the same time, if I wanted the children's seeds to be distinct from the parent's, would setting their seed then change the whole project's seed again?


Solution

  • It is a global seed. No, nodes do not have their own seed.

    The seed, randomize, randf and other random related methods that are available on the global scope are not part of the Node class... Which also means they are available in classes that are not Nodes, for example you can use them from a Resource which do not exist in the scene tree.

    And yes, if you call the seed or randomize functions that are defined in the GLOBAL scope, you are modifying the seed GLOBALLY.

    Godot implements this in the simplest possible way: it instantiates a random number generator and allows accessing it via a global API, and that's that. To be extra clear: this is not tracking multiple seeds, not per scene, not per branch, not per node, or any other pattern you might imagine. Godot has a GLOBAL random number generator that it seeds on startup, and you access it via the random related functions in the GLOBAL scope.


    Warning: looking at the source code, there seem to be no precautions for threading, so this is a potential source of bugs to be aware of. Thus, if you want to use random number generation in a thread other than the main thread, I'd advice to use a dedicated instance of RandomNumberGenerator.


    I don't know why you might imagine the random related functions in the GLOBAL scope are somehow not global. Yet, I have an hypothesis: old Godot 3 examples gave you the idea that you need to call randomize per Node... If that is the case, I want to clear that out.

    It used to be that in Godot 3 you had to call randomize because Godot didn't do it for you, and so any Godot 3 example that needed random would also include a call to randomize.

    That call wasn't to initialize "the Node's random" or anything like that. It was to make sure randomize was called at least once in the game, because the author could not rely on randomize being called on some other part of the project first.

    That practice can be put to rest now with Godot 4, which seeds the global random number generation on startup.


    Anyway, does it matter that it global (Besides the threading issue)?

    I assume you are aware that random number generation is sequential. Given the same seed the random number generator will generate the same numbers in the same order. This means that if multiple places are using the global random number generator, they are all taking from the same sequence, and thus they interfere with each other, which at first glance is not important because the results are meant to be random anyway.

    However, if the player can do something that triggers code that uses the random related functions, or that changes the order the code that uses them is executed, then the initial seed is no longer enough to have repeatable results (now it also depends on what the player does). And again, at first glance this is not important because if the player does different things we expect to have different results.

    However, it also that the player might find a way to manipulate the random number generator to predictably yield results, which is a practice we know as random number manipulation. It is for you to decide if that is a concern.


    So, if random number manipulation is a concern...

    The first approach to avoid it is to create instances of RandomNumberGenerator and use then in well defined contexts. These RandomNumberGenerator you create are NOT GLOBAL, and do not interfere with each other.

    Of course, this also means that you need to seed the RandomNumberGenerator instances. One option for reproducible results is to use the global scope random related functions only to generate seeds for the RandomNumberGenerator. However, that means that order of the seeds is important.

    So, if you can't ensure the RandomNumberGenerator are instantiated and seeded always in the same order... Instead you can generate the seeds before hand, so when you need to seed the RandomNumberGenerator you can take the corresponding seed for it even if the RandomNumberGenerator is instantiated out of order. For example using a noise texture.


    However however, if you do not know how many RandomNumberGenerator you are going to need. Or if you find them to be too slow. You probably don't want to generate multiple seeds, but somehow be able to query random numbers in parallel instead of sequentially and without allowing random number manipulation.

    For that, I advice to use a hash/noise function. Not a noise texture.

    For each "sequence" you can can evaluate the function starting from a different point and advancing by a different interval (or a different vector if we are talking of multidimensional noise). So each "sequence" is defined as a linear function on the noise space. You can use families of infinite lines when you don't know how many "sequences" or you are going to need. And I'm writing "sequence" in quotes because you can query at any position on the line, instead of getting values sequentially. I'm not going into detail because this beyond what you asked, but I hope my description is sufficient to give you an idea of how this can work.

    I made a similar argument in Best Practice for Procedural Generation and Code Updates.

    See: Noise-Based RNG - GDC 2017.