このチュートリアルでは、2Dトップダウンシューティングゲーム(見下ろし型シューティングゲーム)で一般的によく登場する銃を4種類作っていく。具体的には以下の通りだ。
- ハンドガン
- ショットガン
- マシンガン
- レーザーガン
Environment
このチュートリアルは以下の環境で作成しました。
・Godot のバージョン: 3.4.2
・コンピュータのOS: macOS 11.6.5
このチュートリアルでは、銃の作成にフォーカスするため、以下は予め用意しておいた。
ゲームの世界
「World.tscn」というシーンを用意し、見た目は「TileMap」ノードを追加して簡単に作成している。「TileMap」以外に「Player」ノードと複数の「Obstacle」ノードを追加している。これらについては個別のシーンを作成して、インスタンスを追加した形だ。プレイヤーキャラクター
「Player.tscn」シーンとして作成した。ルートノードは「KinematicBody2D」クラスで、子ノードに「Sprite」と「CollisionShape2D」を追加している。「Sprite」のテクスチャは銃をもったスキンヘッドのヒットマンにしている。ヒットマンが持っている銃の画像の先端の位置に「Position2D」クラスの「Muzzle」という名前のノードを配置している。これは後ほど銃を撃った時の弾丸インスタンスの生成される位置を指定するのに使用する。
インプットマップには以下のアクションを追加済みだ。プレイヤーキャラクターの移動や射撃、銃の切り替えに使用する。- up: W キー・・・プレイヤーキャラクターを前進させる時に使う
- down: S キー・・・プレイヤーキャラクターを後退させる時に使う
- fire: マウス左ボタン・・・銃を撃つ
- switch: マウス右ボタン・・・銃の種類を切り替える
ちなみに、チュートリアルの内容を簡潔にするため、プレイヤーキャラクターのスプライトは銃の種類が変わっても同じままだ。見た目はハンドガンっぽいが、マシンガンにもレーザーガンにもなる、なんでもありの銃、ということにしておこう。
障害物
「Obstacle.tscn」というシーンを作成している。Obstacle は日本語で障害物という意味だ。画面上にたくさん置いている茶色い木箱のオブジェクトは全てこのシーンのインスタンスである。ルートノードは「StaticBody2D」クラスにして、その子ノードとして「Sprite」と「CollisionShape2D」を追加している。
解放された(破壊された)時に同じ場所に爆発を表現するための「Smoke.tscn」というシーンのインスタンスを生成するようにしている。「Smoke」シーンは「Particle2D」ノードのみで構成され、こちらもパーティクルの実行が完了したら自動的に解放されるようにしている。
これらの下準備によりプロジェクトを実行すると、以下のように世界とヒットマンと障害物が描画され、現時点でヒットマンは移動操作のみ可能な状態だ。
このチュートリアルのプロジェクトファイルは GitHubリポジトリ に置いている。.zipファイルをダウンロードしていただき、「Start」フォルダの方を Godot Engine にインポートしていただければ、上述の下準備だけ完了したプロジェクトから開始できる。また、取り急ぎ完成形を確認されたい場合は「End」フォルダの方をインポートしていただければOKだ。
また今回プロジェクトにインポート済みのアセットは全て KENNEY(ケニー) のサイトからダウンロードして利用させていただいたものだ。CC0の非常に使いやすいアセットを豊富にご用意いただいていることに感謝申し上げたい。特に今回ご利用させていただいたのは下記のアセットパックだ。
それでは銃の実装を進めていこう。
弾丸のシーンを作る
まずは弾丸のシーンを作ろう。ハンドガン、ショットガン、マシンガンの3種類の銃でこの弾丸のシーンを使いまわせるので、先に作ってしまおうというわけだ。
- 「シーン」メニュー>「新規シーン」を選択する。
- 「ルートノードを生成」で「その他のノード」を選択する。
- ルートノードに「Area2D」クラスを選択し、名前を「Bullet」に変更する。
- ルートノードに「Line2D」クラスの子ノードを追加する。このチュートリアルでのこのノードの用途は弾丸の見た目を作るのに使用するのみだ。もちろん「Line2D」ではなく弾丸用のテクスチャ画像を用意して「Sprite」にしても良い。
- ルートノードに「CollisionShape2D」クラスの子ノードを追加する。
- ルートノードに「VisibilityNotifier2D」クラスの子ノードを追加する。銃で撃った弾が画面外に出てしまったら、それをシグナルで通知し、弾丸のインスタンスを解放するために使用する。
- 一旦シーンを保存しておく。保存先のフォルダは用意しているのでファイルパスが「res://Bullet/Bullet.tscn」になるようにして保存しよう。
ここまででシーンツリーは以下のようになったはずだ。
続いて、各ノードを編集をしていく。
- 2D ワークスペースで「Line2D」ノードのパスを描く。まず1つ目の点を (-5, 0) に、次に2つ目の点を (5, 0) に打って直線のパスを描く。インスペクターで直接入力しても良い。
- インスペクターにて「Line2D」ノードの「Width」プロパティの値を 6 にする。
- 「Default Color」プロパティで弾丸の色を指定する。もちろんあなたの好みの色にしてもらって構わない。このチュートリアルではサンプルとして #708293 の青っぽいグレーの色を指定した。
- 「Capping」>「End Cap Mode」プロパティを「Round」にする。これでパスの終端(2つ目の点)が丸くなったはずだ。角張っているより、この方がずっと弾丸らしい。
2D ワークスペース上で「Line2D」は以下のようになったはずだ。 - 「CollisionShape2D」ノードの「Shape」プロパティに「新規 RectangleShape2D」リソースを割り当てる。
- 2D ワークスペースで「Line2D」で作成した弾丸の形状に合わせてコリジョン形状を調整する。ピッタリでも良いし、弾丸のサイズよりやや内側に小さく作っても良い。このサンプルではリソース「RectangleShape2D」の「Extents」プロパティの値が (5, 2) となっている。
- 2D ワークスペースで「VisibilityNotifier2D」ノードの形状を調整する。この形状が画面外に出た時に発信されるシグナルを利用することになる。「CollisionShape2D」より x 軸方向で狭くして、2D ワークスペースで「CollisionShape2D」のコリジョン形状が確認しやすいようにした。y 軸方向の長さは同じにした。サイズはだいたいで構わない。このサンプルでは「Scale」プロパティの値は (0.5, 0.1) になっている。
ノードの追加とそれぞれのノードのプロパティ編集はここまでだ。
続いてルートノードにスクリプトをアタッチしてコーディングしていく。
- ルートノードにスクリプトをアタッチする。ファイルパスを「res://Bullet/Bullet.gd」として作成しよう。
- 「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)」シグナルを選択して「接続」をクリック(またはシグナルをダブルクリック)して接続する。
接続できたら、自動的に追加されたメソッド_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」というグループに追加済みだ。
これで、外壁に当たれば弾丸だけ解放され、障害物に当たれば、障害物と弾丸が解放されるようになった。
さらにもう一つシグナルを追加する。「VisibilityNotifier2D」ノードが画面上から消えた時に発信されるシグナル「screen_exited()」を「Bullet.gd」スクリプトに接続しよう。手順は先程のシグナル接続と同様で、シーンツリードックで「VisibilityNotifier2D」ノードを選択し、ノードドック>シグナルタブで「screen_exited()」シグナルを接続すればOKだ。
接続できたら、自動的に追加されたメソッド_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回クリックしてマシンガンに切り替えてから射撃してみよう。
レーザーのシーンを作る
最後はレーザーガン(光線銃)を実装するのだが、これは弾丸ではなくレーザーを発射するので、まず先にレーザーのシーンを作成していこう。パーティクルやアニメーションにより最低限のそれらしい演出も加える。
- 「シーン」メニュー>「新規シーン」を選択する。
- 「ルートノードを生成」で「その他のノード」を選択する。
- ルートノードに「RayCast2D」クラスを選択し、名前を「Laser」に変更する。
- ルートノードに「Line2D」クラスの子ノードを追加する。弾丸シーンと同様にレーザーの見た目を作るのに使用する。もちろん「Line2D」ではなくレーザー用のテクスチャ画像を用意して「Sprite」にする方法もあるが今回は不採用だ。
- ルートノードに「Particle2D」クラスの子ノードを追加する。これはレーザーがオブジェクトに当たった箇所に粒子が泡立つような演出を追加するのに利用する。
- ルートノードに「Tween」クラスの子ノードを追加する。これはレーザー発射時にレーザーの幅を 0 から一定の幅までゆっくり太くしていく演出と、レーザー終了時の逆の演出に利用する。
- 一旦シーンを保存しておく。保存先のフォルダは用意しているのでファイルパスが「res://Laser/Laser.tscn」になるようにして保存しよう。
ここまででシーンツリーは以下のようになったはずだ。
続いて、各ノードを編集をしていく。
- インスペクターにて、ルートの「Laser」ノードの「Enabled」プロパティをオンにして、「Cast To」プロパティを (2000, 0) にする。
2D ワークスペース上では以下のスクリーンショットのようになったはずだ。 - 2D ワークスペースで「Line2D」ノードのパスを描く。まず1つ目の点を (0, 0) に、次に2つ目の点を (200, 0) に打って直線のパスを描く。インスペクターで直接入力しても良い。なお、2つ目の点はスクリプトで制御するので、y の値が 0 であれば、x の値は 2D ワークスペース上で確認しやすい適当な値で良い。
- インスペクターにて「Line2D」ノードの「Width」プロパティの値を 16 にする。
- 「Default Color」プロパティでレーザーの色を指定する。もちろんあなたのイメージするレーザーの色にしてもらって構わない。このチュートリアルではサンプルとして #00b1ff の青色を指定した。
2D ワークスペース上で「Line2D」はだいたい以下のようになったはずだ。 - ここから「Particle2D」ノードのプロパティを以下のように編集する。プロパティが多いので大変だが頑張ろう。
- まず先に「Textures」>「Texture」プロパティにリソース「res://Assets/circle_05.png」を適用する。
- 「Emitting」プロパティをオンにする。
- 「Drawing」>「Visibility Rect」プロパティの値を (x: -50, y: -50, w: 100, h: 100) にする。
- 「Transform」>「Position」プロパティを(x: 200, y: 0)にして、「Transform」>「Scale」プロパティを (x: 0.1, y: 0.1) にする
- 「Process Material」プロパティに 「新規 ParticleMaterial」を割り当てる。
ここからは今割り当てたリソース「ParticleMaterial」のプロパティを編集していく。「Emission Shape」>
- 「Shape」プロパティを「Box」に変更する。
- 「Shape」プロパティを「Box」に変更する。
「Direction」>
- 「Direction」プロパティを (x: -1, y: 0, z: 0) にする。x軸の負の方向になる。
- 「Spread」プロパティを 60 にする。60°の幅でパーティクルが移動する。
「Gravity」>
- 「Gravity」プロパティを (x: -300, y: 0, z: 0) にする。x軸の負の方向に重力を加える。
- 「Gravity」プロパティを (x: -300, y: 0, z: 0) にする。x軸の負の方向に重力を加える。
「Initial Velocity」>
- 「Velocity」プロパティを 800 にする。速度を 800 とした。これはおそらく秒速だ。
- 「Velocity」プロパティを 800 にする。速度を 800 とした。これはおそらく秒速だ。
「Scale」>
- 「Scale Curve」プロパティに「新規 CurveTexture」を割り当てる。この次は割り当てたリソースのプロパティ編集だ。
- 「CurveTexture」>
- 「Curve」プロパティに「新規 Curve」プロパティを割り当て、値の変化を以下のスクリーンショットのように2つの点を打ってカーブを作って設定する。
これで、時間経過とともにパーティクル1つひとつが次第に小さくなる。
- 「Curve」プロパティに「新規 Curve」プロパティを割り当て、値の変化を以下のスクリーンショットのように2つの点を打ってカーブを作って設定する。
- 「Scale Curve」プロパティに「新規 CurveTexture」を割り当てる。この次は割り当てたリソースのプロパティ編集だ。
「Color」>
- 「Color Ramp」プロパティにリソース「新規 GradientTexture」を割り当てる。パーティクルが生成されて消失するまでに次第に色を変える演出のためだ。
- 上で割り当てた「GradientTexture」リソースのプロパティにリソース「新規 Gradient」を割り当てる。
- 「Gradient」リソースのプロパティを編集するが、ここはインスペクターで直感的にグラデーションの基準となる色を3つ指定する。
- 一番左端:#001096(深い青色)
- 中央やや左寄りの位置:#2780ff(水色っぽい青色)
- 一番右端:#00ffffff(不透明度0の白色)
すると、結果的に以下のようなリソースのプロパティになる。
- 「Gradient」リソースのプロパティを編集するが、ここはインスペクターで直感的にグラデーションの基準となる色を3つ指定する。
- 「Color Ramp」プロパティにリソース「新規 GradientTexture」を割り当てる。パーティクルが生成されて消失するまでに次第に色を変える演出のためだ。
- まず先に「Textures」>「Texture」プロパティにリソース「res://Assets/circle_05.png」を適用する。
以上で、ノードの追加とそれぞれのノードのプロパティ編集は完了だ。
ここからはルートノード「Laser」にスクリプトをアタッチしてコーディングしていく。
- ルートノード「Laser」にスクリプトをアタッチし、ファイルパスを「res://Laser/Laser.gd」として作成する。
- 「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_time
とmax_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 になったら破壊できるようにする。
リンク
- KENNEY
- Godot 公式オンラインドキュメント:2D移動の概要
- Godot 公式オンラインドキュメント:Line2D
- Godot 公式オンラインドキュメント:RayCast2D
- Godot 公式オンラインドキュメント:パーティクル・システム(2D)
- Godot 公式オンラインドキュメント:Particles2D
- Godot 公式オンラインドキュメント:ParticlesMaterial
- note:[Godot]ゲームで使う数学の覚書
UPDATE
2022/05/07 タイポ修正