Godot4で3D風２D縦スクロールシューティングゲームを作る 16回目 ボスの追加 登場～動くところまで

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

# ボスキャラのBigFireの追加
　ボスキャラを追加します。名前はBigFireです。
　今回は登場して、動きだけ実装します。下図の動作モード１，２，３を繰り返します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/4c744b2b-4bf1-1397-f90a-de6db160d17f.png)


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

https://github.com/footinglow/woden/tree/016_add_boss_bigfire

# 前半：ボスBigFireシーンの追加
　下図のファイルシステムで選択したフォルダやファイルが、ボスBigFireシーン用に本記事で新規に追加したものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/66524204-4e2e-3640-559e-8764a8fde486.png)

## ボスBigFireのモデルファイル
　Blenderでモデリングして、glTF2.0形式でエクスポートしました。
res://assets/blender/boss_bigfire/BossStage1.glb
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/26732b13-afc5-4cca-31c4-136a9d0951b1.png)

## ボスBigFire用のシーンを追加
　ボスBigFire用のシーンを追加します。ルートノードはArea3Dです。
　ボスBigFireのモデルファイル「BossStage1」を子ノードにし、サイズを25～30mくらいにするため、Scaleを5倍に設定しました。

res://character/boss/boss_bigfire.tscn
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/5273ba25-6fe1-29c5-4f87-b6726f41470d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b6f1ab51-e2ed-c7e2-6a5c-185670d85ce4.png)

　衝突判定用に、直方体が1個と、カプセル型を３つの、合計４つのCollisionShape3Dを追加しました。
　ボスBigFireは複雑な形をしていて、単純な1個の球や直方体では、おおざっぱすぎて衝突判定が不自然になるし、メッシュからCollision形状を生成すると正確ですが細かすぎでファイルサイズが大きくなり、処理に時間がかかるため、複数の単純な形状を組み合わせました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/ed1c994f-35e0-7bec-830e-89cdc0522be0.png)
　ルートノードのArea3Dを選択して、Collisionセクションを設定します。Layerプロパティは、4番のEnemyをチェックすることで、MyLaserに検出されます。敵キャラ自身は何も衝突検出しないので、Maskプロパティは設定不要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/23add3bd-fbcc-7b66-fbb4-ba254f07a0fa.png)


## 敵スクリプトの実装の基本
　敵スクリプトを実装する場合、EnemyBaseクラスを継承して、_physics_processメソッド内で衝突チェックをする必要がありますので、下記が基本の実装になります。

```gdscript
extends EnemyBase

func _physics_process(delta):
	# 撃墜チェック
	super.check_destroyed()
	# 移動処理

```

## ボスBigFireのスクリプトの実装
　スクリプトをアタッチして下記のように実装します。

### スクリプト全体
res://character/boss/boss_bigfire.gd
```gdscript
extends EnemyBase

# 動作モード
enum BigFireMode {
	RETURN_TO_BASE_POS,		# 動作モード1:ベース位置に戻る
	REPEAT_LATERAL,			# 動作モード2:反復横移動
	RUSH_FORWARD			# 動作モード3:突進
}
# 現在の動作モードを保持する変数
var _en_mode : BigFireMode

# 移動目標位置
var _v_target_pos : Vector3

# 移動速度の上限
var _d_min_vx_mps = -30.0	# 反復横移動の最高速度
var _d_max_vx_mps = 30.0	# 反復横移動の最高速度
var _d_min_vz_mps = -10.0	# 突進の最高速度
var _d_max_vz_mps = 40.0	# ベース位置に戻る時の最高速度

# パラメータ
var _d_base_pos_z_m = -100		# ベース位置のZ座標
var _d_rush_target_pos_z_m = g_val.d_visible_bottom_z_m + 25.0
								# 突進目標位置のZ座標（画面外の機体が見えなくなる位置）
var _i_repeat_max_cnt = 5		# 反復横移動の回数

# 反復横移動の残り回数
var _i_repeat_remain_cnt

func _ready():
	# 動作モードを初期化する
	init_return_to_base_pos()

func _physics_process(delta):
	# 撃墜チェック
	super.check_destroyed()
	# 移動処理
	match( _en_mode ):
		# 動作モード1:ベース位置に戻る
		BigFireMode.RETURN_TO_BASE_POS:
			move_toward_target_pos(delta)	# 目標位置に向かって移動
			if is_reached_target_pos():		# 目標位置到達判断
				init_repeat_lateral()		# 「動作モード2:反復横移動」に初期化

		# 動作モード2:反復横移動
		BigFireMode.REPEAT_LATERAL:
			process_repeat_lateral(delta)	# 反復横移動の実行
			if _i_repeat_remain_cnt <= 0:	# 反復横移動の完了判断
				init_rush_forward()			# 「 動作モード3:突進」に初期化

		# 動作モード3:突進
		BigFireMode.RUSH_FORWARD:
			move_toward_target_pos(delta)	# 目標位置に向かって移動
			if is_reached_target_pos():		# 目標位置到達判断
				init_return_to_base_pos()	# 「動作モード1:ベース位置に戻る」に初期化

# 目標位置との差分を速度にして、移動する
func move_toward_target_pos(delta):
	var velocity = _v_target_pos - global_position
	velocity.x = clampf(velocity.x, _d_min_vx_mps, _d_max_vx_mps)
	velocity.z = clampf(velocity.z, _d_min_vz_mps, _d_max_vz_mps)
	global_position += velocity * delta

# 目標位置に移動完了判定
func is_reached_target_pos() -> bool:
	return ( global_position.distance_to( _v_target_pos ) < 1.0 )

# 動作モード1:ベース位置に戻る
func init_return_to_base_pos():
	_en_mode = BigFireMode.RETURN_TO_BASE_POS
	# 移動目標位置をベース位置に設定
	_v_target_pos = Vector3(
						global_position.x,
						global_position.y,
						_d_base_pos_z_m )

# 動作モード2:反復横移動
func init_repeat_lateral():
	_en_mode = BigFireMode.REPEAT_LATERAL
	# 反復横跳びの残り回数を初期化
	_i_repeat_remain_cnt = _i_repeat_max_cnt
	# 移動目標位置を設定
	var target_x 
	if global_position.x < 0:
		target_x = g_val.d_visible_bottom_max_x_m
	else:
		target_x = g_val.d_visible_bottom_min_x_m
	_v_target_pos = Vector3(
						target_x,
						global_position.y,
						global_position.z )

func process_repeat_lateral(delta):
	move_toward_target_pos(delta)	
	
	if is_reached_target_pos():
		# 移動目標位置をX軸の反対側にする
		_v_target_pos.x = -_v_target_pos.x
		# カウントダウン
		_i_repeat_remain_cnt -= 1
	
# 動作モード3:突進
func init_rush_forward():
	_en_mode = BigFireMode.RUSH_FORWARD
	# 移動目標位置を突進目標位置（画面外の機体が見えなくなる位置）に設定
	_v_target_pos = Vector3(
						global_position.x,
						global_position.y,
						_d_rush_target_pos_z_m )
```

　スクリプトの説明をします。
### ３つの動作モードの切り替え
　enumで３つの動作モードを定義して、メンバー変数_en_modeに現在実行中の動作モードを保持します。
```gdscript
# 動作モード
enum BigFireMode {
	RETURN_TO_BASE_POS,		# 動作モード1:ベース位置に戻る
	REPEAT_LATERAL,			# 動作モード2:反復横移動
	RUSH_FORWARD			# 動作モード3:突進
}

# 現在の動作モードを保持する変数
var _en_mode : BigFireMode
```
　各動作モードを初期化するメソッドを「init_動作モード名」(例えば、RETURN_TO_BASE_POSの場合は、init_return_to_base_pos）という名前で作成して、初期化メソッド内で、_en_modeに動作モードを設定するようにします。
　_readyメソッドで、最初の動作モードを設定したいので、「動作モード１：ベース位置に戻る」ための初期化メソッド(init_return_to_base_pos)を起動し、動作モードを設定しています。
 ```gdscript
func _ready():
	# 動作モードを初期化する
	init_return_to_base_pos()
```
 　また各動作モードは、_physics_processメソッドで、_en_modeの設定値により、処理を分岐して実行します。
 - **_en_modeがBigFireMode.RETURN_TO_BASE_POSの場合**
 - **_en_modeがBigFireMode.REPEAT_LATERALの場合**
move_toward_target_pos()で、目標位置まで移動処理を実行します。
もし目標位置に到達した場合は、次の動作モードの初期化メソッドを起動して、次の動作モードに遷移します。
 - **_en_modeがBigFireMode.REPEAT_LATERAL**
process_repeat_lateメソッドで横移動を繰り返します。
もし横移動が規定回数実行し終わると、次の動作モードの初期化メソッドを起動して、次の動作モードに遷移します。
```gdscript
func _physics_process(delta):
	# 撃墜チェック
	super.check_destroyed()
	# 移動処理
	match( _en_mode ):
		# 動作モード1:ベース位置に戻る
		BigFireMode.RETURN_TO_BASE_POS:
			move_toward_target_pos(delta)	# 目標位置に向かって移動
			if is_reached_target_pos():		# 目標位置到達判断
				init_repeat_lateral()		# 「動作モード2:反復横移動」に初期化

		# 動作モード2:反復横移動
		BigFireMode.REPEAT_LATERAL:
			process_repeat_lateral(delta)	# 反復横移動の実行
			if _i_repeat_remain_cnt <= 0:	# 反復横移動の完了判断
				init_rush_forward()			# 「 動作モード3:突進」に初期化

		# 動作モード3:突進
		BigFireMode.RUSH_FORWARD:
			move_toward_target_pos(delta)	# 目標位置に向かって移動
			if is_reached_target_pos():		# 目標位置到達判断
				init_return_to_base_pos()	# 「動作モード1:ベース位置に戻る」に初期化
```
### 移動処理について
　動作モード１～３は、いずれも目標位置を決めて、目標に向かって直線的に移動し、目標位置に到達すると完了という動作をします。
　そのため目標位置を保持するメンバ変数を指定しました。
```gdscript
# 移動目標位置
var _v_target_pos : Vector3
```
　目標位置に向う速度を計算します。
　目標位置から現在位置を引き算すると「目標位置への移動量」が計算できます。それをそのまま速度[m/s]として使うと速すぎる（1秒で目標位置に到達する）ため、速度の上限値、下限値を決めて、clampしたものを速度として使用します。 
　速度が求まれば、あとはArea3Dの現在位置に経過時間delta[s]分加算するだけです。
```gdscript
# 目標位置との差分を速度にして、移動する
func move_toward_target_pos(delta):
	var velocity = _v_target_pos - global_position
	velocity.x = clampf(velocity.x, _d_min_vx_mps, _d_max_vx_mps)
	velocity.z = clampf(velocity.z, _d_min_vz_mps, _d_max_vz_mps)
	global_position += velocity * delta
```
```gdscript
# 移動速度の上限
var _d_min_vx_mps = -30.0	# 反復横移動の最高速度
var _d_max_vx_mps = 30.0	# 反復横移動の最高速度
var _d_min_vz_mps = -10.0	# 突進の最高速度
var _d_max_vz_mps = 40.0	# ベース位置に戻る時の最高速度
```
　次は、目標位置に到達したか判定するメソッドを説明します。
　基本的に、現在位置と目標位置が一致した場合、目標位置に到達したと判断します。現在位置と目標位置はVector3型なので、それぞれのメンバ変数であるfloat型のx,y,zが一致した場合、目標位置に達したと判断したいところですが、floatのような少数の場合、完全にイコールにならない場合があるため、現在位置と目標位置の距離が閾値以下になった場合、目標位置に達したと判断するようにします。
 　以下のメソッドでは、Vector3のdistance_toメソッドで現在位置と目標位置の距離を計算し、閾値1.0m未満になった場合、目標位置に移動完了したと判断して、trueを返します。

```gdscript
# 目標位置に移動完了判定
func is_reached_target_pos() -> bool:
	return ( global_position.distance_to( _v_target_pos ) < 1.0 )
```
### 動作モード1:ベース位置に戻る
　初期化メソッドで動作モードと移動目標位置を設定します。
　ベース位置は_d_base_pos_z_mで定義しました。
```gdscript
var _d_base_pos_z_m = -100		# ベース位置のZ座標
```
```gdscript
# 動作モード1:ベース位置に戻る
func init_return_to_base_pos():
	_en_mode = BigFireMode.RETURN_TO_BASE_POS
	# 移動目標位置をベース位置に設定
	_v_target_pos = Vector3(
						global_position.x,
						global_position.y,
						_d_base_pos_z_m )
```

### 動作モード2:反復横移動
　左右の移動を一定回数繰り返すので、その回数の初期値と、残りの回数をメンバ変数で定義します。
```gdscript
var _i_repeat_max_cnt = 5		# 反復横移動の回数
# 反復横移動の残り回数
var _i_repeat_remain_cnt
```
　初期化メソッドで、動作モードと左右への横移動の残り回数を初期化します。
　現在のボスのx座標位置がマイナスの場合は左側にいるので、移動目標位置を右側の端（X座標max値）に設定します。逆に現在のボスのx座標位置がプラスの場合は右側にいるので、移動目標位置を左側の端（X座標min値）に設定します。
```gdscript
# 動作モード2:反復横移動
func init_repeat_lateral():
	_en_mode = BigFireMode.REPEAT_LATERAL
	# 反復横跳びの残り回数を初期化
	_i_repeat_remain_cnt = _i_repeat_max_cnt
	# 移動目標位置を設定
	var target_x 
	if global_position.x < 0:
		target_x = g_val.d_visible_bottom_max_x_m
	else:
		target_x = g_val.d_visible_bottom_min_x_m
	_v_target_pos = Vector3(
						target_x,
						global_position.y,
						global_position.z )
```
　周期処理では、move_toward_target_posメソッドで移動目標位置に向かって移動します。
　is_reached_target_posメソッドで移動目標位置に到達したと判断すると、移動目標位置のx位置の符号を反転することで、移動目標位置が右端の場合は左端に、左端の場合は右端に変更します。
　また、横移動の残り回数をカウントダウンします。
```gdscript
func process_repeat_lateral(delta):
	move_toward_target_pos(delta)	
	
	if is_reached_target_pos():
		# 移動目標位置をX軸の反対側にする
		_v_target_pos.x = -_v_target_pos.x
		# カウントダウン
		_i_repeat_remain_cnt -= 1
```

### 動作モード3:突進
　動作モードと移動目標位置を設定します。
　_d_rush_target_pos_z_mは、画面外 下のz座標位置です。
　
```gdscript	
var _d_rush_target_pos_z_m = g_val.d_visible_bottom_z_m + 25.0
								# 突進目標位置のZ座標（画面外の機体が見えなくなる位置）

# 動作モード3:突進
func init_rush_forward():
	_en_mode = BigFireMode.RUSH_FORWARD
	# 移動目標位置を突進目標位置（画面外の機体が見えなくなる位置）に設定
	_v_target_pos = Vector3(
						global_position.x,
						global_position.y,
						_d_rush_target_pos_z_m )
```

# 後半：ボスBigFireシーン生成シナリオ要素
　以下のファイルシステムで選択されているファイルが、ボスBigFireシーンを生成するために、新規追加したフォルダとシナリオ要素ファイルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/152b4763-36bc-b3ac-a438-8bb8529c91f6.png)
　新規追加したシナリオ要素を追加するため、scenario_stage01.tscnも変更します。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9b1476dc-5103-0532-23bb-0fc000f3793f.png)

## ボスBigFile生成シナリオ要素のシーンを追加
　res://scenario/elements/bossフォルダを作成し、新規シーンを作成して、ファイル名を「produce_boss_bigfire.tscn」に変更します。ルートノードはNode3Dです。
　子ノードは不要です。
res://scenario/elements/boss/produce_boss_bigfire.tscn
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/e1e80c87-9071-05f4-d5e5-c26869393940.png)

## ボスBigFile生成シナリオ要素のスクリプト
　スクリプトをアタッチして、以下のように修正します。
　ボスBigFireのインスタンスを生成し、HP、スコアと初期位置を設定しています。
　画面外 下から登場するので、z座標は画面外に設定しています。
res://scenario/elements/boss/produce_boss_bigfire.gd
```gdscript
extends Node3D

var _scn_enemy = preload("res://character/boss/boss_bigfire.tscn")

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

func start_element():
	# シーンのインスタンスを生成
	var ins = _scn_enemy.instantiate()
	# 敵パラメータの設定
	ins.d_enemy_hp = _d_hp
	ins.i_enemy_score = _i_score
	# パラメータの設定
	ins.position.z = g_val.d_visible_bottom_z_m + 25.0	# 完全に画面外に移動するように
	ins.position.x = 0.0

	# インスタンスを敵管理用ノードに追加
	g_val.node_enemies.add_child(ins)

func is_blocking_to_move_on_next_scenario() -> bool:
	return false
```

## ボスBigFire生成シナリオ要素をシナリオに追加
　デバッグ用に、先ほど作成した、produce_boss_bigfire.tscnをシナリオに追加します。
res://scenario/scenario_stage01.tscn
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/97d2b0b3-f24a-e58d-ea4f-8a087563e932.png)

# global.gdのバグ修正
　単純に間違っていました。すみません。修正します。
res://global/global.gd
```diff_gd_script
-var d_visible_bottom_z_m = -15.0
+var d_visible_bottom_z_m =  15.0
```

# 実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/4a137b40-680c-7d73-ac6c-a943c38d6e33.png)

以上です。