【Godot 4.0】スマホ３Dゲームを作るための勉強　その３３　デバッグ

　ゲームエンジンGodot4.0で3Dスマホゲームを作りたいと思いますが、その前にお勉強しています。
　2023/3/1にstable版がリリースされました。
　Godot_v4.0-stable_win64.exe.zipを使用しています。

# 目的
　倒した敵の砦の周りに、Bulletがとどまる問題があるので対処します。

# ベースプロジェクト
　下記で作成したプロジェクトをベースに機能追加をします。
　【Godot 4.0】スマホ３Dゲームを作るための勉強　その３２　WorldEnvironmentで青空を設定する
　https://qiita.com/FootInGlow/items/a61c28b0c12f96a7e034
　
　github(Godotのプロジェクトマネージャーからインポートして利用できます）
　https://github.com/footinglow/Godot4/tree/main/02_study/S32WorldEnvironment
　※別途ダウンロードが必要です。
　　　res://assets/texture/godot-material-dpa-1.0.1/Readme.txt及び
　　　res://assets/texture/AllSkyFree1.0/Readme.txtを参照

# 問題内容　
　敵の砦を倒した後は、Bullet（白い人）は前進するようにプログラムを組んだつもりですが、敵の砦のあたりにとどまっています。
　![スクリーンショット (516).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/15ea575e-4484-60de-f595-adb044a19319.png)

res://Characters/bullet.gdの_physics_processメソッドの下記の処理で進行方向を決定します。


```GDScript
	# バトル中の相手、センシングした敵の順に目標位置を設定する
	if battle_pos:
		m_v3_enemy_pos = battle_pos
	elif m_v3_sensing_pos:
		if (m_v3_sensing_pos - transform.origin).length() < 0.1:
			m_v3_sensing_pos = null
		m_v3_enemy_pos = m_v3_sensing_pos
	else:
		m_v3_enemy_pos = null
```

1. battle_pos
　move_and_slide()実行後の物理エンジンの衝突結果を読みだして、"EnemyGroup"グループを持つノードとの衝突中の場合に衝突位置が設定されますが、敵の砦を倒した後なのでbattle_posはnullになるはず。

2. m_v3_sensing_pos
　Bulletが敵の砦の一定距離以内に近づくとセットされる変数です。
　倒した敵の砦が設定されていますが、敵の砦の位置と自身の位置が0.1m(10cm)以内になるとm_v3_sensing_posをクリアするようにしています。おそらく、m_v3_sensing_posがクリアされないからBulletがとどまっていると予想されるので、デバッガで見てみます。

# 解析する
　実行中に敵の砦のあたりにとどまっているBulletの変数の値を確認します。

# Bulletを減らす
　Bulletがいっぱいるので、1体になるまで減らします。
　実行中の状態で、シーンの「リモート」をクリックすると現在実行中のSceneTreeを見ることができます。
　root/GameSystem/CurrentStage/Stageの下にBulletがいっぱいいますね。
![スクリーンショット (517).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/179a0d1d-6df0-b39d-69d3-8bdf3c69731f.png)

　適当にBullet(@Bullet@19でもBullet8でも）を選択すると、インスペクタに情報が表示されます。
　Node3D/Transform/Positionのxに100mのような大きい値を設定して移動します。BulletにはVisibleOnScreenNotifier3Dノードを追加してありカメラビュー外に移動すると消えるように実装しているので、移動した瞬間にシーンから削除されます。
　Bulletがひとつになるまで繰り返します。
![スクリーンショット (518).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/8a0004d0-35bf-a826-5a84-bcd4b196e704.png)
シーンのリモートを見ると、@Bullet@3が残りました。
![スクリーンショット (519).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/254fefcf-863a-7c89-ec5b-fe9cfd66e7e5.png)

# スクリプトにBreakをしかける
　判定時の値を見たいのでbreakをかけて、プログラムを停止します。
　res://Characters/bullet.gdを開いて「if battle_pos:」の左側をクリックしてBreakポイントを設定します。
![スクリーンショット (520).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b4bc0ad4-c2ce-6ae5-67e5-0489d92d8e0d.png)
　実行中なので、Breakポイントを仕掛けると同時に停止しました。〇がブレイク箇所で「▶」が次に実行する行です。
　右下の方に、変数の値が表示されています。
![スクリーンショット (521).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/fe01ea58-a2a4-5a6b-0b88-d52551b97083.png)
　また変数表示の上にはステップ実行用のアイコンがありますので、これで1行ずつ処理を進めることができます。
　赤枠で囲ったアイコンの左側がステップイン、右側がステップオーバーです。
　ステップオーバーは1行ずつ実行します。ステップインも1行ずつ実行しますが関数の場合その中に移動します。
![スクリーンショット (522).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/e8641388-a57a-bb90-5789-6aa7381f0ac9.png)

　battle_posが予想通りnullなのは確認できました。
　ステップオーバーアイコンをクリックします。「elif m_v3_sensing_pos:」に「▶」アイコンが移動しました。次に実行する行を表しています。
![スクリーンショット (523).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/5ed72382-ee01-cc93-65ff-9ccc8db3a0c6.png)
　m_v3_sensing_posは設定されていてnullではないので、54行目に行くはずです。
　ステップオーバーします。
![スクリーンショット (524).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/ec864c23-8ff1-b483-792f-14aed1bc23dd.png)

　m_v3_sensing_posの値を確認します。(X=2m,y=0m,z=-8m)です。
　次にtransform.originを確認します。Members/selfの右にある「Object ID:xxxxxxxx」をクリックするとインスペクタに現在Breakで停止したオブジェクトの情報が表示されます。
![スクリーンショット (526).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/571ef5a6-1913-82da-b531-1cd7b8442ea9.png)
　インスペクタのNode3D/Transform/Positionは(X=1.998m、y=0.251m、z=-8m)と表示されていました。
![スクリーンショット (527).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9a6e2409-84e0-e5ba-fd89-039e9293c29e.png)

　進行方向のｚ軸と左右ｘ軸の距離で0.1mくらいを想定していましたが、y軸の値が0.251m差があるので、距離0.1m未満にならないことがわかりました。

# 対策検討
　距離の計算にｙ軸を使わないとか、閾値を0.1mから0.3mくらいに大きくするととりあえず動くようになりそうですが、そもそも敵の砦の中心にたどり着いたら敵の砦を倒したと判定していること問題がありそうです。
　スクリプトを大きく修正して、センシングした敵の砦ノードを覚えておいて、そのオブジェクトが無効になったら倒したと判定するようにします。

# 修正スクリプト

　Enemyからは位置の代わりに、自身のオブジェクトを送るようにします。
　「self」が自分自身を表しています。
res://Characters/Enemy.gd
```GDScript
func _on_bullet_sensor_body_entered(body):
	body.discover_enemy(self)
```

Bulletのスクリプトは下記のように修正します。

res://Characters/bullet.gd
```GDScript
extends CharacterBody3D

@export var m_d_speed_mps = 2.0

@export var m_attack_power_ps = 1.0
@export var m_hp = 1

var m_v_dir = Vector3.FORWARD
var m_node_sensing_enemy = null

# 通過したMultiplicationAreaを記憶するための辞書
var m_dict_multiarea_ids = {}

func _physics_process(delta):
	# まず動く
	velocity = m_v_dir.normalized() * m_d_speed_mps
	move_and_slide()

	# バトル判定
	var enemy = null
	for index in range(get_slide_collision_count()):
		var collision = get_slide_collision(index)
		if (collision.get_collider() != null):
			if collision.get_collider().is_in_group("EnemyGroup"):
				enemy = collision.get_collider()

				# Bulletの攻撃
				enemy.m_hp -= m_attack_power_ps * delta
				# Enemyの攻撃
				m_hp       -= enemy.m_attack_power_ps * delta

	# HPが0になったら消える
	if m_hp < 0:
		queue_free()

	# アニメーションの設定
	if enemy:
		$DesignMySoldier.animation_play("Battle")
	else:
		$DesignMySoldier.animation_play("Walk")

	# バトル中の相手、センシングした敵の順に移動方向を決定する
	if enemy:
		# バトル中の敵に向かうのを最優先にする
		m_v_dir = enemy.transform.origin- transform.origin
	elif m_node_sensing_enemy!=null and is_instance_valid(m_node_sensing_enemy):
		# センシングした敵の砦がある場合は敵の砦に向かう
		m_v_dir = m_node_sensing_enemy.transform.origin- transform.origin
	else:
		# 上記以外はまっすぐ進む
		m_v_dir = Vector3.FORWARD
	
func discover_enemy(enemy):
	m_node_sensing_enemy = enemy

```
　修正した部分について説明します。

　m_v_dirは現在進行している方向を格納するように役割を変更しました。
　また敵の砦のセンシング範囲に入ったときに、通知されるパラメータを位置(Vector3)から敵の砦オブジェクトに変更したので、変数名もm_node_sensing_enemyに変更しました。
```GDScript
var m_v_dir = Vector3.FORWARD
var m_node_sensing_enemy = null
```

処理の流れとしては下記のように整理しました。

①　move_and_slide()で移動する。物理エンジンによる衝突判定を済ませる。
②　衝突した相手がEnemyGroupの場合、お互いのHPを減算する
③　HPが0になったら消滅する
④　アニメーション(歩く　or　戦う）を設定する
⑤　次の周期に動く方向を決める。

　コードについて説明します。

**①　move_and_slide()で移動する。物理エンジンによる衝突判定を済ませる。**
　velocityに移動方向・速度を設定してmove_and_slide()を実行します。
　これにより物理エンジンによる衝突結果が更新されます。
```GDScript
func _physics_process(delta):
	# まず動く
	velocity = m_v_dir.normalized() * m_d_speed_mps
	move_and_slide()
```
**②　衝突した相手がEnemyGroupの場合、お互いのHPを減算する**
　物理エンジンから衝突情報を読みだしています。
　衝突している相手が"EnemyGroup"グループに参加している場合、enemy変数に衝突相手を設定します。
　Bullet自身と衝突相手のHPの減算処理をします。
```GDScript
	# バトル判定
	var enemy = null
	for index in range(get_slide_collision_count()):
		var collision = get_slide_collision(index)
		if (collision.get_collider() != null):
			if collision.get_collider().is_in_group("EnemyGroup"):
				enemy = collision.get_collider()

				# Bulletの攻撃
				enemy.m_hp -= m_attack_power_ps * delta
				# Enemyの攻撃
				m_hp       -= enemy.m_attack_power_ps * delta
```
**③　HPが0になったら消滅する**

```GDScript
	# HPが0になったら消える
	if m_hp < 0:
		queue_free()
```
**④　アニメーション(歩く　or　戦う）を設定する**

```GDScript
	# アニメーションの設定
	if enemy:
		$DesignMySoldier.animation_play("Battle")
	else:
		$DesignMySoldier.animation_play("Walk")
```
**⑤　次の周期に動く方向を決める。**
　enemy変数が設定されている場合、衝突・戦闘中の相手がいるのでenemyに向かうようにm_v_dirを設定します。
　戦闘中の相手がいない場合は、発見した敵の砦があるかチェックします。
　m_node_sensing_enemyがnullの場合はまだ敵の砦を発見していない状態です。
　m_node_sensing_enemyがnull以外の場合、敵の砦を発見しているということです。ただしすでに倒されているかもしれないので、is_instance_valid()で確認をします。
　is_instance_valid(m_node_sensing_enemy)==trueの場合敵の砦を発見してかつ現存しているので、m_node_sensing_enemyの方向にm_v_dirを設定します。
　is_instance_valid(m_node_sensing_enemy)==falseの場合、その敵の砦はすでにqueue_free()により削除されている(倒されている）ということです。
　上記以外の場合は前進したいので、m_v_dirにVector3.FORWARDを設定します。

```GDScript
	# バトル中の相手、センシングした敵の順に移動方向を決定する
	if enemy:
		# バトル中の敵に向かうのを最優先にする
		m_v_dir = enemy.transform.origin- transform.origin
	elif m_node_sensing_enemy!=null and is_instance_valid(m_node_sensing_enemy):
		# センシングした敵の砦がある場合は敵の砦に向かう
		m_v_dir = m_node_sensing_enemy.transform.origin- transform.origin
	else:
		# 上記以外はまっすぐ進む
		m_v_dir = Vector3.FORWARD
	
```

　敵の砦のセンシング範囲（円）に入ると敵の砦ノードが通知されますので、保存します。
```GDScript
func discover_enemy(enemy):
	m_node_sensing_enemy = enemy
```

# 対処確認します。
　敵の砦を倒した後は、前進するようになりました。
![スクリーンショット (529).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/89707e9c-88b8-17e9-3cab-c7bbc5adde81.png)




以上です。