0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Godot4で3D風2D縦スクロールシューティングゲームを作る 3回目 レーザーを発射する

Posted at

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

レーザーを発射する

画面をタッチしている間、一定の間隔で敵を倒すためのレーザーを発射します。

github

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

レーザーシーンを作成する

res://character/player配下に、レーザー用のシーン、my_laserを作成します。
image.png

 レーザーのルートノードはArea3Dです。
image.png
 子ノードのlaser_leftとlaser_rightはMeshInstance3Dです。新規CapsuleMeshを設定した後、細長くして黄色にして、左右に配置しました。
 CollisionShape3Dは、新規BoxShape3Dを設定し、2本のレーザーをカバーするような長方形(6m×1m×5m)をサイズに設定しました。
image.png
 レーザーが移動して画面外に消えたときにレーザーシーンを削除したいので、VisibleOnScreenNotifier3Dノードを追加します。サイズを設定できますが、あまり厳密ではなくてよいので、ディフォルト値のままです。 
 image.png

レーザーのスクリプトを実装する

 ルートノードのmy_laserにスクリプトをアタッチして下記のように修正します。

extends Area3D

@export var _d_speed_mps = 25.0		# 単位はm/s 
@export var _v_dir = Vector3(0, 0, -1)

func set_pos(player_pos):
	global_position = player_pos
	
func _physics_process(delta):
	global_position += _v_dir * _d_speed_mps * delta

# 画面外に出た場合、消える
func _on_visible_on_screen_notifier_3d_screen_exited():
	queue_free()

 レーザーの移動速度_d_speed_mpsと移動する向き_v_dirは、後で変更するパラメータであることを明示するため@exportしています。
 set_posメソッドはレーザーシーンを生成したときに、初期位置を設定するために使用されます。

レーザーが移動して画面外に消えた場合に、シーンを削除する

 レーザーが画面外に消えた場合、VisibleOnScreenNotifier3Dノードのscreen_exitedイベントで検知することができますので、screen_exitedイベントをmy_laserスクリプトに接続します。
 レーザーシーンmy_laser.scnを開き、VisibleOnScreenNotifier3Dを選択した状態で、インスペクターの横のノードのシグナルを選択します。screen_exited()をダブルクリックしてmy_laserに接続します。
image.png
 _on_visible_on_screen_notifier_3d_screen_exited()関数の左に黄色のアイコンが表示されて、接続されたことがわかります(ノード名をVisibleOnScreenNotifier3Dから変更している場合メソッド名は変わります)。
image.png

Playerスクリプトを修正する

レーザーシーンとレーザー発射間隔を管理するためのメンバ変数を追加する

 Player.gdを開き、下記の差分のように、メンバ変数を追加します。_scn_laserには後でレーザーシーン、my_laser.scnをドラッグして登録します。また、発射間隔のパラメータと次に発射するまでの残り時間を記憶するためのメンバ変数を追加しました。

image.png

PlayerシーンのScn Laserプロパティにレーザーのシーンを登録する

 player.gdに「@export var _scn_laser : PackedScene」を追加して、保存した後、Playerシーンのルートノードを選択すると、インスペクタに「Scn Laser」プロパティが表示されます。そこにmy_laser.scnをドラッグアンドドロップして登録します。
image.png

タッチしている間、レーザーシーンを生成する処理を実装する

 Player.gdの最後に下記の処理を実装します。

image.png
「_f_is_screen_touch」フラグでタッチしている状態を判断します。
_d_firing_remain_time_secは次の発射までの残り時間です。残り時間がある場合は、経過時間を減算します。残り時間が無くなったらレーザーを発射して、発射後に残り時間を設定します。
 下記の3行は、レーザーシーンのインスタンスを生成して、Playerの位置をレーザーの位置として設定し、add_siblingでレーザーインスタンスをPlayerと同じ階層に追加します。
 この時add_siblingではなく、add_childするとPlayerの配下にレーザーシーンが追加されるので、Playerが移動するとレーザーも移動するという意図しない動作になります。

			# レーザーを発射する
			var scn : Area3D = _scn_laser.instantiate()
			scn.set_pos(global_position)
			add_sibling(scn)

 実行するとこのようになります。
image.png

キーを押下すると落ちる問題の対処

_input(event)メソッドの中で、「event.position」を常に取得してタッチ位置を更新していましたが、キー押下のeventのように、positionを持たない入力イベントが発生すると落ちることがわかりました。
冗長な感じもしますが、下記のように、タッチしているときのみ、positionを取得するように変更しました。

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

Player.gd全体

 Player.gd全体は下記のようになります。

extends CharacterBody3D

# Player
@export var _d_speed_mps = 100.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に速度を設定して動かす
				velocity = v_velocity
				move_and_slide()
		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)
			# 発射間隔を設定
			_d_firing_remain_time_sec = _d_firing_interval_sec
	else:
		# タッチしたときにすぐ、レーダーが発射するようにする
		_d_firing_remain_time_sec = 0

まとめ

 レーザーを発射すると、シューティングゲームっぽくなりますね。

以上です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?