社攻联盟 - 大型多人在线游戏活动平台

社攻联盟 - 大型多人在线游戏活动平台

shape

随机数生成

  • Home
  • 跨服战场
  • 随机数生成
  • 2026-10-22 21:54:51
  • admin

随机数生成

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

在简要概述生成随机数的实用函数之后,你将学习如何从数组、字典中获取随机元素,以及如何在 GDScript 中使用噪声生成器。最后,我们将了解加密安全的随机数生成以及它与典型随机数生成的区别。

备注

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

Godot 内部使用 PCG 一族的伪随机数生成器。

全局作用域 vs RandomNumberGenerator 类

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

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

RandomNumberGenerator 需要使用更多代码,但允许创建多个实例,每个实例都有自己的种子和状态。

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

randomize() 方法

备注

自 Godot 4.0 以来,当项目启动时,随机种子会自动设置为随机值。这意味着你不再需要在 _ready() 中调用 randomize() 来确保项目运行的结果是随机的。但是,如果你想使用特定的种子编号,或者使用不同的方法生成它,你仍然可以使用 randomize()。

在全局作用域中,你可以找到一个 randomize() 方法。该方法应该在你的项目开始初始化随机种子时只调用一次。多次调用它是不必要的,且可能会对性能产生负面影响。

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

GDScriptC#func _ready():

randomize()

public override void _Ready()

{

GD.Randomize();

}

你也可以使用 seed() 设置固定的随机种子。这样做会在运行过程中为你提供确定性的结果:

GDScriptC#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(),因为它有自己的种子:

GDScriptC#var random = RandomNumberGenerator.new()

random.randomize()

var random = new RandomNumberGenerator();

random.Randomize();

获得一个随机数

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

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

GDScriptC## 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):

GDScriptC## 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 之间的随机浮点数:

GDScriptC## 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 之间的随机整数:

GDScriptC## 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 来为我们做这件事:

GDScriptC#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 _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,因其缺少防重复机制:

GDScriptC#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 模式来代替。

获取一个随机字典值

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

GDScriptC#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> _metals = new()

{

{"copper", new Godot.Collections.Dictionary{{"quantity", 50}, {"price", 50}}},

{"silver", new Godot.Collections.Dictionary{{"quantity", 20}, {"price", 150}}},

{"gold", new Godot.Collections.Dictionary{{"quantity", 3}, {"price", 500}}},

};

public override void _Ready()

{

for (int i = 0; i < 20; i++)

{

GD.Print(GetMetal());

}

}

public Godot.Collections.Dictionary 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 之间的浮点数。我们可以使用它来创建“加权”概率,其中不同的结果具有不同的可能性:

GDScriptC#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 的索引)的可能性则是一半。

由于返回值与数组的大小匹配,因此可以将其用作索引来从另一个数组中获取值,如下所示:

GDScriptC## 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 模式来实现。它的工作原理是在选择数组后从数组中删除一个元素。多次选择之后,数组会被清空。当这种情况发生时,就将数组重新初始化为默认值:

GDScriptC#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 _fruits = ["apple", "orange", "pear", "banana"];

// A copy of the fruits array so we can restore the original value into `fruits`.

private Godot.Collections.Array _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 噪声的示例:

GDScriptC#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 的文档,了解可用于将生成的字节解码为各种类型的数据(如整数或浮点)的其他方法。

Previous Post
Bitcoin 個人應用

Copyright © 2088 社攻联盟 - 大型多人在线游戏活动平台 All Rights Reserved.

友情链接