Godot4で3D風２D縦スクロールシューティングゲームを作る　1回目

　Godot4.1.3で縦スクロールシューティングゲームをつくります。格好よくしたいので見た目は３Ｄで作っていきますが、動きは2Dです。
# 本記事の目標は、Player機をタッチ＆ドラッグで操作するまで
 本記事では、Godot4で新規プロジェクトを作成して、スマホ用縦スクロールシューティングゲーム用のGameシーンを作成します。また、Player機を表示して、スマホ画面上をドラッグすると、追従してPlayer機が移動するようにGDScriptを実装します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b86bfb65-2a4b-baf3-59d6-de3bd253967f.png)
# github
　本記事で実装したものを、githubに登録しました。

https://github.com/footinglow/woden/tree/001_create_project_and_move_player

# スマホゲーム用新規プロジェクト
　Godot4.1.3で新規プロジェクトを作成します。
　スマホ縦持ちで遊べるようにしたいので、プロジェクト設定の「ウインドウ」を設定を下記のように変更します。

 | 項目 | 設定値 |
|:-|:-|
| サイズ/ビューポート  | 幅：500、高さ：900  |
| ストレッチ/モード  | canvas_items※   |
| ポータブル/方向  | Portraitにして、縦方向の表示に固定する |

　※canvas_itemsにすると、スコアなど描画したときに、表示デバイスの解像度に影響を受けずに、大きさの比率を自動調整していい具合に描画してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/e289ad6e-ecd2-0e07-8bd1-d944739ce821.png)
　下記はAndroidアプリをエクスポートする時に必要になる設定です。右上のAdvanced Settingsを有効にすると表示される項目で、レンダリング/テクスチャのImport ETC2 ASTCにチェックを入れる必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/96f46f5e-5794-766d-10f1-a3c84ac62ead.png)
　今回はスマホ用のゲームを開発するため、タッチにより操作をしますが、デバッグはWindowsPC上で実行してマウスで操作します。その場合「マウスでタッチ操作をエミュレート」を有効にすると、Godotがマウス操作をタッチ操作に変換するため、タッチイベント処理のみ実装すればタッチ操作もマウス操作も両方処理することができるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/adce251c-707c-0469-eaf8-7466a66e0970.png)

# フォルダ構成
　フォルダ構成を決めます。

 | フォルダ名 | 内容 |
|:-|:-|
| assets  | ３Ｄモデルや画像などのリソース  |
| Character  | Playerや敵などのキャラクターシーン、スクリプト  |
| Game  | ゲームシステムのシーン、スクリプト  |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/95392023-b4a4-580a-5783-25b1bfcefd04.png)


:::note
　前回アプリを作ったときに、ファイルが増えたらフォルダ分けをすれば良いと思っていたのですが、参照されているシーンを別のフォルダに移動すると、Godotが追従できず参照元のファイルが開けなくなることがありました。最初にきちんと考えてフォルダ構成を決めた方が良いと思います。もしフォルダ構成を変更する場合は、バックアップを取って少しずつ確認しながら実施することをお勧めします。
:::

# 3Dモデル
　BlenderでPlayer機をモデリングしました。個人的にとても気に入っています。glTF2.0にエクスポートしたplayer.glbをassetsフォルダに格納しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/26097912-ee58-6066-c6f0-cb817b4fc4f6.png)

# Playerシーンを作成し、大きさを５ｍに決める。
　Playerシーンを作成し、character/playerフォルダに格納しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/2a57c5d3-1b74-e63b-379a-952586f78570.png)


　ルートノードはとりあえずCharacterBody3Dにしました。Assetsに格納したplayer.glbをドラッグして子ノードにしました。
　Playerのサイズは自動車と同じくらいと想定して5mくらいになるようにScaleのx,y,zを1.7に設定しました。今後、敵など他のキャラクターのサイズを決めるときは、Playerのサイズを基準にすると考えやすいと思います。2倍にしたいときは10mという感じです。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/c0a52934-3d72-bd02-c932-657c8c4fd264.png)

　Player機の衝突判定用にCollisionShape3Dを追加しました。CollisionShape3Dに設定するShapeの形状とサイズは機体デザインよりも小さくします。ShapeにはCapsuleShape3Dを設定し、Height4.5m、Radius0.4mにして、X軸周りに90°傾けました。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/1296c77f-8c41-7c8b-5eed-eac06c3ac4db.png)

# Gameシーンを作成
　Gameシーンを作成して、Gameフォルダに配置しました。
　ルートノードはNode3Dで、Camera3DとDirectionLight3DとPlayerシーンを追加しました。
　Area3DのTouchDetectorArea3Dは、画面をタッチしたときに、Godotが管理している物理空間状態を利用して、3D空間上の位置を計算するために配置したものです。後で説明します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/a162eb63-0e6e-0c12-f24e-30ed88abb860.png)


| ノード種別| ディフォルト値からの変更 |
|:-|:-|
| Camera3D  |    position y=70m, Rotation x=-60°  |
| DirectionalLight3D     | Rotation x=-60°  |
| Player  |    初期値のまま  |
| Area3D (TouchDetectorArea3D)  | 初期値のまま  |
| CollisionShape3D   | BoxShape3D Size (x,y,z)=(1000m, 1m, 1000m), position y=-0.5m  |

# Playerの移動方法
　スマホの画面のタッチ操作でPlayer機を動かします。タッチした場所に移動する方法もありますが、Player機が指で隠れて見えなくなると、細かい操作がしにくいので、スマホ画面上のどこでもよいので、ドラッグした分だけ移動するようにしようと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b86bfb65-2a4b-baf3-59d6-de3bd253967f.png)
あと指の動きに完全に追従すると、ワープみたいになってしまうので、移動目標位置を指に追従するようにして、そこに向かって設定した速度で移動するようにします。

# ドラッグ操作を検知して、Player機を移動するScriptの説明

　playerにスクリプトをアタッチして、res://character/player/player.gdを作成しました。
　全体を見せた後、詳細について説明します。
 
## Scriptの全体
　下記のように修正しました。
```GDScript
extends CharacterBody3D

# Player
@export var _d_speed_mps = 100.0

# ドラッグ操作検知用
var _f_is_screen_touch = false		# 画面にタッチ（ドラッグ含む）している場合true、タッチしていない場合はfalse
var _v2_drag_pos = Vector2.ZERO		# 最新のタッチ位置・ドラッグ位置

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

# 画面をタッチした位置から、検索する３Dオブジェクトまでの最大距離
@export var _d_ray_length_m = 1000		# [m]十分な長さにする

func _input(event):
	# 画面にタッチしている状態と、タッチしていない状態を判断する
	if event is InputEventScreenTouch:
		_f_is_screen_touch = event.is_pressed()
	elif event is InputEventScreenDrag:
		# ドラッグしているということは、タッチしている。（参考：InputEventScreenDragの場合、event.is_pressed()は常にfalse)
		_f_is_screen_touch = true
	else:
		pass

	# ドラッグ位置（タッチ位置）を更新する
	_v2_drag_pos = event.position	

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

		# カメラを利用して3D空間のカメラの３D位置と、カメラからタッチしたピクセルを見た方向の1000m先の３D位置を計算する
		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と呼ばれる、物理３D空間状態の情報を利用して、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
			
			# ３D空間の移動目標位置
			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に速度を設定して動かす
				velocity = v_velocity
				move_and_slide()
		else:
			# タッチ位置に衝突したものが無い
			pass
	else:
		# タッチしていない場合はnullを設定することで、次にタッチしたときに、新しいタッチ位置とPlayer位置を記憶する
		_v_first_touch_pos = null
```
 　
## func _input(event)でスマホ画面のタッチ状態を判定し、タッチ位置を記憶する
　「_input」メソッドを実装すると、UI操作イベントを処理できるようになります。
 　下記の処理で、画面にタッチしている場合は、メンバ変数_f_is_screen_touch=trueに設定し、タッチしていない場合は_f_is_screen_touch=falseに設定します。
```GDScript
	# 画面にタッチしている状態と、タッチしていない状態を判断する
	if event is InputEventScreenTouch:
		_f_is_screen_touch = event.is_pressed()
	elif event is InputEventScreenDrag:
		# ドラッグしているということは、タッチしている。（参考：InputEventScreenDragの場合、event.is_pressed()は常にfalse)
		_f_is_screen_touch = true
	else:
		pass
```
 　スマホのタッチ状態が変わると、event=InputEventScreenTouchが通知されます。その場合event.is_pressed()でタッチ状態を判定することができます。
　ドラッグ状態に変化があるとevent=InputEventScreenDragが通知されます。event.is_pressed()は常にfalseになるため使えませんが、ドラッグしているということは画面をタッチしているのは明白なので、_f_is_screen_touchをtrueに設定します。

　続いて、画面を（最後に）タッチした位置をメンバ変数_v2_drag_posに記憶します。

 ```GDScript
	# ドラッグ位置（タッチ位置）を更新する
	_v2_drag_pos = event.position	
  ```

## func _physics_process(delta)でタッチした画面位置から３D空間上の位置を算出する
　スマホ画面上をタッチした位置は、_inputメソッドで、メンバ変数_v2_drag_posに記憶しています。
　cameraの情報を利用して、_v2_drag_posを3D空間上のカメラのレンズ上の位置に変換したものをfrom3dに設定し、見ている方向の1000m先(_d_ray_length_mの値）の位置をto3dに設定します。
```GDScript
	if _f_is_screen_touch:
		var camera = get_viewport().get_camera_3d()

		# カメラを利用して3D空間のカメラの３D位置と、カメラからタッチしたピクセルを見た方向の1000m先の３D位置を計算する
		var from3d = camera.project_ray_origin(_v2_drag_pos)
		var to3d = from3d + camera.project_ray_normal(_v2_drag_pos) * _d_ray_length_m
```
　from3dからto3dに直線を引いたときに、一番最初に衝突する物体の衝突位置を、Godotが管理している物理空間状態（world's physics 3D space state）から検索します。まずfrom3dとto3dから問い合わせ用の情報、queryを作成します。Area3Dとの衝突も検出してほしいので、queryのcollide_with_areasをtrueにします。
　_physics_processメソッド内であれば、物理空間状態space_stateをget_world_3d().direct_space_stateで取得でき、result = space_state.intersect_ray(query)とメソッドを実行すると、result.positionに衝突位置が格納されます。
　この時タッチした位置に物体がないと、衝突位置が検出できないため、タッチしたときに必ず衝突位置を検出できるように、GameシーンにはArea3dのTouchDetectorArea3Dノードを追加しています。
 
```GDScript
		# 3D ray physics queryの作成
		var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
		query.collide_with_areas = true		# Area3Dを検知できるようにする
		
		#　spaceと呼ばれる、物理３D空間状態の情報を利用して、Area3Dを含む衝突位置を計算する
		var space_state = get_world_3d().direct_space_state
		var result = space_state.intersect_ray(query)
```

:::note
画面タッチ位置から３D空間上の位置を検出する方法は、こちらの記事も参考にしていただければ幸いです。
:::

https://qiita.com/FootInGlow/items/564dfe87dc7fc5135c67

## 3D空間上のタッチ位置（ドラッグ位置）をv_drag_posに保存する
　今タッチ（ドラッグ）している位置に対応する３D空間上の位置、result.positionをv_drag_posに代入します。この後、移動量を計算する時に、Y軸の差分は不要なので（Y軸の差分が出ると困るので）、yの位置を固定します。
```GDScript
		if result:
			# 衝突位置を取得
			var v_drag_pos = result.position
			v_drag_pos.y = global_position.y	# Y方向には移動しないための設定
```

## func _physics_process(delta)でドラッグ開始を検知して、ドラッグ開始位置とPlayer位置を記憶する
　_physics_process()メソッドの最初のif文では、_f_is_screen_touchがfalseの場合、elseの処理で_v_first_touch_pos=nullに設定し、スマホ画面にタッチしていないことを表しています。
　つまり、_f_is_screen_touchがtrueで、(resultも有効で）かつ、_v_first_touch_posがnullの時は初めてタッチした時＝ドラッグ開始を意味しますので、下記の処理で、ドラッグ開始位置を_v_first_touch_posに記憶します。またドラッグ開始時のPlayer機の位置も_v_first_touch_player_posに記憶します。
```GDScript
func _physics_process(delta):
	if _f_is_screen_touch:
        :
		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
        :
	else:
		# タッチしていない場合はnullを設定することで、次にタッチしたときに、新しいタッチ位置とPlayer位置を記憶する
		_v_first_touch_pos = null
 ```
## ドラッグ開始位置からドラッグ位置までの移動量を求めて、Player機の移動目標位置を計算する
　ドラッグによる移動量は、現在のタッチ位置から、ドラッグ開始位置を引いたものになります。
　ドラッグ開始時のPlayer位置に、ドラッグによる移動量を足した位置が、Playerの移動目標位置になります。

```GDScript
			# ３D空間の移動目標位置
			var v_target_pos = _v_first_touch_player_pos + ( v_drag_pos - _v_first_touch_pos )
```

## Player機の速度を移動目標位置に向けて設定する
　Playerの移動目標位置v_target_posから現在のPlayerの位置を引き算したものが、Player機が移動するべき方向と移動量、v_targetになります。
　v_target.normalized()により方向のみを表す成分（単位ベクトル）になり、_d_speed_mpsを掛け算すると、速度v_velocity になります。
　あとはCharacterBody3Dの場合は、velocityに速度を設定して、move_and_slide()を実行すると移動します。
　ただし目標位置までの距離が極小の場合、1回の移動で目標位置を飛び越えます。次の周期でも目標位置を飛び越えて元の位置の戻るため、見た目はPlayer機がぶるぶる震えるようになります。
　この問題を対処するため、この周期で動く移動距離を(速度×delta).length()で求めて、目標位置までの移動距離v_target.length()より大きい場合は、Player位置を移動目標位置に設定します。

```GDScript
			# 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に速度を設定して動かす
				velocity = v_velocity
				move_and_slide()
```

# まとめ
 以上で、ドラッグ操作によりPlayer機を移動できるようになりました。Player機が画面の外に出てしまう問題などありますが、今後対処したいと思います。

 以上です。