Up to date
This page is up to date for Godot 4.3.
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.
備註
電腦不能產生“真正的”亂數。相反,它們依賴`偽亂數產生器 <https://en.wikipedia.org/wiki/Pseudorandom_number_generator>`__(PRNG)。
Godot internally uses the PCG Family of pseudorandom number generators.
全域作用域 vs RandomNumberGenerator 類
Godot 提供了兩種生成亂數的方式:通過*全域作用域*方法或使用 RandomNumberGenerator 類。
全域作用域方法更容易設定,但不能提供太多控制。
RandomNumberGenerator 需要使用更多程式碼,但允許建立多個實例,每個實例都有自己的種子和狀態。
本教學使用全域作用域方法, 只存在於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.
在全域作用域內, 你可以找到一個 randomize() 方法. 這個方法只需要在你的專案開始初始化隨機種子的時候呼叫一次 , 多次呼叫是多餘的, 並且有可能影響性能.
把它放在你的主場景腳本的 _ready() 方法中是個不錯的選擇:
func _ready():
randomize()
public override void _Ready()
{
GD.Randomize();
}
您也可以使用 seed() 設定固定的隨機種子。這樣能在運作中獲得*確定性*的結果:
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() ,因為它有自己的種子:
var random = RandomNumberGenerator.new()
random.randomize()
var random = new RandomNumberGenerator();
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)
// 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() returns a random floating-point number following a normal distribution. This means the returned value is more likely to be around the mean (0.0 by default), varying by the deviation (1.0 by default):
# Prints a random floating-point number from a normal distribution with a mean 0.0 and deviation 1.0.
print(randfn())
// Prints a random floating-point number from a normal distribution with a mean of 0.0 and deviation of 1.0.
GD.Print(GD.Randfn());
rand_range() 接受兩個參數 from 和 to ,並返回一個介於 from 和 to 之間的隨機浮點數:
# 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.RandfRange(-4, 6.5));
randi_range() takes two arguments from
and to, and returns a random integer between from and to:
# Prints a random integer between -10 and 10.
print(randi_range(-10, 10))
// Prints a random integer between -10 and 10.
GD.Print(GD.RandiRange(-10, 10));
獲取一個亂陣列元素
We can use random integer generation to get a random element from an array, or use the Array.pick_random method to do it for us:
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<string> _fruits = new Godot.Collections.Array<string> { "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;
}
To prevent the same fruit from being picked more than once in a row, we can add more logic to the above method. In this case, we can't use Array.pick_random since it lacks a way to prevent repetition:
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 模式來代替.
獲取一個隨機字典值
我們也可以將陣列的類似邏輯應用於字典:
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
加權隨機概率
randf() 方法返回一個介於 0.0 和 1.0 之間的浮點數。我們可以使用它來建立“加權”概率,其中不同的結果具有不同的可能性:
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";
}
}
You can also get a weighted random index using the
rand_weighted() method
on a RandomNumberGenerator instance. This returns a random integer
between 0 and the size of the array that is passed as a parameter. Each value in the
array is a floating-point number that represents the relative likelihood that it
will be returned as an index. A higher value means the value is more likely to be
returned as an index, while a value of 0 means it will never be returned as an index.
For example, if [0.5, 1, 1, 2] is passed as a parameter, then the method is twice
as likely to return 3 (the index of the value 2) and twice as unlikely to return
0 (the index of the value 0.5) compared to the indices 1 and 2.
Since the returned value matches the array's size, it can be used as an index to get a value from another array as follows:
# 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.5, 1, 1, 2 };
var random = new RandomNumberGenerator();
GD.Print(fruits[random.RandWeighted(probabilities)]);
使用 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():
_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()
# Prints "apple", "orange", "pear", or "banana" every time the code runs.
return random_fruit
在運作上面的程式碼時, 仍有可能連續兩次得到同一個水果. 我們摘下一個水果時, 它將不再是一個可能的返回值, 但除非陣列現在是空的. 當陣列為空時, 此時我們將其重設回預設值, 這樣就導致了能再次獲得相同的水果, 但只有這一次.
隨機數函式
當你需要一個 緩慢 根據輸入而變化的值時, 上面顯示的亂數產生方式就顯示出了它們的局限性. 這裡的輸入可以是位置, 時間或其他任何東西.
為此,您可以使用隨機*雜訊*函式。雜訊函式在程式式生成中特別受歡迎,可以生成逼真的地形。 Godot 為此提供了 class_opensimplexnoise,它支援 1D、2D、3D 和 4D 雜訊。這是一個 1D 雜訊的例子:
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 = 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));
}
}
Cryptographically secure pseudorandom number generation
So far, the approaches mentioned above are not suitable for cryptographically secure pseudorandom number generation (CSPRNG). This is fine for games, but this is not sufficient for scenarios where encryption, authentication or signing is involved.
Godot offers a Crypto class for this. This class can perform asymmetric key encryption/decryption, signing/verification, while also generating cryptographically secure random bytes, RSA keys, HMAC digests, and self-signed X509Certificates.
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.