Génération de nombres aléatoires

De nombreux jeux s'appuient sur le hasard pour mettre en œuvre certaines de leurs mécaniques fondamentales. Cette page va vous guider à travers les types courants d'aléatoire et comment les implémenter dans Godot.

Après vous avoir donné un bref aperçu des fonctions utiles qui génèrent des nombres aléatoires, vous apprendrez à obtenir des éléments aléatoires à partir de tableaux, dictionnaires, et comment utiliser un générateur de bruit dans GDScript.

Note

Les ordinateurs ne peuvent pas générer de "vrais" nombres aléatoires. Ils s'appuient plutôt sur des générateurs de nombres pseudo-aléatoires (PRNG).

Portée globale par rapport à la classe RandomNumberGenerator

Godot possède deux façons de générer des nombres aléatoires : via des méthodes portée globale (disponible à tout endroit du code) ou en utilisant la classe RandomNumberGenerator.

Les méthodes de portée globale sont plus faciles à mettre en place, mais elles n'offrent pas autant de contrôle.

RandomNumberGenerator nécessite plus de code à utiliser, mais expose de nombreuses méthodes qui ne se trouvent pas dans la portée globale telles que randi_range() et randfn(). En plus de cela, il permet de créer plusieurs instances chacune avec leur propre graine de génération d'aléatoire (random seed).

Ce tutoriel utilise des méthodes de portée globale, sauf lorsque la méthode n'existe que dans la classe RandomNumberGenerator.

La méthode randomize()

Dans la portée globale, vous pouvez trouver une méthode randomize(). Cette méthode ne doit être appelée qu'une seule fois lorsque votre projet commence à initialiser la graine aléatoire. L'appeler plusieurs fois est inutile et peut avoir un impact négatif sur les performances.

Le placer dans la méthode ready() de votre script de scène principal est un bon choix :

func _ready():
    randomize()

Vous pouvez également définir une graine aléatoire fixe en utilisant seed(). Vous obtiendrez ainsi des résultats déterministes d'une exécution à l'autre : :

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

Lorsque vous utilisez la classe RandomNumberGenerator, vous devez appeler randomize() sur l'instance car elle a sa propre graine(seed) :

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

Obtenir un nombre aléatoire

Examinons certaines des fonctions et méthodes les plus couramment utilisées pour générer des nombres aléatoires dans Godot.

La fonction randi() renvoie un nombre aléatoire entre 0 et 2^32-1. Étant donné que la valeur maximale est énorme, vous souhaitez très probablement utiliser l’opérateur modulo (%) pour lier le résultat entre 0 et le dénominateur :

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

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

randf() renvoie un nombre à virgule flottante aléatoire entre 0 et 1. Ceci est utile pour implémenter un Probabilité aléatoire pondérée, entre autres choses.

randfn() renvoie un nombre flottant aléatoire selon une distribution normale. Cela signifie que la valeur retournée est plus susceptible d’être autour de la moyenne (0,0 par défaut), variant selon l’écart (1,0 par défaut) :

# 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() prend deux arguments from et to, et retourne un nombre aléatoire flottant entre from et to :

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

RandomNumberGenerator.randi_range() prend deux arguments from et to, et retourne un entier aléatoire entre from et to :

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

Obtenir un élément aléatoire du tableau

Nous pouvons utiliser la génération d'entiers aléatoires pour obtenir un élément aléatoire à partir d’un tableau :

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

Pour éviter que le même fruit ne soit cueilli plusieurs fois de suite, nous pouvons ajouter plus de logique à cette méthode :

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

Cette approche peut être utile pour rendre la génération de nombres aléatoires moins répétitive. Néanmoins, elle n'empêche pas les résultats de "ping-pong" entre un ensemble limité de valeurs. Pour éviter cela, utilisez plutôt le design pattern (patron de conception) du sac mélangé.

Obtenir une valeur aléatoire d'un dictionnaire

Nous pouvons également appliquer aux dictionnaires une logique similaire à celle des tableaux :

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

Probabilité aléatoire pondérée

La méthode randf() renvoie un nombre flottant compris entre 0.0 et 1.0. Nous pouvons l'utiliser pour créer une probabilité "pondérée" où différents résultats ont des probabilités différentes :

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"

Un "meilleur" aléatoire en utilisant le sac mélangé (shuffle bags)

En prenant le même exemple que ci-dessus, nous aimerions choisir des fruits au hasard. Cependant, le fait de s'appuyer sur la génération de nombres aléatoires à chaque fois qu'un fruit est sélectionné peut conduire à une distribution moins uniforme. Si le joueur est chanceux (ou malchanceux), il peut obtenir le même fruit trois fois de suite ou plus.

Vous pouvez y parvenir en utilisant le design pattern shuffle bag. Il fonctionne en retirant un élément du tableau après l'avoir choisi. Après plusieurs sélections, le tableau finit par être vide. Lorsque cela se produit, vous le réinitialisez à sa valeur par défaut : :

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

En exécutant le code ci-dessus, il y a une chance d'obtenir le même fruit deux fois de suite. Une fois que nous avons cueilli un fruit, il ne sera plus une valeur de retour possible à moins que le tableau soit maintenant vide. Lorsque le tableau est vide, nous le réinitialisons à sa valeur par défaut, ce qui permet d'obtenir à nouveau le même fruit, mais une seule fois.

Bruit aléatoire

La génération de nombres aléatoires présentée ci-dessus peut montrer ses limites lorsque vous avez besoin d'une valeur qui change lentement en fonction de l'entrée. L'entrée peut être une position, un temps, ou n'importe quoi d'autre.

Pour y parvenir, vous pouvez utiliser des fonctions noise aléatoires. Les fonctions de bruit sont particulièrement populaires dans la génération procédurale pour générer un terrain d'apparence réaliste. Godot fournit OpenSimplexNoise pour cela, qui supporte le bruit 1D, 2D, 3D, et 4D. Voici un exemple avec un bruit 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))