Godot4で3D風２D縦スクロールシューティングゲームを作る 9回目 スコア処理と表示

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

# スコア処理と表示
　敵を撃墜したときにスコアを加算し、画面に表示します。

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

 https://github.com/footinglow/woden/tree/009_display_and_update_score

# 方針
　ゲーム全体を管理しているgame.gdスクリプトが、スコアを管理・表示することにします。
　撃墜判定は敵スクリプトのtwin_body.gdで実施しているため、敵スクリプトがgameスクリプトにスコアの加算を通知する必要があります。
　敵スクリプトはsignalでスコア更新通知を送信します。signalの接続先は、外部から設定してもらうようにして、敵スクリプトが外部に依存関係を持たせないようにします。
 
```plantuml

activate 敵
->敵 : 通知先設定
<<-敵 : スコア更新通知
destroy 敵
```

 
## 敵スクリプトで宣言したシグナルをgameに接続する
　シナリオ要素が敵を生成します。その時に接続先としてgameノードのメソッドを設定しますが、直接gameノードを呼び出すと依存関係が強くなりすぎるため、グローバル変数のノード経由で呼び出すようにします。
　そのため初期設定時に、gameノード自信をglobalに登録して、シナリオ要素は敵を生成したときに、globalに登録されたノードのメソッドをシグナルの接続先として、敵に設定します。
```plantuml
participant game
participant global
participant シナリオ
participant シナリオ要素
participant 敵

activate game
game->global : gameノードの登録
activate global
deactivate game
deactivate global

activate シナリオ要素
シナリオ要素->敵 ** :<<create>>\ninstantiate()
rnote over 敵 : add_to_scoreシグナル宣言
シナリオ要素->global : gameノード取得
activate global
deactivate global
シナリオ要素->敵 : add_to_score.connect\n( gameのメソッド名を通知 )
activate 敵
```


## 敵が撃墜された場合シグナルを発行
　敵スクリプトは撃墜されたことを判定すると、add_to_scoreシグナルを発行して、queue_freeにより消滅します。
 　gameスクリプトがadd_to_scoreシグナルを受信し、スコアの処理を実行し、表示を更新します。
```plantuml
participant game
participant global
participant シナリオ
participant シナリオ要素
participant 敵

activate 敵
rnote over 敵 : 撃墜された判定
敵->game :<<signal>>\nadd_to_score
rnote over game : スコア加算\n表示の更新
destroy 敵
```

# スコア表示領域を追加する
　game.tscnにスコア表示用のControlノードを追加します。
　ルートノード直下にControlを追加して、「HUD」に名称を変更します。
　HUDの子ノードとして、Labelを２つ追加して、名称を「score」と「score_value」に変更します。
 ![スクリーンショット (878).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9ea9b297-060a-9507-3c11-2e4486bb305f.png)

 ２つのLabelはインスペクタを開き、それぞれ下記のように変更しました。
## score : Label 
 「score」文字列を表示します。白の太字に設定します。
 
 | ノード名 | 変更箇所 |
|:-|:-|
| Label/Text  | "score" |
| Label/新規LabelSettings/Font/Size  | 20px  |
| Label/新規LabelSettings/Outline/Size  | 5px  |
| Control/Size/x| 61px |
| Control/Position/x| 11px |

## score_value : Label 
scoreの数字を表示します。黄色の太字に設定します。 
 | ノード名 | 変更箇所 |
|:-|:-|
| Label/Text  | "score" |
| Label/新規LabelSettings/Font/Size  | 20px  |
| Label/新規LabelSettings/Font/Color  | 黄色 Hex:#c2c700 |
| Label/新規LabelSettings/Outline/Size  | 5px  |
| Label/新規LabelSettings/Outline/Color  | 黄色　Hex:#5eb000  |
| Control/Size/x| 118px |
| Control/Position/x| 86px |

# 撃墜された敵がadd_to_scoreシグナルを発行する
　twin_body.gdを開き下記のように修正します。
 
```diff_gd_script
extends Area3D

+signal add_to_score(int)

@export var _d_speed_mps = 20.0
@export var _d_hp = 1
+@export var _i_score = 100

func _physics_process(delta):
	global_position += Vector3(0, 0, 1) * _d_speed_mps * delta
	if _d_hp <= 0:
+		add_to_score.emit(_i_score)
		queue_free()

func _on_visible_on_screen_notifier_3d_screen_exited():
	queue_free()

# HPからダメージポイントを引き算する。余った分はreturn値で返す
func calc_damage(laser_power) -> int:
	var remainder = 0
	if _d_hp <= laser_power:
		# ダメージの方が大きい
		remainder = laser_power - _d_hp
		_d_hp = 0
	else:
		# HPの方が大きい
		_d_hp -= laser_power
		remainder = 0
	# 余ったダメージポイントを返す
	return remainder
```
　発行するシグナルを宣言します。引数は1個でこの敵を倒した時のスコアを設定します。
```diff_gd_script
+signal add_to_score(int)
```
　スコアはパラメータにします。
```diff_gd_script
+@export var _i_score = 100
```
　HPが0になったときが撃墜された時なので、スコアを設定しadd_to_scoreシグナルを発行(emit)します。
```diff_gd_script
func _physics_process(delta):
	global_position += Vector3(0, 0, 1) * _d_speed_mps * delta
	if _d_hp <= 0:
+		add_to_score.emit(_i_score)
		queue_free()
```

# シグナルの接続先ノードをグローバル変数にする
　gameノードは、グローバル変数経由でアクセスするようにします。
```diff_gd_script
extends Node3D

+# 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  
```

# add_to_scoreを受けてスコアを表示する処理を実装する
　game.gdを開き、下記のように実装します。

```diff_gd_script
extends Node3D

var _scn_scenario_stage = preload("res://scenario/scenario_stage01.tscn")

var _node_scenario_ins

+var _i_score : int = 0

func _ready():
+	# gameインスタンスを保存
+	g_val.node_game = self
	
	# インスタンス化したシーンの追加先を設定する
	g_val.node_lasers = $DynamicNodes/lasers
	g_val.node_enemies = $DynamicNodes/enemy/bodies
	g_val.node_bullets = $DynamicNodes/enemy/bullets

	# Playerインスタンスを設定する
	g_val.node_player = $Player
	
	# ステージシナリオをシーンツリーに追加
	_node_scenario_ins = _scn_scenario_stage.instantiate()
	add_child(_node_scenario_ins)

func _process(delta):
	# シナリオを駆動する
	_node_scenario_ins.drive_scenario()

+func _on_add_to_score(add_score):
+	_i_score += add_score
+	$HUD/score_value.text = "%010d" % _i_score 
```
　game.gdでスコアを管理するので、メンバ変数として_i_scoreを宣言します。
```diff_gd_script
+var _i_score : int = 0
```
　add_to_scoreシグナルを受けて、スコアを加算し、score_valueラベルのtextを更新するメソッドを追加します。
```diff_gd_script
+func _on_add_to_score(add_score):
+	_i_score += add_score
+	$HUD/score_value.text = "%010d" % _i_score 
```
　初期設定時に、gameノードを設定します。
```diff_gd_script
func _ready():
+	# gameインスタンスを保存
+	g_val.node_game = $"."
```

# 敵インスタンスを生成したときに、add_to_socreシグナルの接続先を設定する
　シナリオ要素が、敵を生成したときに、add_to_scoreシグナルの接続先を設定します。
　produce_twin_body.gdを開き下記のように変更します。

```diff_gd_script
extends Node

@export	var _i_produce_num = 10
@export var _d_produce_interval_sec = 0.5
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.position.z = -200
	ins.position.x = randf_range(-25, 25)
+	# 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()
```

 生成した敵インスタンスがinsです。insのadd_to_scoreシグナルを、グローバル変数を介してgameノードの_on_add_to_scoreメソッドに接続します。
```diff_gd_script
+	# add_to_scoreシグナルをgame.gdに接続する
+	ins.add_to_score.connect( g_val.node_game._on_add_to_score )
```

# 実行結果
　敵を撃墜すると、スコアが加算されて表示が更新されることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/3ea6f112-df21-b0b3-ae14-57d90d97557f.png)

以上です。