【Godot 4.0】スマホ３Dゲームを作るための勉強　その１１　敵も攻撃するようにしました

 ゲームエンジンGodot4.0で3Dスマホゲームを作りたいと思いますが、その前にお勉強しています。
 [Godot_v4.0-rc1_win64.exe.zip](https://downloads.tuxfamily.org/godotengine/4.0/rc1/Godot_v4.0-rc1_win64.exe.zip)を使用しています。

# 目的
　敵も攻撃するようにしました

# ベースプロジェクト
下記で作成したプロジェクトをベースに機能追加をします。

　【Godot 4.0】スマホ３Dゲームを作るための勉強　その１０　敵を増やしました。またBullet vs Enemyの処理を実装しました。
　https://qiita.com/FootInGlow/items/195236f04bba83cad129

　github(Godotのプロジェクトマネージャーからインポートして利用できます）
　https://qiita.com/FootInGlow/items/195236f04bba83cad129

# bulletシーンを複製してEnemyBulletシーンを作成します。
　bulletシーンを複製しましょう。

　方法１：bullet.tscnを開いて、シーンメニューから「名前を付けてシーンを保存...」
　方法２：ファイルシステム上でbullet.tscnを右クリックから「複製」する
　
　複製して「enemybullet.tscn」に名称変更します。

# EnemyBulletシーンを変更する　
　enemybullet.tscnを開いて、手前方向にまっすぐ進む、画面外に出たら消えるというシンプルなものに修正します。

* ルートノードのBulletをEnemyBulletに名称変更する
* EnemyBulletのCollision Layer/Maskのチェックをいったんはずします。
　後できちんと設定します。
![(_) Enemybullet.tscn - S11_enemy_fire - Godot Engine 2023_03_02 11_31_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/d2668bd0-a2c7-ee78-dadd-3966e9471a1b.png)

* EnemyBulletを右クリックして、「スクリプトをアタッチする」
　Enemybullet.gdという名前でスクリプトを作成します。

* スクリプトを編集する
　下記のように編集します。

``` GDScript
extends CharacterBody3D

@export var m_v_dir = Vector3.BACK
@export var m_d_speed_mps = 1.0

func _physics_process(delta):
	velocity = m_v_dir.normalized() * m_d_speed_mps
	move_and_slide()

func _on_visible_on_screen_notifier_3d_screen_exited():
	queue_free()
```

* @export var m_v_dir = Vector3.BACK
 Player方向に向かうようにしたいので、FORWARDではなくBACK定数を使用します。

# Enemyから発射する
　EnemyからEnemyBulletを発射します。
　6秒おきに３発づつ発射するようにします。3発のEnemyBulletは0.5秒間隔で発射たいと思います。

## Timerノードを２つ追加します
　enemy.tscnを開いて、Timerを２つ追加します。名称を「Timer_fire」と「Timer_short」に変更しましょう。
![enemy.tscn - S11_enemy_fire - Godot Engine 2023_03_02 13_13_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9e2544b9-0f7a-1f81-7418-88a84b1f08c7.png)

**Timer_fire**
 発射動作間隔としてWait Timeを6sに設定します。また、Autostartをオンにしましょう。
![enemy.tscn - S11_enemy_fire - Godot Engine 2023_03_02 13_14_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f18b55f3-d046-583c-8cbc-245920216482.png)

**Timer_short**
　Wait Timeを0.5sにします。Timer_shortはGDScriptからタイマーを開始して、3回timeoutしたら（3回発射したら）停止するように実装しますので、Autostartは初期値のままオフです。
![enemy.tscn - S11_enemy_fire - Godot Engine 2023_03_02 13_14_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/95c90bb8-1ce9-5850-368f-cc0cb0f27f49.png)

## timeoutシグナルをEnemyに接続します
　Timer_fireを選択して、インスペクタの横のノードタブのシグナルを開いて、timeoutシグナルを接続します。
![スクリーンショット (52).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/1df7711e-eca9-3e7f-9134-2f20960772c1.png)
　接続先はEnemyです。
![スクリーンショット (53).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f5774c4b-4323-98f0-4014-1b48f472617b.png)

　Timer_shortについても同様にtimeoutシグナルをEnemyに接続します

　GDScriptが下記のようになります。
　_on_timer_fire_timeoutメソッドと、_on_timer_short_timeoutメソッドが追加されました。

``` GDScript
extends CharacterBody3D

@export var m_attack_power_ps = 1.0
@export var m_hp = 10

func _physics_process(delta):
	if m_hp < 0.0:
		queue_free()
	
func _on_bullet_sensor_body_entered(body):
	body.discover_enemy(transform.origin)

func _on_timer_fire_timeout():
	pass # Replace with function body.

func _on_timer_short_timeout():
	pass # Replace with function body.
```

## 発射ポイントを追加します。
　Enemyノードに、Marker3Dを追加します。
　Node3D/Transform/position/Zを0.7にします。
　![(_) enemy.tscn - S11_enemy_fire - Godot Engine 2023_03_02 13_27_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f0cf91cc-bf5c-6798-db4a-bc9dbcdd9e4b.png)

## GDScriptにPackedSceneを@exportして、enemybullet.tscnを登録します。
 enemy.gdにメンバ変数を追加します。
``` GDScript
@export var m_scn_enemybullet : PackedScene
```
　enemy.tscnを開いて、Enemyノードを選択します。
　ファイルシステムのenemybullet.tscnをインスペクタのEnemy.gd/M Scn EnemyBulletの\<empty>の上にドラッグアンドドロップします。
![スクリーンショット (54).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9c87624b-12d9-c237-e830-396eec0aae12.png)
　登録されました。
![スクリーンショット (55).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/0f196c14-4107-adb2-14f3-02439905c66f.png)

## enemy.gdを修正して、発射する

```GDScript
extends CharacterBody3D

@export var m_attack_power_ps = 1.0
@export var m_hp = 10

@export var m_scn_enemybullet : PackedScene
@export var m_i_fire_num = 3

var m_i_remain_fire_cnt = 0

func _physics_process(delta):
	if m_hp < 0.0:
		queue_free()
	
func _on_bullet_sensor_body_entered(body):
	body.discover_enemy(transform.origin)

func _on_timer_fire_timeout():
	# 発射する段数を設定
	m_i_remain_fire_cnt =  m_i_fire_num
	# タイマースタート
	$Timer_short.start()

func _on_timer_short_timeout():
	# EnemyBulletを発射する
	var scn_enemybullet = m_scn_enemybullet.instantiate()
	scn_enemybullet.transform.origin = $Marker3D.global_transform.origin
	add_sibling(scn_enemybullet)
	
	# 発射残数を減算して、０になったらタイマーを停止する
	m_i_remain_fire_cnt -= 1
	if m_i_remain_fire_cnt <= 0:
		$Timer_short.stop()
	

```

**メンバ変数の追加**
* @export var m_i_fire_num = 3
　一度に発射する弾数をパラメータして定義
* var m_i_remain_fire_cnt = 0
　発射残数をカウントするためのメンバ変数

**func _on_timer_fire_timeout()：Timer_fireのタイムアウト処理**
　初期値6秒周期の発射開始のタイミングです。
* m_i_remain_fire_cnt =  m_i_fire_num
　発射する弾数を初期化する
* $Timer_short.start()
　発射用ショートタイマーを開始する

**func _on_timer_short_timeout():Time_shortのタイムアウト処理**
　0.5間隔で3発発射するためのタイムアウト処理です。3発発射したらタイマーを停止します。

* var scn_enemybullet = m_scn_enemybullet.instantiate()
　enemybullet.tscnシーンのインスタンスを生成します。
* scn_enemybullet.transform.origin = $Marker3D.global_transform.origin
　位置を設定します。$Marker3DはEnemyの子ノードのためグローバル座標系の位置を取得します
* add_sibling(scn_enemybullet)
　Enemyと同じ階層にインスタンスを追加します。
* m_i_remain_fire_cnt -= 1
　弾数を１減算します。
* if m_i_remain_fire_cnt <= 0:
　0（以下）になった場合、予定弾数を発射し終わったということです。
* $Timer_short.stop()
　ショートタイマーを停止します。
　
　実行しましょう。
　![Screenshot 2023_03_02 13_51_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/3b9aac93-00bf-7de3-ce79-c215d5c91852.png)

# BulletとEnemyBulletが相殺するようにします。
　BulletでEnemyBulletを相殺するようにします。

## EnemyBulletのCollision Layer/Maskを設定しましょう。
　enemybullet.tscnを開いて、EnemyBulletノードを選択します。
　インスペクタのCharacterBody3D/CollisionのLayerの3:Enemyをチェックします。
 CollisionのMaskは１～４まですべてチェックします。
![スクリーンショット (57).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/89127c79-9dbb-ba26-7474-0b7e2f017b5c.png)

## EnemyBulletにEnemyGroup設定しましょう。
　Bulletは「EnemyGroup」を持つノードを攻撃しますので、EnemyBulletにもグループを追加します。

　EnemyBulletノードを選択した状態で、ノード/グループを選択します。
　EnemyGroupを入力して、追加ボタンを押下します。
![enemy.tscn - S11_enemy_fire_002 - Godot Engine 2023_03_02 14_00_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/1ffbb54f-334c-0f5a-7b07-13111c23b90d.png)
　追加されました。
![enemy.tscn - S11_enemy_fire_002 - Godot Engine 2023_03_02 14_00_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/aeff9211-47d7-4f13-b554-479e067eefe3.png)
　EnemyBulletノードの横にも四角の中に●があるアイコンが追加されました。
![enemy.tscn - S11_enemy_fire_002 - Godot Engine 2023_03_02 14_00_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/5fd6cc57-ba97-9e72-a05a-dacf18ca546e.png)

　Bulletは衝突したノードのうちEnemyBulletをグループに持つノードとバトルをします。
　enemybullet.gdを開いて攻撃力とHPを設定します。

``` GDScript
extends CharacterBody3D

@export var m_attack_power_ps = 0.5
@export var m_hp = 0.5

@export var m_v_dir = Vector3.BACK
@export var m_d_speed_mps = 1.0

func _physics_process(delta):
	velocity = m_v_dir.normalized() * m_d_speed_mps
	move_and_slide()
	if m_hp < 0.0:
		queue_free()
		
func _on_visible_on_screen_notifier_3d_screen_exited():
	queue_free()
```

**攻撃力とHPを定義**
 少し弱めに定義しました。
* @export var m_attack_power_ps = 0.5
* @export var m_hp = 0.5

**func _physics_process(delta):でHPがなくなったら削除する**
 HPが0以下になった場合、削除します。
* if m_hp < 0.0:
* queue_free()

　実行しましょう。
　BulletとEnemyBulletはぶつかったらお互い避けてしまいますね...

# Bulletはバトル中はバトル相手に向かうように修正する
　Playerを守りたいので、Bulletはバトル相手を追いかけるようにします。

　bullet.gdを開いて修正しましょう。

　修正内容としては下記になります。

* 半径３ｍの円に入ったときに検知して通知されたEnemyの位置の保存先はm_v3_sensing_posに変更する

* m_v3_enemy_posは敵目標位置に変更します。
　優先順位を決めて目標位置を設定します。
　　1番目：現在戦闘中のEnemyもしくはEnemyBullet
　　2番目：半径３ｍの円に入ったときに検知して通知されたEnemyの位置
　　3番目：なし（前進）

 下記が修正後のスクリプトになります。

``` GDScript
extends CharacterBody3D

@export var m_v_dir = Vector3.FORWARD
@export var m_d_speed_mps = 2.0

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

var m_v3_sensing_pos = null
var m_v3_enemy_pos = null

func _physics_process(delta):
	if m_v3_enemy_pos:
		m_v_dir = m_v3_enemy_pos - transform.origin
	else:
		m_v_dir = Vector3.FORWARD
	velocity = m_v_dir.normalized() * m_d_speed_mps
	move_and_slide()
	
	# バトル判定
	var battle_pos = null
	for index in range(get_slide_collision_count()):
		var collision = get_slide_collision(index)
		if (collision.get_collider() == null):
			continue
		if collision.get_collider().is_in_group("EnemyGroup"):
			var enemy = collision.get_collider()

			# バトル中に相手の位置を保持
			battle_pos = enemy.transform.origin

			# 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 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
	
func discover_enemy(enemy_pos):
	m_v3_sensing_pos = enemy_pos
	
func _on_visible_on_screen_notifier_3d_screen_exited():
	queue_free()
```

　スクリプトが長くなってきたので部分ごとに変更点を説明します。

**メンバ変数の追加**
　m_v3_sensing_posを追加しました。
```GDScript
var m_v3_sensing_pos = null
var m_v3_enemy_pos = null
```
**Enemyから通知された位置をクリアする処理を削除**
 Enemyから通知された位置をクリアする処理がelse:の上にありましたが、削除しました。（後の方に移動しました）
```GDScript
	if m_v3_enemy_pos:
		m_v_dir = m_v3_enemy_pos - transform.origin
	else:
		m_v_dir = Vector3.FORWARD
	velocity = m_v_dir.normalized() * m_d_speed_mps
	move_and_slide()
```
**バトル中の相手の位置を保存**
　battle_posを追加して、バトル中の相手の位置を保存します。
``` GDScript
	# バトル判定
	var battle_pos = null
	for index in range(get_slide_collision_count()):
		var collision = get_slide_collision(index)
		if (collision.get_collider() == null):
			continue
		if collision.get_collider().is_in_group("EnemyGroup"):
			var enemy = collision.get_collider()

			# バトル中に相手の位置を保持
			battle_pos = enemy.transform.origin
```
**目標位置を選択**
　目標位置を選択してm_v3_enemy_posに設定する処理を追加しました。
　バトル相手がいる場合はバトル相手の位置をm_v3_enemy_posに保存します。
　次に、m_v3_sensing_posが設定されている場合は、半径３ｍの円で検知した敵の位置を保存します。
　ただし、円で発見した敵がいなくなった場合にm_v3_sensing_posを初期化してとどまらないようにします。
　それ以外は。nullを設定して、ただ前進するのみです。
``` 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
```
**保存する変数を変更**
　半径3mの円で発見した敵の位置を保存するメンバを変更しました。

``` GDScript
func discover_enemy(enemy_pos):
	m_v3_sensing_pos = enemy_pos
```

　実行しましょう。
　先ほどより撃ち落としやすくなったと思います。

以上です。