【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の値はたぶん含みますが、検証用コードでは引けず……

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の値を含む)

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

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” に結果が偏ります。

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() が便利です。

extends Node2D

func _ready() -> void:
    # 乱数初期化
    randomize()

    # 山札作成
    var deck = [1, 2, 3, 4, 5, 6, 7, 8, 9]

    # 山札をシャッフル
    deck.shuffle()

    print(deck)

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

重み付けの抽選を行う

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

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

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

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

結果は以下のように……

参考