亂數產生
許多遊戲仰賴隨機性來實作核心遊戲機制。本頁將帶你了解常見的亂數類型,以及如何在 Godot 中實作它們。
在簡要介紹產生亂數的實用函式後,你會學到如何從陣列、字典中取得隨機元素,並學會在 GDScript 裡使用雜訊產生器。最後,我們也會看看密碼學安全的亂數產生,以及它與一般亂數產生的差異。
全域作用域與 RandomNumberGenerator 類別
Godot 提供兩種產生亂數的方式:透過*全域作用域*的方法,或是使用 RandomNumberGenerator 類別。
全域作用域的方法較易設置,但控制性較低。
RandomNumberGenerator 雖需更多程式碼,但能建立多個實例,各自擁有獨立種子與狀態。
本教學主要使用全域作用域的方法,除非該方法只存在於 RandomNumberGenerator 類別中。
randomize() 方法
備註
自 Godot 4.0 起,專案啟動時會自動將亂數種子設為隨機值。這表示你不必再於 _ready() 裡呼叫 randomize() 來確保每次執行專案時結果都不同。不過,如果你想用特定種子,或用其他方式產生種子,仍可自行呼叫 randomize()。
在全域作用域中,你會找到 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 裡常用的亂數產生函式和方法。
函式 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)
// 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)變動:
# 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 之間的亂數浮點數:
# 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 之間的亂數整數:
# 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 方法來實現:
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 = ["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,因為它無法防止重複:
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
private Godot.Collections.Dictionary<string, Godot.Collections.Dictionary<string, int>> _metals = new()
{
{"copper", new Godot.Collections.Dictionary<string, int>{{"quantity", 50}, {"price", 50}}},
{"silver", new Godot.Collections.Dictionary<string, int>{{"quantity", 20}, {"price", 150}}},
{"gold", new Godot.Collections.Dictionary<string, int>{{"quantity", 3}, {"price", 500}}},
};
public override void _Ready()
{
for (int i = 0; i < 20; i++)
{
GD.Print(GetMetal());
}
}
public Godot.Collections.Dictionary<string, int> 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 的浮點數。你可以用來實作「加權」機率,讓不同結果有不同出現機率:
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``(數值為 1)的兩倍,選到索引 ``0``(數值為 0.5)的機率則是索引 ``1、2 的一半。
由於回傳值就是陣列的索引,你可以用它來從另一個陣列中取值,例如:
# 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 模式來解決。每次選完從陣列移除該元素,直到陣列為空時再初始化回原本的內容:
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<string> _fruits = ["apple", "orange", "pear", "banana"];
// A copy of the fruits array so we can restore the original value into `fruits`.
private Godot.Collections.Array<string> _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 雜訊的範例:
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 (含)的隨機整數:
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 的說明文件。