このチュートリアルでは、2Dトップダウンシューティングゲーム(見下ろし型シューティングゲーム)で一般的によく登場する銃を4種類作っていく。具体的には以下の通りだ。

  • ハンドガン
  • ショットガン
  • マシンガン
  • レーザーガン

Environment
このチュートリアルは以下の環境で作成しました。

Godot のバージョン: 3.4.2
コンピュータのOS: macOS 11.6.5


このチュートリアルでは、銃の作成にフォーカスするため、以下は予め用意しておいた。

  1. ゲームの世界
    「World.tscn」というシーンを用意し、見た目は「TileMap」ノードを追加して簡単に作成している。「TileMap」以外に「Player」ノードと複数の「Obstacle」ノードを追加している。これらについては個別のシーンを作成して、インスタンスを追加した形だ。
    Worldシーンツリー

  2. プレイヤーキャラクター
    「Player.tscn」シーンとして作成した。ルートノードは「KinematicBody2D」クラスで、子ノードに「Sprite」と「CollisionShape2D」を追加している。「Sprite」のテクスチャは銃をもったスキンヘッドのヒットマンにしている。ヒットマンが持っている銃の画像の先端の位置に「Position2D」クラスの「Muzzle」という名前のノードを配置している。これは後ほど銃を撃った時の弾丸インスタンスの生成される位置を指定するのに使用する。
    Playerシーンツリー
    インプットマップには以下のアクションを追加済みだ。プレイヤーキャラクターの移動や射撃、銃の切り替えに使用する。

    • up: W キー・・・プレイヤーキャラクターを前進させる時に使う
    • down: S キー・・・プレイヤーキャラクターを後退させる時に使う
    • fire: マウス左ボタン・・・銃を撃つ
    • switch: マウス右ボタン・・・銃の種類を切り替える
      inputmap
      ちなみに、チュートリアルの内容を簡潔にするため、プレイヤーキャラクターのスプライトは銃の種類が変わっても同じままだ。見た目はハンドガンっぽいが、マシンガンにもレーザーガンにもなる、なんでもありの銃、ということにしておこう。
  3. 障害物
    「Obstacle.tscn」というシーンを作成している。Obstacle は日本語で障害物という意味だ。画面上にたくさん置いている茶色い木箱のオブジェクトは全てこのシーンのインスタンスである。ルートノードは「StaticBody2D」クラスにして、その子ノードとして「Sprite」と「CollisionShape2D」を追加している。
    Obstacleシーンツリー
    解放された(破壊された)時に同じ場所に爆発を表現するための「Smoke.tscn」というシーンのインスタンスを生成するようにしている。「Smoke」シーンは「Particle2D」ノードのみで構成され、こちらもパーティクルの実行が完了したら自動的に解放されるようにしている。
    Smokeシーンツリー


これらの下準備によりプロジェクトを実行すると、以下のように世界とヒットマンと障害物が描画され、現時点でヒットマンは移動操作のみ可能な状態だ。
プロジェクトを実行


このチュートリアルのプロジェクトファイルは GitHubリポジトリ に置いている。.zipファイルをダウンロードしていただき、「Start」フォルダの方を Godot Engine にインポートしていただければ、上述の下準備だけ完了したプロジェクトから開始できる。また、取り急ぎ完成形を確認されたい場合は「End」フォルダの方をインポートしていただければOKだ。

また今回プロジェクトにインポート済みのアセットは全て KENNEY(ケニー) のサイトからダウンロードして利用させていただいたものだ。CC0の非常に使いやすいアセットを豊富にご用意いただいていることに感謝申し上げたい。特に今回ご利用させていただいたのは下記のアセットパックだ。


それでは銃の実装を進めていこう。



弾丸のシーンを作る

まずは弾丸のシーンを作ろう。ハンドガン、ショットガン、マシンガンの3種類の銃でこの弾丸のシーンを使いまわせるので、先に作ってしまおうというわけだ。

  1. 「シーン」メニュー>「新規シーン」を選択する。
  2. 「ルートノードを生成」で「その他のノード」を選択する。
  3. ルートノードに「Area2D」クラスを選択し、名前を「Bullet」に変更する。
  4. ルートノードに「Line2D」クラスの子ノードを追加する。このチュートリアルでのこのノードの用途は弾丸の見た目を作るのに使用するのみだ。もちろん「Line2D」ではなく弾丸用のテクスチャ画像を用意して「Sprite」にしても良い。
  5. ルートノードに「CollisionShape2D」クラスの子ノードを追加する。
  6. ルートノードに「VisibilityNotifier2D」クラスの子ノードを追加する。銃で撃った弾が画面外に出てしまったら、それをシグナルで通知し、弾丸のインスタンスを解放するために使用する。
  7. 一旦シーンを保存しておく。保存先のフォルダは用意しているのでファイルパスが「res://Bullet/Bullet.tscn」になるようにして保存しよう。

ここまででシーンツリーは以下のようになったはずだ。
プロジェクトを実行


続いて、各ノードを編集をしていく。

  1. 2D ワークスペースで「Line2D」ノードのパスを描く。まず1つ目の点を (-5, 0) に、次に2つ目の点を (5, 0) に打って直線のパスを描く。インスペクターで直接入力しても良い。
    Line2Dプロパティ
  2. インスペクターにて「Line2D」ノードの「Width」プロパティの値を 6 にする。
    Line2Dプロパティ
  3. 「Default Color」プロパティで弾丸の色を指定する。もちろんあなたの好みの色にしてもらって構わない。このチュートリアルではサンプルとして #708293 の青っぽいグレーの色を指定した。
    Line2Dプロパティ
  4. 「Capping」>「End Cap Mode」プロパティを「Round」にする。これでパスの終端(2つ目の点)が丸くなったはずだ。角張っているより、この方がずっと弾丸らしい。
    Line2Dプロパティ
    2D ワークスペース上で「Line2D」は以下のようになったはずだ。
    2DワークスペースのLine2D
  5. 「CollisionShape2D」ノードの「Shape」プロパティに「新規 RectangleShape2D」リソースを割り当てる。
  6. 2D ワークスペースで「Line2D」で作成した弾丸の形状に合わせてコリジョン形状を調整する。ピッタリでも良いし、弾丸のサイズよりやや内側に小さく作っても良い。このサンプルではリソース「RectangleShape2D」の「Extents」プロパティの値が (5, 2) となっている。
    CollisionShape2Dプロパティ
    2DワークスペースのLine2D
  7. 2D ワークスペースで「VisibilityNotifier2D」ノードの形状を調整する。この形状が画面外に出た時に発信されるシグナルを利用することになる。「CollisionShape2D」より x 軸方向で狭くして、2D ワークスペースで「CollisionShape2D」のコリジョン形状が確認しやすいようにした。y 軸方向の長さは同じにした。サイズはだいたいで構わない。このサンプルでは「Scale」プロパティの値は (0.5, 0.1) になっている。
    VisibilityNotifier2Dプロパティ
    VisibilityNotifier2Dプロパティ

ノードの追加とそれぞれのノードのプロパティ編集はここまでだ。


続いてルートノードにスクリプトをアタッチしてコーディングしていく。

  1. ルートノードにスクリプトをアタッチする。ファイルパスを「res://Bullet/Bullet.gd」として作成しよう。
  2. 「Bullet.gd」スクリプトを以下のように編集する。
###Bullet.gd###
extends Area2D

# 1秒あたりの弾丸のスピード
var speed = 1500
# 弾丸が飛んでいく方向ベクトル:いったん(0, 0)
var direction = Vector2.ZERO

# 物理プロセス: 60回/秒呼ばれる組み込みメソッド
func _physics_process(delta):
	# 弾丸の現在の回転角度からコサイン関数で弾丸が飛んでいく方向ベクトルの x の値を取得
	direction.x = cos(global_rotation)
	# 弾丸の現在の回転角度からサイン関数で弾丸が飛んでいく方向ベクトルの y の値を取得
	direction.y = sin(global_rotation)
	# 方向 × スピードで弾丸を毎フレーム移動させる
	translate(direction * speed * delta)

続いて、弾丸が物理ボディに当たったら発信されるシグナルをこのスクリプトに接続しよう。ルートノード「Bullet」が「Area2D」クラスなので、シーンツリードックでルートノード「Bullet」を選択し、そのままノードドック>シグナルタブにて「body_entered(body)」シグナルを選択して「接続」をクリック(またはシグナルをダブルクリック)して接続する。
body_enteredシグナル接続した状態
接続できたら、自動的に追加されたメソッド_on_Bullet_body_enteredを以下のように編集しよう。

###Bullet.gd###
# 弾丸が物理ボディに当たったら発信されるシグナルによって呼ばれるメソッド
func _on_Bullet_body_entered(body):
	# もし当たったボディが障害物だったら
	if body.is_in_group("Obstacles"):
		# 障害物のオブジェクトを解放する
		body.queue_free()
	# 弾丸インスタンスを解放する
	queue_free()

ちなみに、事前に「Obstacle」シーンのルートノードは「Obstacles」というグループに追加済みだ。
Obstaclesグループ

これで、外壁に当たれば弾丸だけ解放され、障害物に当たれば、障害物と弾丸が解放されるようになった。


さらにもう一つシグナルを追加する。「VisibilityNotifier2D」ノードが画面上から消えた時に発信されるシグナル「screen_exited()」を「Bullet.gd」スクリプトに接続しよう。手順は先程のシグナル接続と同様で、シーンツリードックで「VisibilityNotifier2D」ノードを選択し、ノードドック>シグナルタブで「screen_exited()」シグナルを接続すればOKだ。
screen_exited()シグナル接続した状態

接続できたら、自動的に追加されたメソッド_on_VisibilityNotifier2D_screen_exitedを以下のように編集しよう。

###Bullet.gd###
# VisibilityNotifier2D ノードが画面外に出た時に発信されるシグナルで呼ばれるメソッド
func _on_VisibilityNotifier2D_screen_exited():
	# 弾丸インスタンスを解放する
	queue_free()

これで画面外に出た時もその弾丸は解放されるようになった。


以上で弾丸シーンは完成だ。



ハンドガンを実装する

まずは一番簡単なハンドガン(拳銃)から撃てるようにしていこう。編集するスクリプトは「Player.gd」なのだが、すでに下準備の段階である程度のコードが出来上がっているので、そちらを先に確認しておこう。

Player.gd のコードの下準備部分を見る
###Player.gd###
extends KinematicBody2D

# プリロードした弾丸シーンの参照
const bullet_scn = preload("res://Bullet/Bullet.tscn")

# 現在使用中の銃(銃の種類ごとの数字の割り当ては以下のコメント)
var gun = 0
# 0: hand
# 1: shot
# 2: machine
# 3: lazer

# プレイヤーキャラクターのスピード
var speed = 200
# プレイヤーキャラクターの方向を伴うスピード
var velocity = Vector2()

# Muzzleノードの参照:銃口の位置
onready var muzzle = $Muzzle

# シーンが読み込まれたら呼ばれるメソッド
func _ready():
	rotation_degrees = 270 # ゲーム開始時プレイヤーに上を向かせる
	
# 物理プロセス:デフォルトで60回/秒呼ばれるメソッド
func _physics_process(delta):
	move() # プレイヤーキャラクターを移動させるメソッドを呼ぶ
	switch_gun() # 銃の種類を切り替えるメソッドを呼ぶ
	fire() # 銃を撃つメソッドを呼ぶ
	
# プレイヤーキャラクターを移動させるメソッド
func move():
	look_at(get_global_mouse_position()) # キャラクターにマウスカーソルの方を向かせる
	velocity = Vector2() # ベロシティ(方向をもった速度)を(0, 0)に初期化
	if Input.is_action_pressed("down"): # Sキーを押したら...
		velocity = Vector2(-speed, 0).rotated(rotation) # ベロシティを後ろ向きにセット
	if Input.is_action_pressed("up"): # Wキーを押したら...
		velocity = Vector2(speed, 0).rotated(rotation) # ベロシティを前向きにセット
	velocity = move_and_slide(velocity) # ベロシティに合わせて移動

# 銃の種類を切り替えるメソッド
func switch_gun():
	if Input.is_action_just_pressed("switch"): # マウス右ボタンをクリックをした場合...
		if gun < 3: # 銃の割り当て番号が 3 未満の場合は...
			gun += 1 # 割り当て番号を 1 増やす
		else: # 銃の割り当て番号が 3(最後の数字)の場合は...
			gun = 0 # 銃の割り当て番号を 0 にする
		print("Switched to ", gun) # デバッグ用に出力パネルに表示

# 銃を撃つメソッド
func fire():
	pass


ということで、fireメソッドが今のところ中身が空っぽだ。これを次のように更新する。ちなみに fire は日本語で「(銃などを)撃つ」という意味だ。

###Player.gd###

# 銃を撃つメソッド
func fire():
	# 銃の種類がハンドガン(0)かつマウス左ボタンをクリックした場合
	if gun == 0 and Input.is_action_just_pressed("fire"):
		# 弾丸インスタンスを生成して発射するメソッドを呼ぶ
		put_bullet()

ここでput_bulletというメソッドが登場したが、これはこれから定義するメソッドだ。fireメソッドの下に以下のコードを追加して定義しよう。

###Player.gd###

# 弾丸インスタンスを生成して発射するメソッド
func put_bullet():
	# 弾丸シーンのインスタンスの参照
	var bullet = bullet_scn.instance()
	# 弾丸インスタンスの位置を銃口の位置と同じにする
	bullet.global_position = muzzle.global_position
	# 弾丸インスタンスの向きを Player の向きと同じにする
	bullet.rotation_degrees = rotation_degrees
	# Playerではなくその親ノード(World)の子にする
	get_parent().add_child(bullet)
	# Worldの2番目の子にする(タイルマップより前面、プレイヤーキャラクターより背面)
	get_parent().move_child(bullet, 1)

これでハンドガンの実装ができたはずだ。プロジェクトを実行して確認してみよう。
ハンドガン



ショットガンを実装する

次はショットガン(散弾銃)を実装する。トップダウンシューティングでのショットガンは、複数の弾丸がそれぞれ少しずつ角度の差をつけて前方に飛んでいくような仕様が一般的だろう。一回で広範囲の複数オブジェクトを一掃できる強力な銃だ。

まずはfireメソッドから更新しよう。

###Player.gd###

func fire():
	# 銃の種類がハンドガン(0)かつマウス左ボタンをクリックした場合
	if gun == 0 and Input.is_action_just_pressed("fire"):
		put_bullet()
	# 銃の種類がショットガン(1)かつマウス左ボタンをクリックした場合
	if gun == 1 and Input.is_action_just_pressed("fire"):
		# 5回ループ
		for n in 5:
			# 弾丸インスタンスを生成して発射するメソッドの引数に値を渡して呼ぶ
			put_bullet(n)

fireメソッドに2つ目のifブロックを追加した。プロパティgunの値が 1(ショットガンの割り当て番号)の場合にマウス左クリックでショットガンを撃てる。ifブロックの中身はforループで5回put_bulletを呼んでいるが、先程のハンドガンの時と違い、引数にループの周回数nを渡している。このメソッドが受け取った引数をどう処理するかは、このあとput_bulletメソッドを更新していくのでそこで確認しよう。

func put_bullet(dir_offset = 2): # 引数dir_offsetを追加し、デフォルト値は2とした
	var bullet = bullet_scn.instance()
	bullet.global_position = muzzle.global_position
	bullet.rotation_degrees = rotation_degrees + (20 - 10 * dir_offset) # 更新
	get_parent().add_child(bullet)
	get_parent().move_child(bullet, 1)

少しややこしいが、メソッドを呼ぶときに引数dir_offsetが未入力の場合、デフォルト値の2が自動的に引数に渡される。メソッドのブロック内3行目で弾丸の回転角度(向き)を指定しているのだが、例えばハンドガンの場合は、引数を指定せずにこのメソッドを呼んでいるので、引数にはデフォルト値の 2 が渡されて、20 - 10 * dir_offsetの部分が 0 になり、弾丸の角度はプレイヤーキャラクターの向いている角度と同じになる。

一方、ショットガンの場合は、fireメソッド内のforループで5回このput_bulletメソッドが呼ばれており、ループの周回数 n(0からカウントして4まで)を引数dir_offsetに渡す形にしている。よって、ループが何周目かによって弾丸の角度は以下のように変化する。

  • ループ0周目:プレイヤーキャラクターの向いている角度 + 20°
  • ループ1周目:プレイヤーキャラクターの向いている角度 + 10°
  • ループ2周目:プレイヤーキャラクターの向いている角度 + 0°
  • ループ3周目:プレイヤーキャラクターの向いている角度 + -10°
  • ループ4周目:プレイヤーキャラクターの向いている角度 + -20°

上記のコードにより、5発の弾丸が「Player」が向いている方向に対して -20° から +20° の範囲で 10° ずつ異なる角度で同時に発射され、一度に広範囲を射撃できる銃の完成だ。なお、5 回程度のループであればコンピュータ上で一瞬で処理されるので、ほぼ同時にそれぞれの角度に弾丸が飛んでいくことになる。


これでショットガンの実装ができたはずだ。プロジェクトを実行して確認する際、マウス右ボタンを1回クリックしてショットガンに切り替えてから射撃してみよう。
ショットガン



マシンガンを実装する

続いてマシンガン(機関銃)を実装していこう。マシンガンは一回いっかいトリガーを引いて射撃する銃とは違い、トリガーを引いている間、自動的に連続して射撃できる銃だ。ショットガンのようにワンショットで広範囲の射撃はできないが、高速で自動射撃するため、プレイヤーキャラクター自身が回転すればすぐに広範囲のオブジェクトを一掃できる。

それでは「Player.gd」スクリプトにマシンガン用のコードを追加していこう。

まずはプロパティintervalを追加した。

###Player.gd###

var speed = 200
var velocity = Vector2()
# マシンガンの次の弾丸が発射されるまでのカウント
var interval: int = 0 # 追加

マシンガンの仕様として、マウス左ボタンを押したままにすれば自動的に弾丸が連続して発射されるようにするのだが、_physics_processメソッドでfireメソッドを毎フレーム呼ぶと、弾丸と弾丸の間隔が短すぎて弾丸が止まって見える(以下のGIF画像参照)。
マシンガン毎フレーム発射時

物理プロセスの 60 FPS(毎秒60フレーム)というフレームレートはかなり早いのだ。そこで今回は、毎フレーム、intervalプロパティに +1 してそのカウントが 5 を超えたら弾丸を発射するようにする。つまり、5 フレームに 1 回発射する計算だ。

そこを踏まえてfireメソッドにマシンガン用のifブロックを追加していこう。

func fire():
	# 銃の種類がハンドガン(0)かつマウス左ボタンをクリックした場合
	if gun == 0 and Input.is_action_just_pressed("fire"):
		put_bullet()
	# 銃の種類がショットガン(1)かつマウス左ボタンをクリックした場合
	if gun == 1 and Input.is_action_just_pressed("fire"):
		for n in 5:
			put_bullet(n)
	# 銃の種類がマシンガン(2)かつマウス左ボタンを押している場合
	if gun == 2 and Input.is_action_pressed("fire"):
		# 次の弾丸までのカウントを +1 する
		interval += 1
		# カウントが5以上だったら
		if interval >= 5:
			# カウントを0に戻して
			interval = 0
			# 弾丸インスタンスを生成して発射するメソッドを引数なしで呼ぶ
			put_bullet()

なお、ハンドガンとショットガンはInputクラスのis_action_just_pressedメソッドをifの条件に使っているが、こちらは左ボタンを押し続けても連続的には入力を検知されないようになっている。一方、マシンガンの場合はis_action_pressedメソッドを使っている。「just」がないだけで似たような名前のメソッドだが、こちらは押し続けていても毎フレーム入力が検知されるので、「押しっぱなし」の操作で利用するのに向いているのだ。


これでマシンガンの実装ができたはずだ。プロジェクトを実行して確認する際、マウス右ボタンを2回クリックしてマシンガンに切り替えてから射撃してみよう。
マシンガン



レーザーのシーンを作る

最後はレーザーガン(光線銃)を実装するのだが、これは弾丸ではなくレーザーを発射するので、まず先にレーザーのシーンを作成していこう。パーティクルやアニメーションにより最低限のそれらしい演出も加える。

  1. 「シーン」メニュー>「新規シーン」を選択する。
  2. 「ルートノードを生成」で「その他のノード」を選択する。
  3. ルートノードに「RayCast2D」クラスを選択し、名前を「Laser」に変更する。
  4. ルートノードに「Line2D」クラスの子ノードを追加する。弾丸シーンと同様にレーザーの見た目を作るのに使用する。もちろん「Line2D」ではなくレーザー用のテクスチャ画像を用意して「Sprite」にする方法もあるが今回は不採用だ。
  5. ルートノードに「Particle2D」クラスの子ノードを追加する。これはレーザーがオブジェクトに当たった箇所に粒子が泡立つような演出を追加するのに利用する。
  6. ルートノードに「Tween」クラスの子ノードを追加する。これはレーザー発射時にレーザーの幅を 0 から一定の幅までゆっくり太くしていく演出と、レーザー終了時の逆の演出に利用する。
  7. 一旦シーンを保存しておく。保存先のフォルダは用意しているのでファイルパスが「res://Laser/Laser.tscn」になるようにして保存しよう。

ここまででシーンツリーは以下のようになったはずだ。
Laserシーンツリー


続いて、各ノードを編集をしていく。

  1. インスペクターにて、ルートの「Laser」ノードの「Enabled」プロパティをオンにして、「Cast To」プロパティを (2000, 0) にする。
    Laserノードのプロパティ
    2D ワークスペース上では以下のスクリーンショットのようになったはずだ。
    2Dワークスペース上のLaserノード
  2. 2D ワークスペースで「Line2D」ノードのパスを描く。まず1つ目の点を (0, 0) に、次に2つ目の点を (200, 0) に打って直線のパスを描く。インスペクターで直接入力しても良い。なお、2つ目の点はスクリプトで制御するので、y の値が 0 であれば、x の値は 2D ワークスペース上で確認しやすい適当な値で良い。
    Line2Dノードのプロパティ
  3. インスペクターにて「Line2D」ノードの「Width」プロパティの値を 16 にする。
    Line2Dノードのプロパティ
  4. 「Default Color」プロパティでレーザーの色を指定する。もちろんあなたのイメージするレーザーの色にしてもらって構わない。このチュートリアルではサンプルとして #00b1ff の青色を指定した。
    Line2Dノードのプロパティ
    2D ワークスペース上で「Line2D」はだいたい以下のようになったはずだ。
    2DワークスペースのLine2D
  5. ここから「Particle2D」ノードのプロパティを以下のように編集する。プロパティが多いので大変だが頑張ろう。
    1. まず先に「Textures」>「Texture」プロパティにリソース「res://Assets/circle_05.png」を適用する。
      Particle2Dノードのプロパティ
    2. 「Emitting」プロパティをオンにする。
      Particle2Dノードのプロパティ
    3. 「Drawing」>「Visibility Rect」プロパティの値を (x: -50, y: -50, w: 100, h: 100) にする。
      Particle2Dノードのプロパティ
    4. 「Transform」>「Position」プロパティを(x: 200, y: 0)にして、「Transform」>「Scale」プロパティを (x: 0.1, y: 0.1) にする
      Particle2Dノードのプロパティ
    5. 「Process Material」プロパティに 「新規 ParticleMaterial」を割り当てる。
      Particle2Dノードのプロパティ
      ここからは今割り当てたリソース「ParticleMaterial」のプロパティを編集していく。

      • 「Emission Shape」>

        • 「Shape」プロパティを「Box」に変更する。
          Particle2Dノードのプロパティ
      • 「Direction」>

        • 「Direction」プロパティを (x: -1, y: 0, z: 0) にする。x軸の負の方向になる。
        • 「Spread」プロパティを 60 にする。60°の幅でパーティクルが移動する。
          Particle2Dノードのプロパティ
      • 「Gravity」>

        • 「Gravity」プロパティを (x: -300, y: 0, z: 0) にする。x軸の負の方向に重力を加える。
          Particle2Dノードのプロパティ
      • 「Initial Velocity」>

        • 「Velocity」プロパティを 800 にする。速度を 800 とした。これはおそらく秒速だ。
          Particle2Dノードのプロパティ
      • 「Scale」>

        • 「Scale Curve」プロパティに「新規 CurveTexture」を割り当てる。この次は割り当てたリソースのプロパティ編集だ。
          Particle2Dノードのプロパティ
        • 「CurveTexture」>
          • 「Curve」プロパティに「新規 Curve」プロパティを割り当て、値の変化を以下のスクリーンショットのように2つの点を打ってカーブを作って設定する。
            これで、時間経過とともにパーティクル1つひとつが次第に小さくなる。
            Particle2Dノードのプロパティ
      • 「Color」>

        • 「Color Ramp」プロパティにリソース「新規 GradientTexture」を割り当てる。パーティクルが生成されて消失するまでに次第に色を変える演出のためだ。
          Particle2Dノードのプロパティ
        • 上で割り当てた「GradientTexture」リソースのプロパティにリソース「新規 Gradient」を割り当てる。
          Particle2Dノードのプロパティ
          • 「Gradient」リソースのプロパティを編集するが、ここはインスペクターで直感的にグラデーションの基準となる色を3つ指定する。
            Particle2Dノードのプロパティ
            • 一番左端:#001096(深い青色)
            • 中央やや左寄りの位置:#2780ff(水色っぽい青色)
            • 一番右端:#00ffffff(不透明度0の白色)
              すると、結果的に以下のようなリソースのプロパティになる。
              Particle2Dノードのプロパティ

以上で、ノードの追加とそれぞれのノードのプロパティ編集は完了だ。


ここからはルートノード「Laser」にスクリプトをアタッチしてコーディングしていく。

  1. ルートノード「Laser」にスクリプトをアタッチし、ファイルパスを「res://Laser/Laser.gd」として作成する。
  2. 「Laser.gd」スクリプトを以下のように編集する。
###Laser.gd###
extends RayCast2D

# Line2Dノードの参照
onready var line = $Line2D
# Particle2Dノードの参照
onready var particle = $Particles2D
# Tweenノードの参照
onready var tween = $Tween

# シーンが読み込まれたら呼ばれるメソッド
func _ready():
	# Particle2DノードのEmittingプロパティをオフにする(インスペクターでオンにしたままの時の対策)
	particle.emitting = false
	# Tweenノードのアニメーションの設定:Line2DのWidthプロパティを0から10に0.5秒かけて変化させる
	tween.interpolate_property(line, "width", 0, 10.0, 0.5)
	# Tweenノードのアニメーション開始
	tween.start()

# 物理プロセス:60FPSで呼ばれるメソッド
func _physics_process(delta):
	# もしRayCast2D(ルートノード)が物理ボディに当たっている場合は...
	if is_colliding():
		# Line2Dノードの2つ目の点(終端)の位置をRayCast2Dが物理ボディに当たった位置に設定する
		line.set_point_position(1, to_local(get_collision_point()))
		
		# もし当たったのが障害物だったら...
		if get_collider().is_in_group("Obstacles"):
			# 障害物インスタンスの参照
			var obstacle = get_collider()
			# 障害物インスタンスのレーザー照射時間(irradiated_timeプロパティ)に delta の値を加算する
			obstacle.irradiated_time += delta
			# もしレーザー照射時間が最大照射時間(max_irradiationプロパティ)を超えたら...
			if obstacle.irradiated_time > obstacle.max_irradiation: 
				# 障害物インスタンスを解放する
				obstacle.queue_free()
	
	# もしRayCast2D(ルートノード)が物理ボディに当たっていない場合は...
	else:
		# Line2Dノードの2つ目の点(終端)の位置をRayCast2D(ルートノード)の先端の位置と同じにする
		line.set_point_position(1, cast_to)
	
	# Particle2Dノードの位置をLine2Dノードのパスの終端の位置と同じにする
	particle.position = line.points[1]
	# Particle2DノードのEmittingプロパティをオンにする(パーティクルのアニメーション開始)
	particle.emitting = true
	
	# マウス左ボタンから指を離した場合...
	if Input.is_action_just_released("fire"):
		# レーザーを止めるメソッドを呼ぶ
		stop_laser()

# レーザーを止めるメソッドを定義
func stop_laser():
	# Tweenノードのアニメーションを設定:Line2DノードのWidthプロパティを10から0に0.5秒かけて変化させる
	tween.interpolate_property(line, "width", 10.0, 0, 0.5)
	# Tweenノードのアニメーション開始
	tween.start()
	# Tweenノードのアニメーションが終わるまで待機
	yield(tween, "tween_completed")
	# Tweenノードを解放する
	queue_free()

このコードについて、少し補足しておく。下準備として作成済みの障害物のシーン「Obstacle.tscn」のルートノードにアタッチしている「Obstacle.gd」スクリプトにて、irradiated_timemax_irradiationという2つのプロパティを定義している。前者はレーザーの照射時間、後者はレーザーの最大照射時間として用意したものだ。レーザーが当たってすぐに障害物が破壊されるより、一定時間(最大照射時間:0.2秒)照射されたら破壊される設定の方がレーザーっぽさが出るのではないか、という考えからこのような仕組みを作った次第だ。


これでレーザーシーンが用意できた。次は「Player」シーンを更新して、レーザーを発射できるようにしていく。



レーザーガンを実装する

レーザーシーンができたので、レーザーガンを実装していこう。レーザーガンの仕様として、まず発射時に、先に作成した「Laser.tscn」のインスタンスを「Player」シーンに追加するようにしていく。プレイヤーの操作はマシンガンと同様に、マウス左ボタンを押したままにしている間は発射し続けられるようにする。一方、ボタンから指を離すと、先にコーディングした「Laser.gd」スクリプトにより、レーザーが消えて、インスタンスも解放される。

それでは具体的に「Player.gd」スクリプトを編集していこう。まずはfireメソッドの 4 つ目のifブロックを以下のように追加してほしい。

###Player.gd###

func fire():
	# 銃の種類がハンドガン(0)かつマウス左ボタンをクリックした場合
	if gun == 0 and Input.is_action_just_pressed("fire"):
		put_bullet()
	# 銃の種類がショットガン(1)かつマウス左ボタンをクリックした場合
	if gun == 1 and Input.is_action_just_pressed("fire"):
		for n in 5:
			put_bullet(n)
	# 銃の種類がマシンガン(2)かつマウス左ボタンを押している場合
	if gun == 2 and Input.is_action_pressed("fire"):
		# 次の弾丸までのカウントを +1 する
		interval += 1
		# カウントが5以上だったら
		if interval >= 5:
			# カウントを0に戻して
			interval = 0
			# 弾丸インスタンスを生成して発射するメソッドを引数なしで呼ぶ
			put_bullet()
	# 銃の種類がレーザーガン(3)かつマウスの左ボタンを押している場合
	if gun == 3 and Input.is_action_just_pressed("fire"):
		# レーザーインスタンスを生成して発射するメソッドを呼ぶ
		load_laser()

追加した 4 つ目のifブロックで、銃がレーザーの時にマウスの左ボタンを押し続けている間はload_laserメソッドを呼ぶようにした。このメソッドはこれから定義するところだ。以下のコードをput_bulletメソッドの下に追加しよう。

###Player.gd###

# レーザーインスタンスを生成して発射するメソッド
func load_laser():
	# Laser.tscnのインスタンスの参照
	var laser = laser_scn.instance()
	# Laserインスタンスの位置を銃口の位置と同じにする
	laser.position = muzzle.position
	# LaserインスタンスをPlayerルートノードに子ノードとして追加する
	add_child(laser)
	# LaserインスタンスノードをPlayerルートノードの子ノードのうち0番目(最背面)に移動する
	# Playerノードの子Spriteノードのテクスチャ画像(の銃口部分)より背面にするため
	move_child(laser, 0)

マウス左ボタンを押して「Laser」インスタンスが生成された後は、「Laser.gd」スクリプトの方でレーザーの位置、向き、長さ、幅、先端のパーティクルの位置は全て制御される。指が離れた時の「Laser」インスタンスの解放も含めて、だ。


以上で、レーザーガンの実装は完了だ。プロジェクトを実行して確認する際、マウス右ボタンを3回クリックしてレーザーガンに切り替えてから射撃してみよう。
プロジェクトを実行してレーザーガンの確認

完成してから気がついたが、レーザーは他の色にした方がよかったかもしれない。まるで水鉄砲か高水圧洗浄機のようだ。


最後にもう一度プロジェクトを実行し、4つの銃を切り替えながらプレイしてみよう。
プロジェクトを実行して全種類の銃の確認



おわりに

今回、トップダウンシューティングゲームによく登場する銃 4 種類を実装した。もし、もっと細かく作り込むなら、例えば、以下のような要素を追加するとさらに面白くなるかもしれない。

  • 銃の種類によって弾丸の見た目や速度を変える。
  • 銃を切り替えたらプレイヤーキャラクターのスプライトも変更する。
  • 弾丸をリロードしたりレーザーのエネルギーを充填するアニメーションや間を設ける。
  • 弾丸がオブジェクトに当たって解放される時に煙や破片のようなパーティクルを追加して演出する。
  • 銃の種類ごとに当たったオブジェクトへのダメージを設定し、オブジェクト側にもいわゆるHPなどの一定のライフの値を設定し、ライフが 0 になったら破壊できるようにする。


リンク


UPDATE
2022/05/07 タイポ修正