Godot4で3D風２D縦スクロールシューティングゲームを作る 15回目 スクロール速度を変化するシナリオ要素を追加

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

# スクロール速度を変化するシナリオ要素を追加
　背景のスクロールの速度を変化するためのシナリオ要素を作成します。ついでに、画面から敵と敵の弾がすべて消えるまで待つシナリオ要素も追加します。

# スクロール速度変更の方法について
　今の実装では、天球の星空画像を1秒間に-5°回転させることで、背景がスクロールしているように見せています。スクロールの速度を指定するのに、-5°の数字を変更するのは符号を間違ったりして混乱しそうなので、倍率変数を追加して、倍率変数を変更すると背景のスクロールの速度が変化するようにします。

- 変更前：固定
　スクロール速度　＝　-5[°/秒]
- 変更後：倍率変数を追加
　スクロール速度　＝　-5[°/秒] **×** **倍率変数**
 
　またいきなり指定した倍率になるのではなく、時間をかけて徐々に倍率変数が目標倍率に変化するようにします。そのためスクロール速度の変更パラメータは、目標速度倍率と倍率変更完了時間の２つを指定するようにします。例えば現在の倍率変数が1.0で、目標速度の倍率を7.0倍、倍率変更完了時間を5.0秒に指定すると以下のグラフのように、5秒間かけて倍率変数が1.0から7.0に徐々に変化するようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/7c45c076-21be-cf38-df89-d3c212cf4c4b.png)

　経過時間に比例して倍率を変更するので、Timerノードを使用して、time_leftプロパティから経過時間を取得します(経過時間＝倍率変更完了時間 - time_left）

　また、経過時間に比例した倍率を計算するため、remap関数を使用します。
```gdscript
float remap(value: float, istart: float, istop: float, ostart: float, ostop: float)
```
　入力値の範囲をistart、istopに設定し、出力したい値の範囲をostart,ostopに指定して、valueに入力値を指定すると、入力値から入力値範囲内の位置を計算して、出力値範囲から対応する値を出力します。
　入力値が入力値範囲外の場合、出力値も範囲外になります。
　今回の場合入力値が経過時間、出力値は倍率になります。

入力値範囲：istart:0.0秒～istop:5.0秒
出力値範囲：ostart:1.0倍～ostop:7.0秒

| 入力値value | 出力 | 説明 |
|:-:|:-:|:-|
| 0.0秒  | 1.0倍  |下限 |
| 2.5秒  | 4.0倍  |入力値が中間の場合出、力値も中間 |
| 4.0秒  | 5.8倍 | 入力値が上限よりに8割の場合、出力値も |
| 5.0秒  | 7.0倍  |上限 |
| -1.0秒 | -0.2倍 |範囲外の入力値の場合、出力値も範囲外 |
| 6.0秒 | 8.2倍  | 範囲外の入力値の場合、出力値も範囲外|
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9b5b6f74-2aa9-681f-2836-7293446e9d33.png)


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

https://github.com/footinglow/woden/tree/015_add_scenario_element_for_changing_scroll_speed

# 追加・修正ファイル
　下図で選択されているのが本記事で追加・修正したファイルで、２か所の赤枠で囲んだファイルが新規追加した２つのシナリオ要素です。
 　上の赤枠の「control_scroll_speed.gd/tscn」が背景のスクロール速度を変化するためのシナリオ要素でgame.gd、WorldEnvironment.gd、global.gdと合わせて、スクロール速度変更を実現します。
  下の赤枠の「wait_until_all_disappear.gd/tscn」は、画面上から敵や敵の弾がいなくなるまで待つためのシナリオ要素です。
　新規追加したシナリオ要素は、scenario_stage01.tscnシナリオファイルに追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/34db78b7-8922-5653-50f3-f0ba4b8048aa.png)

# 背景のスクロール速度を変化するシナリオ要素の追加

## 背景スクロール速度の倍率変数を追加
　背景のスクロールを制御しているWorldEnvironment.gdに倍率変数を追加します。

res://game/WorldEnvironment.gd
```diff_gd_script
extends WorldEnvironment

+var d_scroll_speed_magni = 1.0
var _d_speed_degps = -5.0		# 回転速度 [degree/sec]

func _process(delta):
-	get_environment().sky_rotation.x += deg_to_rad(m_d_speed_degps * delta)
+	get_environment().sky_rotation.x += deg_to_rad(_d_speed_degps * d_scroll_speed_magni * delta)
```
　メンバ変数に「d_scroll_speed_magni」が倍率変数で、_processメソッド内の回転量に掛け算するようにしました。

## シナリオ要素からWorldEnvironmentをアクセスできるようにグローバル変数を追加
　シナリオ要素から、WorldEnvironment.gdのメンバ変数、「倍率変数」にアクセスしたいので、グローバルデータにnode_worldを追加して、game.gdの_readyメソッドでWorldEnvironmentノードを設定し、「g_val.node_world」でいつでもどこからでも、WorldEnvironmentにアクセスできるようにします。

res://global/global.gd
```diff_gd_script
extends Node3D

# アップグレードアイテムの種別
enum EnItemKind{
	POWER_UP,
	SPEED_UP	
}

# ゲーム画面の見える範囲
# ゲーム画面の上端をtop、下端をbotommとする
var d_visible_top_z_m = -200.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
var node_others : Node3D  = null
+var node_world : WorldEnvironment  = null

# Playerのインスタンス
var node_player : Area3D = null 
```
res://game/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
	g_val.node_others = $DynamicNodes/others
+	g_val.node_world = $WorldEnvironment

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

## スクロール速度変更シナリオ要素用シーンを追加
　res://scenario/elements配下に新規シーンを追加します。ルートノードはNode3Dにしてファイル名はcontrol_scroll_speed.tscnにします。
　また子ノードに、倍率変更完了時間までの経過時間を計算するため、Timerノードを追加します。1回だけ時間を計りたいので、One Shotプロパティをオンにします。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/ca92f9e0-921a-7ec2-322d-4def1b502774.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/9e5916b2-8746-ff3b-6651-07cc34c67be9.png)

## スクロール速度変更シナリオのスクリプトを実装
　control_scroll_speed.tscnシーンのルートノードにスクリプトをアタッチし、以下のように実装します。
 
```gdscript
extends Node

@export var _d_target_scroll_magni = 7.0	# 目標とするスクロール速度の倍率
@export var _d_period_time_sec = 5.0		# 目標倍率完了時間

var _d_init_speed_magni = 1.0

func start_element():
	# シナリオ要素開始時点の、スクロール倍率変数を取得
	_d_init_speed_magni = g_val.node_world.d_scroll_speed_magni
	# タイマー開始
	$Timer.start(_d_period_time_sec)

func is_blocking_to_move_on_next_scenario() -> bool:
	# シナリオを次に進める
	return false

func _physics_process(delta):
	if !$Timer.is_stopped():
		# タイマーが動いている間はスクロール速度倍率を制御する
		var d_past_time = _d_period_time_sec - $Timer.time_left
		var sc_magni = remap(
					d_past_time,								# value
					0,					_d_period_time_sec,		# istart, istop
					_d_init_speed_magni,_d_target_scroll_magni	# ostart, ostop
				)
		g_val.node_world.d_scroll_speed_magni = sc_magni
	else:
		pass
```
　スクリプトを説明すると、スクロール速度のパラメータとして、目標とするスクロール速度の倍率と倍率変更完了時間の２つを実装します。exportして、変更可能にします。
```gdscript
@export var _d_target_scroll_magni = 7.0	# 目標とするスクロール速度の倍率
@export var _d_period_time_sec = 5.0		# 目標倍率完了時間
```
　スクロール速度の倍率変数は、現在の倍率から目標倍率まで変化させるので、シナリオ要素が開始した時点の現在倍率を保存します。パラメータの目標倍率完了時間を指定して、Timerノードを開始します。
```gdscript
var _d_init_speed_magni = 1.0

func start_element():
	# スクロール開始時点の、スクロール速度倍数を取得
	_d_init_speed_magni = g_val.node_world.d_scroll_speed_magni
	# タイマー開始
	$Timer.start(_d_period_time_sec)
```

　スクロースの速度を変更している間にシナリオを止める理由はないので、falseを返して次のシナリオ要素を開始を許可します。
```gdscript
func is_blocking_to_move_on_next_scenario() -> bool:
	# シナリオを次に進める
	return false
```
　_physics_process()でぐるぐる回っています。
　Timerノードを開始してからタイマーが満了して停止するまでの間、経過時間を計算して、remap関数でスクロール速度の倍率を計算し、WorldEnvironmentノードの倍率変数を更新します。
```gdscript
func _physics_process(delta):
	if !$Timer.is_stopped():
		# タイマーが動いている間はスクロール速度倍率を制御する
		var d_past_time = _d_period_time_sec - $Timer.time_left
		var sc_magni = remap(
					d_past_time,								# value
					0,					_d_period_time_sec,		# istart, istop
					_d_init_speed_magni,_d_target_scroll_magni	# ostart, ostop
				)
		g_val.node_world.d_scroll_speed_magni = sc_magni
	else:
		pass
```
:::note
　WorldEnvironmentのd_scroll_speed_magniは、外部から読み書きされるのに排他していませんが、複数のシーン（シナリオ要素）が同時刻にスクロール速度の倍率を変更することはないという前提です。
:::

# 画面から敵と敵の弾がすべて消えるまで待つシナリオ要素の追加
res://scenario/elements配下に新規シーンを作成します。ルートノードはNode3Dで、ファイル名はwait_until_all_disappear.tscnです。

 res://scenario/elements/wait_until_all_disappear.tscn
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/8a4bf0f7-0c0f-3ede-ccae-f567afff1e1c.png)
　ルートノードにスクリプトをアタッチして下記のように修正します。
　敵、敵の弾、アイテムなど動的に生成されたインスタンスは、gameシーンのDynamicNodes配下の決められたノードに子ノードとして追加され、撃墜されたり画面外に出るなどして、不要になった場合は削除されます。
　つまり、DynamicNodes配下のenemy/bodiesノードとenemy/bulletsノードとothersノードの、いずれかの子ノード数が0より大きい場合は、敵か敵の弾、もしくはアイテムが存在すると判断できます。その場合trueを返して次のシナリオ要素に進むのをブロックし、子ノード数が全て0の場合は、falseを返して、次のシナリオ要素に進むのを許可します。

res://scenario/elements/wait_until_all_disappear.gd
```gdscript
extends Node

func start_element():
	pass
	
func is_blocking_to_move_on_next_scenario() -> bool:
	# 敵が残っている場合、シナリオは進めない
	if g_val.node_enemies.get_child_count() > 0:
		return true
	# 敵の弾が残っている場合、シナリオは進めない
	if g_val.node_bullets.get_child_count() > 0:
		return true
	# アイテムが残っている場合、シナリオは進めない
	if g_val.node_others.get_child_count() > 0:
		return true

	# シナリオを進める
	return false
```

　動的に生成したインスタンスの追加先のうち、敵と敵の弾、アイテムの追加先を赤枠で示しています。
res://game/game.tscn
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/a3cd4ed4-771b-9182-f6dd-1a786c16321f.png)

# 新規追加したシナリオ要素をシナリオに追加
　本記事で追加した、control_scroll_speed.tscnを２つ、wait_until_all_disappear.tscnをひとつ、シナリオに追加しました。
res://scenario/scenario_stage01.tscn
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/7d076f89-a0fd-da60-7ee1-c97a8b9b76fe.png)

　上の方のスクロール速度変更シナリオ要素control_scroll_speedは、目標速度倍率を7.0倍、目標倍率完了時間を5秒にしました。スクロール速度の初期値は1.0倍なので、5秒かけて1.0倍から7.0倍に変化します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/98fa49d1-d1b2-3001-10d4-c2aca6e2e0cb.png)

　下の方のスクロール速度変更シナリオ要素control_scroll_speed2は目標速度倍率を1.0倍、目標倍率完了時間は同じ5秒にしました。スクロール速度が7.0倍になっているはずなので、5秒かけて7.0倍から1.0倍に変化します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/63916b85-2621-987e-18e9-196bd3a91307.png)

　以上です。