Godot4で3D風２D縦スクロールシューティングゲームを作る　6回目 レーザーで敵を倒せるようにする

Godot4.1.3でシューティングゲームを作っていきます。
# 衝突判定を実装する
　自機が発射したレーザーが敵に当たった時に敵が消滅するように。また自機が敵もしくは敵の弾に当たったときに消滅するように実装します。
　Area3Dのarea_enterd(area: Area3D)シグナルを使用します。

# github
　本記事で実装したものをgithubで公開しています。

https://github.com/footinglow/woden/tree/006_collision

# 衝突検出のためのレイヤーの設定をする
　自機は、敵もしくは敵の弾に衝突したことを検出し、レーザーは敵のみに衝突しますので、自機、レーザー、敵、敵の弾の4種類の衝突用レイヤーがあればよいと思います。
　Layar1はディフォルト設定のため、Collision設定を忘れていた時にわかるように、未使用にします。衝突しないものに使います。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/07283fc9-0348-a80e-9b78-8c869948b658.png)

# PlayerのルートノードをArea3Dに変更する
　PlayerのルートノードはとりあえずCharacterBody3Dにしていましたが、Area3Dに変更して、衝突を検出できるようにします。
　Player.tscnを開き、ルートノードを右クリックして「型を変更」を実行し、Area3Dを選択します。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/ff8f931f-410a-5730-340f-1d9f31a956a1.png)
　Player.gdを開き、先頭のextendsをArea3Dに変更します。
 ```diff_gd_script
 -extends CharacterBody3D
 +extends Area3D
 ```
　CharacterBody3Dはmove_and_slideメソッドで移動しますが、Area3Dの場合は直接positionに対して移動量を更新します。
 ```diff_gd_script
				# CharacterBody3Dのvelocityに速度を設定して動かす
-			velocity = v_velocity
-			move_and_slide()
+			position += v_velocity * delta
```
 
# レーザーが衝突した敵を消去する
　レーザーと敵の衝突をArea3Dのarea_enteredイベントで検出しますが、レーザー側でイベントを受けて、検出するようにします。衝突した場合、1回で敵もレーザーも消えるのではなく、固い敵や、強いレーザーを実現したいので、以下のようにします。

・レーザーはレーザーパワーを数値として持つ
・敵はHP（ヒットポイント）を数値として持つ
・レーザーが敵に衝突した場合、敵のHPからレーザーパワーを引き算する
・HPが0になると敵は消滅
・レーザーパワーが0になった場合、レーザーが消滅

 ```plantuml
物理エンジン->my_laser : area_entered
my_laser -> 敵 : calc_damage(レーザーのパワー） 
敵->敵:HPからレーザーパワーを引き算
敵 --> my_laser : レーザーパワーの余り 
敵->敵:HPが0になった場合消滅
my_laser->my_laser : レーザーパワーが0に\nなった場合消滅
```
## レーザーが当たったときの敵の実装
　res://character/enemy/twin_body.tscnを開き、ルートノードを選択して、インスペクターのCollisionを開きます。Collisionは、自身の種類をLayerに設定するので、4番のEnemyをチェックします。敵は衝突を検出しない(area_enteredイベントを受ける必要がない）のでMaskは何も設定しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/64786529-467a-0b80-9484-dbc71ec602ae.png)
　敵のスクリプト(twin_body.gd)を以下のように修正します。シーケンスのように、calc_damageメソッドは物理エンジンからmy_laserを経由して呼ばれるので、数値計算のみの軽い処理にとどめて、重い処理や物理エンジンにかかわる処理は自身の_physics_process()メソッドの中で実行するようにします。
 　
```diff_gd_script
extends Area3D

@export var _d_speed_mps = 20.0
+ @export var _d_hp = 1

func _physics_process(delta):
	global_position += Vector3(0, 0, 1) * _d_speed_mps * delta
+ 	if _d_hp <= 0:
+ 		queue_free()

func _on_visible_on_screen_notifier_3d_screen_exited():
	queue_free()

+ # HPからダメージポイントを引き算する。余った分はreturn値で返す
+ func calc_damage(laser_power) -> int:
+ 	var remainder = 0
+ 	if _d_hp <= laser_power:
+ 		# ダメージの方が大きい
+ 		remainder = laser_power - _d_hp
+ 		_d_hp = 0
+ 	else:
+ 		# HPの方が大きい
+ 		_d_hp -= laser_power
+ 		remainder = 0
+ 	# 余ったダメージポイントを返す
+ 	return remainder
```
　calc_damageメソッドでは、敵のHPからレーザーパワーを引き算して、余ったレーザーパワーをreturnで返します。
　HPが0になった場合に、消滅する処理は_physics_process()メソッドの中で実行します、

## 敵に衝突したときのレーザーの実装
　res://character/player/my_laser.tscnを開き、ルートノードを選択して、インスペクターのCollisionを開きます。Layerは3番のMyLaserをチェックします。敵との衝突を検出したいので、Maskは4番のEnemyをチェックします。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/60e436c2-64f7-ccb0-2b4e-c6a973c20747.png)

　ルートノードを選択した状態で、インスペクタの右のノード/シグナルにある、area_entered(area:Area3D)をスクリプトに接続します。
　my_laser.gdスクリプトを以下のように修正します。

```diff_gd_script
extends Area3D

@export var _d_speed_mps = 25.0		# 単位はm/s　
+ @export var _d_power_point = 1
@export var _v_dir = Vector3(0, 0, -1)

func set_pos(player_pos):
	global_position = player_pos
	
func _physics_process(delta):
	global_position += _v_dir * _d_speed_mps * delta
+ 	if _d_power_point <=0:
+ 		queue_free()

# 画面外に出た場合、消える
func _on_visible_on_screen_notifier_3d_screen_exited():
	queue_free()
+
+ func _on_area_entered(area_enemy):
+ 	_d_power_point = area_enemy.calc_damage(_d_power_point)
```
　_on_area_enteredの引数は、CollisionのMaskで設定した、EnemyのArea3Dです。calc_damageメソッドをレーザーパワーを引数にして起動し、return値をレーザーパワーに戻します。
 　_physics_processメソッドの中でレーザーの消滅判定をします。

# 敵（もしくは敵の弾）が衝突したときのPlayerの実装
　res://character/player/player.tscnを開き、ルートノードを選択して、インスペクターのCollisionを開きます。Layerは2番のPlayerをチェックします。敵もしくは敵の弾を検出したいので、Maskは4番のEnemyと5番のEnemyBulletをチェックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9946673e-423b-faf4-722e-43bfc54cdb48.png)
　ルートノードを選択した状態で、インスペクタの右のノード/シグナルにある、area_entered(area:Area3D)をスクリプトに接続します。
　palyer.gdスクリプトを以下のように修正します。

```diff_gd_script
extends Area3D

# Player
@export var _d_speed_mps = 100.0
+@export var _d_hp = 1.0

# ドラッグ操作検知用
var _f_is_screen_touch = false		# 画面にタッチ（ドラッグ含む）している場合true、タッチしていない場合はfalse
var _v2_drag_pos = Vector2.ZERO		# 最新のタッチ位置・ドラッグ位置

# タッチしたときの、３D上のタッチ位置とPlayer位置
var _v_first_touch_pos   = null
var _v_first_touch_player_pos = Vector3.ZERO

# 画面をタッチした位置から、検索する３Dオブジェクトまでの最大距離
@export var _d_ray_length_m = 1000		# [m]十分な長さにする

# レーザーの発射タイミング
@export var _scn_laser : PackedScene
@export var _d_firing_interval_sec = 0.2
var _d_firing_remain_time_sec = 0

func _input(event):
	# 画面にタッチしている状態と、タッチしていない状態を判断する
	if event is InputEventScreenTouch:
		_f_is_screen_touch = event.is_pressed()
		# ドラッグ位置（タッチ位置）を更新する
		_v2_drag_pos = event.position	
	elif event is InputEventScreenDrag:
		# ドラッグしているということは、タッチしている。（参考：InputEventScreenDragの場合、event.is_pressed()は常にfalse)
		_f_is_screen_touch = true
		# ドラッグ位置（タッチ位置）を更新する
		_v2_drag_pos = event.position	
	else:
		pass

func _physics_process(delta):
	if _f_is_screen_touch:
		var camera = get_viewport().get_camera_3d()

		# カメラを利用して3D空間のカメラの３D位置と、カメラからタッチしたピクセルを見た方向の1000m先の３D位置を計算する
		var from3d = camera.project_ray_origin(_v2_drag_pos)
		var to3d = from3d + camera.project_ray_normal(_v2_drag_pos) * _d_ray_length_m

		# 3D ray physics queryの作成
		var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
		query.collide_with_areas = true		# Area3Dを検知できるようにする
		
		#　spaceと呼ばれる、物理３D空間状態の情報を利用して、Area3Dを含む衝突位置を計算する
		var space_state = get_world_3d().direct_space_state
		var result = space_state.intersect_ray(query)
		if result:
			# 衝突位置を取得
			var v_drag_pos = result.position
			v_drag_pos.y = global_position.y	# Y方向には移動しないための設定
			if _v_first_touch_pos == null :
				# 初めてタッチしたときの、3D位置を記憶する
				_v_first_touch_pos = v_drag_pos
				# 初めてタッチしたときの、プレイヤーの3D位置を記憶する
				_v_first_touch_player_pos = global_position
			else:
				pass
			
			# ３D空間の移動目標位置
			var v_target_pos = _v_first_touch_player_pos + ( v_drag_pos - _v_first_touch_pos )
			
			# Player位置から移動目標位置までの相対位置を計算
			var v_target : Vector3 = v_target_pos - global_position
			# 移動目標位置に向かう速度ベクトルを生成
			var v_velocity : Vector3 = v_target.normalized() * _d_speed_mps
			if v_target.length() < ( v_velocity * delta ).length():
				# 目的地までの距離が、delta当たりの移動量よりも小さい場合は、飛び越えてしまうため、Playerの位置を目標位置にする
				global_position = v_target_pos
			else:
				# CharacterBody3Dのvelocityに速度を設定して動かす
				position += v_velocity * delta
		else:
			# タッチ位置に衝突したものが無い
			pass
	else:
		# タッチしていない場合はnullを設定することで、次にタッチしたときに、新しいタッチ位置とPlayer位置を記憶する
		_v_first_touch_pos = null
		
	if _f_is_screen_touch:
		# タイミングをとりながらlaser発射
		if _d_firing_remain_time_sec > 0:
			# 次の発射までの残り時間を減算するのみ
			_d_firing_remain_time_sec -= delta
		else:
			# レーザーを発射する
			var scn : Area3D = _scn_laser.instantiate()
			scn.set_pos(global_position)
			add_sibling(scn)
			# 発射間隔を設定
			_d_firing_remain_time_sec = _d_firing_interval_sec
	else:
		# タッチしたときにすぐ、レーダーが発射するようにする
		_d_firing_remain_time_sec = 0

+	# HPが0になった場合、Playerは消滅する
+	if _d_hp <= 0.0:
+		queue_free()
		
+func _on_area_entered(area):
+	# 敵もしくは敵の弾の攻撃があたったため、HPを0にする
+	_d_hp = 0.0
```

# おわりに
　次のは敵の弾を実装したいと思います。

 以上です。