随机数生成

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

在简要概述生成随机数的实用函数之后, 你将学习如何从数组或字典中获取随机元素, 以及如何在GDScript中使用噪声生成器.

备注

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

全局作用域 vs RandomNumberGenerator 类

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

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

RandomNumberGenerator则需要写更多代码, 但提供许多在全局作用域内找不到的方法, 如 randi_range()randfn() . 除此之外, 它还允许创建多个实例, 每个实例都有自己的种子.

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

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.
var random = RandomNumberGenerator.new()
random.randomize()
print(random.randfn())

rand_range() 接受两个参数 fromto ,并返回一个介于 fromto 之间的随机浮点数:

# Prints a random floating-point number between -4 and 6.5.
print(rand_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

加权随机概率

randf() 方法返回一个介于 0.0 和 1.0 之间的浮点数。我们可以使用它来创建“加权”概率,其中不同的结果具有不同的可能性:

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

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

随机噪音

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

为此,您可以使用随机噪声函数。噪声函数在程序式生成中特别受欢迎,可以生成逼真的地形。 Godot 为此提供了 OpenSimplexNoise,它支持 1D、2D、3D 和 4D 噪声。这是一个 1D 噪声的例子:

var _noise = OpenSimplexNoise.new()

func _ready():
    randomize()
    # Configure the OpenSimplexNoise instance.
    _noise.seed = randi()
    _noise.octaves = 4
    _noise.period = 20.0
    _noise.persistence = 0.8

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