Godot4で3D風２D縦スクロールシューティングゲームを作る 11回目 隕石を追加する

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

# 敵として隕石を追加する
　敵というかお邪魔キャラクターとして、隕石を追加します。
　隕石なのでまっすぐ進みますが、斜めに進むようにします。

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

https://github.com/footinglow/woden/tree/011_add_big_asteroid

# 隕石のモデル
　Blenderで隕石のモデルを作成しました。こんなのが簡単に作れるとは、すごい時代です。
　テクスチャはこちらからお借りしました。

 https://polyhaven.com/a/red_laterite_soil_stones
 
　glTF2.0ファイルで出力したものを下記に格納しました。
　res://assets/blender/asteroid/asteroid.glb
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/d26e1811-ed6b-a40b-eec5-cecef82cf68c.png)

# 追加・修正ノード
　隕石を追加するため、下図の赤枠で囲ったノードを追加・修正します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/6f5c25d8-1395-6d48-86b1-13004aa011f5.png)

# 隕石本体を追加する


## 隕石本体のシーンを追加
　res://character/enemy配下に新規シーン、asteroid_big.tscnを追加します。ルートノードはArea3Dにします。
　res://assets/blender/asteroid/asteroid.glbを子ノードとして追加します。
　また、CollisionObject3DとvisibleOnScreenNotifier3Dを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9efcb75c-7115-63ca-a906-3e2997ea3c96.png)
## ルートノード(Area3d)のCollisionのプロパティ変更
　敵なので、Layerは「Enemy」をチェックします。衝突検出はしないのでMaskはチェックしません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9a9c3688-5bbd-a638-a61a-8361b64842e1.png)

## asteroidのプロパティ変更
　大きな隕石にするので5mのPlayer機の縦横2倍くらいになるように、Scaleを6倍にしました。後で設定する球状の衝突判定に合わせるように位置を微調整しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/4893bdb8-44aa-33c0-51fe-387ffcfe30d6.png)

## CollisionObject3Dのプロパティ変更
　衝突形状は球にしました。半径は、隕石のモデルからはみ出さないように適当に設定しました。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/eb8b04aa-107c-c796-d82d-fe86f1958ae8.png)

## 隕石のスクリプトを実装
　スクリプトをアタッチして、下記のように実装しました。
　VisibleOnScreenNotifier3Dのscreen_exited()シグナルを_on_visible_on_screen_notifier_3d_screen_exitedメソッドに接続します。
　上から下にまっすぐ進むtwin_bodyと違い、隕石は斜めに進むため、「v_dir」メンバーを用意して、移動方向を設定できるようにしました。

```gdscript
extends EnemyBase

# 固有のプロパティ
var d_speed_mps = 25.0
var v_dir = Vector3(0, 0, 1)

func _physics_process(delta):
	# 撃墜チェック
	super.check_destroyed()
	# 移動処理
	global_position += v_dir * d_speed_mps * delta
	
func _on_visible_on_screen_notifier_3d_screen_exited():
	queue_free()
```

# 隕石を生成するシナリオ要素を実装

# 隕石を生成するシナリオ要素のシーンを作成
　res://scenario/elements配下で、新規シーンproduce_asteroid_big.tscnを作成します。ルートノードはNode3Dです。子ノードにTimerを追加して、名前を「Timer_for_produce」に変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/31f5b955-321a-256b-758e-c003c4042120.png)

# 隕石を生成するシナリオ要素のスクリプトを実装
　スクリプトをアタッチして、隕石用のスクリプトを新規に作成します。
　隕石用のシナリオ要素のスクリプトは、twin_Bodyとほぼ同じなので、twin_Body.gdとの差分を示します。
　Timer_for_produceのtimeoutシグナルをスクリプトに接続します。

　隕石のHPは10にして、レーザーを10発当てないと消滅しないように、固めに設定します。

```diff_gd_script
extends Node

# 敵生成パラメータ
@export	var _i_produce_num = 10
@export var _d_produce_interval_sec = 0.5

# 敵のパラメータ
-@export var _d_hp = 1
-@export var _i_score = 100
+@export var _d_hp = 10
+@export var _i_score = 200
+@export var _d_speed_min_mps = 10.0
+@export var _d_speed_max_mps = 30.0

-var _scn_enemy = preload("res://character/enemy/twin_body.tscn")
+var _scn_enemy = preload("res://character/enemy/asteroid_big.tscn")

var _i_produced_num = 0;	# 生産した数

func start_element():
	$Timer_for_produce.start(_d_produce_interval_sec)

func is_blocking_to_move_on_next_scenario() -> bool:
	# 敵キャラの生産が完了したら次のシナリオ要素に進ませるためfalseにする
	return ( _i_produced_num < _i_produce_num )

func _on_timer_for_produce_timeout():
	# 敵キャラクターのインスタンスを生成
	var ins = _scn_enemy.instantiate()

	# 敵パラメータの設定
	ins.d_enemy_hp = _d_hp
	ins.i_enemy_score = _i_score
	ins.position.z = g_val.d_visible_top_z_m
	ins.position.x = randf_range(g_val.d_visible_top_min_x_m, g_val.d_visible_top_max_x_m)
+	var v_target_pos = Vector3(randf_range(g_val.d_visible_bottom_min_x_m, g_val.d_visible_bottom_max_x_m), 0, g_val.d_visible_bottom_z_m)
+	ins.v_dir = ( v_target_pos - ins.position ).normalized()
+	ins.d_speed_mps = randf_range(_d_speed_min_mps, _d_speed_max_mps)

	# add_to_scoreシグナルをgame.gdに接続する
	ins.add_to_score.connect( g_val.node_game._on_add_to_score )

	# 敵インスタンスをシーンツリーに追加
	g_val.node_enemies.add_child(ins)
	
	# 生産完了したら、タイマーを停止する
	_i_produced_num += 1
	if ( _i_produced_num >= _i_produce_num ):
		$Timer_for_produce. stop()
```
 
　インスタンス生成後の差分では、移動方向と速度を計算しています。
　画面下部のX座標範囲からランダムで決めた位置を目標位置として、v_target_posに設定します。目標位置v_target_posから現在位置ins.positionを引いて、normalizedすると方向成分が取得できます。
　あと速度はパラメータの範囲で、ランダムに計算して、隕石のインスタンスに設定します。
```diff_gd_script
+	var v_target_pos = Vector3(randf_range(g_val.d_visible_bottom_min_x_m, g_val.d_visible_bottom_max_x_m), 0, g_val.d_visible_bottom_z_m)
+	ins.v_dir = ( v_target_pos - ins.position ).normalized()
+	ins.d_speed_mps = randf_range(_d_speed_min_mps, _d_speed_max_mps)
```

# 画面下部のX座標の範囲をグローバル変数に定義
　カメラは上方から斜めに見下ろしているため、手前の画面下部より、奥の画面上部の方が横方向に見える範囲が広いです。画面下部のXの範囲は-25m～+25mに対し、画面上部は-75m～+75くらいです。グローバル変数のX座標範囲の定義を、画面上部下部にわけて用意し設定しました。
 
```diff_gd_script
extends Node3D

# ゲーム画面の見える範囲
# ゲーム画面の上端をtop、下端をbotommとする
var d_visible_top_z_m = -200.0
-var d_visible_top_min_x_m = -25.0
-var d_visible_top_max_x_m =  25.0
+var d_visible_top_min_x_m = -75.0
+var d_visible_top_max_x_m =  75.0

+var d_visible_bottom_z_m = -15.0
+var d_visible_bottom_min_x_m = -25.0
+var d_visible_bottom_max_x_m =  25.0


# gameのインスタンス
var node_game : Node3D = null 

# 生成したインスタンスの追加先
var node_lasers : Node3D = null
var node_enemies : Node3D = null 
var node_bullets : Node3D  = null

# Playerのインスタンス
var node_player : Area3D = null 
```

# twin_bodyの初期位置を修正
　twin_bodyはまっすぐ進むため、画面上部の端の方を初期位置にすると、下まで進んだ時に画面外に出てしまうという問題がありますので、初期位置のX位置は、画面下部のX座標範囲から決定するように訂正します。

　res://scenario/elements/produce_twin_body.gd
```diff_gd_script
extends Node

# 敵生成パラメータ
@export	var _i_produce_num = 10
@export var _d_produce_interval_sec = 0.5

# 敵のパラメータ
@export var _d_hp = 1
@export var _i_score = 100

var _scn_enemy = preload("res://character/enemy/twin_body.tscn")

var _i_produced_num = 0;	# 生産した数

func start_element():
	$Timer_for_produce.start(_d_produce_interval_sec)

func is_blocking_to_move_on_next_scenario() -> bool:
	# 敵キャラの生産が完了したら次のシナリオ要素に進ませるためfalseにする
	return ( _i_produced_num < _i_produce_num )

func _on_timer_for_produce_timeout():
	# 敵キャラクターのインスタンスを生成
	var ins = _scn_enemy.instantiate()

	# 敵パラメータの設定
	ins.d_enemy_hp = _d_hp
	ins.i_enemy_score = _i_score
	ins.position.z = g_val.d_visible_top_z_m
- 	ins.position.x = randf_range(	g_val.d_visible_top_min_x_m,
-									g_val.d_visible_top_max_x_m)
+	ins.position.x = randf_range(	g_val.d_visible_bottom_min_x_m,
+									g_val.d_visible_bottom_max_x_m)

	# add_to_scoreシグナルをgame.gdに接続する
	ins.add_to_score.connect( g_val.node_game._on_add_to_score )

	# 敵インスタンスをシーンツリーに追加
	g_val.node_enemies.add_child(ins)
	
	# 生産完了したら、タイマーを停止する
	_i_produced_num += 1
	if ( _i_produced_num >= _i_produce_num ):
		$Timer_for_produce. stop()
```

# シナリオに隕石生成シナリオ要素を追加
　シナリオに、produce_asteroid_big.tscnを子ノードとして追加しました。
res://scenario/scenario_stage01.tscn
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/4a3ac596-39cd-be1b-0910-150b13d6bf5d.png)

# 実行結果
　隕石が表示されました。固いので破壊するのが大変です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f70e4da3-4e08-f27f-d65e-c461b2fd1ed4.png)

# 終わりに
　アイテムをとると、レーザーのパワーを上げるようにしたいです。

　以上です。