🎲

Godot Engineで偏りのある乱数を扱う(重み付き乱択/正規乱数)

2024/10/27に公開
5

この記事は?

オープンソースのゲームエンジンであるGodot Engineの乱数生成機能のうち「偏りのある乱数」、具体的には重み付き乱択正規乱数をコード付きで紹介するものです。

(ランダムな数字が均等に出現する)一様乱数と比較し「偏りのある乱数」は、ゲーム制作においてキャラクターやバトルなどのアルゴリズムや、自然なアニメーションなどを表現・実装するのにとても便利です。

まだ触れたことがないという方は、ぜひ活用してみてください。

重み付き乱択

たとえばRPGの戦闘において、以下のようなアルゴリズムを設計するのに便利なのが、重み付き乱択です。

RPGにおける敵キャラの行動例:
- 敵キャラAは、物理攻撃をしてくる確率が高く、そこそこの頻度で魔法を使う
- 敵キャラBは、主に魔法攻撃を使うが、まれに逃げる

サンプルコード

Godot Engine 4.3から、RandomNumberGenerator::rand_weightedメソッドが実装され、重み付き乱択が簡単に使えるようになりました。

上述のアルゴリズムをコードにしてみましょう[1]。敵キャラA, Bにのそれぞれに設定した行動が、指定した確率で抽選され、コンソールに出力されるシンプルなものです。何回か実行すれば、指定した確率で敵キャラが行動を選択していることがわかると思います。

extends Node


func _ready() -> void:
    # 乱数生成器をインスタンス化
    var rng = RandomNumberGenerator.new()

    # Dictionary型のキーが敵の行動クラスを、値が頻度を表します
    # 敵キャラAの行動を宣言
    var enemy_a_actions := {
        PhysicalAttack : 0.7, # 7/10の確率で物理攻撃
        MagicAttack : 0.3, # 3/10の確率で魔法攻撃
    }

    # 敵キャラBの行動を宣言
    var enemy_b_actions := {
        MagicAttack : 1.0, # 10/11の確率で魔法攻撃
        RunAway : 0.1, # 1/11の確率で逃げる
    }

    # 重み付き乱択によるアクションの抽選から実行までの処理をまとめたCallable
    var run_action = func(enemy_name: String, enemy: Dictionary) -> void:
            # 辞書からアクション(キー)を抽出
            var actions: Array = enemy.keys()
            # 辞書から頻度(値)を抽出
            var weights := PackedFloat32Array(enemy.values())
            # rand_weighted関数を使って、アクションの抽選を実行
            var selected_action = actions[rng.rand_weighted(weights)]
            # 選択されたアクションを実行
            selected_action.execute(enemy_name)

    # Callableを呼び出して、敵キャラAの行動を抽選 → 実行
    run_action.call("敵A", enemy_a_actions)

    # Callableを呼び出して、敵キャラBの行動を抽選 → 実行
    run_action.call("敵B", enemy_b_actions)

    # _ready()はここで終わり
    return


# 以下のコードでは敵キャラのアクションを内部クラスとして定義。
# 選択されたアクションごとにメッセージを表示するstaticメソッドを実装しています。

# 物理攻撃コマンド
class PhysicalAttack extends RefCounted:
    static func execute(enemy_name: String) -> void:
        print("%sは物理攻撃を実行した!" % enemy_name)


# 魔法攻撃コマンド
class MagicAttack extends RefCounted:
    static func execute(enemy_name: String) -> void:
        print("%sは魔法攻撃を実行した!" % enemy_name)


# 逃げるコマンド
class RunAway extends RefCounted:
    static func execute(enemy_name: String) -> void:
        print("%sは逃げ出した!" % enemy_name)

オンラインで重み付き乱択の動作確認

ブラウザからも動作チェックができるよう、GDScript Playgroundのリンクをシェアします。リンク先で「Run」を押せば上記のコードが走って、右側のテキストボックスに結果が出力されます。
https://gd.tumeo.space/?KYDwLsB2AmDOAEA5A9tYAoTAzArpAxvAPoBOwAhtAJ4AUAlPALQB88AbsgJbQBc68A+AGJ4gRzlADqaB8V0AIRoAs1QEkMgEoZAzwyBOhkD9DGsBoyv0FtyJeCUgBzeAF54AJXIxkAW0Q57AI2AkA4lHfkwyEgB0kMAA7vSYgsLwACKc+GCcyJAGVIDR6oB2DIC1DIA-DIAyDICupumAMhGAq0qA9QyAlwyq8oCADIAkCrmA3BmAZXrygBYRgOoMgH4MgJoMugIi+ZmAxwwVAILFJfKAxdqAAFH97Abw3vZUROTr8YmQCDyWAN4LkQAKABZUsHHkADZjYGDk+ADW8DzwAAwBAOwANFFfAHoAIzvdKALo9AOHOgHMGQCWToAwF0A3KaAYJMjoIALLkExxO4PZ6vD4BADMfxEhOBoMhUMAKtmAVZtkQsAL4RQSDEYVABCkxm80i+kMKzWLk2CSSuwOqIEGKx+BxjxebyBAXeJPgIOBQPB0Np9MikSseDGIXIVHxnyBKqB6s1UMAwAmAJQZANEMjOZA3ggFnEwD+DIANuUAsgyiQB5RoBrBkAEQwOwBFDGVAO0MgHOGZTpQC9RoAPBMA0gyASIZAPnaRS6UPSgDPFOHyLqACwZAIEMgH0GADCN2u5Bc1wwvKWJDwwu2FnguAINAFRGS9mAbwAymASJxTH8BW9YlskikGCx2FxeBLdSJAHnxgA+zNMRmPKQAQ-zlAJD-8njgC-FFeNwyPEU7N5jEgkY3tgUBJ7AC7hXVfqKbtMtPd1MeZ4XnoSwhMAnAmKcYBivAxxysA0AAGLXMgviEgATPej60C++jXDgwCwPQdAga6j4wEQ4GQdBiGAEWp4jyIA-vKAMYMgBmDDUO6xgmibyJmZGLIYsDAPW8SIa2STtte2ywAA2sYJgBBR0BURBUEQNANDUepsB0AAugJIiJv6gCqDIAMQxllxyh8UUAnCaJGkSZAASgMA+A4BA3bBKsvbkP2pELCIVbXDWdbAPIgA+KoAzgynh0HFDKMEylCeibwIASYTwPxkTNpATkBPg1Y0AARPkYxFRO3lrBs0migFkRBdWtb1pFMVxTUCUclySbpZltnZS2NXOQVIXFfk7LlcslVEEKg26S6USkBQ1D0IA9gyAMoMa1QoAQr6APEMgBRDAsZBgDgxiYOgIiAKdygDQcukgDNDNkgCTDFCK0dekVnyIAoYqABcJlSqEWcWAFnagCV-oAQAznfAxnmZZUaxoAKgxFoGgCHDIAwwyAN0M2SAB0M7SAFyePQOrADwJPgiOAL0MyMPXxgCjEXFgAiDL0YPg-CyK3YAewzKA96D4DWsAIGcFxXLc9xyss4BQHA1jAFgFbIHgGl8JEBO+HEHZ4IQrnuZ5PZ9gO8DDqOpjzqwHDcPL34AA762AxUAKSwCtTNIjZHRloAgP9FfA1uTcAPnawF4PakirPs5z3MIFK2JC3ioAQDACBWJL0uy4hpsCIrRMqwQItuR5wBed7aza0OI5jiYhuLibK4W2OVtFbbK0B07rvu57Wt+cAfsiPaDpBxzXPkDz1gGkaJrR2LccJzLkBywsafK52asgNnmtTYXuvFwbTBG0uKe6lXU823b9qxU3Htez7bekUAA


正規乱数

正規分布の乱数は、たとえばRPGのダメージを算出や、アニメーションの表現をするのに便利な機能です。まれに「コクのある乱数」みたいに呼ばれるものですね[2]

Godot Engineでは、RandomNumberGenerator::randfnメソッドを使うことで簡単に実装できます。

randfn()メソッドの詳細

randfn()は公式ドキュメントでは以下のように紹介されています。

float randfn(mean: float = 0.0, deviation: float = 1.0)

Returns a normally-distributed, pseudo-random floating-point number from the specified mean and a standard deviation. This is also known as a Gaussian distribution.
Note: This method uses the Box-Muller transform algorithm.


つまり?

少し分かりづらいのですが、ざっくり補足 & 要約するとこんな感じです。
randfn()メソッドは、与えられた平均値と標準偏差をもとに、浮動小数点の乱数を正規分布で出力します。
randfn(mean, deviation)の引数のうちmeanが平均値で、deviationが標準偏差(値のばらつき)を表しています。deviationの値を小さくすると乱数が平均値に収束し、逆に値を大きくすると均等な乱数が出力されます。

サンプルコード

こちらはゲームのコードではなく、実際に正規分布で乱数が生成されるのかを確認できるようしてみます。

以下のサンプルコードでは、500回連続で乱数を生成し、出力された数字を数え上げて、結果をコンソールに出力します。一目で正規乱数かどうかがわかるはずです。

extends Node

func _ready():
    var array: Array[int] # 生成した乱数を代入するための配列
    var rng = RandomNumberGenerator.new() # 乱数生成器をインスタンス化

    # for文で500回乱数を生成
    for i in 500:
        # 平均値を9.0、標準偏差を3.0に設定し、randfn()を実行。
        # 生成した正規乱数をカウントしやすいようにroundi()で四捨五入。
        var value = roundi(rng.randfn(9.0, 3.0))
        # 配列に代入
        array.append(value)

    # コンソールに出力し、カウントする数字の最大値を指定
    # このコードでは0~20までの数値の出現回数をカウントする
    var max_number = 20

    # コンソールにヒストグラムを表示する
    for i in max_number:
        # 指定した整数が何回生成されたかを、カウントし出力
        print("%02d : " % i + "*".repeat(array.count(i)))

このコードを実行した結果は以下の通り。生成された数字が、平均値として指定した9.0の付近に偏っていることがわかります。しっかり正規分布っぽくなっていますね。

引数のdeviationをいじると、もっと極端に偏った乱数や、一様乱数なども生成できるので、ぜひご自分でも試してみてください。

00 : *
01 :
02 : *******
03 : *******
04 : ************
05 : **************************
06 : ******************************************
07 : *****************************************************
08 : ************************************************************
09 : ********************************************************************
10 : ******************************************************************
11 : ************************************************
12 : *************************************
13 : ***********************************
14 : ******************
15 : ************
16 : *****
17 : *
18 : *
19 :

オンラインで正規乱数の動作確認

ブラウザからも動作チェックができるよう、GDScript Playgroundのリンクをシェアします。リンク先で「Run」を押せば上記のコードが走って、右側のテキストボックスに結果が出力されます。
https://gd.tumeo.space/?KYDwLsB2AmDOAEA5A9tYAodAzArpAxvAPoBOwAhtAJ4AUAlAFzrwvwBu5J8nJ5VD8AIIleVANoBLSGAC68AMTxA+K6AEI0DqDIH0GQI5ygB1NASQyBjuUCmioE0GQNEMGwIEMgOwZAsomB0JWasOXEpADm8ALzwASuRhkAFtEHCCAI2ASAHEoKPIwZBIAOkhgAHd6BXhdVUALNT1AEoZAZ4ZAToZAfoYywDRlTFZsrCTAcNNAcwYAVgAGdsA9tV09VScWBq4JeCl4DvamOrrFQGc9QHF1QBIFPQBOZPbAQAZATCtANLtAeQVAO909AGZ1wGsGQFoowCztNQ3eGCxIej1AfO1AGQjAIAYB6cVVTUBja0A8ZG9QDVDIAyhmKgAmGNSAEIYTIARBkAEQyAMQYziRkHhoBJ6M1ANtqgAtjQApckYvtNnJx2OQADY4YDeeDozHYtzuZL3aCPGhrdoAGngp3adDo3xm8AcZ2MItYPD4yXIAAd5VBoDQODTgMLvopAM0MxUAvQyAH4ZANcMZ0AX4qAbKVbuCoeYdIB1bWsgABzQDkmstAODGVy18EAygzWbUGwCTDM1APYM7QAfgAmdqAPwZmtYdItrKbAH-OXX01sh5m+LngQXIICIkDCkS4PijtVYOv1xrOgCWGUqQwANDIBLhkABwx6QAWEYAuTyzdSGo1GkFz+cLxaiUzJ2Q9mkALqY6QAyDIBVeS6qkAqgyAGIYNIBpBj0GwzagtUpY8pIUjANAARABSdoR6DwAQX+BXgcAangF4AVBe2cAlQkaBlKhknwDFpBoCQhToIA

脚注
  1. Godot Engine 4.3以上であれば、コピペすれば動くはずです。 ↩︎

  2. 味覚を表現するのに使われる「コク」という単語を「自然な」という意味で転用したものだと思います(遡れる範囲では、2016年11月の深津貴之さんのtweetが初出)。
    「コク」や「お腹がシクシク痛む」など、日本語にはカジュアルに使われる難解な言い回しが多いですね。 ↩︎

Discussion

カフェ俺カフェ俺

いつも勉強させてもらっている初心者です。
重み付き乱択のサンプルコードが、Dictionaryのキーとして直接クラス名を使っていると言うエラーが出ます

SLMNLLSLMNLL

コメントありがとうございます。Godot Engineのバージョンやコードを教えていただければ、原因がわかるかもしれません(Godotは稀に的外れなエラーメッセージを吐き出すので…)。

記事内のサンプルコードの動作確認

GDScript PlaygroundGodot Engine 4.3.stable.official.77dcf97d8にアップデートされて、重み付け乱択が実行できるようになったので、記事内のコードをそのまま貼り付けてみました。

ぜひ動かしてみてください(エラーが出ずに動くと思います)。
https://gd.tumeo.space/?KYDwLsB2AmDOAEA5A9tYAoTAzArpAxvAPoBOwAhtAJ4AUAlPALQB88AbsgJbQBc68A+AGJ4gRzlADqaB8V0AIRoAs1QEkMgEoZAzwyBOhkD9DGsBoyv0FtyJeCUgBzeAF54AJXIxkAW0Q57AI2AkA4lHfkwyEgB0kMAA7vSYgsLwACKc+GCcyJAGVIDR6oB2DIC1DIA-DIAyDICupumAMhGAq0qA9QyAlwyq8oCADIAkCrmA3BmAZXrygBYRgOoMgH4MgJoMugIi+ZmAxwwVAILFJfKAxdqAAFH97Abw3vZUROTr8YmQCDyWAN4LkQAKABZUsHHkADZjYGDk+ADW8DzwAAwBAOwANFFfAHoAIzvdKALo9AOHOgHMGQCWToAwF0A3KaAYJMjoIALLkExxO4PZ6vD4BADMfxEhOBoMhUMAKtmAVZtkQsAL4RQSDEYVABCkxm80i+kMKzWLk2CSSuwOqIEGKx+BxjxebyBAXeJPgIOBQPB0Np9MikSseDGIXIVHxnyBKqB6s1UMAwAmAJQZANEMjOZA3ggFnEwD+DIANuUAsgyiQB5RoBrBkAEQwOwBFDGVAO0MgHOGZTpQC9RoAPBMA0gyASIZAPnaRS6UPSgDPFOHyLqACwZAIEMgH0GADCN2u5Bc1wwvKWJDwwu2FnguAINAFRGS9mAbwAymASJxTH8BW9YlskikGCx2FxeBLdSJAHnxgA+zNMRmPKQAQ-zlAJD-8njgC-FFeNwyPEU7N5jEgkY3tgUBJ7AC7hXVfqKbtMtPd1MeZ4XnoSwhMAnAmKcYBivAxxysA0AAGLXMgviEgATPej60C++jXDgwCwPQdAga6j4wEQ4GQdBiGAEWp4jyIA-vKAMYMgBmDDUO6xgmibyJmZGLIYsDAPW8SIa2STtte2ywAA2sYJgBBR0BURBUEQNANDUepsB0AAugJIiJv6gCqDIAMQxllxyh8UUAnCaJGkSZAASgMA+A4BA3bBKsvbkP2pELCIVbXDWdbAPIgA+KoAzgynh0HFDKMEylCeibwIASYTwPxkTNpATkBPg1Y0AARPkYxFRO3lrBs0migFkRBdWtb1pFMVxTUCUclySbpZltnZS2NXOQVIXFfk7LlcslVEEKg26S6USkBQ1D0IA9gyAMoMa1QoAQr6APEMgBRDAsZBgDgxiYOgIiAKdygDQcukgDNDNkgCTDFCK0dekVnyIAoYqABcJlSqEWcWAFnagCV-oAQAznfAxnmZZUaxoAKgxFoGgCHDIAwwyAN0M2SAB0M7SAFyePQOrADwJPgiOAL0MyMPXxgCjEXFgAiDL0YPg-CyK3YAewzKA96D4DWsAIGcFxXLc9xyss4BQHA1jAFgFbIHgGl8JEBO+HEHZ4IQrnuZ5PZ9gO8DDqOpjzqwHDcPL34AA762AxUAKSwCtTNIjZHRloAgP9FfA1uTcAPnawF4PakirPs5z3MIFK2JC3ioAQDACBWJL0uy4hpsCIrRMqwQItuR5wBed7aza0OI5jiYhuLibK4W2OVtFbbK0B07rvu57Wt+cAfsiPaDpBxzXPkDz1gGkaJrR2LccJzLkBywsafK52asgNnmtTYXuvFwbTBG0uKe6lXU823b9qxU3Htez7bekUAA


また、例えば以下のようなDictionaryのキーとしてクラスを指定しただけのコードでも、エラーを吐かずに動作しました。ですので、少なくともGodot 4.3以降であれば、Dictionaryのキーとしてクラスを指定すること自体は問題ないと思われます。

✅ Dictionaryのキーにクラスを指定するだけのコード
extends Node

func _ready():
    var dict = {
    	Node: 1.0
    }
    print(dict) # 出力結果: { <GDScriptNativeClass#-9223372026520010166>: 1 }

https://gd.tumeo.space/?KYDwLsB2AmDOAEA5A9tYAodAzArpAxvAPoBOwAhtAJ4AUAlAFzrwvwBu5J80AlvmPAC88AN7NWASBRoG8AIwA6AAziWAX1XwADiR6QwNXvzpA

カフェ俺カフェ俺

返信ありがとうございます
GDScript Playgroundではエラーが出ないのですが、実際GodotEngineにコードを張り付けるとエラーが出ます。エラー出ながらも出力結果はちゃんと出ます。
コードは重み付き乱択のサンプルコードそのままで、Godot_v4.3-stable_winを使用しています。

エラー内容
行番号 12:Key "<Object#null>" was already used in this dictionary (at line 11).
行番号 18:Key "<Object#null>" was already used in this dictionary (at line 17).

SLMNLLSLMNLL

ありがとうございます。Godotに貼り付けたところ、同じエラーを確認しました。
Dictionaryのキーが重複している」という旨のエラーですね。Godotがキーに設定したクラスを全て<Object#null>と解釈してしまっていることで発生しているのかなと想像しています。
そのままコードを保存して「現在のプロジェクトをリロード」をすると、エラーは解消されました。

(Godotあるあるですが)コードを貼り付けて謎のエラーが出る場合は、プロジェクトのリロードで解消されることが多いです。困った時のおまじないとしてお試しください。

カフェ俺カフェ俺

さらに返信ありがとうございます。恐縮です。
個人開発の素人には、こういうエラーもおまじないも本当に混乱するポイントです。
助かりました、ありがとうございます。
これからも記事楽しみに拝見させて頂きます。