Godot4で3D風２D縦スクロールシューティングゲームを作る 14回目 レーザーパワーアップ

Godot4.1.3でシューティングゲームを作っていきます。

# レーザーのパワーアップ！
　アップグレードアイテムが、パワーアップの時にゲットした場合、プレイヤー機が発射するレーザーのビジュアルと威力がアップするように実装します。
　初期段階は、正面方向にレーザー2本発射します。
　パワーアップを取ると、左右斜め方向にリング型のレーザーを発射します。
　さらにパワーアップを取ると、正面方向のレーザーの本数が増えて威力も強くなり、5段階目は一番強力な半月型のレーザーにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/5202e8e7-3e12-d938-bad9-44339ee117c8.png)

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

https://github.com/footinglow/woden/tree/014_pick_up_items_to_power_up

# レーザーのシーンの追加
　my_laser.tscnを複製して、４つの新しいレーザー用シーンを作成します。
　下図で選択されているのが、新規に追加したレーザー用シーンと、5段階目のレーザーシーンで使用するテクスチャ画像です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/debf0051-a726-9aae-e87b-4ece5f228dea.png)

| ファイル名| 用途 |
|:-|:-|
| laser_half_moon.png  | my_laser5.tscn用のテクスチャ画像  |
| my_laser3.tscn  | 第3段階目の3本のレーザー用シーン  |
| my_laser4.tscn  | 第4段階目の4本のレーザー用シーン  |
| my_laser5.tscn  | 第5段階目の半月型のレーザー用シーン  |
| my_ring_laser.tscn  | 第2弾目の前方斜めに発射するリング型レーザー用シーン  |

my_laser.tscnを複製後、共通で以下のことを実施しています。
 - ファイル名変更
 - CollisionShape3Dの大きさを修正
 - ルートノードのArea3Dのarea_entered(Area:Area3D)シグナルが接続されていることを確認
 - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/4809520f-7a28-ba8d-b6ad-5965d6f34cb6.png)
 - VisibleOnScreenNotifier3Dのscreen_exited()シグナルが接続されていることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/26bd6870-9702-d15c-80d5-fbd319d8487b.png)


## リング型レーザーの追加
　リングの形は、MeshInstance3Dのmeshプロパティで選択できるTorusMeshを使いました。
res://character/player/my_ring_laser.tscn
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/7b461a3a-225b-9e11-4dbb-1c8e85eb3270.png)
　MaterialプロパティにStandardMaterial3Dを設定して、AlbedoセクションのColorプロパティを設定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/7c7f51e6-604c-9852-2dc6-06b45c675617.png)
　リングの形を縦横に伸縮するようなアニメーションを設定したかったので、AnimationPlayerで、0.4秒間にScaleのx、zプロパティの値を1.0～0.5まで変更するようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/593173be-65e3-bb2d-3231-da4ed97b77ba.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/6b61882f-fee9-f39a-1b6b-68111ddf2a43.png)

## 3、4段階目のレーザー用シーンの追加
　MeshInstance3Dを増やしたのみです。

## 5段階目のレーザ用シーンの追加
　5段階目のレーザーはテクスチャ画像を使用するため、MeshInstance3DのmeshプロパティをPlaneMeshにしました。
 res://character/player/my_laser5.tscn
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/c70b25cc-4637-64a2-27bc-c4c53289cff2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/0390ecb3-09ce-755e-557b-fda7c0588f64.png)
　Materialプロパティは新規StandardMaterial3Dを追加し、AlbedoセクションのTextureプロパティにres://assets/texture/laser/laser_half_moon.pngを設定しました。
　画像の透明部分を有効にしたいので、TransparencyセクションのTransparencyをAlphaに変更しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/7ad324dd-8bf5-7aa5-d72f-da18a231cd3f.png)

# レーザースクリプトの修正
　レーザー用の機能は移動方向と威力が異なるだけで共通の処理が多いため、my_laser.gdをすべてのレーザーで共有します。そのため移動方向と威力を外部から変更するようにします。
　自分ルールで、アンダースコアで始まる変数は、スクリプトファイル内部用としていたので、外部に公開する変数、_d_power_pointと_v_dir、は先頭のアンダースコアを削除します。
　ついでに、レーザーの速度が遅いので、25m/sから50m/sに変更しました。
　
res://character/player/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)
+var _d_speed_mps = 50.0		# 単位はm/s　
+var d_power_point = 1
+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:
+    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)
+    d_power_point = area_enemy.calc_damage(d_power_point)
```

# レーザーの発射処理の修正

```diff_gd_script
extends Area3D

# Player
@export var _d_speed_mps = 25.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

+# パワーアップ段階（レベル）の定義
+var _i_laser_power_level = 0
+
+# レーザー用シーンと威力の定義
+var _aa_laser_scn_and_pow=[
+	 [ preload("res://character/player/my_laser.tscn"), 1 ],
+	 [ preload("res://character/player/my_laser.tscn"), 1 ],
+	 [ preload("res://character/player/my_laser3.tscn"), 2 ],
+	 [ preload("res://character/player/my_laser4.tscn"), 3 ],
+	 [ preload("res://character/player/my_laser5.tscn"), 4 ]
+	]
+
+# リングレーザ用ーシーン
+var _scn_ring_laser : PackedScene = preload("res://character/player/my_ring_laser.tscn")

# スピードアップ倍率の定義
const _d_speed_magnification_max : float = 3.0
const _d_speed_magnification_min : float = 1.0
var _d_speed_magnification = _d_speed_magnification_min

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 * _d_speed_magnification
			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()
+			var scn : Area3D = _aa_laser_scn_and_pow[_i_laser_power_level][0].instantiate()
			scn.set_pos(global_position)
+			scn.d_power_point = _aa_laser_scn_and_pow[_i_laser_power_level][1]
			g_val.node_lasers.add_child(scn)

+			# リングレーザーを発射する
+			if _i_laser_power_level > 0:
+				# 左斜め前方に発射
+				var scn_l : Area3D = _scn_ring_laser.instantiate()
+				scn_l.set_pos(global_position)
+				scn_l.v_dir = Vector3(-0.5, 0, -1)
+				g_val.node_lasers.add_child(scn_l)
+
+				# 右斜め前方に発射
+				var scn_r : Area3D = _scn_ring_laser.instantiate()
+				scn_r.set_pos(global_position)
+				scn_r.v_dir = Vector3( 0.5, 0, -1)
+				g_val.node_lasers.add_child(scn_r)
			else:
				pass

			# 発射間隔を設定
			_d_firing_remain_time_sec = _d_firing_interval_sec
	else:
		# タッチしたときにすぐ、レーダーが発射するようにする
		_d_firing_remain_time_sec = 0

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

func _on_item_sensor_area_entered(area):
	# アップグレードアイテム
	if area.en_item_kind == g_val.EnItemKind.SPEED_UP :
		# パワーアップアイテム
		_d_speed_magnification = clampf(_d_speed_magnification + 0.4, _d_speed_magnification_min, _d_speed_magnification_max)
+	elif area.en_item_kind == g_val.EnItemKind.POWER_UP :
+		_i_laser_power_level = clampi(
+						_i_laser_power_level+1,
+						0,
+						_aa_laser_scn_and_pow.size() - 1)
	else:
		pass
	area.queue_free()
```
　変更点について説明すると、まず、パワーアップアイテムのレベルを保持するためのメンバ変数、_i_laser_power_levelを追加しました。初期値が０でパワーアップアイテムをゲットするごとに１ずつ増えます。
　前方に進むレーザー用のPackedSceneと威力の２つの情報は、パワーアップのレベルによって変わるため、2次元配列にしました。以下の表のようにアクセスします。


 | パラメータ | データアクセス方法 |
|:-|:-|
| レーザー用シーン(PackedScene)  | _aa_laser_scn_and_pow[_i_laser_power_level][0]  |
  レーザーの威力  |_aa_laser_scn_and_pow[_i_laser_power_level][1]  |


```diff_gd_script
-@export var _scn_laser : PackedScene

+# パワーアップ段階（レベル）の定義
+var _i_laser_power_level = 0
+
+# レーザー用シーンと威力の定義
+var _aa_laser_scn_and_pow=[
+	 [ preload("res://character/player/my_laser.tscn"), 1 ],
+	 [ preload("res://character/player/my_laser.tscn"), 1 ],
+	 [ preload("res://character/player/my_laser3.tscn"), 2 ],
+	 [ preload("res://character/player/my_laser4.tscn"), 3 ],
+	 [ preload("res://character/player/my_laser5.tscn"), 4 ]
+	]
```
　また、リング型レーザーのPackedSceneも定義しました。
```diff_gd_script
+# リングレーザ用ーシーン
+var _scn_ring_laser : PackedScene = preload("res://character/player/my_ring_laser.tscn")
```
　前方に進むレーザーのPackedSceneの持ち方を変更したので、instanciateするところを変更します。
　また、レーザーの威力を設定する行を追加しました。
```diff_gd_script
			# 主砲のレーザーを発射する
-   			var scn : Area3D = _scn_laser.instantiate()
+			var scn : Area3D = _aa_laser_scn_and_pow[_i_laser_power_level][0].instantiate()
			scn.set_pos(global_position)
+			scn.d_power_point = _aa_laser_scn_and_pow[_i_laser_power_level][1]
			g_val.node_lasers.add_child(scn)
```

　パワーアップの2段階目から、リング型レーザーを発射します。_i_laser_power_leveは0始まりなので、1以上（０より大きく）なったら発射するようにします。
　生成したレーザーインスタンスのv_dirに、前方斜め方向を設定します。
```diff_gd_script
+			# リングレーザーを発射する
+			if _i_laser_power_level > 0:
+				# 左斜め前方に発射
+				var scn_l : Area3D = _scn_ring_laser.instantiate()
+				scn_l.set_pos(global_position)
+				scn_l.v_dir = Vector3(-0.5, 0, -1)
+				g_val.node_lasers.add_child(scn_l)
+
+				# 右斜め前方に発射
+				var scn_r : Area3D = _scn_ring_laser.instantiate()
+				scn_r.set_pos(global_position)
+				scn_r.v_dir = Vector3( 0.5, 0, -1)
+				g_val.node_lasers.add_child(scn_r)
```
　アップグレードアイテムを検知する処理に、パワーアップアイテムの場合を追加します。
　パワーアップアイテムをゲットすると、_i_laser_power_levelに1加算します。値の範囲は_aa_laser_scn_and_pow配列の範囲にしたいので、0以上、_aa_laser_scn_and_pow.size()-1にクランプします。　
```diff_gd_script
func _on_item_sensor_area_entered(area):
	# アップグレードアイテム
	if area.en_item_kind == g_val.EnItemKind.SPEED_UP :
		# パワーアップアイテム
		_d_speed_magnification = clampf(_d_speed_magnification + 0.4, _d_speed_magnification_min, _d_speed_magnification_max)
+	elif area.en_item_kind == g_val.EnItemKind.POWER_UP :
+		_i_laser_power_level = clampi(
+						_i_laser_power_level+1,
+						0,
+						_aa_laser_scn_and_pow.size() - 1)
	else:
```

# シナリオにアップグレードアイテム用シナリオ要素をたくさん追加
　デバッグ用に、たくさん追加しました。
 res://scenario/scenario_stage01.tscn
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/cb531842-9633-4d76-7d94-274712ac8722.png)

# 実行
　パワーアップすると、隕石を簡単に破壊できるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/2d340ebc-6816-595c-88a3-df4cfd00b007.png)

 以上です。