Godot4.1.3でシューティングゲームを作っていきます。
敵が弾を撃つ
敵が弾を撃ってPlayerを攻撃するようにします。
github
本記事で実装したものをgithubで公開しています。
方針
最初は実装を簡単にしたいので、ノード間の相互作用をなるべく少なくして、twin_bodyシーンに追加すると自動でPlayerに向かって弾を撃つようにシーンを実装します。
シーケンス図を書きました。
敵シーンの子ノードとして生成された「fire_bullet_periodically」が敵の弾を生成するシーンのノードです。GodotEngineからfire_bullet_periodicallyの_readyメソッドが呼ばれると、パラメータの周期時間でTimerを開始し、周期的に敵の弾であるenemy_bulletのインスタンスを生成します。
敵の弾であるenemy_bulletのインスタンスは生成後、初期位置と目標位置を設定されると、自律的に飛んでいくようにします。
fire_bullet_periodicallyシーンと、enemy_bulletシーンの合計2つのシーンとアタッチするスクリプトを追加しますが、その前にPlayerの位置を取得するための仕組みを実装します。
Playerの位置をglobal.gdに登録する
弾をPlayerに向かって撃つために、Playerの位置情報が必要になります。今回追加する弾を撃つシーン以外でもPlayerの位置を知りたくなりそうなので、Playerノードはglobal.gdのメンバから参照できるようにします。
global.gdにPlayerノードを参照するメンバ変数を追加
extends Node3D
# 生成したインスタンスの追加先
var node_lasers : Node3D = null
var node_enemies : Node3D = null
var node_bullets : Node3D = null
+# Playerのインスタンス
+var node_player : Area3D = null
game.gdの_readyメソッドでPlayerノードを設定
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
+ # 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()
これで、Playerのインスタンスは、g_val.node_playerで、どこからでも参照できるようになりました。
敵の弾のシーン「enemy_bullet」を作成する
敵の弾は、初期位置と目標位置を設定すると、決められた速度でまっすぐ進むようにします。
敵の弾の画像
敵の弾の画像は、gimp2のエアブラシを使って作成した後、上下につぶして楕円形にしました。
res://assets/texture/enemy_bullet/EnemyBullet.pngに格納しました。
敵の弾のシーン
res://character/enemy_bulletフォルダを作成し、新規シーンでenemy_bullet.tscnを作成します。ルートノードはArea3Dです。
CollisionShape3Dの追加と設定
Area3Dの衝突形状とサイズを設定するため子ノードにCollisionShape3Dを追加します。
敵の弾は楕円形ですが、処理速度が速い(と思われる)球にしたいので、ShapeはSphereShape3Dにします。大きさはPlayer機(約5m)の半分以下2mにしたいので、半径(Radius)を1mに設定します。
以下の絵は、後述するSprite3Dを設定をした後の3Dビューの表示です。
続いて、Player機のArea3Dが、本Area3Dの衝突を検出するための設定をします。
ルートノードを選択して、インスペクターのCollisionObject3D/CollisionのLayerの、EnemyBulletをチェックします。敵の弾のArea3Dは衝突の検出はしないためMaskはチェックしません(衝突はPlayerのArea3Dが検出します)。
Sprite3Dの追加と設定
画像を3Dに表示するため、Sprite3Dを追加します。インスペクタを開いて、Sprite3DのTextureにres://assets/texture/enemy_bullet/EnemyBullet.pngをドラッグアンドドロップして登録します(クイックロードを使用してもよいです)。
Y軸プラス側から敵の弾の画像が見えるようにしたいので、SpriteBase3DのAxisを「Y-Axis」に変更します。つぎにPixel Sizeで画像の表示サイズを調整します。今回用意した画像の場合、Pixel Sizeは0.035にすると丁度良くなりました。
敵の弾のGDScriptを実装する
ルートノードにスクリプトをアタッチして以下のように修正します。
VisibleOnScreenNotifier3Dのscreen_exited()シグナルを_on_visible_on_screen_notifier_3d_screen_exitedメソッドに接続する必要があります。
extends Area3D
@export var _d_speed_mps = 25.0
@export var _d_rotate_speed_degps = 180
var _v_dir = Vector3.ZERO
func set_pos(v_pos, v_target_pos):
global_position = v_pos
_v_dir = ( v_target_pos - v_pos).normalized()
func _physics_process(delta):
position += _v_dir * _d_speed_mps * delta
rotate_y(deg_to_rad(_d_rotate_speed_degps * delta))
func _on_visible_on_screen_notifier_3d_screen_exited():
queue_free()
スクリプトの詳細を説明します。
敵の弾の速度をパラメータ化しました。twin_bodyが20m/sなので、少し速い速度で25m/sに設定しました。
また敵の弾は回転します。2秒で1周するように、180°/secの速度を設定しました。
@export var _d_speed_mps = 25.0
@export var _d_rotate_speed_degps = 180
敵の弾のインスタンスを生成した後は、set_posメソッドにより、初期位置と移動方向を設定します。
2つの引数はグローバル座標(global_position)で、初期位置と、目標位置を指定されることを期待しています。初期位置は弾を発射する敵の位置で、目標位置はPlayerの位置になることが多いと思います。
初期位置はそのまま敵の弾のglobal_positionに設定します。目標位置までのベクトル(方向と距離)は引き算すると得ることができます。距離を1mにしたいので、normalizedメソッドの結果を方向情報として取得します。
func set_pos(v_pos, v_target_pos):
global_position = v_pos
_v_dir = ( v_target_pos - v_pos).normalized()
次に_physics_processメソッド内で実際に移動します。方向×速度(_v_dir * _d_speed_mps)で1秒間に進む移動量を得ることができます。_physics_processメソッドは基本的に1/60秒に1回起動されますが実際の経過時間がdeltaで通知されるので、deltaを掛け算すると、経過時間あたりの移動量を計算できます。
rotate_yで敵の弾を回転します。単位はラジアンなので、変換する必要があります。こちらも1秒あたりの回転量なので、deltaを掛け算します。
func _physics_process(delta):
position += _v_dir * _d_speed_mps * delta
rotate_y(deg_to_rad(_d_rotate_speed_degps * delta))
VisibleOnScreenNotifier3Dのscreen_exited()シグナルを受けた場合は敵の弾を消去します。
func _on_visible_on_screen_notifier_3d_screen_exited():
queue_free()
敵の弾を生成するシーン「fire_bullet_periodically」を実装する
fire_bullet_periodicallyシーンの実装
res://character/enemy_bullet配下に新規シーンでfire_bullet_periodically.tscnを追加します。ルートノードはMarker3Dにします。
敵の弾の初期位置が敵の中心位置で良い場合はMarker3Dにする意味はありませんが、大きい敵キャラを作った場合に敵の弾の初期位置を射出口の位置に移動したいときに、Maker3Dは3Dビュー上でプラスマークで表示されるため、位置を視認できて便利だと思います。
また周期的に実行するので、Timerノードを追加して、「FireTimer」に名称を変更します。インスペクタの初期値はディフォルトのままです。
fire_bullet_periodicallyのスクリプトの実装
ルートノードにスクリプトをアタッチして以下のように修正します。
extends Marker3D
@export var _d_fire_interval_sec = 5.0
var _scn_bullet : PackedScene = preload("res://character/enemy_bullet/enemy_bullet.tscn")
func _ready():
$FireTimer.start(_d_fire_interval_sec)
func _on_fire_timer_timeout():
var ins = _scn_bullet.instantiate()
ins.set_pos( global_position, g_val.node_player.global_position)
g_val.node_bullets.add_child(ins)
スクリプトの説明をします。
弾を発生する周期はパラメータにして、_readyメソッドでタイマーを起動するときの引数に設定します。
@export var _d_fire_interval_sec = 5.0
func _ready():
$FireTimer.start(_d_fire_interval_sec)
先ほど実装した、敵の弾のシーン(PackedScene)をprealoadで読み込みます。preloadにより事前に読み込むことで、ゲーム中に余計な処理をしないようにします。
var _scn_bullet : PackedScene = preload("res://character/enemy_bullet/enemy_bullet.tscn")
敵の弾のインスタンスを生成し、set_posメソッドで初期位置と目標位置を設定します。親ノードである敵キャラのルートノードと、Marker3Dの位置は同じなので、Marker3Dのglobal_positionを初期位置とします。目標位置はPlayer機の位置なので、グローバル変数のg_val.node_playerでPlayerのインスタンスを取得し、そのglobal_positionを目標位置として設定します。最後に、所定のノードにインスタンスを追加します。
func _on_fire_timer_timeout():
var ins = _scn_bullet.instantiate()
ins.set_pos( global_position, g_val.node_player.global_position)
g_val.node_bullets.add_child(ins)
twin_Bodyに敵の弾を生成するノードを追加する
twin_bodyにfire_bullet_periodicallyを追加するのみです。
実行して動作確認をする
実行すると、敵が生成されてから5秒後に敵に弾をPlayer機に向けて撃ってきます。
敵の弾のインスタンスはbulletsノードの下に追加されました。
まとめ
敵の弾が登場すると、シューティングゲームっぽくなりますね。
でも3D表示にしていると、弾の軌道が予測しにくいです。やっぱりゲーム中は真上から見るように変更するかも。
以上です。