見出し画像

GodotでエアホッケーゲームをAIの検証も兼ねて作ってみた


■ご挨拶

いつもありがとうございます!AKと申します。

表題では、エアホッケーゲームを作ってみた!となっていますが、資格試験の合間で行っているので現状はまだまだ開発途中です。。なので少しタイトル詐欺みたいになっています、、すみません(;´Д`)
なので正確には今回は、「エアホッケーゲームで開発する中で発生した問題をAIになおしてもらった」というストーリーでお届けします。タイトル変えた方がいいな、、(笑)
ちなみにエアホッケーゲームにした意図としては、「比較的簡単なゲームをいくつか作れるという自分の実績と基礎作りのため」というのがあります。
お楽しみいただければ幸いです。
※ゲーム開発の際にAIを用いています。どの生成AIがおすすめかについては以下の記事も参考にしていただければ幸いです。

https://note.com/preview/ndcadf197ab44?prev_access_key=12101ab62ae75cc05091fa755193c8dd

■AIに解決してもらう問題について

「マレットでパックに当てても動かない!衝突判定のためのCollisionShape2Dもあるのに!(笑)」という問題です。
以下のgifのような状態です。
※敵AIもバグっていますが気にしないでください(;´Д`)

動かない、、

そしてここからはAIの実力を試しつつ、修正していきます。
主にo3-highとGemini 2.0 pro Experimental 0205で比較します。
最初に投げかける質問は同じで、後は返答に応じてこちらが回答するようにします。
※補足:GeminiのthinkingモデルはExperimental 0205に及びませんでしたので省略します。o1とo3-highの差についてはわずかにo3に軍配でしたので同じく省略します。

①o3-mini-high

まず、こちらはスムーズに解決してくれました。
修正時のコードをまとめます。

extends CharacterBody2D

@export var speed = 300  # パドルの移動速度
@export var impulse_strength = 200  # 衝突時に付与するインパルスの強さ

func _physics_process(_delta):
    var direction = Vector2.ZERO
    if Input.is_action_pressed("move_up"):
        direction.y -= 1
    if Input.is_action_pressed("move_down"):
        direction.y += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_right"):
        direction.x += 1

    if direction.length() > 0:
        direction = direction.normalized()

    velocity = direction * speed
    move_and_slide()

    # 衝突情報の取得とログ出力、インパルスの付与
    var collision_count = get_slide_collision_count()
    var impulseApplied = false  # 同じフレームで複数回の適用を防止する例
    for i in range(collision_count):
        var collision = get_slide_collision(i)
        var collider = collision.get_collider()  # RigidBody2D である Puck を取得
        print("衝突対象:", collider.name, " (ID:", collider.get_instance_id(), ")")
        # 衝突対象が RigidBody2D であれば、インパルスを付与
        if collider is RigidBody2D and not impulseApplied:
            # 衝突の法線方向にインパルスを与える(数値は例として impulse_strength を使用)
            var impulse = collision.get_normal() * impulse_strength
            collider.apply_impulse(impulse)
            impulseApplied = true

修正してもらった上で、以下のように結局何が問題だったのか?
そしてどのような意図で今回の修正をしてもらったのか?をまとめてもらいました。

原因のまとめ

根本原因:CharacterBody2D の性質
Player1(マレット)が CharacterBody2D を継承しているため、自身の移動はコードで直接制御されています。
その結果、物理シミュレーションによる自動的な力の伝達(たとえば、衝突時の反発やインパルスの適用)は行われません。
つまり、衝突は検出されても、プレイヤー自体が物理的な「押す」力をパック(RigidBody2D)に伝えないため、パックが動かないという現象が発生していました。


修正の意図と選択理由:意図:
衝突が検出された際に、手動でパックに対して物理的な力(インパルス)を与えることで、プレイヤーの動きに応じたパックの反応を実現すること。
これにより、CharacterBody2D をそのまま利用しつつ、期待通りにパック(RigidBody2D)が動くようにします。
なぜこの修正を選んだか:
既存コードの変更が最小限:
Player1 の移動ロジックや入力処理はそのまま維持し、衝突検出後の追加処理としてインパルス付与を実装したため、全体のリファクタリングが不要でした。
柔軟な制御:
衝突時の状況(衝突法線の方向など)を利用して、パックに与えるインパルスを明示的に計算できるので、ゲームの挙動を細かく調整可能です。
物理シミュレーションの活用:
パック自体は RigidBody2D で物理シミュレーションが働くため、インパルスを与えることで正しく動作するようになります。


結論として:
問題の原因は、CharacterBody2D を用いた移動方法が物理エンジンによる自動的な力の伝達を行わないため、衝突してもパックに押す力が働かなかった点にあります。
そのため、衝突検出時に明示的に apply_impulse() を用いてパックにインパルスを与える修正を加えたことで、期待通りの物理的な反応(パックが動く)が実現されました。

なるほど、、apply_impulse()の撃力を加える必要があるのですね。。
高校の物理でやったようなうっすい記憶が、、、(笑)


②Gemini 2.0 pro Experimental 0205

こちらもクリアできました。
少しコードは異なるのですが、指摘されることや質問される内容は非常に似通っていて面白かったです。

extends CharacterBody2D

@export var speed = 300  # パドルの移動速度
@export var impulse_strength = 100 #インパルス強度

func _physics_process(_delta):
	var direction = Vector2.ZERO
	if Input.is_action_pressed("move_up"):
		direction.y -= 1
	if Input.is_action_pressed("move_down"):
		direction.y += 1
	if Input.is_action_pressed("move_left"):
		direction.x -= 1
	if Input.is_action_pressed("move_right"):
		direction.x += 1

	if direction.length() > 0:
		direction = direction.normalized()

	velocity = direction * speed
	move_and_slide() # 衝突情報を取得
	
	for i in get_slide_collision_count():
		var collision = get_slide_collision(i)
		if collision.get_collider() is RigidBody2D:
			var puck = collision.get_collider()
			# 衝突した相手がRigidBody2Dならインパルスを加える
			puck.apply_impulse(collision.get_normal() * impulse_strength)

完璧に動作したとのこと、おめでとうございます! そして、最終的に動作したコードを共有いただき、ありがとうございます。

**原因の特定**

今回の問題の根本的な原因は、以下の2点に集約されます。

1. **`RigidBody2D` への力の適用方法の誤り:**
* 当初のコードでは、`CharacterBody2D` の `move_and_slide()` メソッドを使って Player1 (マレット) を移動させていましたが、このメソッドは `RigidBody2D` である Puck に直接力を伝えるようには設計されていません。
* `RigidBody2D` には、`apply_impulse()` や `apply_force()` といった、外部から力を加えるための専用のメソッドを使用する必要があります。

2. **`move_and_slide()` の戻り値と衝突情報の取得方法の誤解 (Godot 4):**
* 以前のバージョンの Godot (Godot 3.x など) では、`move_and_slide()` は衝突情報を戻り値として返していましたが、Godot 4 では仕様が変更されました。
* Godot 4 では、`move_and_slide()` は戻り値を返さず、衝突情報は `get_slide_collision_count()` と `get_slide_collision()` を使って取得する必要があります。
* 最初の修正提案では、この Godot 4 での変更点を考慮していなかったため、`move_and_slide()` の戻り値 (実際には `false`) に対して `get_collider()` を呼び出そうとしてエラーが発生しました。

**修正意図**

修正の意図は、上記の問題点を解決し、`RigidBody2D` である Puck に対して、`CharacterBody2D` である Player1 が衝突した際に、物理的に正しい力を加えることでした。具体的には:

1. **`move_and_slide()` の正しい使用:** `move_and_slide()` は Player1 の移動を処理しつつ、衝突を検出する役割を果たします。
2. **衝突情報の取得:** `get_slide_collision_count()` と `get_slide_collision()` を使用して、発生した衝突の情報を取得します。
3. **`RigidBody2D` の特定:** 衝突相手が `RigidBody2D` (つまり Puck) であることを確認します。
4. **`apply_impulse()` による力の適用:** 衝突の法線ベクトルと指定した `impulse_strength` に基づいて、`apply_impulse()` メソッドで Puck に瞬間的な力を加えます。これにより、Puck が物理的に正しい挙動で動きます。

**その他の設定について**

`CollisionLayer`、`CollisionMask`、`RigidBody2D` の `Mode` などの設定も、衝突判定や物理挙動に影響を与える重要な要素です。しかし、今回の問題の直接的な原因は、上記で説明した力の適用方法と衝突情報の取得方法の誤りでした。

最初にこれらの設定について確認したのは、問題の切り分けを行うためです。もし、これらの設定が誤っていた場合(例えば、`CollisionLayer` と `CollisionMask` が互いに衝突しない設定になっていた場合)、コードを修正しても Puck は動かなかったでしょう。

結果として、これらの設定は問題なかったため、コードの修正だけで解決できましたが、問題解決のプロセスとしては、関連する可能性のある要素を一つずつ確認していくことが重要です。

修正後はどちらも以下のようになりました!
すごい、、、。
※敵AIはパックを追従するだけのプログラムなので一緒に追いかけてます(笑)

動くようになった!!


■修正内容の違い

AIにそれぞれのコードの違いをまとめてもらいました。
Geminiは最低限の修正ですが、忠実に再現するイメージ。
o3は修正しつつもプラスワンなイメージですね。
o3のプラスワンである、「複数回のインパクトがあった場合は1回にする」というのはバグを防ぐうえでは必要かもしれませんが、ゲーム的には連続的に跳ね返るのも面白い気もするので、優劣は付けがたい結果でした。

o3
複数の衝突があった場合にインパルスの過剰適用を防ぐため、安定した物理挙動が得られる。
衝突情報をログ出力しているため、デバッグ時にどのオブジェクトと衝突しているかを確認しやすい。
Gemini
コードがシンプルで読みやすい。
もしすべての衝突に対して個別のインパルス付与を意図する場合(例えば、複数の接触点それぞれから適切な反発が欲しい場合)には、こちらの実装が適している。

■最後に

という事でエアホッケーゲームを通して、AIの実力を簡単に試してみましたが、いかがでしたでしょうか。
ただ、今回レベルの依頼であればそこまで差が付かなかったというのが現状です。
今後もう少し触りつつ、同時にゲーム開発もどんどんレベルアップしていければと思います。
AIとともに僕も成長するぞ!!(笑)

では本日はここまでとします!
次回以降もゲームどんどん作っていきますのでよければいいねやコメントを頂けると幸いです!


いいなと思ったら応援しよう!

この記事が参加している募集