【Godot】乱数の使い方について

# 概要
この記事では、Godot Engine における乱数の使い方を説明します。

# ビルドイン関数での乱数制御
GDScriptのビルドイン関数での乱数は以下のものが用意されています

* `float rand_range(float from, float to)`: from〜toの乱数を浮動小数値で返す
* `Array rand_seed(int seed)`: 乱数のシード値を設定
* `float randf()`: 浮動小数値 0.0〜1.0の乱数を返す
* `int randi()`: 整数値 0〜4294967295(2^32-1)の乱数を返す
* `void randomize()`: 現在の時間の値を使用して乱数のシード値を設定する

## rand_range(from, to): from〜toの乱数を浮動小数値で返す
from〜toの乱数を浮動小数値で返します。
ソースコードを読んだ印象では、toの値はたぶん含みますが、検証用コードでは引けず……

```gd
extends Node2D

func _ready() -> void:
	for i in range(10000000):
		var rnd = rand_range(0, 100)
		if rnd > 99.99999:
			print(rnd)
```

```:出力結果
99.999996
99.999993
```

floatなので、引くのはかなり難しいかなと……
ちなみに整数値版の `RandomNumberGenerator::randi_range()` の場合は終端の値を簡単に引けます。
(ビルドイン関数には整数値版はなさそう……？　→`randi()%n`で代用）

## rand_seed(int seed): 乱数のシード値を設定
乱数のシード値を設定します。
カードゲームのリプレイなどで乱数を再現したい場合など。

## randf(): 浮動小数値 0.0〜1.0の乱数を返す
`rand_range()` がfloatなので必要ないけれど、0.0〜1.0 が欲しい場合に使うかもしれない……

## randi(): 整数値 0〜4294967295(2^32-1)の乱数を返す
`rand_range()` の整数値バージョンがないので、整数値の乱数が欲しい場合に使います。

## randomize(): 現在の時間の値を使用して乱数のシード値を設定
乱数の初期化として使います。ゲーム起動時にこの関数を呼び出すようにすれば、おおよそ毎回違う乱数になります。
再現性のある乱数にしたい場合には、あえて呼び出さないようにしてもいいかもしれません。

# RandomNumberGenerator
乱数を複数用意する場合には `RandomNumberGenerator` を使用します。
例えば、カードゲームなどのリプレイ保存などで、カードには `RandomNumberGenerator` で乱数を固定化して、演出などではビルドイン関数の乱数を使用する、という方法です。

また、ビルドインにはない以下の関数が使用できます。

* `int randi_range(int from, int to)`:fromからtoの値を整数値で返す 
* `float randfn(float median, float deviation)`:ガウス分布の乱数を浮動小数値で返す

## rndi_range(from, to): fromからtoの値を整数値で返す
fromからtoの値を整数値で返します (from/toの値を含む)

使用例は以下のとおりです

```gd:使用例
extends Node2D

func _ready() -> void:
	# 乱数生成
	var rng = RandomNumberGenerator.new()
	
	# 乱数初期化
	rng.randomize()
	
	# 1〜3の乱数を引く
	for i in range(10):
		print(rng.randi_range(1, 3))

```

この処理は 1〜3 の値をランダムで出力します。
`randi()%3 + 1` で代用できるのですが、この関数の方がわかりやすさは高いです。

## randfn(median, deviation):ガウス分布の乱数を浮動小数値で返す
medianが平均値で、deviationが偏差となります。
数学は得意ではないので正確な定義は間違っているかもしれませんが、偏差とは偏りのある振れ幅のことで、例えば `randfn(10, 1)` と指定すると 平均がおおよそ "10" になるようにして、おおよそ "10±1 (= 9.0〜11.0)" の値を返します。（おおよそなので実際にはその範囲を飛び越えます）
そして、中央値 "10" に結果が偏ります。

```gd:使用例
extends Node2D

func _ready() -> void:
	# 乱数生成
	var rng = RandomNumberGenerator.new()
	
	# 乱数初期化
	rng.randomize()
	
	# ガウス分布の乱数を生成
	var sum = 0
	for i in range(100):
		var n = rng.randfn(10, 1)
		sum += n
		
	print("平均値: %f"%(sum / 100))
```

```:実行結果
平均値: 9.923579
```

# Array.shuffle()
カードゲームの山札を作る場合には、`Array.shuffle()` が便利です。

```gd:シャッフルの例
extends Node2D

func _ready() -> void:
	# 乱数初期化
	randomize()
	
	# 山札作成
	var deck = [1, 2, 3, 4, 5, 6, 7, 8, 9]
	
	# 山札をシャッフル
	deck.shuffle()
	
	print(deck)
```

ただ、ドキュメントによると、ビルドイン関数の `randi()` が内部で使われてしまうようなので、乱数を複数使いたい場合には、独自にシャッフル関数を作るなど、工夫が必要となるかもしれません。

* [Godot Docs » Godot API » Array](https://docs.godotengine.org/ja/stable/classes/class_array.html)

# 重み付けの抽選を行う
最後に重み付けの抽選を行うサンプルコードの紹介です。
このクジには、

* 大当たり(SSR): 1本
* 当たり(SR): 10本
* はずれ(R): 100本

が入っているとして、10連ガチャで引きます。
引いた後は、はずれでもクジの箱に入れ直すという鬼畜仕様ですが、それゆえ10連すべてが SSR になる可能性もあります……！

```gd
extends Node2D

# 結果格納
var result = []

# 重み付けで抽選
# @return 抽選結果のインデックス番号
func weighted_pick(arr) -> int:
	var total_weight = 0
	var pick = 0
 
	# トータルの重みを計算する
	for v in arr:
		total_weight += v

	# 抽選する
	var rnd = randi()%total_weight

	for i in arr.size():
		if rnd < arr[i]:
			# 抽選対象決定
			pick = i
			break
			
		#  次の対象を調べる
		rnd -= arr[i]
	
	return pick

func _ready() -> void:
	# 乱数初期化
	randomize()
	
	# 重み付け配列のクジを作成
	# 0: 1本
	# 1: 10本
	# 2: 100本
	var gacha = [1, 10, 100]
	
	# 10連ガチャ
	for i in range(10):
		var ret = weighted_pick(gacha)
		result.append(ret)

func _get_rarity(idx):
	match idx:
		0:
			return "SSR"
		1:
			return "SR"
		_:
			return "R"

func _draw():
	# デフォルトフォントを取得
	var font = Control.new().get_font("font")
	
	# 結果を描画
	var i = 0
	for idx in result:
		draw_string(font, Vector2(12, 16 * (i + 1)), "%d: %s"%[i, _get_rarity(idx)])
		i += 1
```

結果は以下のように……
![TestRandom__DEBUG__と_Qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/eb4b9772-6f68-b146-8d49-8af951070f41.png)

20回ほど試しましたが、1度も SSR を引くことはなかったので、SSRを引くのはかなり難しそうです。

# 参考

* [Godot Docs » Godot API » @GDScript](https://docs.godotengine.org/ja/stable/classes/class_@gdscript.html)
* [Godot Docs » Godot API » RandomNumberGenerator](https://docs.godotengine.org/ja/stable/classes/class_randomnumbergenerator.html)
* [Godot Docs » Godot API » Array](https://docs.godotengine.org/ja/stable/classes/class_array.html)