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.
Checking the stable version of the documentation...
随机数生成
许多游戏依靠随机性来实现核心游戏机制。本页将指导你了解常见的随机性类型,以及如何在 Godot 中实现它们。
在简要概述生成随机数的实用函数之后,你将学习如何从数组、字典中获取随机元素,以及如何在 GDScript 中使用噪声生成器。最后,我们将了解加密安全的随机数生成以及它与典型随机数生成的区别。
全局作用域 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 中最常用的一些生成随机数的函数和方法。
函数 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)
// 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";
}
}
你也可以使用 RandomNumberGenerator 实例的 rand_weighted() 方法获取一个加权的随机索引。该方法会返回一个介于 0 和作为参数传递的数组大小之间的随机整数。数组中的每个值都是一个浮点数,表示其被返回为索引的相对概率。值越高,该值被返回为索引的可能性就越大,而值为 0 则表示它永远不会被返回为索引。
例如,如果将 [0.5, 1, 1, 2] 作为参数传递,那么该方法返回 3(值为 2 的索引)的可能性是返回 1 和 2 的两倍,而返回 0(值为 0.5 的索引)的可能性则是一半。
由于返回值与数组的大小匹配,因此可以将其用作索引来从另一个数组中获取值,如下所示:
# 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 提供“更好”的随机性
以上面同样的例子为例,我们希望随机挑选水果。然而,每次选择水果时依靠随机数生成会导致分布不那么均匀。如果玩家足够幸运(或不幸),他们可能会连续三次或更多次得到相同的水果。
你可以使用 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,它支持 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 = 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(含)之间的 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 的文档,了解可用于将生成的字节解码为各种类型的数据(如整数或浮点)的其他方法。