Godot4.1.3でシューティングゲームを作っていきます。
インスタンス化したシーンを一元管理する
Playerのスクリプトで生成したレーザーのインスタンスや、敵キャラtwin_bodyのインスタンスは、add_childやadd_siblingでシーンツリーに追加しているため、下図のように、生成したノードの子供の階層や同じ階層に追加しています。
先々、敵全体にダメージを与えたり、敵の弾を一括で消去する予定があります。インスタンスが散らばっていても、グループを使って検索する方法などありますが、時間がかかりそうなので、特定のノードの子として一元管理しようと思います。
特定のノードを指定するために、Godotの自動読み込み (Autoload)機能を使用します。
github
本記事で実装したものをgithubで公開しています。
自動読み込み(Autoload)機能
自動読み込み (Autoload)機能を使用すると、登録したスクリプトやノードのデータ、メソッドが唯一のインスタンスとして生成されて、いつ・どこからでもアクセスできるようになります。
いわゆるデザインパターンのシングルトンパターンとして使用することができ、複数のシーンで共有したい、統一された情報を扱う場合に最適なので、インスタンスの管理情報を、自動読み込み対象にします。
res://globalフォルダを作成して、新規スクリプトglobal.gdを生成します。
プロジェクト設定の「自動読み込み」タブを選択し、パス:res://global/global.gd、ノード名:g_valを設定して、追加ボタンを押下すると、以下のように登録されます。
これで、グローバル変数「g_val」を経由して、どのシーン、もしくはスクリプトからでもglobal.gd内のメンバ変数やメソッドを使用できるようになりました。
インスタンスの生成先ノードを用意する
game.tscnを開き、ルートノードの「game」にNode3Dを追加し、名称をDynamicNodesに変更します。DynamicNodesに子ノードを追加し、階層化してインスタンスを管理します。
ノード名 | 用途 |
---|---|
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機います。
まとめ
自動読み込み(Autoload)機能を使用して、インスタンスを一元管理できるようにしました。
他の言語同様、グローバル変数は便利な反面、読み書きのタイミングを考えないとバグのもとになるので、気を付けて使いたいと思います。
以上です。