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.

Генерація випадкових чисел

Багато ігор покладаються на випадковість для реалізації основної механіки гри. Ця сторінка допоможе вам ознайомитися з поширеними типами випадковості та тим, як їх реалізувати в Godot.

Давши вам короткий огляд корисних функцій, які генерують випадкові числа, ви дізнаєтеся, як отримувати випадкові елементи з масивів, словників і як використовувати генератор шуму в GDScript. Нарешті, ми розглянемо криптографічно безпечну генерацію випадкових чисел і чим вона відрізняється від типової генерації випадкових чисел.

Примітка

Комп'ютери не можуть генерувати "справжні" випадкові числа. Замість цього вони покладаються на генератори псевдовипадкових чисел (PRNG-и).

Godot внутрішньо використовує Сімейство PCG генераторів псевдовипадкових чисел.

Глобальна область та клас RandomNumberGenerator

Годо пропонує два способи генерації випадкових чисел: за допомогою методів глобальної області видимості або за допомогою класу class_RandomNumberGenerator.

Глобальні методи легше налаштувати, але вони не пропонують багато контролю.

RandomNumberGenerator вимагає більше коду для використання, але дозволяє створювати кілька екземплярів, кожен із власним початковим значенням і станом.

Ця стаття використовує методи глобальної області, за винятком випадків, коли метод існує лише в класі RandomNumberGenerator.

Метод randomize()

Примітка

Починаючи з Godot 4.0, випадкове початкове значення автоматично встановлюється на випадкове значення під час запуску проекту. Це означає, що вам більше не потрібно викликати randomize() у _ready(), щоб переконатися, що результати будуть випадковими для виконання проектів. Однак ви все ще можете використовувати randomize(), якщо хочете використати певний початковий номер, або створити його за допомогою іншого методу.

У глобальній області ви можете знайти метод randomize(). Цей метод слід викликати лише один раз, коли ваш проект починає ініціалізувати випадкове початкове число. Викликати його кілька разів не потрібно, і це може негативно вплинути на продуктивність.

Найкраще буде розмістити його в скрипті головної сцени в методі _ready():

func _ready():
    randomize()

Ви також можете встановити фіксоване випадкове початкове число за допомогою seed(). Це дасть вам детерміновані результати для прогонів:

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

Використовуючи клас RandomNumberGenerator, ви повинні викликати randomize() на екземплярі, оскільки він має власне початкове значення:

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

Отримання випадкового числа

Давайте розглянемо деякі з найбільш часто використовуваних функцій і методів для генерування випадкових чисел у Godot.

Функція randi() повертає випадкове число від 0 до 2^32 - 1. Оскільки максимальне значення величезне, ви, швидше за все, захочете використати оператор модуля (%), щоб зв’язати результат між 0 і знаменником:

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

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

randf() повертає випадкове число з плаваючою комою від 0 до 1. Це корисно, серед іншого, для реалізації системи Зважена випадкова ймовірність.

randfn() повертає випадкове число з плаваючою комою після нормального розподілу. Це означає, що повернене значення, швидше за все, буде приблизно середнім (0,0 за замовчуванням), змінюючись на відхилення (1,0 за замовчуванням):

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

randf_range() приймає два аргументи from і to і повертає випадкове число з плаваючою комою між from і to:

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

randi_range() приймає два аргументи від і до і повертає випадкове ціле число між від і до:

# Prints a random integer between -10 and 10.
print(randi_range(-10, 10))

Отримання випадкового елемента масиву

Ми можемо використати генерацію випадкових цілих чисел, щоб отримати випадковий елемент із масиву, або використати метод Array.pick_random, щоб зробити це за нас:

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

Щоб запобігти тому, щоб один і той самий фрукт збирали більше одного разу поспіль, ми можемо додати більше логіки до наведеного вище методу. У цьому випадку ми не можемо використовувати Array.pick_random, оскільки в ньому немає способу запобігти повторенню:

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

Цей підхід може допомогти зробити генерацію випадкових чисел менш повторюваною. Тим не менш, це не перешкоджає результатам з "пінг-понгом" між обмеженим набором значень. Щоб запобігти цьому, використовуйте натомість шаблон shuffle bag.

Отримання випадкового значення словника

Ми також можемо застосувати подібну логіку від масивів до словників:

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

Зважена випадкова ймовірність

Метод randf() повертає число з плаваючою комою від 0,0 до 1,0. Ми можемо використовувати це для створення «зваженої» ймовірності, де різні результати мають різну ймовірність:

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"

Ви також можете отримати зважений випадковий індекс за допомогою методу rand_weighted() на екземплярі RandomNumberGenerator. Він повертає випадкове ціле число від 0 до розміру масиву, який передається як параметр. Кожне значення в масиві є числом з плаваючою комою, яке представляє відносну ймовірність того, що воно буде повернуто як індекс. Вище значення означає, що значення з більшою ймовірністю буде повернуто як індекс, тоді як значення 0 означає, що воно ніколи не буде повернуто як індекс.

Наприклад, якщо [0.5, 1, 1, 2 передається як параметр, тоді метод має вдвічі більшу ймовірність повернути 3 (індекс значення 2) і удвічі меншу ймовірність повернення 0 (індекс значення 0.5) порівняно з індексами 1 і 2.

Оскільки повернуте значення відповідає розміру масиву, його можна використовувати як індекс для отримання значення з іншого масиву наступним чином:

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

"Краща" випадковість за допомогою перемішування сумки

Візьмемо наведений вище приклад, ми хочемо вибирати фрукти навмання. Однак, при генерації випадкових чисел щоразу, коли вибирається фрукт, можна отримати менш рівномірний розподіл. Якщо гравцеві пощастить (або не пощастить), він може отримати один і той же фрукт три, або більше, рази поспіль.

Ви можете зробити це за допомогою візерунка перетасувати пакет. Він працює шляхом видалення елемента з масиву після його вибору. Після кількох виборів масив залишається порожнім. Коли це станеться, ви повторно ініціалізуєте його до значення за замовчуванням:

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

Під час виконання наведеного вище коду є шанс отримати один і той же фрукт двічі поспіль. Після того, як ми виберемо фрукти, масив стане порожнім і не зможе нічого повертати. Коли масив порожній, ми вертаємо його до значення за замовчуванням, завдяки чому можна знову отримати той самий фрукт, але лише один раз.

Випадковий шум

Генерація випадкових чисел, показана вище, може показати свої межі, коли вам потрібно значення, яке повільно змінюється залежно від введення. Введенням може бути позиція, час, або будь-що інше.

Щоб досягти цього, ви можете використовувати функції випадкового шуму. Функції шуму особливо популярні в процедурній генерації для створення реалістичного ландшафту. Для цього Godot надає FastNoiseLite, який підтримує 1D, 2D і 3D шум. Ось приклад із одновимірним шумом:

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

Криптографічно захищена генерація псевдовипадкових чисел

Наразі згадані вище підходи не придатні для криптографічно безпечної генерації псевдовипадкових чисел (CSPRNG). Це добре для ігор, але цього недостатньо для сценаріїв, де використовуються шифрування, автентифікація або підпис.

Godot пропонує для цього клас Crypto. Цей клас може виконувати асиметричне шифрування/дешифрування, підписування/перевірку, а також генерувати криптографічно безпечні випадкові байти, ключі RSA, дайджести HMAC і самопідписані class_X509Certificates.

Недоліком CSPRNG є те, що вона набагато повільніша за стандартну генерацію псевдовипадкових чисел. Його API також менш зручний у використанні. Як результат, слід уникати CSPRNG для елементів ігрового процесу.

Приклад використання класу Crypto для створення 2 випадкових цілих чисел від 0 до 2^32 - 1 (включно):

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)

Дивись також

Перегляньте документацію class_PackedByteArray щодо інших методів, які можна використовувати для декодування згенерованих байтів у різні типи даних, такі як цілі чи числа з плаваючою точкою.