随机数生成

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

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

备注

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

全局作用域 vs RandomNumberGenerator 类

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

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

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

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

randomize() 方法

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

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

GDScriptC#

  1. func _ready():
  2. randomize()
  1. public override void _Ready()
  2. {
  3. GD.Randomize();
  4. }

您也可以使用 seed() 设置固定的随机种子。这样能在运行中获得确定性的结果:

GDScriptC#

  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())
  1. public override void _Ready()
  2. {
  3. GD.Seed(12345);
  4. GD.Seed("Hello world".Hash());
  5. }

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

GDScriptC#

  1. var random = RandomNumberGenerator.new()
  2. random.randomize()
  1. var random = new RandomNumberGenerator();
  2. random.Randomize();

获得一个随机数

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

函数 randi() 返回 0 到 2^32-1 之间的随机数。由于最大值很大,您很可能希望使用模运算符 (%) 将结果限制在 0 和分母之间:

GDScriptC#

  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)
  1. // Prints a random integer between 0 and 49.
  2. GD.Print(GD.Randi() % 50);
  3. // Prints a random integer between 10 and 60.
  4. GD.Print(GD.Randi() % 51 + 10);

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

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

GDScriptC#

  1. # Prints a random floating-point number from a normal distribution with a mean 0.0 and deviation 1.0.
  2. var random = RandomNumberGenerator.new()
  3. random.randomize()
  4. print(random.randfn())
  1. // Prints a normally distributed floating-point number between 0.0 and 1.0.
  2. var random = new RandomNumberGenerator();
  3. random.Randomize();
  4. GD.Print(random.Randfn());

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

GDScriptC#

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

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

GDScriptC#

  1. # Prints a random integer between -10 and 10.
  2. var random = RandomNumberGenerator.new()
  3. random.randomize()
  4. print(random.randi_range(-10, 10))
  1. // Prints a random integer number between -10 and 10.
  2. random.Randomize();
  3. GD.Print(random.RandiRange(-10, 10));

获取一个随机数组元素

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

GDScriptC#

  1. var _fruits = ["apple", "orange", "pear", "banana"]
  2. func _ready():
  3. randomize()
  4. for i in range(100):
  5. # Pick 100 fruits randomly.
  6. print(get_fruit())
  7. func get_fruit():
  8. var random_fruit = _fruits[randi() % _fruits.size()]
  9. # Returns "apple", "orange", "pear", or "banana" every time the code runs.
  10. # We may get the same fruit multiple times in a row.
  11. return random_fruit
  1. private string[] _fruits = { "apple", "orange", "pear", "banana" };
  2. public override void _Ready()
  3. {
  4. GD.Randomize();
  5. for (int i = 0; i < 100; i++)
  6. {
  7. // Pick 100 fruits randomly.
  8. GD.Print(GetFruit());
  9. }
  10. }
  11. public string GetFruit()
  12. {
  13. string randomFruit = _fruits[GD.Randi() % _fruits.Length];
  14. // Returns "apple", "orange", "pear", or "banana" every time the code runs.
  15. // We may get the same fruit multiple times in a row.
  16. return randomFruit;
  17. }

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

GDScriptC#

  1. var _fruits = ["apple", "orange", "pear", "banana"]
  2. var _last_fruit = ""
  3. func _ready():
  4. randomize()
  5. # Pick 100 fruits randomly.
  6. for i in range(100):
  7. print(get_fruit())
  8. func get_fruit():
  9. var random_fruit = _fruits[randi() % _fruits.size()]
  10. while random_fruit == _last_fruit:
  11. # The last fruit was picked, try again until we get a different fruit.
  12. random_fruit = _fruits[randi() % _fruits.size()]
  13. # Note: if the random element to pick is passed by reference,
  14. # such as an array or dictionary,
  15. # use `_last_fruit = random_fruit.duplicate()` instead.
  16. _last_fruit = random_fruit
  17. # Returns "apple", "orange", "pear", or "banana" every time the code runs.
  18. # The function will never return the same fruit more than once in a row.
  19. return random_fruit
  1. private string[] _fruits = { "apple", "orange", "pear", "banana" };
  2. private string _lastFruit = "";
  3. public override void _Ready()
  4. {
  5. GD.Randomize();
  6. for (int i = 0; i < 100; i++)
  7. {
  8. // Pick 100 fruits randomly.
  9. GD.Print(GetFruit());
  10. }
  11. }
  12. public string GetFruit()
  13. {
  14. string randomFruit = _fruits[GD.Randi() % _fruits.Length];
  15. while (randomFruit == _lastFruit)
  16. {
  17. // The last fruit was picked, try again until we get a different fruit.
  18. randomFruit = _fruits[GD.Randi() % _fruits.Length];
  19. }
  20. _lastFruit = randomFruit;
  21. // Returns "apple", "orange", "pear", or "banana" every time the code runs.
  22. // The function will never return the same fruit more than once in a row.
  23. return randomFruit;
  24. }

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

获取一个随机字典值

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

GDScript

  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 range(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

加权随机概率

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

GDScriptC#

  1. func _ready():
  2. randomize()
  3. for i in range(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"
  1. public override void _Ready()
  2. {
  3. GD.Randomize();
  4. for (int i = 0; i < 100; i++)
  5. {
  6. GD.Print(GetItemRarity());
  7. }
  8. }
  9. public string GetItemRarity()
  10. {
  11. float randomFloat = GD.Randf();
  12. if (randomFloat < 0.8f)
  13. {
  14. // 80% chance of being returned.
  15. return "Common";
  16. }
  17. else if (randomFloat < 0.95f)
  18. {
  19. // 15% chance of being returned
  20. return "Uncommon";
  21. }
  22. else
  23. {
  24. // 5% chance of being returned.
  25. return "Rare";
  26. }
  27. }

使用 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 噪声的例子:

GDScriptC#

  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))
  1. private OpenSimplexNoise _noise = new OpenSimplexNoise();
  2. public override void _Ready()
  3. {
  4. GD.Randomize();
  5. // Configure the OpenSimplexNoise instance.
  6. _noise.Seed = (int)GD.Randi();
  7. _noise.Octaves = 4;
  8. _noise.Period = 20.0f;
  9. _noise.Persistence = 0.8f;
  10. for (int i = 0; i < 100; i++)
  11. {
  12. GD.Print(_noise.GetNoise1d(i));
  13. }
  14. }