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 requires more code to use, but allows creating multiple instances, each with their own seed and state. This is useful in certain scenarios like networked multiplayer, replay systems, games that feature rewind mechanics, and more.

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

Random seed and internal state

By default, Godot uses a random seed set according to the device's local time. This means that results will be different on every run. To get deterministic results, you can set a fixed seed using the seed() method. The seed is an integer that initializes the random number generator's state. If you use the same seed, you'll get the same sequence of random numbers every run.

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

When using the RandomNumberGenerator class, you can set the RandomNumberGenerator.seed property on individual instances:

var random = RandomNumberGenerator.new()
random.seed = 12345

You can set the seed back to a randomly generated value using the randomize() global scope method. This is also available as the RandomNumberGenerator.randomize() method on RandomNumberGenerator instances.

Random number generators also feature an internal state that changes every time a random number is generated. This state is used to generate the next random number in the sequence.

Unlike global scope random number generation, RandomNumberGenerator features a state property. This is useful if you are performing multiple random number operations, and you want to return to a previous state of the random number generator without changing the seed. To do so, store the current state in a variable, and then set the state property back to that variable when needed:

var random = RandomNumberGenerator.new()
# Changing the seed will reset the state, so make sure to set the seed first.
random.seed = 12345
var previous_random_state = random.state
# Each call to a random function on this instance alters its state.
print(random.randi())

random.state = previous_random_state
# This will return the same value as the previous call,
# even though we didn't change the seed.
print(random.randi())

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"

You can also get a weighted random index using the RandomNumberGenerator.rand_weighted() method on a RandomNumberGenerator instance. This returns a random integer between 0 and the size of the array that is passed as a parameter. Each value in the array is a floating-point number that represents the relative likelihood that it will be returned as an index. A higher value means the value is more likely to be returned as an index, while a value of 0 means it will never be returned as an 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.