Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

随机数生成

许多游戏依靠随机性来实现核心游戏机制. 本页将指导你了解常见的随机性类型, 以及如何在Godot中实现它们.

After giving you a brief overview of useful functions that generate random numbers, you will learn how to get random elements from arrays, dictionaries, and how to use a noise generator in GDScript. Lastly, we'll take a look at cryptographically secure random number generation and how it differs from typical random number generation.

备注

计算机不能产生“真正的”随机数。相反,它们依赖伪随机数生成器(PRNG)。

Godot internally uses the PCG Family of pseudorandom number generators.

全局作用域 vs RandomNumberGenerator 类

Godot 提供了两种生成随机数的方式:通过全局作用域方法或使用 RandomNumberGenerator 类。

全局作用域方法更容易设置,但不能提供太多控制。

RandomNumberGenerator requires more code to use, but allows creating multiple instances, each with their own seed and state.

本教程使用全局作用域方法, 只存在于RandomNumberGenerator类中的方法除外.

randomize() 方法

备注

Since Godot 4.0, the random seed is automatically set to a random value when the project starts. This means you don't need to call randomize() in _ready() anymore to ensure that results are random across project runs. However, you can still use randomize() if you want to use a specific seed number, or generate it using a different method.

In global scope, you can find a randomize() method. This method should be called only once when your project starts to initialize the random seed. Calling it multiple times is unnecessary and may impact performance negatively.

把它放在你的主场景脚本的 _ready() 方法中是个不错的选择:

func _ready():
    randomize()

You can also set a fixed random seed instead using seed(). Doing so will give you deterministic results across runs:

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中最常用的一些生成随机数的函数和方法.

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)

randf() returns a random floating-point number between 0 and 1. This is useful to implement a 加权随机概率 system, among other things.

randfn() 返回遵循 正态分布 的随机浮点数。这意味着返回值更有可能在平均值附近(默认为 0.0),随偏差变化(默认为 1.0):

# Prints a random floating-point number from a normal distribution with a mean 0.0 and deviation 1.0.
var random = RandomNumberGenerator.new()
random.randomize()
print(random.randfn())

randf_range() takes two arguments from and to, and returns a random floating-point number between from and to:

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

RandomNumberGenerator.randi_range() 接受两个参数 fromto ,并返回一个介于 fromto 之间的随机整数:

# Prints a random integer between -10 and 10.
var random = RandomNumberGenerator.new()
random.randomize()
print(random.randi_range(-10, 10))

获取一个随机数组元素

我们可以使用随机整数生成来从数组中获得一个随机元素:

var _fruits = ["apple", "orange", "pear", "banana"]

func _ready():
    randomize()

    for i in range(100):
        # Pick 100 fruits randomly.
        print(get_fruit())


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

为了防止连续多次采摘相同的水果,我们可以给这个方法添加更多的逻辑:

var _fruits = ["apple", "orange", "pear", "banana"]
var _last_fruit = ""


func _ready():
    randomize()

    # 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():
    randomize()

    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

加权随机概率

The randf() method returns a floating-point number between 0.0 and 1.0. We can use this to create a "weighted" probability where different outcomes have different likelihoods:

func _ready():
    randomize()

    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"

使用 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():
    randomize()
    _fruits_full = _fruits.duplicate()
    _fruits.shuffle()

    for i in 100:
        print(get_fruit())


func get_fruit():
    if _fruits.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

在运行上面的代码时, 仍有可能连续两次得到同一个水果. 我们摘下一个水果时, 它将不再是一个可能的返回值, 但除非数组现在是空的. 当数组为空时, 此时我们将其重置回默认值, 这样就导致了能再次获得相同的水果, 但只有这一次.

随机噪音

当你需要一个 缓慢 根据输入而变化的值时, 上面显示的随机数生成方式就显示出了它们的局限性. 这里的输入可以是位置, 时间或其他任何东西.

To achieve this, you can use random noise functions. Noise functions are especially popular in procedural generation to generate realistic-looking terrain. Godot provides FastNoiseLite for this, which supports 1D, 2D and 3D noise. Here's an example with 1D noise:

var _noise = FastNoiseLite.new()

func _ready():
    randomize()
    # 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 摘要、自签名的 X509Certificate

The downside of CSPRNG is that it's much slower than standard pseudorandom number generation. Its API is also less convenient to use. As a result, CSPRNG should be avoided for gameplay elements.

Example of using the Crypto class to generate 2 random integers between 0 and 2^32 - 1 (inclusive):

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)

参见

See PackedByteArray's documentation for other methods you can use to decode the generated bytes into various types of data, such as integers or floats.