Godot Engineで偏りのある乱数を扱う(重み付き乱択/正規乱数)
この記事は?
オープンソースのゲームエンジンである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」を押せば上記のコードが走って、右側のテキストボックスに結果が出力されます。
正規乱数
正規分布の乱数は、たとえば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」を押せば上記のコードが走って、右側のテキストボックスに結果が出力されます。
Discussion
いつも勉強させてもらっている初心者です。
重み付き乱択のサンプルコードが、Dictionaryのキーとして直接クラス名を使っていると言うエラーが出ます
コメントありがとうございます。Godot Engineのバージョンやコードを教えていただければ、原因がわかるかもしれません(Godotは稀に的外れなエラーメッセージを吐き出すので…)。
記事内のサンプルコードの動作確認
GDScript Playgroundが
Godot Engine 4.3.stable.official.77dcf97d8
にアップデートされて、重み付け乱択が実行できるようになったので、記事内のコードをそのまま貼り付けてみました。ぜひ動かしてみてください(エラーが出ずに動くと思います)。
また、例えば以下のような
Dictionary
のキーとしてクラスを指定しただけのコードでも、エラーを吐かずに動作しました。ですので、少なくともGodot 4.3
以降であれば、Dictionary
のキーとしてクラスを指定すること自体は問題ないと思われます。返信ありがとうございます
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).
ありがとうございます。Godotに貼り付けたところ、同じエラーを確認しました。
「
Dictionary
のキーが重複している」という旨のエラーですね。Godotがキーに設定したクラスを全て<Object#null>
と解釈してしまっていることで発生しているのかなと想像しています。そのままコードを保存して「現在のプロジェクトをリロード」をすると、エラーは解消されました。
(Godotあるあるですが)コードを貼り付けて謎のエラーが出る場合は、プロジェクトのリロードで解消されることが多いです。困った時のおまじないとしてお試しください。
さらに返信ありがとうございます。恐縮です。
個人開発の素人には、こういうエラーもおまじないも本当に混乱するポイントです。
助かりました、ありがとうございます。
これからも記事楽しみに拝見させて頂きます。