【Godot 4.0】スマホ３Dゲームを作るための勉強　その１８　ゲームシステムを作る　その３　ステージを増やしてクリアすると次のステージに進むようにする

　ゲームエンジンGodot4.0で3Dスマホゲームを作りたいと思いますが、その前にお勉強しています。
　2023/3/1にstable版がリリースされました。
 [Godot_v4.0-stable_win64.exe.zip](https://godotengine.org/download/windows/)を使用しています。

# 目的
　ゲームシステムをつくっていきます。
　ステージを追加して、クリアすると次のステージに切り替わるようにします。

# ベースプロジェクト
下記で作成したプロジェクトをベースに機能追加をします。
　【Godot 4.0】スマホ３Dゲームを作るための勉強　その１７　ゲームシステムを作る　その２
　https://qiita.com/FootInGlow/items/c68af46ada40f09af453
　
　github(Godotのプロジェクトマネージャーからインポートして利用できます）
　https://github.com/footinglow/Godot4/tree/main/02_study/S17_GameSystem_002

# StageItemsを追加する
　敵の砦以外にもStageに配置する障害物を追加します。

　StaticBody3Dをルートノードとして追加します。
　シーンメニューから新規シーンを実行します。「その他のノード」ボタンを押下してStaticBody3Dを選択します。名称を「RockyMountain]（岩山）にしましょう。
　
　StageItemsフォルダに保存します。rocky_mountain.tscnという名前になると思います。
![スクリーンショット (160).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/cf3e927c-eee0-8d93-a24f-c948b235437f.png)

　岩山の土台を追加します。
　RockyMountainを右クリックして子ノードを追加からMeshInstance3Dを追加します。名称を「Base」に変更します。
　「Base」を選択した状態で、インスペクタのMeshInstance3D/Meshに新規BoxMeshを設定します。
　MeshInstance3D/Meshの右のアイコンをクリックして詳細設定を表示します。
　Sizeをx=1.0、y=0.5m、z=1.0に変更します。
　Node3D/Transform/position/yに0.25mを設定します。

　Materialに新規StandardMaterial3Dを設定します。
　色も設定しましょう。
　Materialの右の球アイコンをクリックして詳細設定を表示して、albedoを開いて、colorを設定します。
　濃いグレーにしました(R,G,B=60)

　次に岩山の頂上を追加します。
　Baseを右クリックして子ノードを追加からMeshInstance3Dを追加します。名称を「Top」に変更します。
　「Top」を選択した状態で、インスペクタのMeshInstance3D/Meshに新規BoxMeshを設定します。
　MeshInstance3D/Meshの右のアイコンをクリックして詳細設定を表示します。
　Sizeをx=0.5、y=0.5m、z=0.5に変更します。
　Node3D/Transform/position/yに0.25mを設定します。

　MaterialもBase同様に設定します。
![スクリーンショット (150).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/c991d736-3fc5-c6b2-8e11-bf84c91ca8f9.png)

　次に衝突形状を設定します。Base（土台）とx,zは同じサイズにします。高さｙは高くします。
　RockyMountainを右クリックして子ノードを追加からCollisionShape3Dを追加します。
　CollisionShape3Dを選択した状態で、インスペクタのCollisionShape3D/Shapeに新規BoxShape3Dを設定します。
　Node3D/Transform/position/yに0.5mを設定します。
![スクリーンショット (151).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/2582c821-7a75-0825-47de-609412622574.png)

　ステージへの配置はあとでやりましょう。

# ステージを追加する
　res://Stages/stage001.tscnを複製して、stage002.tscn、stage003.tscnを作成します。
　![スクリーンショット (144).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/6af5cf68-6414-d1e7-497c-945f11b679a4.png)
「stage002.tscn」という名称で保存します。
![スクリーンショット (145).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/690f4218-5094-b966-5402-b3b7735443d7.png)

　同様の手順でstage003.tscnも複製します。
![スクリーンショット (147).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/13696f1e-8a69-db56-c4ec-b61df5137efa.png)

　各ステージを修正します。。

## stage001.tscnを修正する
　stage001.tscnを開きます。
　Enemyを削除して、敵の砦を1個だけにします。
![スクリーンショット (153).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/a79c1ee2-2bfa-3548-61ca-46b72fe5f090.png)

## stage002.tscnを修正する
　stage002.tscnを開きます。
　Enemyを削除して、敵の砦を1個だけにします。
　Enemy2を選択して、真ん中あたりに移動しましょう。
　
　ファイルシステムのres://StageItems/rocky_mountain.tscnを、シーン内のStageにドラッグアンドドロップします。
　Node3D/Transform/position/xに0.5m、zに-4mを設定します。
　Node3D/Transform/rotation/yに45°を設定します。
![スクリーンショット (155).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b4dd4fb8-f21a-11ec-b18f-8211f169807c.png)

## stage003.tscnを修正する
　stage003.tscnを開きます。

　ファイルシステムのres://StageItems/rocky_mountain.tscnを、シーン内のStageにドラッグアンドドロップします。
　Node3D/Transform/position/zに-5mを設定します。
　Node3D/Transform/rotation/yに45°を設定します。
![スクリーンショット (157).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/953dac29-c01f-ac2c-28c9-8ce8637ff7bf.png)

# ステージクリアで次のステージに切り替わるようにする
　GameSystemのスクリプトを修正して、ステージクリアでステージが切り替わるようにします。

## GameSystemシーンからStageノードを削除する
　Stageシーンはスクリプトから追加・削除するように変更するため、GameSystemシーンからStageを削除します。
　![スクリーンショット (158).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/816e9aca-42ba-2ee1-d95b-b0bc8040885f.png)
　削除しました。
　![スクリーンショット (159).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/66b2b93c-986a-98f5-5851-0eec14762e54.png)

## GameSystemのスクリプトを修正する
　res://game_system.gdを開いて、下記のように修正しましょう。

　stage001から始まり、クリアするとstage002、stage003に進み、stage003をクリアするとstage001に戻ります。
　ステージクリア失敗すると同じステージのやり直しです。

``` GDScript
extends Node3D

# ステージシーンリスト（PackedScene型）
var m_nodearray_stages = [
	preload("res://Stages/stage001.tscn"),
	preload("res://Stages/stage002.tscn"),
	preload("res://Stages/stage003.tscn"),
]

# 現在実行中のステージ番号
var m_i_current_stage_idx = 0

func set_new_stage(idx):
	# CurrentStageにあるステージをすべて削除する
	var nodes = $CurrentStage.get_children()
	for node in nodes:
		node.queue_free()
	# idx番目のステージシーンのインスタンスを生成して、CurrentStageに追加する
	var new_stage = m_nodearray_stages[idx].instantiate()
	$CurrentStage.add_child(new_stage)

func _ready():
	set_new_stage(m_i_current_stage_idx)
	
func _physics_process(delta):
	var nodes = $CurrentStage.get_children()
	if nodes:
		# nodesは1個か0個のどちらかなので、0番目を固定的に使用する
		if nodes[0].is_stage_clear():
			print("Stage Clear")
			# ステージは0,1,2,0,1,2,0...と繰り返す
			m_i_current_stage_idx = ( m_i_current_stage_idx + 1 ) % m_nodearray_stages.size()
			set_new_stage(m_i_current_stage_idx)

		if nodes[0].is_stage_failed():
			print("Stage Failed")
			set_new_stage(m_i_current_stage_idx)
```

　スクリプトの詳細を説明します。

### ステージシーンの一覧を作成する
 ステージシーンの一覧を定義しています。
```GDScript
var m_nodearray_stages = [
	preload("res://Stages/stage001.tscn"),
	preload("res://Stages/stage002.tscn"),
	preload("res://Stages/stage003.tscn"),
]
```
　preload関数を使用してステージシーンを読み込み、配列に定義しています。
　シーンを読み込むと、PackedScene型になります。
　配列なのでm_nodearray_stages[0]、m_nodearray_stages[1]というようにアクセスできます。

### ステージシーンの入れ替えメソッドの実装
```GDScript
func set_new_stage(idx):
	# CurrentStageにあるステージをすべて削除する
	var nodes = $CurrentStage.get_children()
	for node in nodes:
		node.queue_free()
	# idx番目のステージシーンのインスタンスを生成して、CurrentStageに追加する
	var new_stage = m_nodearray_stages[idx].instantiate()
	$CurrentStage.add_child(new_stage)
```
* var nodes = $CurrentStage.get_children()
　CurrentStageノードの子ノードをすべて取得します。
　すべてといってもステージシーンは削除して1個追加の繰り返しなので、
　起動時直後の子ノードがない場合は、
　　[]
　ステージを追加した後で、次のステージに進む場合、もしくはステージをやり直す場合は
　　[Stage:<Node3D#30198990031>]
　のように0個か1個のノードの配列がnodesに設定されます。

* for node in nodes:
 nodesにあるNode型の配列の1個ずつ処理します。nodesが空の場合は何もせずにforブロックを抜けます。

* node.queue_free()
 Nodeを削除（依頼）します。

* var new_stage = m_nodearray_stages[idx].instantiate()
　m_nodearray_stages[idx]はPackedScene型のシーンデータが格納されているので、instantiate()でインスタンスを作成します。
　BulletやEnemyBulletを追加した時と同じです。

*  $CurrentStage.add_child(new_stage)
　CurrentStageノードの子ノードとしてステージシーンのインスタンスを追加します。

```GDScript
var m_i_current_stage_idx = 0

func _ready():
	set_new_stage(m_i_current_stage_idx)
```
* 	set_new_stage(m_i_current_stage_idx)
　起動直後の初期設定で、新しいシーンを設定しています。

```GDScript
func _physics_process(delta):
	var nodes = $CurrentStage.get_children()
	if nodes:
		# nodesは1個か0個のどちらかなので、0番目を固定的に使用する
		if nodes[0].is_stage_clear():
			print("Stage Clear")
			# ステージは0,1,2,0,1,2,0...と繰り返す
			m_i_current_stage_idx = ( m_i_current_stage_idx + 1 ) % m_nodearray_stages.size()
			set_new_stage(m_i_current_stage_idx)

		if nodes[0].is_stage_failed():
			print("Stage Failed")
			set_new_stage(m_i_current_stage_idx)
```
　ステージシーンは削除・追加を繰り返すため、ステージシーンが存在しない場合も考慮しています。

* func _physics_process(delta):
　_physics_process()の中で毎周期判定します。

* var nodes = $CurrentStage.get_children()
　CurrentStageの子ノードをすべて取得します。

* if nodes:
　ステージシーンが存在することを確認しています。
　CurrentStageの子ノードが1個以上あるとtrueになります。

* if nodes[0].is_stage_clear():
　nodesには0個以上のNode型の要素（Stageシーン）が並びますが、今回は0個か1個のどちらかなので、nodes[0]固定で使用します。
　現在プレイしているステージのres://Stages/stage_normal.gdスクリプトのis_stage_clear()メソッドを利用します。

* m_i_current_stage_idx = ( m_i_current_stage_idx + 1 ) % m_nodearray_stages.size()
　ステージクリアしたので、次のステージに進む処理です。
　m_i_current_stage_idxをカウントアップしますが、m_nodearray_stages.size()の余りを代入するので、０，１，２，の次は０になります。m_nodearray_stagesにはステージシーン３つ設定しているので、size()は3を返します。

* set_new_stage(m_i_current_stage_idx)
　m_i_current_stage_idxで指定した番号のステージシーンに差し替えます

* if nodes[0].is_stage_failed():
　nodesには0個以上のNode型の要素（Stageシーン）が並びますが、今回は0個か1個のどちらかなので、nodes[0]固定で使用します。
　現在プレイしているステージのres://Stages/stage_normal.gdスクリプトのis_stage_failed()メソッドを利用します。

＊ set_new_stage(m_i_current_stage_idx)
　ステージクリア失敗した場合、同じステージをやり直すので、m_i_current_stage_idxの値は変更せずに同じステージシーンに差し替えをします。

　実行します。
　敵の砦をすべて倒すと次のシーンにいきます。
　EnemyBulletがPlayerの陣地に到達すると同じステージのやり直しになります。

　以上です。