Godot4で3D風２D縦スクロールシューティングゲームを作る 8回目 敵が弾を撃つ

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

# 敵が弾を撃つ
　敵が弾を撃ってPlayerを攻撃するようにします。

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

https://github.com/footinglow/woden/tree/008_firing_enemy_bullet

# 方針
　最初は実装を簡単にしたいので、ノード間の相互作用をなるべく少なくして、twin_bodyシーンに追加すると自動でPlayerに向かって弾を撃つようにシーンを実装します。
　シーケンス図を書きました。
　敵シーンの子ノードとして生成された「fire_bullet_periodically」が敵の弾を生成するシーンのノードです。GodotEngineからfire_bullet_periodicallyの_readyメソッドが呼ばれると、パラメータの周期時間でTimerを開始し、周期的に敵の弾であるenemy_bulletのインスタンスを生成します。
　敵の弾であるenemy_bulletのインスタンスは生成後、初期位置と目標位置を設定されると、自律的に飛んでいくようにします。

```plantuml
GodotEngine-->敵シーン **: <<create>>\ninstantiate\n(シナリオ要素の\nproduce_xxxがトリガ)
activate 敵シーン
敵シーン-->fire_bullet_periodically ** : <<create>>\n子ノード生成
deactivate 敵シーン
activate fire_bullet_periodically
fire_bullet_periodically-->FireTimer ** : <<create>>\n子ノード生成
deactivate fire_bullet_periodically

GodotEngine->fire_bullet_periodically : _ready()
activate fire_bullet_periodically
fire_bullet_periodically->FireTimer : start(周期パラメータ)
activate FireTimer
deactivate fire_bullet_periodically

note over fire_bullet_periodically,FireTimer : 以降自律的に敵の弾を生成

FireTimer->fire_bullet_periodically : timeoutイベント
activate fire_bullet_periodically

fire_bullet_periodically-->enemy_bullet ** : <<create>>\ninstantiate
activate enemy_bullet
fire_bullet_periodically->enemy_bullet : set_pos(初期位置, 目標位置)
fire_bullet_periodically->enemy_bullet : add_child()
note over enemy_bullet : 敵の弾が自律的に移動

```
　fire_bullet_periodicallyシーンと、enemy_bulletシーンの合計2つのシーンとアタッチするスクリプトを追加しますが、その前にPlayerの位置を取得するための仕組みを実装します。

# Playerの位置をglobal.gdに登録する
　弾をPlayerに向かって撃つために、Playerの位置情報が必要になります。今回追加する弾を撃つシーン以外でもPlayerの位置を知りたくなりそうなので、Playerノードはglobal.gdのメンバから参照できるようにします。

## global.gdにPlayerノードを参照するメンバ変数を追加
```diff_gd_script
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ノードを設定
 ```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

+	# 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のエアブラシを使って作成した後、上下につぶして楕円形にしました。
 ![EnemyBullet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/59c04d55-2ff5-7912-cd55-accd8d2ccfa0.png)
　res://assets/texture/enemy_bullet/EnemyBullet.pngに格納しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/ec941480-39fd-3ab7-470c-2192d3f8b282.png)

## 敵の弾のシーン
　res://character/enemy_bulletフォルダを作成し、新規シーンでenemy_bullet.tscnを作成します。ルートノードはArea3Dです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/22620a00-41e0-3ecc-e3bd-bff4a53cbe32.png)
### CollisionShape3Dの追加と設定
　Area3Dの衝突形状とサイズを設定するため子ノードにCollisionShape3Dを追加します。
　敵の弾は楕円形ですが、処理速度が速い（と思われる）球にしたいので、ShapeはSphereShape3Dにします。大きさはPlayer機(約5m)の半分以下2mにしたいので、半径（Radius）を1mに設定します。
　以下の絵は、後述するSprite3Dを設定をした後の3Dビューの表示です。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/27049eb3-129f-f621-f64d-93ffe5dde775.png)
　続いて、Player機のArea3Dが、本Area3Dの衝突を検出するための設定をします。
　ルートノードを選択して、インスペクターのCollisionObject3D/CollisionのLayerの、EnemyBulletをチェックします。敵の弾のArea3Dは衝突の検出はしないためMaskはチェックしません（衝突はPlayerのArea3Dが検出します）。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/5df7e13c-0fbb-efbf-8152-3304f622e1a4.png)
 ### Sprite3Dの追加と設定
　画像を3Dに表示するため、Sprite3Dを追加します。インスペクタを開いて、Sprite3DのTextureにres://assets/texture/enemy_bullet/EnemyBullet.pngをドラッグアンドドロップして登録します（クイックロードを使用してもよいです）。
　Y軸プラス側から敵の弾の画像が見えるようにしたいので、SpriteBase3DのAxisを「Y-Axis」に変更します。つぎにPixel Sizeで画像の表示サイズを調整します。今回用意した画像の場合、Pixel Sizeは0.035にすると丁度良くなりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/7c309f87-e1fb-f535-1546-2fdab2202af4.png)

## 敵の弾のGDScriptを実装する
　ルートノードにスクリプトをアタッチして以下のように修正します。
　VisibleOnScreenNotifier3Dのscreen_exited()シグナルを_on_visible_on_screen_notifier_3d_screen_exitedメソッドに接続する必要があります。
```gdscript
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の速度を設定しました。
```gdscript
@export var _d_speed_mps = 25.0
@export var _d_rotate_speed_degps = 180
```
　敵の弾のインスタンスを生成した後は、set_posメソッドにより、初期位置と移動方向を設定します。
　２つの引数はグローバル座標(global_position)で、初期位置と、目標位置を指定されることを期待しています。初期位置は弾を発射する敵の位置で、目標位置はPlayerの位置になることが多いと思います。
　初期位置はそのまま敵の弾のglobal_positionに設定します。目標位置までのベクトル(方向と距離）は引き算すると得ることができます。距離を1mにしたいので、normalizedメソッドの結果を方向情報として取得します。
```gdscript
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を掛け算します。
```gdscript
func _physics_process(delta):
	position += _v_dir * _d_speed_mps * delta
	rotate_y(deg_to_rad(_d_rotate_speed_degps * delta))
```
　VisibleOnScreenNotifier3Dのscreen_exited()シグナルを受けた場合は敵の弾を消去します。
```gdscript
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」に名称を変更します。インスペクタの初期値はディフォルトのままです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/28101ce5-cff3-3b29-4f18-bef5171d97ec.png)

## fire_bullet_periodicallyのスクリプトの実装
　ルートノードにスクリプトをアタッチして以下のように修正します。
```gdscript
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メソッドでタイマーを起動するときの引数に設定します。
```gdscript
@export var _d_fire_interval_sec = 5.0

func _ready():
	$FireTimer.start(_d_fire_interval_sec)	
```
　先ほど実装した、敵の弾のシーン(PackedScene)をprealoadで読み込みます。preloadにより事前に読み込むことで、ゲーム中に余計な処理をしないようにします。
```gdscript
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を目標位置として設定します。最後に、所定のノードにインスタンスを追加します。
```gdscript
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を追加するのみです。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/1a99a13c-58c6-851d-0fad-3996fe0abcfa.png)

# 実行して動作確認をする
　実行すると、敵が生成されてから5秒後に敵に弾をPlayer機に向けて撃ってきます。
　敵の弾のインスタンスはbulletsノードの下に追加されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/dbf3ab46-5a83-69e5-e4c5-4b531e1f0b88.png)

# まとめ
　敵の弾が登場すると、シューティングゲームっぽくなりますね。
 でも3D表示にしていると、弾の軌道が予測しにくいです。やっぱりゲーム中は真上から見るように変更するかも。

以上です。