Генерация случайных чисел
Во многих играх случайность используется для реализации основных игровых механик. Эта страница расскажет вам о распространённых типах случайности и о том, как их реализовать в Godot.
После краткого обзора полезных функций генерации случайных чисел вы узнаете, как получать случайные элементы из массивов и словарей, а также как использовать генератор шума в GDScript. Наконец, мы рассмотрим криптографически безопасную генерацию случайных чисел и её отличия от обычной генерации случайных чисел.
Примечание
Компьютеры не способны генерировать "true" случайные числа. Вместо этого они используют генераторы псевдослучайных чисел (PRNGs).
Godot использует внутри себя PCG Family генераторов псевдослучайных чисел.
Глобальная область применения по сравнению с классом RandomNumberGenerator
Godot предоставляет два способа генерации случайных чисел: через методы глобальной области видимости или используя класс RandomNumberGenerator.
Методы глобального охвата проще в настройке, но они не дают такого контроля.
RandomNumberGenerator требует больше кода для использования, но позволяет создавать несколько экземпляров, каждый со своим собственным начальным значением и состоянием.
В этом учебнике используются методы глобальной области видимости, за исключением случаев, когда метод существует только в классе RandomNumberGenerator.
Метод randomize()
Примечание
Начиная с Godot 4.0, начальное значение автоматически устанавливается случайным образом при запуске проекта. Это означает, что вам больше не нужно вызывать randomize() в _ready(), чтобы гарантировать случайность результатов при каждом запуске проекта. Однако вы по-прежнему можете использовать randomize(), если хотите использовать конкретное начальное значение, или сгенерировать его другим методом.
В глобальной области видимости можно найти метод randomize(). Этот метод следует вызывать только один раз, когда ваш проект начинает инициализировать случайное зерно. Вызов его несколько раз не нужен и может отрицательно сказаться на производительности.
Хорошим выбором будет поместить его в метод _ready() основного скрипта сцены:
func _ready():
randomize()
public override void _Ready()
{
GD.Randomize();
}
Вы также можете задать фиксированное начальное значение случайной выборки, используя seed(). Это даст детерминированные результаты во всех запусках:
func _ready():
seed(12345)
# To use a string as a seed, you can hash it to a number.
seed("Hello world".hash())
public override void _Ready()
{
GD.Seed(12345);
// To use a string as a seed, you can hash it to a number.
GD.Seed("Hello world".Hash());
}
При использовании класса RandomNumberGenerator необходимо вызвать randomize() для экземпляра, поскольку у него есть собственное начальное число:
var random = RandomNumberGenerator.new()
random.randomize()
var random = new RandomNumberGenerator();
random.Randomize();
Получение случайного числа
Давайте рассмотрим некоторые наиболее часто используемые функции и методы генерации случайных чисел в Godot.
Функция randi() возвращает случайное число в диапазоне от 0 до 2^32 - 1. Поскольку максимальное значение очень велико, вам, скорее всего, потребуется использовать оператор деления по модулю (%), чтобы ограничить результат нулём и знаменателем:
# Prints a random integer between 0 and 49.
print(randi() % 50)
# Prints a random integer between 10 and 60.
print(randi() % 51 + 10)
// Prints a random integer between 0 and 49.
GD.Print(GD.Randi() % 50);
// Prints a random integer between 10 and 60.
GD.Print(GD.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))
// Prints a random floating-point number from a normal distribution with a mean 0.0 and deviation 1.0.
GD.Print(GD.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))
// Prints a random floating-point number between -4 and 6.5.
GD.Print(GD.RandRange(-4.0, 6.5));
randi_range() принимает два аргумента from и to и возвращает случайное целое число между from и to:
# Prints a random integer between -10 and 10.
print(randi_range(-10, 10))
// Prints a random integer number between -10 and 10.
GD.Print(GD.RandRange(-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
// Use Godot's Array type instead of a BCL type so we can use `PickRandom()` on it.
private Godot.Collections.Array<string> _fruits = ["apple", "orange", "pear", "banana"];
public override void _Ready()
{
for (int i = 0; i < 100; i++)
{
// Pick 100 fruits randomly.
GD.Print(GetFruit());
}
for (int i = 0; i < 100; i++)
{
// Pick 100 fruits randomly, this time using the `Array.PickRandom()`
// helper method. This has the same behavior as `GetFruit()`.
GD.Print(_fruits.PickRandom());
}
}
public string GetFruit()
{
string randomFruit = _fruits[GD.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 randomFruit;
}
Чтобы предотвратить повторное использование одного и того же фрукта, можно добавить больше логики в описанный выше метод. В этом случае мы не можем использовать 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
private string[] _fruits = ["apple", "orange", "pear", "banana"];
private string _lastFruit = "";
public override void _Ready()
{
for (int i = 0; i < 100; i++)
{
// Pick 100 fruits randomly.
GD.Print(GetFruit());
}
}
public string GetFruit()
{
string randomFruit = _fruits[GD.Randi() % _fruits.Length];
while (randomFruit == _lastFruit)
{
// The last fruit was picked. Try again until we get a different fruit.
randomFruit = _fruits[GD.Randi() % _fruits.Length];
}
_lastFruit = randomFruit;
// 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 randomFruit;
}
Такой подход может быть полезен для того, чтобы сделать генерацию случайных чисел менее повторяющейся. Тем не менее, он не предотвращает колебания результатов между ограниченным набором значений. Чтобы избежать этого, используйте шаблон 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
private Godot.Collections.Dictionary<string, Godot.Collections.Dictionary<string, int>> _metals = new()
{
{"copper", new Godot.Collections.Dictionary<string, int>{{"quantity", 50}, {"price", 50}}},
{"silver", new Godot.Collections.Dictionary<string, int>{{"quantity", 20}, {"price", 150}}},
{"gold", new Godot.Collections.Dictionary<string, int>{{"quantity", 3}, {"price", 500}}},
};
public override void _Ready()
{
for (int i = 0; i < 20; i++)
{
GD.Print(GetMetal());
}
}
public Godot.Collections.Dictionary<string, int> GetMetal()
{
var (_, randomMetal) = _metals.ElementAt((int)(GD.Randi() % _metals.Count));
// Returns a random metal value dictionary every time the code runs.
// The same metal may be selected multiple times in succession.
return randomMetal;
}
Взвешенная случайная вероятность
Метод 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"
public override void _Ready()
{
for (int i = 0; i < 100; i++)
{
GD.Print(GetItemRarity());
}
}
public string GetItemRarity()
{
float randomFloat = GD.Randf();
if (randomFloat < 0.8f)
{
// 80% chance of being returned.
return "Common";
}
else if (randomFloat < 0.95f)
{
// 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)])
// Prints a random element using the weighted index that is returned by `RandWeighted()`.
// 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".
string[] fruits = ["apple", "orange", "pear", "banana"];
float[] probabilities = [0.5f, 1, 1, 2];
var random = new RandomNumberGenerator();
GD.Print(fruits[random.RandWeighted(probabilities)]);
"Лучшая" случайность при использовании мешочков для перемешивания
Возьмём тот же пример, что и выше: мы хотели бы выбирать фрукты случайным образом. Однако, полагаясь на генерацию случайных чисел каждый раз при выборе фрукта, можно добиться менее равномерного распределения. Если игроку повезёт (или не повезёт), он может получить один и тот же фрукт три или более раз подряд.
Это можно сделать с помощью шаблона shuffle bag. Он удаляет элемент из массива после его выбора. После нескольких выборов массив становится пустым. В этом случае вы заново инициализируете его значением по умолчанию:
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
private Godot.Collections.Array<string> _fruits = ["apple", "orange", "pear", "banana"];
// A copy of the fruits array so we can restore the original value into `fruits`.
private Godot.Collections.Array<string> _fruitsFull;
public override void _Ready()
{
_fruitsFull = _fruits.Duplicate();
_fruits.Shuffle();
for (int i = 0; i < 100; i++)
{
GD.Print(GetFruit());
}
}
public string GetFruit()
{
if(_fruits.Count == 0)
{
// Fill the fruits array again and shuffle it.
_fruits = _fruitsFull.Duplicate();
_fruits.Shuffle();
}
// Get a random fruit, since we shuffled the array,
string randomFruit = _fruits[0];
// and remove it from the `_fruits` array.
_fruits.RemoveAt(0);
// 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 randomFruit;
}
При выполнении приведённого выше кода, существует вероятность получить один и тот же фрукт дважды подряд. После того, как мы выбрали фрукт, он больше не будет возможным возвращаемым значением, если только массив не станет пустым. Когда массив опустеет, мы сбрасываем его к значению по умолчанию, что позволяет получить тот же фрукт снова, но только один раз.
Случайный шум
Генерация случайных чисел, показанная выше, может оказаться ограниченной, когда вам нужно значение, которое медленно меняется в зависимости от входных данных. Входными данными могут быть положение, время или что-то ещё.
Для этого можно использовать случайные функции шума. Функции шума особенно популярны в процедурной генерации для создания реалистичного ландшафта. Godot предоставляет для этого класс FastNoiseLite, который поддерживает одномерный, двумерный и трёхмерный шум. Вот пример с одномерным шумом:
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))
private FastNoiseLite _noise = new FastNoiseLite();
public override void _Ready()
{
// Configure the FastNoiseLite instance.
_noise.NoiseType = FastNoiseLite.NoiseTypeEnum.SimplexSmooth;
_noise.Seed = (int)GD.Randi();
_noise.FractalOctaves = 4;
_noise.Frequency = 1.0f / 20.0f;
for (int i = 0; i < 100; i++)
{
GD.Print(_noise.GetNoise1D(i));
}
}
Криптографически безопасная генерация псевдослучайных чисел
На данный момент упомянутые выше подходы не подходят для криптографически безопасной генерации псевдослучайных чисел (CSPRNG). Этого достаточно для игр, но недостаточно для сценариев, где требуется шифрование, аутентификация или подпись.
Godot предлагает для этого класс Crypto. Этот класс может выполнять асимметричное шифрование/дешифрование, подписывание/верификацию, а также генерировать криптографически безопасные случайные байты, ключи RSA, дайджесты HMAC и самоподписанные сертификаты X509Certificate.
Недостатком CSPRNG (криптографически безопасная генерация псевдослучайных чисел) является то, что он работает гораздо медленнее стандартной генерации псевдослучайных чисел. Его API также менее удобен в использовании. Поэтому CSPRNG следует избегать в игровых элементах.
Пример использования класса Crypto для генерации двух случайных целых чисел от 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)
См. также
См. документацию PackedByteArray для получения информации о других методах, которые можно использовать для декодирования сгенерированных байтов в различные типы данных, такие как целые числа или числа с плавающей точкой.