Godot4で3D風２D縦スクロールシューティングゲームを作る 7回目 インスタンス化したシーンを一元管理する

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

# インスタンス化したシーンを一元管理する
　Playerのスクリプトで生成したレーザーのインスタンスや、敵キャラtwin_bodyのインスタンスは、add_childやadd_siblingでシーンツリーに追加しているため、下図のように、生成したノードの子供の階層や同じ階層に追加しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/bb25f3a2-446c-6894-a196-599b1fe7c434.png)

　先々、敵全体にダメージを与えたり、敵の弾を一括で消去する予定があります。インスタンスが散らばっていても、グループを使って検索する方法などありますが、時間がかかりそうなので、特定のノードの子として一元管理しようと思います。
 　特定のノードを指定するために、Godotの自動読み込み (Autoload)機能を使用します。

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

https://github.com/footinglow/woden/tree/007_manage_dynamic_instances_centrally

# 自動読み込み(Autoload)機能
　自動読み込み (Autoload)機能を使用すると、登録したスクリプトやノードのデータ、メソッドが唯一のインスタンスとして生成されて、いつ・どこからでもアクセスできるようになります。
　いわゆるデザインパターンのシングルトンパターンとして使用することができ、複数のシーンで共有したい、統一された情報を扱う場合に最適なので、インスタンスの管理情報を、自動読み込み対象にします。
　res://globalフォルダを作成して、新規スクリプトglobal.gdを生成します。
　プロジェクト設定の「自動読み込み」タブを選択し、パス：res://global/global.gd、ノード名:g_valを設定して、追加ボタンを押下すると、以下のように登録されます。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/2b54459c-f08c-f961-1cc7-732ea4e583ff.png)
　これで、グローバル変数「g_val」を経由して、どのシーン、もしくはスクリプトからでもglobal.gd内のメンバ変数やメソッドを使用できるようになりました。

# インスタンスの生成先ノードを用意する
　game.tscnを開き、ルートノードの「game」にNode3Dを追加し、名称をDynamicNodesに変更します。DynamicNodesに子ノードを追加し、階層化してインスタンスを管理します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/e1e79d44-565c-4e95-da85-ddd94cddfae7.png)
| ノード名 | 用途 |
|:-|:-|
| lasers  | 自機が発射するレーザーのインスタンスの生成先  |
| enemy/bodies  | 敵本体のインスタンスの生成先  |
| enemy/bullets  | 敵の弾のインスタンスの生成先  |

# レーザーと敵キャラtwin_bodyのインスタンスの生成先を変更する

## global.gdの修正
　先ほど追加したglobal.gdを以下のように修正します。
 ```gdscript
extends Node3D

# 生成したインスタンスの追加先
var node_lasers : Node3D = null
var node_enemies : Node3D = null 
var node_bullets : Node3D  = null
```
## game.gdの修正
　global.gdのメンバー変数はg_valを経由して読み書きできるので、game.gdの_readyメソッドで初期値を設定します。初期値はDynamicNodesノード配下のインスタンス管理用ノードをそれぞれ設定します。シーンのノードをテキストエディタにドラッグアンドドロップすると「$DynamicNodes/lasers」のような文字列を挿入できます。
 ```diff_gd_script
extends Node3D

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

var _node_scenario_ins

func _ready():
+	# インスタンス化したシーンの追加先を設定する
+	g_val.node_lasers = $DynamicNodes/lasers
+	g_val.node_enemies = $DynamicNodes/enemy/bodies
+	g_val.node_bullets = $DynamicNodes/enemy/bullets
+	
+	# ステージシナリオをシーンツリーに追加
	_node_scenario_ins = _scn_scenario_stage.instantiate()
	add_child(_node_scenario_ins)

func _process(delta):
	# シナリオを駆動する
	_node_scenario_ins.drive_scenario()
```
 # Player.gdの修正
　自機から発射したレーザーのインスタンスを、g_val.node_lasersを使って、$DynamicNodes/lasersノードに追加します。
 
```diff_gd_script
extends Area3D

# Player
@export var _d_speed_mps = 100.0
@export var _d_hp = 1.0

# ドラッグ操作検知用
var _f_is_screen_touch = false		# 画面にタッチ（ドラッグ含む）している場合true、タッチしていない場合はfalse
var _v2_drag_pos = Vector2.ZERO		# 最新のタッチ位置・ドラッグ位置

# タッチしたときの、３D上のタッチ位置とPlayer位置
var _v_first_touch_pos   = null
var _v_first_touch_player_pos = Vector3.ZERO

# 画面をタッチした位置から、検索する３Dオブジェクトまでの最大距離
@export var _d_ray_length_m = 1000		# [m]十分な長さにする

# レーザーの発射タイミング
@export var _scn_laser : PackedScene
@export var _d_firing_interval_sec = 0.2
var _d_firing_remain_time_sec = 0

func _input(event):
	# 画面にタッチしている状態と、タッチしていない状態を判断する
	if event is InputEventScreenTouch:
		_f_is_screen_touch = event.is_pressed()
		# ドラッグ位置（タッチ位置）を更新する
		_v2_drag_pos = event.position	
	elif event is InputEventScreenDrag:
		# ドラッグしているということは、タッチしている。（参考：InputEventScreenDragの場合、event.is_pressed()は常にfalse)
		_f_is_screen_touch = true
		# ドラッグ位置（タッチ位置）を更新する
		_v2_drag_pos = event.position	
	else:
		pass

func _physics_process(delta):
	if _f_is_screen_touch:
		var camera = get_viewport().get_camera_3d()

		# カメラを利用して3D空間のカメラの３D位置と、カメラからタッチしたピクセルを見た方向の1000m先の３D位置を計算する
		var from3d = camera.project_ray_origin(_v2_drag_pos)
		var to3d = from3d + camera.project_ray_normal(_v2_drag_pos) * _d_ray_length_m

		# 3D ray physics queryの作成
		var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
		query.collide_with_areas = true		# Area3Dを検知できるようにする
		
		#　spaceと呼ばれる、物理３D空間状態の情報を利用して、Area3Dを含む衝突位置を計算する
		var space_state = get_world_3d().direct_space_state
		var result = space_state.intersect_ray(query)
		if result:
			# 衝突位置を取得
			var v_drag_pos = result.position
			v_drag_pos.y = global_position.y	# Y方向には移動しないための設定
			if _v_first_touch_pos == null :
				# 初めてタッチしたときの、3D位置を記憶する
				_v_first_touch_pos = v_drag_pos
				# 初めてタッチしたときの、プレイヤーの3D位置を記憶する
				_v_first_touch_player_pos = global_position
			else:
				pass
			
			# ３D空間の移動目標位置
			var v_target_pos = _v_first_touch_player_pos + ( v_drag_pos - _v_first_touch_pos )
			
			# Player位置から移動目標位置までの相対位置を計算
			var v_target : Vector3 = v_target_pos - global_position
			# 移動目標位置に向かう速度ベクトルを生成
			var v_velocity : Vector3 = v_target.normalized() * _d_speed_mps
			if v_target.length() < ( v_velocity * delta ).length():
				# 目的地までの距離が、delta当たりの移動量よりも小さい場合は、飛び越えてしまうため、Playerの位置を目標位置にする
				global_position = v_target_pos
			else:
				# CharacterBody3Dのvelocityに速度を設定して動かす
				position += v_velocity * delta
		else:
			# タッチ位置に衝突したものが無い
			pass
	else:
		# タッチしていない場合はnullを設定することで、次にタッチしたときに、新しいタッチ位置とPlayer位置を記憶する
		_v_first_touch_pos = null
		
	if _f_is_screen_touch:
		# タイミングをとりながらlaser発射
		if _d_firing_remain_time_sec > 0:
			# 次の発射までの残り時間を減算するのみ
			_d_firing_remain_time_sec -= delta
		else:
			# レーザーを発射する
			var scn : Area3D = _scn_laser.instantiate()
			scn.set_pos(global_position)
-			add_sibling(scn)
+			g_val.node_lasers.add_child(scn)
			# 発射間隔を設定
			_d_firing_remain_time_sec = _d_firing_interval_sec
	else:
		# タッチしたときにすぐ、レーダーが発射するようにする
		_d_firing_remain_time_sec = 0

	# HPが0になった場合、Playerは消滅する
	if _d_hp <= 0:
		queue_free()
		
func _on_area_entered(area):
	# 敵もしくは敵の弾の攻撃があたったため、HPを0にする
	_d_hp = 0.0
```
 # produce_twin_body.gdの修正
　twin_bodyのインスタンスを、g_val.node_enemiesを使って、$DynamicNodes/enemy/bodiesノードに追加します。
 
```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_child(ins)
+	g_val.node_enemies.add_child(ins)
	
	# 生産完了したら、タイマーを停止する
	_i_produced_num += 1
	if ( _i_produced_num >= _i_produce_num ):
		$Timer_for_produce. stop()
```

# 動作確認
　指定したノードにインスタンスが生成されることを確認できました。twin_bodyは画面上には１機しかいませんが、上部に隠れてもう1機います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/96dfe065-ed97-0d49-09d0-30f26b824be8.png)

# まとめ
　自動読み込み(Autoload)機能を使用して、インスタンスを一元管理できるようにしました。
　他の言語同様、グローバル変数は便利な反面、読み書きのタイミングを考えないとバグのもとになるので、気を付けて使いたいと思います。

以上です。