Generación de números aleatorios

Muchos juegos se basan en la aleatoriedad para implementar la mecánica central del juego. Esta página lo guía a través de los tipos comunes de aleatoriedad y cómo implementarlos en Godot.

Después de brindarle una breve descripción general de las funciones útiles que generan números aleatorios, aprenderá cómo obtener elementos aleatorios de matrices, diccionarios y cómo usar un generador de ruido en GDScript.

Nota

Las computadoras no pueden generar números aleatorios "verdaderos". En su lugar, se basan en generadores de números pseudoaleatorios (PRNGs).

Alcance global versus clase RandomNumberGenerator

Godot expone dos formas de generar números aleatorios: a través de métodos de global scope o usando la clase: ref: class_RandomNumberGenerator.

Los métodos de alcance global son más fáciles de configurar, pero no ofrecen tanto control.

RandomNumberGenerator requiere más código para usar, pero expone muchos métodos que no se encuentran en el alcance global, como randi_range() y randfn(). Además de eso, permite crear múltiples instancias, cada una con su propia semilla.

Este tutorial usa métodos de alcance global, excepto cuando el método sólo existe en la clase RandomNumberGenerator.

El método randomize()

En el ámbito global, puede encontrar un método randomize(). Este método debe llamarse sólo una vez cuando su proyecto comienza a inicializar la semilla aleatoria. Llamarlo varias veces es innecesario y puede afectar el rendimiento de manera negativa.

Ponerlo en el método _ready() de la secuencia de comandos de la escena principal es una buena opción:

func _ready():
    randomize()

También puede establecer una semilla aleatoria fija en su lugar usando seed(). Si lo hace, obtendrá resultados deterministicos en las ejecuciones:

func _ready():
    seed(12345)
    # To use a string as a seed, you can hash it to a number.
    seed("Hello world".hash())

Al usar la clase RandomNumberGenerator, debe llamar a randomize() en la instancia ya que tiene su propia semilla:

var random = RandomNumberGenerator.new()
random.randomize()

Obtener un número aleatorio

Veamos algunas de las funciones y métodos más utilizados para generar números aleatorios en Godot.

La función randi() <class_@GDScript_method_randi>`devuelve un número aleatorio entre 0 y 2^32-1. Dado que el valor máximo es enorme, lo más probable es que desee utilizar el operador de módulo (``%`) para delimitar el resultado entre 0 y el denominador:

# Prints a random integer between 0 and 49.
print(randi() % 50)

# Prints a random integer between 10 and 60.
print(randi() % 51 + 10)

randf() devuelve un número de punto flotante aleatorio entre 0 y 1. Esto es útil para implementar un sistema : ref: Probabilidad aleatoria ponderada, entre otras cosas.

randfn() devuelve un número de punto-flotante aleatorio siguiendo una distribución normal. Esto significa que es más probable que el valor devuelto esté alrededor de la media (0.0 por defecto), variando por la desviación (1.0 por defecto):

# Prints a random floating-point number from a normal distribution with a mean 0.0 and deviation 1.0.
var random = RandomNumberGenerator.new()
random.randomize()
print(random.randfn())

rand_range() toma dos argumentos desde y hasta, y devuelve un número de punto-flotante entre desde y hasta:

# Prints a random floating-point number between -4 and 6.5.
print(rand_range(-4, 6.5))

RandomNumberGenerator.randi_range() toma dos argumentos desde and hasta, y devuelve un entero aleatorio entre desde y hasta:

# Prints a random integer between -10 and 10.
var random = RandomNumberGenerator.new()
random.randomize()
print(random.randi_range(-10, 10))

Obtener un elemento aleatorio de un array

Podemos usar la generación de enteros aleatorios para obtener un elemento aleatorio de una array:

var _fruits = ["apple", "orange", "pear", "banana"]

func _ready():
    randomize()

    for i in range(100):
        # Pick 100 fruits randomly.
        print(get_fruit())


func get_fruit():
    var random_fruit = _fruits[randi() % _fruits.size()]
    # Returns "apple", "orange", "pear", or "banana" every time the code runs.
    # We may get the same fruit multiple times in a row.
    return random_fruit

Para evitar que se recoja la misma fruta más de una vez seguida, podemos agregar más lógica a este método:

var _fruits = ["apple", "orange", "pear", "banana"]
var _last_fruit = ""


func _ready():
    randomize()

    # Pick 100 fruits randomly.
    for i in range(100):
        print(get_fruit())


func get_fruit():
    var random_fruit = _fruits[randi() % _fruits.size()]
    while random_fruit == _last_fruit:
        # The last fruit was picked, try again until we get a different fruit.
        random_fruit = _fruits[randi() % _fruits.size()]

    # Note: if the random element to pick is passed by reference,
    # such as an array or dictionary,
    # use `_last_fruit = random_fruit.duplicate()` instead.
    _last_fruit = random_fruit

    # Returns "apple", "orange", "pear", or "banana" every time the code runs.
    # The function will never return the same fruit more than once in a row.
    return random_fruit

Este enfoque puede ser útil para hacer que la generación de números aleatorios se sienta menos repetitiva. Aún así, no evita que los resultados "hagan ping-pong" entre un conjunto limitado de valores. Para evitar esto, use el patrón shuffle bag en su lugar.

Obtener un valor de diccionario aleatorio

Podemos aplicar una lógica similar de arrays a diccionarios:

var metals = {
    "copper": {"quantity": 50, "price": 50},
    "silver": {"quantity": 20, "price": 150},
    "gold": {"quantity": 3, "price": 500},
}


func _ready():
    randomize()

    for i in range(20):
        print(get_metal())


func get_metal():
    var random_metal = metals.values()[randi() % metals.size()]
    # Returns a random metal value dictionary every time the code runs.
    # The same metal may be selected multiple times in succession.
    return random_metal

Probabilidad aleatoria ponderada

El método randf() devuelve un número de punto flotante entre 0.0 y 1.0. Podemos usar esto para crear una probabilidad "ponderada" donde diferentes resultados tienen diferentes probabilidades:

func _ready():
    randomize()

    for i in range(100):
        print(get_item_rarity())


func get_item_rarity():
    var random_float = randf()

    if random_float < 0.8:
        # 80% chance of being returned.
        return "Common"
    elif random_float < 0.95:
        # 15% chance of being returned.
        return "Uncommon"
    else:
        # 5% chance of being returned.
        return "Rare"

Una aleatoriedad "mejor" usando shuffle bags

Tomando el mismo ejemplo anterior, nos gustaría recoger frutas al azar. Sin embargo, confiar en la generación de números aleatorios cada vez que se selecciona una fruta puede llevar a una distribución menos uniforme. Si el jugador tiene suerte (o mala suerte), podría obtener la misma fruta tres o más veces seguidas.

Puede lograr esto usando el patrón shuffle bag. Funciona eliminando un elemento del array después de elegirlo. Después de varias selecciones, la matriz termina vacía. Cuando eso sucede, lo reinicializa a su valor predeterminado:

var _fruits = ["apple", "orange", "pear", "banana"]
# A copy of the fruits array so we can restore the original value into `fruits`.
var _fruits_full = []


func _ready():
    randomize()
    _fruits_full = _fruits.duplicate()
    _fruits.shuffle()

    for i in 100:
        print(get_fruit())


func get_fruit():
    if _fruits.empty():
        # Fill the fruits array again and shuffle it.
        _fruits = _fruits_full.duplicate()
        _fruits.shuffle()

    # Get a random fruit, since we shuffled the array,
    # and remove it from the `_fruits` array.
    var random_fruit = _fruits.pop_front()
    # Prints "apple", "orange", "pear", or "banana" every time the code runs.
    return random_fruit

Al ejecutar el código anterior, existe la posibilidad de obtener la misma fruta dos veces seguidas. Una vez que seleccionamos una fruta, ya no será un valor de retorno posible a menos que el array esté vacío. Cuando el array esté vacío, la restablecemos a su valor predeterminado, lo que hace posible tener la misma fruta nuevamente, pero sólo una vez.

Ruido aleatorio

La generación de números aleatorios que se muestra arriba puede mostrar sus límites cuando necesita un valor que lentamente cambia según el input. El input puede ser una posición, una hora o cualquier otra cosa.

Para lograr esto, puede utilizar funciones de ruido aleatorio. Las funciones de ruido son especialmente populares en la generación de procedimientos para generar un terreno de apariencia realista. Godot proporciona OpenSimplexNoise para esto, que admite ruido 1D, 2D, 3D y 4D. Aquí hay un ejemplo con ruido 1D:

var _noise = OpenSimplexNoise.new()

func _ready():
    randomize()
    # Configure the OpenSimplexNoise instance.
    _noise.seed = randi()
    _noise.octaves = 4
    _noise.period = 20.0
    _noise.persistence = 0.8

    for i in 100:
        # Prints a slowly-changing series of floating-point numbers
        # between -1.0 and 1.0.
        print(_noise.get_noise_1d(i))