【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/f067ae62fe974b82f28e
　
　github(Godotのプロジェクトマネージャーからインポートして利用できます）
　https://github.com/footinglow/Godot4/tree/main/02_study/S18_GameSystem_003

# 画面遷移を決める
　画面遷移を決めます。
　最初は「Ready」と表示します。
　画面をタッチすると、プレイ中に遷移するようにします。ゲーム開始直後は２秒くらい「Go！」と表示します。
　またステージをクリアもしくはクリア失敗した場合は、「Stage Clear」「Stage Failed」と表示をして、画面タッチすると次のステージもしくは同じステージのやり直しの「Ready」が表示されるようにします。
　![GodotParts.pptx - PowerPoint 2023_03_09 20_32_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/831edcba-cf10-0cf3-f4fc-0ad751fa9542.png)

　ではGameSystemに実装していきます。

## タッチイベントを検知する
　タッチイベントを検出するようにします。
　res://game_system.gdを開いて下記を追加します。

```GDScript
# タッチイベント処理
var m_f_touch = false

func _input(event):
	if ( event is InputEventScreenTouch ) and event.pressed:
		m_f_touch = true
```

　「event is InputEventScreenTouch」は画面をタッチもしくは指を離すときに通知されるイベントです。
　タッチした時はevent.pressed=trueになるのでand条件で見ることでタッチを検出します。

```GDScript
func _physics_process(delta):
  :
 （省略）
  : 
	# _physics_process()の最後にm_f_touchフラグを落とす
	m_f_touch = false
```
　_physics_process()の一番最後にm_f_touchをfalseにすると、次の周期でm_f_touchがtrueの場合、今タッチイベントが発生したと判断できます。

# Readyを表示する
　ゲーム画面の上に「Ready」の文字列を表示しましょう。
　res://game_system.tscnを開きます。

　GameSystemを右クリックして、子ノードを追加から「Node」を追加しましょう。
　2Dの文字列を表示するだけなので、Node3Dではありません。
　名称を「Messages」に変更します。

　Messagesを右クリックして、子ノードを追加から「Label」を追加しましょう。(Label3Dではありません）
　![キャプチャ 2023_03_09 20_57_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/44823c90-e34c-b49c-9e1c-fd73d5427e52.png)
　名称を「Ready」に変更します。
![(_) game_system.tscn - S18_GameSystem_003 - Godot Engine 2023_03_09 21_00_33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/2d2546ac-707c-5017-770b-8f11c77bbe44.png)

　「Ready」を選択した状態でインスペクタのLabel/Textに「Ready]と入力します。
　またLabelSettingsの＜空＞をクリックして、「新規LabelSettings」を実行します。
![スクリーンショット (58).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/2f360828-30e4-04b9-36ac-21e1a4021135.png)
設定した「LabelSettings」をクリックすると詳細設定が開くので、Fontの中の「Size」を70に変更します。
![スクリーンショット (59).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/70ea998f-934d-60c4-6a3e-a274e16949ed.png)

:::note
　詳細設定の色が黄緑なのは、エディタ設定のインタフェース/テーマの「アクセントカラー」を変更したためです。
![スクリーンショット (60).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/fe3e923d-6192-098c-20b7-8ba100f5bbfa.png)
:::
　画面上部の「2D」をクリックしましょう。２Dの編集画面に「Ready」という文字列が表示されています。
![スクリーンショット (61).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/bb12d720-ed75-fca6-28bd-a66c5adb1d52.png)
　画面中央に表示したいので、「ビュー」メニューの右にあるアイコンをクリックして、中央を示すアイコンを選択します。
![スクリーンショット (62).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/2a0fc9bc-3360-6bb9-1dd4-ed654cdfa542.png)
中央に移動しました。（マウスホイール操作で表示範囲を拡大しました）
![スクリーンショット (63).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/a4c6e983-95da-a6b3-e9fb-3b0a9a0aec92.png)

　実行してみましょう。画面中央に「Ready」が表示されました。
　3D画面より2Dの画面の方が優先して表示されるようですね。
![キャプチャ 2023_03_09 21_12_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9a32b404-9297-e24d-ca80-cc3576596e71.png)

# READY状態とプレイ状態の遷移を作成する
　まずREADY状態の遷移をつくります。
　res://game_system.gdを開いて編集します。

```GDScript
extends Node3D

enum EN_GAME_STS {
	READY,
	IN_PLAY,
}
var m_en_game_sts = EN_GAME_STS.READY

# ステージシーンリスト（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

# タッチイベント処理
var m_f_touch = false

func _input(event):
	if ( event is InputEventScreenTouch ) and event.pressed:
		m_f_touch = true
			
func set_new_stage(idx):
	# CurrentStageにあるステージをすべて削除する
	var nodes = $CurrentStage.get_children()
	print(nodes)
	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):
	match m_en_game_sts :
		EN_GAME_STS.READY:
			# 画面タッチされたら、Ready表示を刑してプレイ中にする
			if m_f_touch:
				$Messages/Ready.hide()
				m_en_game_sts = EN_GAME_STS.IN_PLAY
		EN_GAME_STS.IN_PLAY:
			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)
	# _physics_process()の最後にm_f_touchフラグを落とす
	m_f_touch = false
```

　変更した点について説明します。

```GDScript
enum EN_GAME_STS {
	READY,
	IN_PLAY,
}
var m_en_game_sts = EN_GAME_STS.READY
```
ゲーム状態をenumで定義しました。
m_en_game_stsに現在のゲーム状態を保持するようにします。初期値はREADYです。

```GDScript
func _physics_process(delta):
	match m_en_game_sts :
		EN_GAME_STS.READY:
			# 画面タッチされたら、Ready表示を刑してプレイ中にする
			if m_f_touch:
				$Messages/Ready.hide()
				m_en_game_sts = EN_GAME_STS.IN_PLAY
		EN_GAME_STS.IN_PLAY:
　　　　　　　:
　　　　　　(以下今までの_physics_processの内容）
```
　_physics_process()メソッドはゲーム状態によって処理内容が変わります。

* m_en_game_stsがEN_GAME_STS.READYの場合
　「if m_f_touch:」によりタッチ判断をします。タッチされるまでは何もしません。
　タッチ検出した場合、「$Messages/Ready.hide()」でReadyの表示を消します。hide()関数でノードの表示を消すことができます。
　次の周期ではプレイ中の処理を実行したいので、m_en_game_sts = EN_GAME_STS.IN_PLAYに設定することで、次に_physics_process()が呼ばれたときはEN_GAME_STS.READYの場合の処理を実行するようになります。
* m_en_game_stsがEN_GAME_STS.READYの場合
　今までと同じステージクリア/失敗の処理を実行します。

　実行します。
　タッチするとREADYの文字が消えます。
![「【Godot 4.0】スマホ３Dゲームを作るための勉強　その１９　ゲームシステムを作る　その４」を編集 - Qiita - Google Chrome 2023_03_09 21_34_47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/918a324b-ac6f-995e-8245-ff1da82eb970.png)

　しかし放っておくと敵の砦からEnemyBullet(敵ソルジャー）がどんどん出てきます。

# プレイ中に遷移してから敵の砦が攻撃開始するように変更する
　原因はEnemy.tscnのTimer_fireノードのAutoStartをONにしているためです。
　初期値はAutoStartをOFFにして、ゲーム状態がIN_PLAY(プレイ中）に遷移するときにAutoStartをONにするようにしましょう。

　res://Characters/enemy.tscnを開きます。
　Timer_fireを選択して、インスペクタの「Timer/AutoStart」をOffにします。
![S18_GameSystem_003 (DEBUG) 2023_03_09 21_44_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/50e26f92-1b6d-e11c-75fd-90ffceb67e07.png)

　プレイ中開始/停止を指示したいノードを一括して制御するために、グループ機能の「call_group」メソッドを使います。

GODOT DOC Scripting Groups
https://docs.godotengine.org/en/latest/tutorials/scripting/groups.html#managing-groups

　プレイ開始/中止を制御するノードに「StartStopControl」グループを設定して、play_start/play_stopメソッドを実装しましょう。

　res://Characters/enemy.tscnを開きます。
　Enemyを選択して、インスペクタの右のノード/グループを選択します。
　![スクリーンショット (65).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/37ce3c48-525c-82b0-58a5-84f906c24c4c.png)
「StartStopControl」を入力して、「追加」ボタンを押下します。
![スクリーンショット (66).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/a4a45155-76bc-87ab-52c1-97b99c53d3a6.png)
追加されました。Enemyには合計３つのグループが登録されています。
![スクリーンショット (67).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/1ed5c58b-95e6-9f2f-75cd-5d8090fb6325.png)



　res://Characters/Enemy.gdを開いて、play_start/play_stopメソッドを追加します。

```GDScript
func game_start():
	$Timer_fire.start()
	
func game_stop():
	$Timer_fire.stop()
```

　それぞれTimer_fireタイマーを開始、停止するメソッドになっています。

　ではGameSystemから制御しましょう。
　res://game_system.gdを開きます。

```GDScript
		EN_GAME_STS.READY:
			# 画面タッチされたら、Ready表示を刑してプレイ中にする
			if m_f_touch:
				$Messages/Ready.hide()
				m_en_game_sts = EN_GAME_STS.IN_PLAY
				# "GameControl"グループを持つ、PlayerとEnemyのスクリプトのgame_start()メソッドを起動する
				get_tree().call_group("StartStopControl", "game_start")
		EN_GAME_STS.IN_PLAY:
```

* get_tree().call_group("StartStopControl", "game_start")
 get_tree()で取得したアクティブなノードに対して、"StartStopControl"グループを持つノードにアタッチされたスクリプトの「game_start」メソッドを起動します。

　実行しましょう。
　タッチするまでは敵が攻撃しなくなりました。

# GameSystem/Messagesのメッセージ表示をすべてhide()するメソッドの追加
　ゲーム状態の実装を進める前に、GameSystem/Messagesのメッセージ表示をすべてhide()するメソッドを追加します。
　res://game_system.gdを開いて修正します。

```GDScript
func hide_all_messages():
	# メッセージを表示をすべて隠す
	var nodes = $Messages.get_children()
	for node in nodes:
		node.hide()
```


```GDScript
func _physics_process(delta):
	match m_en_game_sts :
		EN_GAME_STS.READY:
			# 画面タッチされたら、Ready表示を刑してプレイ中にする
			if m_f_touch:
				hide_all_messages()
				m_en_game_sts = EN_GAME_STS.IN_PLAY
```
　「$Messages/Ready.hide()」から「hide_all_messages()」に変更しました。

# ゲーム状態を実装する

## メッセージを追加する
　res://game_system.tscnを開きます。
　「Ready」ノードをコピーしてから、Messagesノードを右クリックして「貼り付け」します。
　合計3つ張り付けて、名称を「Go」「StageClear」「StageFailed」にします。
![スクリーンショット (69).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9f1afb42-329f-a459-c52a-0b542eb17efb.png)

　「Go]を選択して、インスペクタのLabel/Textに「Go！」を設定します。
　同様に、StageClearは「Stage Clear」、StageFailedは「Stage Failed」にします。
![スクリーンショット (73).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/fa1c3a1b-5bdc-0198-5bd4-046e8c265910.png)
![スクリーンショット (74).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/2a7ed16b-c4db-da28-abd8-d5d5af05f220.png)
![スクリーンショット (75).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/4c93cad1-9a36-7821-1fe8-388bb7a61f23.png)


　初期状態はReadyのみ表示したいので、シーン内のGo,StageClear,StageFailedの右に表示されている目のアイコンをクリックして、目をつむっているアイコンに変更してください。
![スクリーンショット (71).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/0ce9357f-c02e-eba9-f94d-2c3bf48221fc.png)


## ゲーム状態遷移を実装する
　res://game_system.gdを開いて、ゲーム状態遷移を実装します。
　下記のように変更します。

```GDScript
extends Node3D

enum EN_GAME_STS {
	READY,
	IN_PLAY,
	STAGE_CLEAR,
	STAGE_FAILED,
}
var m_en_game_sts = EN_GAME_STS.READY

# ステージシーンリスト（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

# タッチイベント処理
var m_f_touch = false

func _input(event):
	if ( event is InputEventScreenTouch ) and event.pressed:
		m_f_touch = true
			
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 hide_all_messages():
	# メッセージを表示をすべて隠す
	var nodes = $Messages.get_children()
	for node in nodes:
		node.hide()
		
func _ready():
	set_new_stage(m_i_current_stage_idx)

func _physics_process(delta):
	match m_en_game_sts :
		EN_GAME_STS.READY:
			# 画面タッチされたら、Ready表示を刑してプレイ中にする
			if m_f_touch:
				hide_all_messages()
				m_en_game_sts = EN_GAME_STS.IN_PLAY
				# "GameControl"グループを持つ、PlayerとEnemyのスクリプトのgame_start()メソッドを起動する
				get_tree().call_group("StartStopControl", "game_start")
				# 「Go」を表示して２秒後にメッセージを消す
				$Messages/Go.show()
				await get_tree().create_timer(2.0).timeout
				hide_all_messages()
		EN_GAME_STS.IN_PLAY:
			var nodes = $CurrentStage.get_children()
			if nodes:
				# nodesは1個か0個のどちらかなので、0番目を固定的に使用する
				if nodes[0].is_stage_clear():
					# ゲーム状態を「ステージクリア」状態にして「Stage Clear」を表示
					m_en_game_sts = EN_GAME_STS.STAGE_CLEAR
					hide_all_messages()
					$Messages/StageClear.show()
				if nodes[0].is_stage_failed():
					# ゲーム状態を「ステージ失敗」状態にして「Stage Failed」を表示
					m_en_game_sts = EN_GAME_STS.STAGE_FAILED
					hide_all_messages()
					$Messages/StageFailed.show()
		EN_GAME_STS.STAGE_CLEAR:
			# 画面タッチされたら、Readyに遷移する
			if m_f_touch:					
				# 次のステージに進む
				# ステージは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)
				# ゲーム状態を「レディ」状態にして、「Ready」を表示する
				m_en_game_sts = EN_GAME_STS.READY
				hide_all_messages()
				$Messages/Ready.show()
		EN_GAME_STS.STAGE_FAILED:
			# 画面タッチされたら、Readyに遷移する
			if m_f_touch:
				set_new_stage(m_i_current_stage_idx)
				# ゲーム状態を「レディ」状態にして、「Ready」を表示する
				m_en_game_sts = EN_GAME_STS.READY
				hide_all_messages()
				$Messages/Ready.show()
	# _physics_process()の最後にm_f_touchフラグを落とす
	m_f_touch = false
```

　変更点を説明します。

```GDScript
enum EN_GAME_STS {
	READY,
	IN_PLAY,
	STAGE_CLEAR,
	STAGE_FAILED,
}
```
　ゲーム状態「STAGE_CLEAR」と「STAGE_FAILED]を追加しました。

　次に「_physics_process()」メソッドの中のEN_GAME_STS.READY状態の処理です。
```GDScript
func _physics_process(delta):
	match m_en_game_sts :
		EN_GAME_STS.READY:
			# 画面タッチされたら、Ready表示を刑してプレイ中にする
			if m_f_touch:
				hide_all_messages()
				m_en_game_sts = EN_GAME_STS.IN_PLAY
				# "GameControl"グループを持つ、PlayerとEnemyのスクリプトのgame_start()メソッドを起動する
				get_tree().call_group("StartStopControl", "game_start")
				# 「Go」を表示して２秒後にメッセージを消す
				$Messages/Go.show()
				await get_tree().create_timer(2.0).timeout
				hide_all_messages()
```
　「Ready」表示中の処理です。
　タッチ検出した処理の最後に処理を追加して、「$Messages/Go.show()」で「Go！」を表示するようにしました。　
　また２秒後に「Go！」の表示を消したいのですが、awaitを使用して、「await get_tree().create_timer(2.0).timeout」とすると２秒経過後に次の行「hide_all_messages()」を実行します。
　2秒待っている間も_physics_process()は周期的に呼び出されています。

:::note
 GODOT3.5の時のyield関数が、Godot4.0ではawaitに変更されています。
:::

```GDScript
		EN_GAME_STS.IN_PLAY:
			var nodes = $CurrentStage.get_children()
			if nodes:
				# nodesは1個か0個のどちらかなので、0番目を固定的に使用する
				if nodes[0].is_stage_clear():
					# ゲーム状態を「ステージクリア」状態にして「Stage Clear」を表示
					m_en_game_sts = EN_GAME_STS.STAGE_CLEAR
					hide_all_messages()
					$Messages/StageClear.show()
				if nodes[0].is_stage_failed():
					# ゲーム状態を「ステージ失敗」状態にして「Stage Failed」を表示
					m_en_game_sts = EN_GAME_STS.STAGE_FAILED
					hide_all_messages()
					$Messages/StageFailed.show()
```
　プレイ中の処理です
　ステージクリア/ステージ失敗判定時の処理が、状態遷移処理に変更しました。
　m_en_game_stsに新しいゲーム状態を設定して、hide_all_messages()でメッセージ表示（何も表示されていないと思いますが）を消した後、StageClearもしくはStageFailedのメッセージを表示します。

```GDScript
		EN_GAME_STS.STAGE_CLEAR:
			# 画面タッチされたら、Readyに遷移する
			if m_f_touch:					
				# 次のステージに進む
				# ステージは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)
				# ゲーム状態を「レディ」状態にして、「Ready」を表示する
				m_en_game_sts = EN_GAME_STS.READY
				hide_all_messages()
				$Messages/Ready.show()
```
　「Stage Clear」を表示中の状態です。
　タッチされるまではなにもしません。
　タッチを検出すると、次のステージに進む処理を実行します。
　その後READY状態に遷移(m_en_game_sts = EN_GAME_STS.READY)して、メッセージ表示を消した後、「Ready」を表示します。

```GDScript
		EN_GAME_STS.STAGE_FAILED:
			# 画面タッチされたら、Readyに遷移する
			if m_f_touch:
				set_new_stage(m_i_current_stage_idx)
				# ゲーム状態を「レディ」状態にして、「Ready」を表示する
				m_en_game_sts = EN_GAME_STS.READY
				hide_all_messages()
				$Messages/Ready.show()
```
　「Stage Failed」を表示中の状態です。
 　処理に内容は「Stage Clear」表示中の時とほぼ同じ処理です。

　実行しましょう。
　実行するとReadyが表示されて、タッチするとGo！が2秒間表示されます。
　ステージをクリアすると「Stage Clear」と表示されて、タッチすると次のステージに進み「Ready」が表示されます。
　ステージ失敗すると「Stage Failed」が表示されて、タッチすると次のステージに進み「Ready」が表示されます。
![S19_GameSystem_004 (DEBUG) 2023_03_09 22_50_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/056d2418-7029-e909-103e-cf7b2d1beef5.png)


以上