0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

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

インスタンス化したシーンを一元管理する

 Playerのスクリプトで生成したレーザーのインスタンスや、敵キャラtwin_bodyのインスタンスは、add_childやadd_siblingでシーンツリーに追加しているため、下図のように、生成したノードの子供の階層や同じ階層に追加しています。
image.png

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

github

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

自動読み込み(Autoload)機能

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

インスタンスの生成先ノードを用意する

 game.tscnを開き、ルートノードの「game」にNode3Dを追加し、名称をDynamicNodesに変更します。DynamicNodesに子ノードを追加し、階層化してインスタンスを管理します。
image.png

ノード名 用途
lasers 自機が発射するレーザーのインスタンスの生成先
enemy/bodies 敵本体のインスタンスの生成先
enemy/bullets 敵の弾のインスタンスの生成先

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

global.gdの修正

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

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」のような文字列を挿入できます。

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ノードに追加します。

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		# 最新のタッチ位置・ドラッグ位置

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

# 画面をタッチした位置から、検索する3Dオブジェクトまでの最大距離
@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空間のカメラの3D位置と、カメラからタッチしたピクセルを見た方向の1000m先の3D位置を計算する
		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と呼ばれる、物理3D空間状態の情報を利用して、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
			
			# 3D空間の移動目標位置
			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ノードに追加します。

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機しかいませんが、上部に隠れてもう1機います。
image.png

まとめ

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

以上です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?