Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

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. Enfin, nous regarderons à la génération de nombres aléatoires cryptographiquement sécurisée et comment elle diffère de la génération de nombres aléatoires classique.

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).

Godot utilise de manière interne la famille PCG de générateurs de nombres aléatoires.

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 pour l'utiliser, mais permet de créer plusieurs instances, chacune avec leur propre graine et état.

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()

Note

Depuis Godot 4.0, la graine aléatoire est automatiquement définie à une valeur aléatoire lorsque le projet démarre. Cela signifie que vous n'avez plus besoin d'appeler randomize() dans _ready() afin de s'assurer que les résultats sont aléatoires avec chaque exécution de projet. Cependant, vous pouvez toujours utiliser randomize() si vous voulez utiliser une graine spécifique, ou la générer en utilisant une méthode différente.

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, afin d'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 système 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 renvoyé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.
print(randfn(0.0, 1.0))

rand_range() prend deux arguments from et to, et renvoie un nombre aléatoire flottant entre from et to :

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

randi_range() prend deux arguments from et to, et renvoie un nombre aléatoire flottant entre from et to :

# Prints a random integer between -10 and 10.
print(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, ou bien utiliser la méthode Array.pick_random afin qu'elle le fasse pour nous :

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

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

    for i in range(100):
        # Pick 100 fruits randomly, this time using the `Array.pick_random()`
        # helper method. This has the same behavior as `get_fruit()`.
        print(_fruits.pick_random())

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. Dans ce cas, nous ne pouvons utiliser Array.pick_random car elle manque un moyen d'empêcher les répétitions :

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


func _ready():
    # 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():
    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():
    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"

Vous pouvez également obtenir un index aléatoire pondéré en utilisant la méthode rand_weighted() sur une instance RandomNumberGenerator. Cela renvoie un entier aléatoire entre 0 et la taille du tableau qui est passé en paramètre. Chaque valeur dans le tableau est un flottant qui représente la probabilité relative qu'il sera retourné comme un index. Une valeur plus élevée signifie que la valeur est plus susceptible d'être retournée comme un index, alors qu'une valeur de 0 signifie qu'elle ne sera jamais retournée comme un index.

Par exemple, si [0.5, 1, 1, 2] est passé comme paramètre, la méthode est deux fois plus susceptible de renvoyer 3. (l'index de la valeur 2) et deux fois moins probable de renvoyer 0 (l'index de la valeur 0.5) par rapport aux indexes 1 et 2.

Comme la valeur renvoyée correspond à la taille du tableau, il peut être utilisé comme index pour obtenir une valeur d'un autre tableau comme suit :

# Prints a random element using the weighted index that is returned by `rand_weighted()`.
# Here, "apple" will be returned twice as rarely as "orange" and "pear".
# "banana" is twice as common as "orange" and "pear", and four times as common as "apple".
var fruits = ["apple", "orange", "pear", "banana"]
var probabilities = [0.5, 1, 1, 2];

var random = RandomNumberGenerator.new()
print(fruits[random.rand_weighted(probabilities)])

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():
    _fruits_full = _fruits.duplicate()
    _fruits.shuffle()

    for i in 100:
        print(get_fruit())


func get_fruit():
    if _fruits.is_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()
    # Returns "apple", "orange", "pear", or "banana" every time the code runs, removing it from the array.
    # When all fruit are removed, it refills the array.
    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 de bruit 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 FastNoiseLite pour cela, qui supporte le bruit 1D, 2D, 3D, et 4D. Voici un exemple avec un bruit 1D :

var _noise = FastNoiseLite.new()

func _ready():
    # Configure the FastNoiseLite instance.
    _noise.noise_type = FastNoiseLite.NoiseType.TYPE_SIMPLEX_SMOOTH
    _noise.seed = randi()
    _noise.fractal_octaves = 4
    _noise.frequency = 1.0 / 20.0

    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))

Génération de nombres pseudo-aléatoires sécurisée cryptographiquement

Jusqu'à présent, les approches mentionnées ci-dessus ne sont pas adaptées à la génération de nombres pseudo-aléatoires cryptographiquement sécurisés. C'est bien pour les jeux, mais cela ne suffit pas pour les scénarios où le chiffrement, l'authentification ou la signature sont impliqués.

Godot propose une classe Crypto pour cela. Cette classe peut effectuer le cryptage/décryptage de clé asymétrique, la signature/vérification, tout en générant des octets aléatoires cryptographiquement sécurisés, des clés RSA, des digestes HMAC et des X509Certificate autosignés.

Le désavantage de la génération de nombres pseudo-aléatoires cryptographiquement sécurisée est qu'elle est beaucoup plus lente que la génération de nombres pseudo-aléatoires standard. Son API est également moins pratique à utiliser. Par conséquent, la CSPNRG devrait être évitée pour les éléments de gameplay.

Exemple d'utilisation de la classe Crypto pour générer 2 entiers aléatoires entre 0 et 2^32 - 1 (inclusif) :

var crypto := Crypto.new()
# Request as many bytes as you need, but try to minimize the amount
# of separate requests to improve performance.
# Each 32-bit integer requires 4 bytes, so we request 8 bytes.
var byte_array := crypto.generate_random_bytes(8)

# Use the ``decode_u32()`` method from PackedByteArray to decode a 32-bit unsigned integer
# from the beginning of `byte_array`. This method doesn't modify `byte_array`.
var random_int_1 := byte_array.decode_u32(0)
# Do the same as above, but with an offset of 4 bytes since we've already decoded
# the first 4 bytes previously.
var random_int_2 := byte_array.decode_u32(4)

prints("Random integers:", random_int_1, random_int_2)

Voir aussi

Voir la documentation de PackedByteArray pour d'autres méthodes que vous pouvez utiliser pour décoder les octets générés en divers types de données, comme des entiers ou des flottants.