【Godot 4.0】スマホ３Dゲームを作るための勉強　その８　発射する

 ゲームエンジンGodot4.0で3Dスマホゲームを作りたいと思いますが、その前にお勉強しています。
 [Godot_v4.0-rc1_win64.exe.zip](https://downloads.tuxfamily.org/godotengine/4.0/rc1/Godot_v4.0-rc1_win64.exe.zip)を使用しています。

# 目的
　何か発射しましょう。

# 内容
　
　1. 新たにシーンを作成して、弾丸を作成します。
　2. 弾丸シーンをＧＤＳｃｒｉｐｔから生成・追加しましょう
　3. タイマーシグナルを利用して一定時間置きに発射しましょう。

# ベースプロジェクト
　下記で作成したプロジェクトをベースに機能追加をします。

　【Godot 4.0】スマホ３Dゲームを作るための勉強　その７　３D空間のタッチしたところに移動したい
　https://qiita.com/FootInGlow/items/564dfe87dc7fc5135c67

# 新規シーンを作成する
　シーンメニューから新規シーンを選択します。
![スクリーンショット (19).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/e3bc5d9a-0116-5da8-6f31-3e32642386c8.png)
　その他のノードボタンを押下して、CharacterBody3Dノードを追加します。
![スクリーンショット (20).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/ff6b8c02-50ca-933f-35b2-7c4895677e83.png)

　下記のようにしましょう。
　CharacterBody3Dノードは「Bullet」に名前を変更します。
![(_) S08_shoot_001 - Godot Engine 2023_02_27 19_16_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/1a491e67-0351-272e-b22f-bf0ec0dd2ed9.png)

　一度Ctrl-Sキーなどでシーンを保存しましょう。
　保存するとシーンファイルが追加されます。
　シーンファイルは拡張子が.tscnで、前回作成したシーンはglobal.tscn、今回作成したシーンはbullet.tscnです。ファイル名を変更している場合は適宜読み替えてください。
　![(_) S08_shoot_001 - Godot Engine 2023_02_27 19_16_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/c12fb314-e80f-4cc7-6bcf-241b985221b9.png)

* MeshInstance3Dにカプセル型を設定します。
 インスペクタ内のMeshInstance3D/Meshを新規CapsuleMeshに設定します。
  少し小さくしましょう。Radiusを0.1m、Heightを0.5mにします。
![bullet.tscn - S08_shoot_001 - Godot Engine 2023_03_01 19_51_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/2b0d3c14-93ec-6e55-ca42-fd33cf6924dc.png)


* CollisionShape3Dに衝突形状を設定
MeshInstance3Dと同じ形・大きさにします。
 インスペクタ内のCollisionShape3D/Shapeに新規CapsuleShape3Dを設定します。
 Radiusを0.1m、Heightを0.5mにします。
![bullet.tscn - S08_shoot_001 - Godot Engine 2023_03_01 19_53_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/50c417a3-fd49-588c-61cd-ecd6a9c873ba.png)

##　スクリプトを設定する
　bulletを右クリックして「スクリプトをアタッチ」を選択します。
　前方に進むスクリプトです。

``` GDScript
extends CharacterBody3D

@export var m_v_dir = Vector3(0.0, 0.0, -1.0)
@export var m_d_speed_mps = 2.0

func _physics_process(delta):
	velocity = m_v_dir.normalized() * m_d_speed_mps
	move_and_slide()
```

## 一次的にシーンの動きを確認する
　globalシーンにBulletシーンを一時的に追加して動きを確認します。

　ファイルシステムにあるBullet.tscnを、
　![スクリーンショット (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/a3ee64e6-8d38-138e-857a-31faf91db59d.png)
　シーン内のglobalにドラッグアンドドロップします。
　![スクリーンショット (10).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/96405dbe-4bf3-3739-d8b7-b0794c7b95d2.png)

　global直下にBulletが追加されました。
　他のノードとぶつからないように、y=1.5mにします。
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 14_21_47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f1e6426f-5c8d-8e8a-6083-ce786c7c3bf1.png)

　実行すると、球の前方にBulletで追加したノードが進むようになります。

　確認したらglobalに追加したBulletは削除してください。

# Playerノードから発射するようにする
　スクリーンにタッチしている間、1秒周期で繰り返しPlayerからBulletを発射するようにします。
　先ほどはIDE上のドラッグアンドドロップでBulletシーンをglobalシーンに追加しましたが、同じことをGDScriptから実行します。

# Timerノードを追加して周期的にsignalを受信する
　Godotにはノード間で通信をするためのsignal通信の仕組みがあります。
　Timerノードは指定した時間経過すると、timeoutシグナルを発行することができ、同じシーン内の任意のスクリプトで受信することができます。タイマーは自動で繰り返されます。1回だけタイマーを起動することも簡単にできるので、時間でイベントが欲しいときにはとても便利です。

　Playerノードを右クリックして「子ノードを追加」からTimerを追加します。
　また名称をTimer_fireにします。
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 21_12_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/08dbd2df-eae6-e9d6-3b4d-f2d7b05f7cdc.png)

　Timer_fireをクリック選択した状態でインスペクタを表示します。
　Autostartをオンにします。実行すると自動的にタイマーが開始します。
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 21_36_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b78f6925-811e-6787-ba9f-8693db3f8bf9.png)

　続いてインスペクタの隣の「ノード」タブをクリックします。
　シグナルを選択（選択すると青になります）すると下記のように、Timerノードが発行するsignalが表示されます。
　timeoutしたときにPlayerノードに通知してほしいので、timeoutを右クリックして「接続」します。
![スクリーンショット (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/021c22dd-d3e0-aeab-91d5-b606d6503acd.png)
　同じシーンの中のスクリプトを持つノードを選択できます。受信側メソッド名を確認して、Playerを選択して接続ボタンを押下します。
![スクリーンショット (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/e35b40cc-c1fd-916f-f15e-35ba09f88eb9.png)

　Playerのスクリプトを見ると、先ほどのダイアログに表示されていた受信側メソッドが追加されます。
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 21_23_38 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/1d1e0746-388e-4603-5ece-60954138eaf3.png)
 これでTimerノードのtimeoutシグナルがPlayerのスクリプトに接続されました。接続されていることはIDEの３か所で確認することができます。

　まずシーンのsignalを追加したTimer_fireノードの右に電波のようなアイコンが表示されます。
　この電波のようなアイコンをクリックすると、
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 21_23_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/c12cf871-37f9-e951-3a5c-274ba67a52d4.png)
　ノード/シグナル表示がアクティブになります。
　先ほど接続したtimeoutを見ると下に「..::_on_timer_fire_timeout()関数と接続したことが表示されています。
　「..」が１個上の階層を意味するので、シーン内でTimerの親ノードのスクリプトの_on_timer_fire_timeout()関数を表しています。
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 21_23_38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/77a87c7b-40ff-a4ac-4b4d-30b50dca37f7.png)
　この関数をダブルクリックすると接続したPlayerのスクリプトの該当の関数が表示されます。
![bullet.tscn - S08_shoot_001 - Godot Engine 2023_03_01 20_06_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/92e9abce-f6ed-0d79-0cc9-7ef69b42c025.png)
　関数の左側にある「➡コ」みたいなアイコンをクリックすると、送信元であるソースノード、送信先のターゲットノードとその間を接続するシグナル名を表示します。
![スクリーンショット (43).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f66f845e-52b2-3196-0741-50dc7413381d.png)

# PlayerのGDScriptの修正してtimeout時にBulletシーンを追加する
　タッチしている間、１秒周期で発射するように Player.gdスクリプトを下記のように修正します。
 またドラッグアンドドロップでPlayerのインスペクタにBullet.tscnを登録する必要がありますので後で説明します。

``` GDScript
extends CharacterBody3D

@export var m_speed_mps = 5.0
@export var m_ray_length_m = 1000		# [m]十分な長さにする
@export var m_scn_bullet : PackedScene

var m_touch_pos = Vector2.ZERO
var m_d_move_to_pos_x_m = 0
var m_f_is_screen_touch = false

func _input(event):
	if ( event is InputEventScreenDrag ) or ( event is InputEventScreenTouch ):
		m_touch_pos = event.position
		# スマホタッチ中フラグ
		m_f_is_screen_touch = event.is_pressed() or ( event is InputEventScreenDrag )

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

	# カメラを利用して3D空間のカメラ位置とタッチしたピクセルに対応する方向の1000m先の位置を計算する
	var from3d = camera.project_ray_origin(m_touch_pos)
	var to3d = from3d + camera.project_ray_normal(m_touch_pos) * m_ray_length_m

	# 3D ray physics queryの作成
	var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
	query.collide_with_areas = true		# Area3Dを検知できるようにする
	
	# Godotの3Dの物理とコリジョンを保存しているspaceという情報を使用してオブジェクト検出
	var space_state = get_world_3d().direct_space_state
	var result = space_state.intersect_ray(query)
	if result:
		m_d_move_to_pos_x_m = result.position.x

	# 移動制御は毎周期実行する
	var d_diff_x_m = (m_d_move_to_pos_x_m - transform.origin.x);
	var d_speed_mps = m_speed_mps if d_diff_x_m>0 else -m_speed_mps
	var d_move_diff_x_m = d_speed_mps * delta
	if absf(d_move_diff_x_m) > absf(d_diff_x_m):
		# 移動すると目標位置を超えるため、目標位置を設定する
		transform.origin.x = m_d_move_to_pos_x_m
	else:
		move_and_collide(Vector3(d_move_diff_x_m, 0, 0))

func _on_timer_fire_timeout():
	# スマホタッチ中の場合、発射する
	if m_f_is_screen_touch:
		var scn_bullet = m_scn_bullet.instantiate()
		scn_bullet.transform.origin = transform.origin
		add_sibling(scn_bullet)
```

## Bulletシーンファイルをスクリプトのメンバ変数に設定する
* @export var m_scn_bullet : PackedScene
　m_scn_bulletを@exportしてインスペクタに表示します。PackedSceneクラス型の宣言をします。

::: note
 Godot3.5の時はこのように宣言していました。4.0で変更された点のひとつです。
```
export(PackedScene) var m_scn_bullet
```
:::
　シーン内のPlayerをクリック選択してからインスペクタを見ると、M Scn Bulletという名前で表示されています。まだ何も登録していないので、\<empty>です。
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 14_37_47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/deb701e6-86ed-2639-90d0-8d521f98b1db.png)
　ファイルシステムのbullet.tscnを\<empty>にドラッグアンドドロップして登録しましょう。
![スクリーンショット (11).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f8f97553-9926-e4e7-1e6d-621fbfd07cc1.png)
　登録したシーンのイメージが表示されます。
![キャプチャ 2023_02_28 14_38_31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/382925a8-8429-84b2-e08b-e5d87833759e.png)

## スマホタッチ中フラグの追加
　_inputを修正してスマホタッチ中のフラグを設定するようにしました。

* var m_f_is_screen_touch = false
 スマホタッチ状態を保持できるようにメンバ変数を追加します。

* m_f_is_screen_touch = event.is_pressed() or ( event is InputEventScreenDrag )
　_input()の中で、eventがInputEventScreenDrag且つevent.is_pressed()がtrueの時、もしくはeventがInputEventScreenDragのときに、タッチ中と判断しています。

## timeoutシグナル受信したときに発射処理の実装
　timeoutをしたときに、スマホタッチしている場合は、Bulletを発射する処理を実装しました。

* func _on_timer_fire_timeout():
 Timer_fireのtimeoutシグナルを接続したときに追加したメソッドです。

* if m_f_is_screen_touch:
 スマホタッチ中のみ以下の処理を実行

* var scn_bullet = m_scn_bullet.instantiate()
 新規にBulletシーンのインスタンスを生成します。
 m_scn_bulletには先ほどドラッグアンドドロップでbullet.tscnをセットされています。

* scn_bullet.transform.origin = transform.origin
 Playerと同じ位置を設定します。

* add_sibling(scn_bullet)
 Playerと同じ階層にBulletシーンインスタンスを追加します。 　
 シーンを追加する場合、add_childとadd_siblingがあります。
 add_childではPlayerの子ノードとして追加されます。その場合Playerが移動するとBulletもついてきて期待する動作とは異なる結果になります。
 add_siblingとするとPlayerと同じ階層に追加されます。

　実行しましょう。

![S08_shoot_001 (DEBUG) 2023_02_28 21_50_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/154a4493-4fcf-8a18-b7bb-ccbf2abf6b4a.png)

　シーンのリモートを確認すると、add_sibling()で追加したBulletはPlayerと同じ階層に追加されていることがわかります。
![「【Godot 4.0】スマホ３Dゲームを作るための勉強　その８　発射したい」を編集 - Qiita - Google Chrome 2023_02_28 21_50_46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/67883ec0-1281-0687-36c3-99d3eaa0c9c7.png)

　問題が２つあります。ひとつはBulletが消えないことです。
　２つ目の問題は、BulletやPlayerの球がおかしな方向（Z軸方向）に動くことです。これは衝突判定をしているPlayerの中にBulletを生成したので、衝突による速度計算がおかしくなっているためだと思います。

# 画面外に出た場合Bulletを消すようにする
　Bulletが画面外にでたところで消すようにしましょう。
　bullet.tscnを開いて、Bulletを右クリックして「子ノードを追加」からVisibleOnScreenNotifier3Dを追加します。
　現在のカメラから見える範囲に入った場合、出た場合にsignalを発行することができます。
　![(_) bullet.tscn - S08_shoot_001 - Godot Engine 2023_02_28 21_59_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/ed018317-b0b2-3009-f466-2a0563091208.png)
　ノード/シグナルを表示して、screen_exitedを右クリックして、Bulletのスクリプトに接続します。　
　![スクリーンショット (15).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/48c6c98c-74ee-1086-790b-45b3e0e6c950.png)
　受信側メソッドを確認して接続ボタンを押下します。
![スクリーンショット (16).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/193bdf51-a6c0-605b-807b-d1e39e18cd63.png)

　_on_visible_on_screen_notifier_3d_screen_exitedメソッドが追加されました。
　下記のように修正しましょう。

``` GDScript
extends CharacterBody3D

@export var m_v_dir = Vector3(0.0, 0.0, -1.0)
@export var m_d_speed_mps = 2.0

func _physics_process(delta):
	velocity = m_v_dir.normalized() * m_d_speed_mps
	move_and_slide()

func _on_visible_on_screen_notifier_3d_screen_exited():
	queue_free()
```

* queue_free()
 自身のノードインスタンスを開放（削除）します。

　実行しましょう。
　リモートで確認するとBulletが画面から消えると、シーンからも消えるのが確認できます

# Bulletの発射位置を指定する
　Collision Layer/Maskを設定して衝突しないようにすればよいのですが、登場位置を正確に設定する必要もあるので、今回はBulletの発射位置を指定して対処します。（Collision Layer/Maskは次回制御します）
　global.tscnを開いて、Playerを右クリックして「子ノードを追加」からMarker3Dを追加します。
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 22_26_34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/3ca3d891-8d51-9f00-452f-471fd5dc84b3.png)
　Node3D/Transform/Zに-0.7mを設定します。
　球の半径が0.5m、Bulletの半径が0.1mなのでその合計より前方にしました。
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 22_26_36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/e07ee378-b186-6584-b9ef-fd7f08d98e6c.png)
Marker3Dはこのように球の前方に表示されます。
![global.tscn - S08_shoot_001 - Godot Engine 2023_02_28 22_28_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/95b9620d-2e0c-2861-79ef-c686a7812c7b.png)

::: note
 Godot3.5のとき、Marker3DはPosition3Dという名称でした。
:::

　Marker3Dの位置を利用してBulletの発射位置を設定します。
``` GDScript
extends CharacterBody3D

@export var m_speed_mps = 5.0
@export var m_ray_length_m = 1000		# [m]十分な長さにする
@export var m_scn_bullet : PackedScene

var m_touch_pos = Vector2.ZERO
var m_d_move_to_pos_x_m = 0
var m_f_is_screen_touch = false

func _input(event):
	if ( event is InputEventScreenDrag ) or ( event is InputEventScreenTouch ):
		m_touch_pos = event.position
		# スマホタッチ中フラグ
		m_f_is_screen_touch = event.is_pressed() or ( event is InputEventScreenDrag )

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

	# カメラを利用して3D空間のカメラ位置とタッチしたピクセルに対応する方向の1000m先の位置を計算する
	var from3d = camera.project_ray_origin(m_touch_pos)
	var to3d = from3d + camera.project_ray_normal(m_touch_pos) * m_ray_length_m

	# 3D ray physics queryの作成
	var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
	query.collide_with_areas = true		# Area3Dを検知できるようにする
	
	# Godotの3Dの物理とコリジョンを保存しているspaceという情報を使用してオブジェクト検出
	var space_state = get_world_3d().direct_space_state
	var result = space_state.intersect_ray(query)
	if result:
		m_d_move_to_pos_x_m = result.position.x

	# 移動制御は毎周期実行する
	var d_diff_x_m = (m_d_move_to_pos_x_m - transform.origin.x);
	var d_speed_mps = m_speed_mps if d_diff_x_m>0 else -m_speed_mps
	var d_move_diff_x_m = d_speed_mps * delta
	if absf(d_move_diff_x_m) > absf(d_diff_x_m):
		# 移動すると目標位置を超えるため、目標位置を設定する
		transform.origin.x = m_d_move_to_pos_x_m
	else:
		move_and_collide(Vector3(d_move_diff_x_m, 0, 0))

func _on_timer_fire_timeout():
	# スマホタッチ中の場合、発射する
	if m_f_is_screen_touch:
		var scn_bullet = m_scn_bullet.instantiate()
		scn_bullet.transform.origin = $Marker3D.global_transform.origin
		add_sibling(scn_bullet)
```

　func _on_timer_fire_timeout():の位置設定処理を変更します

* scn_bullet.transform.origin = $Marker3D.global_transform.origin

　Marker3Dのglobal座標をBulletに設定します。
　子ノードは「\\$＋ノード名」で表すので、\\$Marker3Dです。シーンからスクリプトエディター画面にドラッグアンドドロップすると相対パスを考慮してスクリプトに挿入されて便利です。
　またMarker3DはPlayerの子ノードですの、\\$Marker3D.transform.originはPlayrのローカル座標系です。
　BulletはPlayerと同じ世界座標系で動作するためtransformではなくglobal_transformを使用しています。

　実行しましょう。
　球とBulletが押し合うことはなくなります。

以上