随机数生成

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

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

注解

计算机不能产生”真正的”随机数.相反,它们依赖 `伪随机数生成器<https://en.wikipedia.org/wiki/Pseudorandom_number_generator>`__ (PRNGs).

全局作用域 vs 随机数生成器 (RandomNumberGenerator)类

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

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

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

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

randomize() 方法

在全局作用域内,你可以找到一个 randomize() 方法.**这个方法只需要在你的项目开始初始化随机种子的时候调用一次**, 多次调用是多余的,并且有可能影响性能.

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

  1. func _ready():
  2. randomize()

你也可以使用 :ref:`seed() <class_@GDScript_method_seed>`来设置一个固定的随机种子.这样做将使你在不同的运行中得到 确定 的结果:

  1. func _ready():
  2. seed(12345)
  3. # To use a string as a seed, you can hash it to a number.
  4. seed("Hello world".hash())

当使用RandomNumberGenerator类时,你应该在实例上调用``randomize()``,因为它有它自己的种子:

  1. var rng = RandomNumberGenerator.new()
  2. rng.randomize()

获得一个随机数

让我们来看看Godot中最常用的一些生成随机数的函数和方法.

函数 randi() 返回一个在0到2^32-1之间的随机数.由于最大值很大,你会想使用模数运算符(%)将结果限制在0到分母之间:

  1. # Prints a random integer between 0 and 49.
  2. print(randi() % 50)
  3. # Prints a random integer between 10 and 60.
  4. print(randi() % 51 + 10)

randf() 返回一个0到1之间的随机浮点数. 在实现 加权随机概率 系统等时非常有用.

:ref:`randfn() <class_RandomNumberGenerator_method_randfn>`按照 `正态分布(维基百科)<https://zh.wikipedia.org/wiki/正态分布>`__ 返回随机浮点数.这意味着返回值更有可能在平均值附近(默认为0.0),随偏差产生变化(默认为1.0):

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

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

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

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

  1. # Prints a random floating-point number between -10 and 10.
  2. var rng = RandomNumberGenerator.new()
  3. rng.randomize()
  4. print(rng.randi_range(-10, 10))

获取一个随机数组元素

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

  1. var fruits = ["apple", "orange", "pear", "banana"]
  2. func _ready():
  3. randomize()
  4. for i in 100:
  5. # Pick 100 fruits randomly.
  6. # (``for i in 100`` is a faster shorthand for ``for i in range(100)``.)
  7. print(get_fruit())
  8. func get_fruit():
  9. var random_fruit = fruits[randi() % fruits.size()]
  10. # Returns "apple", "orange", "pear", or "banana" every time the code runs.
  11. # We may get the same fruit multiple times in a row.
  12. return random_fruit

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

  1. var fruits = ["apple", "orange", "pear", "banana"]
  2. var last_fruit = ""
  3. func _ready():
  4. randomize()
  5. # Pick 100 fruits randomly.
  6. # Note: ``for i in 100`` is a shorthand for ``for i in range(100)``.
  7. for i in 100:
  8. print(get_fruit())
  9. func get_fruit():
  10. var random_fruit = fruits[randi() % fruits.size()]
  11. while random_fruit == last_fruit:
  12. # The last fruit was picked, try again until we get a different fruit.
  13. random_fruit = fruits[randi() % fruits.size()]
  14. # Note: if the random element to pick is passed by reference,
  15. # such as an array or dictionary,
  16. # use `last_fruit = random_fruit.duplicate()` instead.
  17. last_fruit = random_fruit
  18. # Returns "apple", "orange", "pear", or "banana" every time the code runs.
  19. # The function will never return the same fruit more than once in a row.
  20. return random_fruit

这种方法可以让随机数生成的感觉不那么重复.不过,它仍然不能防止结果在有限的一组值之间”乒乓反复”.为了防止这种情况,请使用 shuffle bag 模式来代替.

获取一个随机字典值

我们也可以将类似的逻辑从数组应用到字典上:

  1. var metals = {
  2. "copper": {"quantity": 50, "price": 50},
  3. "silver": {"quantity": 20, "price": 150},
  4. "gold": {"quantity": 3, "price": 500},
  5. }
  6. func _ready():
  7. randomize()
  8. for i in 20:
  9. print(get_metal())
  10. func get_metal():
  11. var random_metal = metals.values()[randi() % metals.size()]
  12. # Returns a random metal value dictionary every time the code runs.
  13. # The same metal may be selected multiple times in succession.
  14. return random_metal

加权随机概率

:ref:`randf() <class_@GDScript_method_randf>`方法返回一个介于0.0和1.0之间的浮点数.我们可以用它来创建一个”加权”的概率,不同的结果拥有不同的可能性:

  1. func _ready():
  2. randomize()
  3. for i in 100:
  4. print(get_item_rarity())
  5. func get_item_rarity():
  6. var random_float = randf()
  7. if random_float < 0.8:
  8. # 80% chance of being returned.
  9. return "Common"
  10. elif random_float < 0.95:
  11. # 15% chance of being returned.
  12. return "Uncommon"
  13. else:
  14. # 5% chance of being returned.
  15. return "Rare"

使用shuffle bag的”更好”随机性

以上面同样的例子为例,我们希望随机挑选水果.然而,每次选择水果时依靠随机数生成会导致分布不那么 均匀 .如果玩家足够幸运(或不幸),他们可能会连续三次或更多次得到相同的水果.

你可以使用 shuffle bag 模式来实现.它的工作原理是在选择数组后从数组中删除一个元素.多次选择之后,数组会被清空.当这种情况发生时,就将数组重新初始化为默认值:

  1. var fruits = ["apple", "orange", "pear", "banana"]
  2. # A copy of the fruits array so we can restore the original value into `fruits`.
  3. var fruits_full = []
  4. func _ready():
  5. randomize()
  6. fruits_full = fruits.duplicate()
  7. fruits.shuffle()
  8. for i in 100:
  9. print(get_fruit())
  10. func get_fruit():
  11. if fruits.empty():
  12. # Fill the fruits array again and shuffle it.
  13. fruits = fruits_full.duplicate()
  14. fruits.shuffle()
  15. # Get a random fruit, since we shuffled the array,
  16. # and remove it from the `fruits` array.
  17. var random_fruit = fruits.pop_front()
  18. # Prints "apple", "orange", "pear", or "banana" every time the code runs.
  19. return random_fruit

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

随机噪音

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

为此,你可以使用随机的 噪声 函数.噪声函数在程序生成中特别流行,用来生成视觉上逼真的地形. Godot为此提供了 OpenSimplexNoise ,它支持1D,2D,3D和4D噪声.以下是一个1D噪声的示例:

  1. var noise = OpenSimplexNoise.new()
  2. func _ready():
  3. randomize()
  4. # Configure the OpenSimplexNoise instance.
  5. noise.seed = randi()
  6. noise.octaves = 4
  7. noise.period = 20.0
  8. noise.persistence = 0.8
  9. for i in 100:
  10. # Prints a slowly-changing series of floating-point numbers
  11. # between -1.0 and 1.0.
  12. print(noise.get_noise_1d(i))