Up to date
This page is up to date for Godot 4.3.
If you still find outdated information, please open an issue.
随机数生成
许多游戏依靠随机性来实现核心游戏机制. 本页将指导你了解常见的随机性类型, 以及如何在Godot中实现它们.
在简要概述生成随机数的实用函数之后,你将学习如何从数组、字典中获取随机元素,以及如何在 GDScript 中使用噪点生成器。最后,我们将了解加密安全的随机数生成以及它与典型随机数生成的区别。
备注
计算机不能产生“真正的”随机数。相反,它们依赖伪随机数生成器(PRNG)。
Godot内部使用`PCG Family <https://www.pcg-random.org/>`__ of 伪随机数生成器。
全局作用域 vs RandomNumberGenerator 类
Godot 提供了两种生成随机数的方式:通过全局作用域方法或使用 RandomNumberGenerator 类。
全局作用域方法更容易设置,但不能提供太多控制。
RandomNumberGenerator需要使用更多代码,但允许建立多个实例,每个实例都有自己的种子和状态。
本教程使用全局作用域方法, 只存在于RandomNumberGenerator类中的方法除外.
randomize() 方法
备注
自Godot 4.0以来,当项目启动时,随机种子会自动设置为随机值。这意味着你不再需要在 _ready() 中调用 randomize() 来确保项目运行的结果是随机的。但是,如果你想使用特定的种子编号,或者使用不同的方法生成它,你仍然可以使用 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中最常用的一些生成随机数的函数和方法.
The function randi() returns a random
number between 0 and 2^32 - 1. Since the maximum value is huge, you most
likely want to use the modulo operator (%) to bound the result between 0 and
the denominator:
# 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() returns a random floating-point number following a normal distribution. This means the returned value is more likely to be around the mean (0.0 by default), varying by the deviation (1.0 by default):
# Prints a random floating-point number from a normal distribution with a mean 0.0 and deviation 1.0.
print(randfn())
// Prints a random floating-point number from a normal distribution with a mean of 0.0 and deviation of 1.0.
GD.Print(GD.Randfn());
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.RandfRange(-4, 6.5));
randi_range() takes two arguments from
and to, and returns a random integer between from and to:
# Prints a random integer between -10 and 10.
print(randi_range(-10, 10))
// Prints a random integer between -10 and 10.
GD.Print(GD.RandiRange(-10, 10));
获取一个随机数组元素
We can use random integer generation to get a random element from an array, or use the Array.pick_random method to do it for us:
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 = new Godot.Collections.Array<string> { "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;
}
To prevent the same fruit from being picked more than once in a row, we can add more logic to the above method. In this case, we can't use Array.pick_random since it lacks a way to prevent repetition:
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
加权随机概率
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";
}
}
You can also get a weighted random index using the
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.
For example, if [0.5, 1, 1, 2] is passed as a parameter, then the method is twice
as likely to return 3 (the index of the value 2) and twice as unlikely to return
0 (the index of the value 0.5) compared to the indices 1 and 2.
Since the returned value matches the array's size, it can be used as an index to get a value from another array as follows:
# 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.5, 1, 1, 2 };
var random = new RandomNumberGenerator();
GD.Print(fruits[random.RandWeighted(probabilities)]);
使用 shuffle bag 达到“更好”随机性
以上面同样的例子为例, 我们希望随机挑选水果. 然而, 每次选择水果时依靠随机数生成会导致分布不那么 均匀 . 如果玩家足够幸运(或不幸), 他们可能会连续三次或更多次得到相同的水果.
你可以使用 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()
# Prints "apple", "orange", "pear", or "banana" every time the code runs.
return random_fruit
在运行上面的代码时, 仍有可能连续两次得到同一个水果. 我们摘下一个水果时, 它将不再是一个可能的返回值, 但除非数组现在是空的. 当数组为空时, 此时我们将其重置回默认值, 这样就导致了能再次获得相同的水果, 但只有这一次.
随机噪音
当你需要一个 缓慢 根据输入而变化的值时, 上面显示的随机数生成方式就显示出了它们的局限性. 这里的输入可以是位置, 时间或其他任何东西.
为了实现这一点,你可以使用随机噪声函数。噪声函数在程序式生成中特别受欢迎,可用于生成逼真的地形。Godot 为此提供了 FastNoiseLite,它支持 1D、2D 和 3D 噪声。以下是 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))
private FastNoiseLite _noise = new FastNoiseLite();
public override void _Ready()
{
// Configure the FastNoiseLite instance.
_noise.NoiseType = 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(含)之间的 2 个随机整数的示例:
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 的文档,了解可用于将生成的字节解码为各种类型的数据(如整数或浮点)的其他方法。