第5回目の今回は、さらに敵キャラクターの種類を増やしていく。それぞれの敵キャラクターの動きに違いを持たせ、それらをタイルマップ上に複数配置してゲームの難易度を高めていこう。具体的に今回は以下の敵キャラクターを作成していく。
- バニー(うさぎ)
- カメレオン
- プラント(植物)
Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るプラットフォーマー
Bunny シーンを作る
まず追加する最初の敵キャラクターはバニーだ。Part 4 でマッシュルームを作った時のおさらいだと思って進めていこう。では「Enemy」シーンを継承して「Bunny」シーンを作るところから開始する。以下の手順でシーンを作成して保存しよう。
- 「シーン」メニュー>「新しい継承シーン」を選択する。
- 継承元として「Enemy.tscn」を選択して開く。
- ルートノードを「Bunny」に変更する。
- シーンを保存する。この時「res://Enemies/」に「Bunny」フォルダを作成して、そこに「Bunny.tscn」という名前で保存しよう。ファイルパスは「res://Enemies/Bunny/Bunny.tscn」になる。
これで「Bunny」シーンが用意できた。
各ノードのプロパティを編集する
ルートノードの Script Variables を編集する
「Bunny」ルートノードの Script Variables を以下のように変更しよう。
- Gravity: 512
- speed: 48
AnimatedSprite ノードのアニメーションを編集する
まず忘れずに行いたいのが「AnimatedSprite」の「Frames」プロパティを 「ユニーク化」 することだ。ユニーク化できたら、「SpriteFrames」をクリックしてアニメーションを編集していく。
以下の内容でアニメーションを用意しよう。
- アニメーション名: fall
- 速度: 24 FPS
- スプライトシート: res://Assets/Enemies/Bunny/Fall.png
- ループ: オン
- アニメーション名: hit
- 速度: 24 FPS
- スプライトシート: res://Assets/Enemies/Bunny/Hit (34x44).png
- ループ: オフ
- アニメーション名: jump
- 速度: 24 FPS
- スプライトシート: res://Assets/Enemies/Bunny/Jump.png
- ループ: オン
- アニメーション名: run
- 速度: 24 FPS
- スプライトシート: res://Assets/Enemies/Bunny/Run (34x44).png
- ループ: オン
なお「res://Assets/Enemies/Bunny/」フォルダには「Idle (34x44).png」も用意してあるが、このチュートリアルでは不要だ。
もしスプライトシートの画像にブラーがかかっている(ぼやけている)場合は、該当のファイルをファイルシステムドックで選択した状態で、インポートドックから「プリセット」>「2D Pixel」選択 > 「再インポート」を実施しておこう。
CollisionShape2D ノードのコリジョン形状を設定する
「Bunny」ルートノード直下の「CollisionShape2D」ノードから編集する。こちらも忘れないうちに「Shape」プロパティの値を 「ユニーク化」 しておこう。
続けて、コリジョン形状を調整する。バニーは少し縦長のデザインなので「CapsuleShape2D」の形を当てはめやすいだろう。
スプライトテクスチャに対して、だいたい以下のような形、配置になればOKだ。前回のマッシュルーム同様、足先は地面との衝突検知のためにピッタリ合わせ、頭のてっぺんは別のHitBoxの方の「CollisionShape2D」を配置するので少し空けている。なお、便宜上、下の画像ではシーンドックで不要なノードを非表示にしてコリジョン形状を見やすくしている。
このコリジョン形状の編集によって、インスペクター上の関係するプロパティの値はそれぞれ、以下のようになっている。2Dワークスペースでの調整が苦手な場合は、これらの数値を直接インスペクターで入力しても構わない。
- Radius: 8
- Height: 10
- Position: (0, 9)
次に「HitBox」ノードの子である「CollisionShape2D」ノードのコリジョン形状を編集する。まずは「Shape」プロパティの値を 「ユニーク化」 しておこう。
こちらのコリジョン形状は、頭の上に配置し、先に設定したルートノード直下の「CollisionShape2D」より幅を狭くし、また形状が重ならないようにする。
コリジョン形状の編集により関係するプロパティは以下のようになっている。
- Extents: (5, 2)
- Position: (0, -6)
VisibilityEnabler2D ノードの形状を設定する
最後に「VisibilityEnabler2D」ノードの形状も編集しておこう。調整作業自体は2Dワークスペースでドラッグ操作で行うのが直感的でわかりやすいだろう。下のスクリーンショットのように、だいたいバニーのスプライトテクスチャと同じくらいにした。
関連するプロパティの具体的な値は以下のようになった。
- Position: (0, 2)
- Scale: (0.875, 1.25)
以上でプロパティの編集は終わりだ。次はスクリプトをアタッチして、コードで動きを制御していこう。
Bunny シーンに新しいノードを追加する
「Bunny」ルートノードに、「Enemy」シーンにはなかった「Area2D」ノードを追加しよう。この「Area2D」ノードには「CollisionShape2D」ノードを追加しよう。
これはバニーの動きに変化をつけるための簡単な仕掛けだ。バニーにプレイヤーキャラクターが一定の距離まで近づいたら、「Area2D」のシグナルを利用して、バニーの動きを変えるというものだ。具体的には、バニーがプレイヤーキャラクターから離れている間は「AnimatedSprite」の「run」アニメーションで走らせるが、一定距離内に近づくと「jump」&「fall」アニメーションに切り替えて、ジャンプして移動させる。
どれくらいの距離でアニメーションを切り替えるかは「Area2D」ノードの子「CollisionShape2D」のコリジョン形状を決定する「Radius」プロパティ次第だ。今回は値を 100 とした。「Position」プロパティはデフォルトの (0, 0) のままなので、単純にプレイヤーキャラクターが半径 100 px 以内に近づくとバニーがジャンプで移動し始めるという動きを想定している。
Bunny.gd スクリプトをアタッチして編集する
前回の Part 4 では、「Mushroom」シーンで「Mushroom」ノードから「Enemy.gd」スクリプトを最初にデタッチして、その後、「Enemy.gd」を継承した「Mushroom.gd」スクリプトをアタッチしていた。
しかし、このあと数種類の敵キャラクターを作るごとに「Enemy.gd」スクリプトをデタッチする作業が煩わしいので、このタイミングで「Enemy」シーンの「Enemy」ルートノードからスクリプトをデタッチしておこう。そうすれば、「Enemy」シーンを継承して作成したシーンで、毎回スクリプトをデタッチする必要がなくなる。
では「Enemy.tscn」シーンを開いて、「Enemy」ルートノードを右クリックし、「スクリプトをデタッチ」を選択しよう。
次に「Bunny.tscn」シーンを開いて、「Bunny」ルートノードにスクリプトをアタッチする。
継承元を「res://Enemies/Enemy.gd」、ファイルパスを「res://Enemies/Bunny/Bunny.gd」としてスクリプトを作成してアタッチしよう。
スクリプトがアタッチできたら、コードを編集していこう。
extends "res://Enemies/Enemy.gd"
export var jump_force = 200 # バニーのジャンプ力
var is_jumping = false # ジャンプで移動しているかどうかのステータス
バニーはジャンプ移動するので、ジャンプ力を示すプロパティとしてjump_force
を定義した。値は200
としている。プレイヤーキャラクターよりややジャンプ力を低くしている。
is_jumping
プロパティは、走って移動しているのか、ジャンプして移動しているのかを示すステータスの役割として定義した。最初は走って移動なので値をfalse
にしている。プレイヤーキャラクターが「Area2D」ノードの範囲に入ったらtrue
に変更する予定だ。
次に_ready
メソッドで最初に再生するアニメーションとして、「run」アニメーションを指定した。
func _ready():
sprite.play("run")
続いて_physics_process
メソッドを編集していく。
func _physics_process(delta):
# Mushroom と同じ
if is_on_wall():
speed *= -1
sprite.flip_h = !sprite.flip_h
# ジャンプで移動中の場合
if is_jumping:
if is_on_floor():
velocity.y = -jump_force
else:
if velocity.y < 0:
sprite.play("jump")
else:
sprite.play("fall")
# Mushroom と同じ
velocity.x = -speed
velocity.y += gravity * delta
velocity = move_and_slide(velocity, Vector2.UP)
if is_jumping:
のブロックが Part 4 で作ったマッシュルームと違うところだ。
冒頭で定義したis_jumping
プロパティの値がtrue
だった場合にブロック内のコードを実行する。そしてすぐにネストされたif / else
構文になる。
if is_on_floor():
で地面に接している場合、ということになるが、この場合はvelocity.y
にジャンプ力であるjump_force
に-
をつけて代入している。これにより、バニーがジャンプする。
ジャンプしている間はis_on_floor
メソッドの戻り値がfalse
になるので、else
ブロックに入る。ここでさらにネストされたif / else
構文になっている。
一旦ジャンプ力とイコールになったvelocity.y
の値が、毎フレーム重力の影響を受け、やがて0
に近づいていく。そして0
になった時がジャンプの高さの頂点だ。つまりif velocity.y < 0:
は『ジャンプの頂点に達するまでは』という条件であり、その間は「jump」アニメーションを再生するよう指定している。
そしてelse
ブロックはvelociy.y
の値が0
以下、つまり『ジャンプの頂点から地面に落ちている間は』という条件になる。この間は「fall」アニメーションを再生するように指定している。
さて、is_jumping
プロパティの値の切り替えを行うには、「Area2D」のシグナルを利用する。一旦、シーンドックに戻って「Area2D」ノードを選択し、ノードドック>「シグナル」タブにて、以下の2つのシグナルを「Bunny.gd」スクリプトに接続しよう。
- body_entered(body)
- body_exited(body)
接続すると、スクリプトにメソッドが追加されたはずだ。そのメソッドは以下のように編集する。
# Area2D ノードのシグナル「body_entered(body)」を接続
func _on_Area2D_body_entered(body):
if body.is_in_group("Players"):
is_jumping = true
# Area2D ノードのシグナル「body_exited(body)」を接続
func _on_Area2D_body_exited(body):
is_jumping = false
sprite.play("run")
どちらもメソッドのブロック内冒頭でif body.is_in_group("Players"):
を記述している。以前のチュートリアルで、「Player」シーンの「Player」ノードを「Players」ノードグループに追加したので、その設定を再利用している。プレイヤーキャラクター以外にも、他の敵キャラクターが近くにいる場合が想定できるので、この if 構文が必要なのだ。ちなみにif body.name == "Player":
としてもらっても良い(「Player」ノードの名前を変えない限り)。
そして「Area2D」のコリジョン範囲に入ったらis_jumping
プロパティをtrue
としてジャンプでの移動に切り替え、範囲から出たらis_jumping
プロパティをfalse
としつつ「run」アニメーションを再生させている。
ここまでできたら「Bunny.tscn」および「Bunny.gd」を保存して、バニーの動きを確認していこう。
Level1 シーンに Bunny シーンのインスタンスノードを追加する
では、バニーが想定通りの動きをするか見ていこう。「Level1」ノードに「Bunny.tscn」のインスタンスノードを追加する。
今回もひとまず2Dワークスペースで、すぐに動作を確認できそうな位置に「Bunny」ノードを配置しよう。
配置できたらプロジェクトを実行して挙動を確認してみよう。
Chameleon シーンを作る
次は別の敵キャラクターのカメレオンを作っていく。バニーの手順と同様にサクサク進めていこう。
- 「シーン」メニュー>「新しい継承シーン」を選択する。
- 継承元として「Enemy.tscn」を選択して開く。
- ルートノードを「Chameleon」に変更する。
- シーンを保存する。フォルダを作成して、ファイルパスは「res://Enemies/Chameleon/Chameleon.tscn」とする。
各ノードのプロパティを編集する
スクリプトを作成してルートノードにアタッチする
「Enemy.gd」スクリプトに記述している Script Variables をカメレオンにも適用して、値を編集したいので、先に「Enemy.gd」を継承したスクリプトを作成、アタッチしよう。
「Chameleon」ルートノードを選択して、スクリプトをアタッチする。以下の設定でスクリプトを作成すること。
- 継承元: res://Enemies/Enemy.gd
- パス: res://Enemies/Chameleon/Chameleon.gd
ルートノードの Script Variables を編集する
「Bunny」ルートノードの Script Variables を以下のように変更しよう。
- Gravity: 512
- speed: 16
AnimatedSprite ノードのアニメーションを編集する
まずは「AnimatedSprite」の「Frames」プロパティを 「ユニーク化」 しよう。ユニーク化できたら、「SpriteFrames」をクリックしてアニメーションを編集していく。
以下の内容でアニメーションを用意しよう。
- アニメーション名: attack
- 速度: 12 FPS
- スプライトシート: res://Assets/Enemies/Chameleon/Attack (84x38).png
- ループ: オン
- アニメーション名: hit
- 速度: 24 FPS
- スプライトシート: res://Assets/Enemies/Chameleon/Hit (84x38).png
- ループ: オフ
- アニメーション名: idle
- 速度: 24 FPS
- スプライトシート: res://Assets/Enemies/Chameleon/Idle (84x38).png
- ループ: オン
- アニメーション名: run
- 速度: 24 FPS
- スプライトシート: res://Assets/Enemies/Chameleon/Run (84x38).png
- ループ: オン
繰り返しになるが、もしスプライトシートの画像にブラーがかかっている(ぼやけている)場合は、該当のファイルをファイルシステムドックで選択した状態で、インポートドックから「プリセット」>「2D Pixel」選択 > 「再インポート」を実施しておこう。
あとは、カメレオンのスプライトテクスチャが、カメレオンの体をやや右寄りにデザインされているので、体の中心がだいたい y 軸上にくるように「AnimatedSprite」ノードの「Position」プロパティの x の値を -16 に変更しておく。
CollisionShape2D ノードのコリジョン形状を設定する
まずは「Chameleon」ルートノード直下の「CollisionShape2D」から編集する。
カメレオンの体に合わせてコリジョン形状を編集する。「idle」アニメーション時の足元をピッタリ合わせて、頭のてっぺんは「HitBox」のコリジョン形状を配置のために少し空けておこおう。
コリジョン形状に関わるプロパティの値は以下の通りだ。
- Radius: 11
- Height: 2
- Position: (7, 0)
続けて「HitBox」ノード下の「CollisionShape2D」ノードのコリジョン形状を編集しよう。これまで通り、頭のてっぺんに配置し、幅をルートノード直下の「CollisionShape2D」より狭くして重ならないようにする。
コリジョン形状に関わるプロパティの値は以下の通りだ。
- Extents: (7, 2)
- Position: (0, -7)
VisibilityEnabler2D ノードの形状を設定する
「VisibilityEnabler2D」ノードの形状もカメレオンのスプライトテクスチャに合わせて調整しよう。
関連するプロパティの値は以下の通りだ。
- Position: (4.5, 3)
- Scale: (1.031, 1)
Chameleon ノードに RayCast2D ノードを追加する
「AnimatedSprite」ノードの「attack」アニメーションを見てみると、カメレオンは舌を伸ばして攻撃してくるデザインだ。そこで、プレイヤーキャラクターが、カメレオンに対して水平( x 軸)方向に一定距離近づくと、近寄ってきて、さらに一定距離近づくと舌を伸ばして攻撃してくる、という動きにしていく。
プレイヤーキャラクターとの水平方向の距離を検知するために「Chameleon」ルートノードに「RayCast2D」クラスのノードを追加しよう。このノードは指定した直線距離での衝突判定機能を提供してくれるので非常に便利だ。
「RayCast2D」は2Dワークスペース上では、矢印で表示されている。この矢印をカメレオンが「attack」アニメーションで出した時の舌の向きと位置に合わせる。この作業のために一時的に「AnimatedSprite」ノードのプロパティを以下のように編集しよう。
- Animation: attack
- Frame: 6
- Playing: オフ
これで2Dワークスペース上には、舌を伸ばした状態のカメレオンのスプライトテクスチャが表示されているはずだ。このデザインに合わせて「RayCast2D」を調整していく。
関係するプロパティを以下のように編集しよう。
- Enabled: オン > RayCast2Dが有効にする
- Cast To: (-120, 0)> 左向きの長さ 120 px の矢印にする
- Position: (0, 6) > カメレオンの舌と同じ高さにする
2Dワークスペース上では以下のスクリーンショットのようになっているはずだ。
Chameleon.gd スクリプトを編集する
さっき追加した「RayCast2D」ノードを利用しつつ「Chameleon.gd」スクリプトをコーディングしていく。
まずは必要なプロパティの定義から始めよう。「RayCast2D」ノードにアクセスしやすくするため、onready
キーワード付きでraycast
プロパティにこのノードの参照を渡した。
extends "res://Enemies/Enemy.gd"
onready var raycast = $RayCast2D
次に_ready
メソッドで、「AnimatedSprite」ノードの最初のアニメーションを「idle」に指定した。
func _ready():
sprite.play("idle")
続いて_physics_process
メソッドを定義する。
func _physics_process(delta):
velocity.y += gravity * delta
if raycast.is_colliding():
if raycast.get_collider().name == "Player":
if position.distance_to(raycast.get_collision_point()) > 80:
run()
else:
attack()
velocity.x = 0
else:
sprite.play("idle")
velocity.x = 0
velocity.y += gravity * delta
velocity = move_and_slide(velocity, Vector2.UP)
冒頭からif
構文だ。
「RayCast2D」のis_colliding
メソッドは、「RayCast2D」ノードの矢印に何らかのオブジェクトが衝突していればtrue
、そうでなければfalse
を返す。つまり最初のif
構文は、『「RayCast2D」の矢印にオブジェクトが衝突していれば』という条件だ。
次のネストされたif
構文を確認しよう。get_collider
メソッドは、衝突しているオブジェクトを返す。そのname
プロパティにはそのオブジェクトの名前が入っている。つまりこのif
構文は、衝突したオブジェクトの名前が「Player」だったら、という条件になっている。ちなみにis_on_group
メソッドを使いたいところだが、生憎、get_collider
で取得するオブジェクトにはそのメソッドが含まれないので使用できない。
そしてさらにネストされた3つ目のif
構文を見てみよう。メソッドの引数に他のメソッドが入っているのでややこしく見えるが、一つずつ確認していく。
まずposition
は「Chameleon」ノードの「Position」プロパティで、どこにカメレオンがいるのかを x, y 座標の Vector2 型の値で保持している。Vector2 クラスにはdistance_to
というメソッドがある。このメソッドは自身の x, y 座標と、引数で渡した Vextor2 の座標までの長さ(直線距離)を float 型で返す。今回その引数には別のメソッドget_collision_point
を渡している。このメソッドは「RayCast2D」ノードがオブジェクトと衝突している位置を Vector2 型の値で返す。
つまり、この3つ目のif
構文は、『「Chameleon」ノードの位置から「RayCast2D」ノードがオブジェクトと衝突した位置までの距離が80
px より大きかったら』という条件になっている。そのif
条件を満たした場合、run
メソッドが呼ばれる。このrun
メソッドはこのあと定義する。
一方、オブジェクトとカメレオンとの直線距離が80
px 以下の場合はelse
ブロックに入り、「attack」メソッドが呼ばれる。このattack
メソッドもこのあと定義する。そのすぐ後にvelociy.x = 0
としている。舌で攻撃を仕掛けてくる時は移動はしない、ということだ。
そして、2ブロック戻って、最初のif
条件を満たさない場合、つまり「RayCast2D」ノードの矢印がオブジェクトに衝突していない場合は、ただ「idle」アニメーションを再生するのみで、移動もしない。
一番外側のif / else
ブロックを抜けると、velociy.y
の値に重力を適用し、最後に現在のvelocity
の値を引数にとってmove_and_slide
メソッドによりカメレオンの動きが制御される。
ちなみにif
構文を2つネストさせたが、and
や&&
でつないで全て一行のifブロックにすることも可能だ。今回は可読性を重視してこのような形にした。
では、次に新しいメソッドを定義しておく。まずはrun
メソッドから。
func run():
sprite.play("run")
if is_on_wall():
speed *= -1
sprite.flip_h = !sprite.flip_h
sprite.position.x *= -1
raycast.cast_to.x *= -1
velocity.x = -speed
実はこれは Part 4 で作ったマッシュルームの_physics_process
メソッド内の内容とほとんど同じだ。このスクリプトの場合、_physics_process
メソッド内のコード量が多くなり、ゴチャゴチャするので、スッキリさせるためにただ分離した格好だ。反対にメソッドに分けない方が良い人はこのメソッドを定義せず、中のコードをそのままrun
を実行している箇所に移動させても良い。
念のため、内容を確認しておこう。まず最初に「run」アニメーションを再生する。
続いてis_on_wall
で壁に当たったらという条件のif
ブロックに入る。壁に当たったら、進行方向を x 軸方向に反転、スプライトテクスチャも同じく反転、スプライトの位置も元々左に-16
ずらしているので、sprite.position.x *= -1
として反転させる。そして今回追加した「RayCast2D」ノードの矢印の向きも同じく反転させた(cast_to.x *= -1
)。
あとは、velociy.x
は常にspeed
プロパティの値で一定(左右の方向は変わる)、velociy.y
には常に重力gravity
がかかっている。このvelociy
を引数にしてmove_and_slide
メソッドによりカメレオンがゲーム画面上を移動することになる。
繰り返しになるが、このメソッドはカメレオンの「RayCast2D」の矢印とプレイヤーキャラクターが衝突していて(矢印の長さ 120px 以下)、かつカメレオンとプレイヤーキャラクターとの距離が80
pxより大きい場合に呼ばれる。
続いてattack
メソッドを定義しておこう。
func attack():
sprite.play("attack")
if sprite.frame == 6 or sprite.frame == 7:
if raycast.is_colliding() and raycast.get_collider().name == "Player":
if position.distance_to(raycast.get_collision_point()) < 50:
print("Player is hit")
まず「attack」アニメーションを再生する。
次はif
構文だ。『attack」アニメーションのフレームが6
か7
だったら』という条件だがピンとくるだろうか。実は「attack」アニメーションのフレーム6
と7
だけがカメレオンが舌を伸ばしているデザインのスプライトテクスチャになっているのだ。
ネストされた2つ目のif
構文を見ると、またis_colliding
メソッドでオブジェクトと衝突しているか、get_collider().name == "Player"
で衝突したオブジェクトの名前は「Player」かをチェックしている。これは厳密にカメレオンが舌を伸ばしている時に「RayCast2D」がプレイヤーキャラクターを捉えているか、を判定するためだ。
そしてさらにネストされた3つ目のif
構文だ。position.distance_to(raycast.get_collision_point())
はさっき_physics_process
内にあったコードと同じだ。『カメレオンの位置からプレイヤーキャラクターの位置までの直線距離が50
px 未満だったら』という条件になっている。この50
px というのは実はフレーム6
および7
のスプライトテクスチャの伸ばしている舌の長さとほぼイコールなのだ。
この3つ目のif
条件を満たした場合に、本来はプレイヤーキャラクターをゲームオーバーにするかライフを減らしたいところだが、そこはまだ未実装なので、今の時点ではprint
メソッドで「Player is hit」とだけ出力されるようにした。
Level1 シーンに Chameleon シーンのインスタンスノードを追加する
では、カメレオンが想定通りの動きをするか見ていこう。「Level1」ノードに「Chameleon.tscn」のインスタンスノードを追加する。「Mushroom」と「Bunny」は確認の邪魔になるので一時的に非表示にしておこう。
今回もひとまず2Dワークスペースで、すぐに動作を確認できそうな位置に「Chameleon」ノードを配置しよう。
配置できたらプロジェクトを実行して挙動を確認してみよう。
以下の動きが確認できた。
- プレイヤーキャラクターとの距離が 120 px 以下になるまで「idle」アニメーションで移動はしない
- プレイヤーキャラクターとの距離が 120 - 80 px では「run」アニメーションで近づいてくる
- プレイヤーキャラクターとの距離が 80 px 以下で舌を伸ばしてくる(移動はしない)
なお、舌がプレイヤーキャラクターに当たった時、出力パネルには以下のように表示された。本来ならゲームオーバーかもしれないが、動きとしては想定通りだ。
これでカメレオンはひとまず完成としておこう。なかなかタフなチュートリアルになってきた。まだ敵キャラクターを作るのかと、信じられない気持ちかもしれない。しかし頑張るのだ。
Plant シーンを作る
次はプラントという敵キャラクターを作ろう。プラントはその名の通り植物のキャラクターだ。だから、移動はしない。その代わりに、遠隔攻撃を仕掛けてくる厄介なキャラクターにしよう。
ほとんどの手順はこれまでに作ってきた敵キャラクターと同じなので、スクリーンショットやすでに説明済みの内容は省かせていただく。
では以下の手順で「Plant」シーンを作成しよう。
- 「Enemy.tscn」を継承してシーンを作成
- ルートノードの名前を「Plant」に変更
- 「res://Enemies/Plant/Plant.tscn」のパスでシーンを保存
各ノードのプロパティを編集する
スクリプトを作成してルートノードにアタッチする
- 「Plant」ルートノードにスクリプトをアタッチする
- この時、継承元として「Enemy.gd」を選択する
- パスは「res://Enemies/Plant/Plant.gd」とする
ルートノードの Script Variables を編集する
以下の値で設定する。
- Gravity: 512
- Speed: 0
AnimatedSprite ノードのアニメーションを編集する
まずは「Frames」プロパティの「SpriteFrames」をユニーク化しておこう。そのあとは以下の内容でアニメーションを作成しよう。
- attack
- 速度: 8 FPS
- スプライトシート: res://Assets/Enemies/Plant/Attack (44x42).png
- ループ: オン
- hit
- 速度: 24 FPS
- スプライトシート: res://Assets/Enemies/Plant/Hit (44x42).png
- ループ: オフ
- idle
- 速度: 12 FPS
- スプライトシート: res://Assets/Enemies/Plant/Idle (44x42).png
- ループ: オン
プラントのスプライトテクスチャのデザインも若干右に本体が寄っているので、インスペクターで「Position」プロパティを(-4, 0)に変更しておこう。
CollisionShape2D ノードのコリジョン形状を設定する
まずは「Plant」ルートノード直下の「CollisionShape2D」ノードから編集する。
- Radius: 10
- Height: 12
- Position: (0, 5)
続いて「HitBox」直下の「CollisionShape2D」ノードを編集する。
- Extents: (6, 2)
- Position: (0, -13)
VisibilityEnabler2D ノードの形状を設定する
「VisibilityEnabler2D」の形状もスプライトテクスチャに合わせておこう。
- Position: (-4, 0)
- Scale: (1, 1)
Plant ノードに RayCast2D ノードを追加する
カメレオンの時と同様に「Plant」ルートノードに「RayCast2D」ノードを追加する。プロパティは以下の通りにする。カメレオンよりさらに矢印の射程を長くして「Cast To」プロパティの x の値を 300 px とした。矢印に衝突したら遠隔攻撃してくる仕様にする予定だ。
- Enabled: オン
- Cast To: (300, 0)
Plant.gd スクリプトを編集する
「Plant.gd」スクリプトを編集してプラントの動きを作っていこう。スクリプトは以下のようになった。今回は説明を掻い摘んで、スクリプトに直接コメントで入れている。
extends "res://Enemies/Enemy.gd"
# RayCast2D ノードの参照を raycast プロパティに渡す
onready var raycast = $RayCast2D
# _ready メソッド
func _ready():
# 最初は idle アニメーションを再生する
sprite.play("idle")
#_physics _process メソッド
func _physics_process(delta):
# もし RayCast2D の矢印にオブジェクトが衝突して、それが「Player」だったら attack メソッドを呼び出す
if raycast.is_colliding() and raycast.get_collider().name == "Player":
attack()
# もし RayCast2D の矢印のオブジェクトに何も衝突しなければ idle アニメショーンを再生するのみ
else:
sprite.play("idle")
# attack メソッド
func attack():
# attack アニメーションを再生
sprite.play("attack")
# スプライトのフレームが 4 だったら
if sprite.frame == 4:
# spawn_bullet メソッドを呼び出す
spawn_bullet()
# spawn_bullet メソッド
func spawn_bullet():
# 一旦 print メソッドで書き出しのみ
print("Spawn Bullet!!!")
ということで、カメレオンの時よりシンプルなコードになった。attack
メソッドにより「attack」アニメーションを再生し、そのフレームが4
の時にspawn_bullet
メソッドが呼ばれる。このspawn_bullet
は後ほど更新するが、口から種を前方に飛ばすアクションだ。これには別途、種のシーンを作成し、そのインスタンスを都度「Plant」ルートノードに追加する必要があるが、その作業は一旦置いておいて、ここまでのコードに問題がないか確認しよう。
Level1 シーンに Plant シーンのインスタンスノードを追加する
コード確認のため、ひとまず「Level1」シーンのわかりやすい位置に「Plant」シーンのインスタンスを作って追加しよう。
ではプロジェクトを実行してプラントの動きを確認しよう。今回は「デバッグ」メニュー>「コリジョン形状を表示」をオンにして、「RayCast2D」の矢印が見えるようにしておこう。
プレイヤーキャラクターが「RayCast2D」の矢印に当たっている時は、プラントの「attack」アニメーションが再生され、出力パネルには「Spawn Bullet!!!」が表示された。高台に乗って「RayCast2D」の矢印から逃れるとプラントの「idle」アニメーションが再生された。概ね問題なさそうだ。
Seed シーンを作成する
シーンを作成してノードを追加する
プラントが口から飛ばす種のシーンとして「Seed」シーンを新規で作成しよう。
- 「シーン」メニュー>「新規シーン」を選択する
- 「ルートノードを生成」で「その他のノード」を選択する
- 「Area2D」クラスのノードをルートノードにする
- 「Area2D」ノードの名前を「Seed」に変更する
- 「Seed」ルートノードに「Sprite」ノードを追加する
- 「Seed」ルートノードに「CollisionShape2D」ノードを追加する
- 「Seed」ルートノードに「VisibilityNotifier2D」ノードを追加する
- 「res://Enemies/Plant/Seed.tscn」をパスとしてシーンを保存する
必要なノードは揃ったので、それぞれのプロパティを編集していこう。
ノードのプロパティを編集する
以下の手順で「Seed」シーンのノードのプロパティを編集しよう。
- 「Sprite」ノードの「Texture」プロパティにアセットの「res://Assets/Enemies/Plant/Bullet.png」を適用する
- 「CollisionShape2D」ノードの「Shape」プロパティに「新規 CircleShape2D」を適用する
- 「CollisionShape2D」ノードのコリジョン形状を調整する(「Radius」プロパティの値を 4 にする)
- 「VisibilityNotifier2D」ノードの形状を調整する(「Scale」プロパティの値を(0.4, 0.4)にする)
これでインスペクターでのプロパティの編集は終わりだ。続けてスクリプトをアタッチして種の動きをコーディングしていこう。
Seed.gd スクリプトをアタッチして編集する
「Seed」ルートノードに新規でスクリプトをアタッチする。パスは「res://Enemies/Plant/Seed.gd」としておこう。
スクリプトの内容はひとまず以下のようにした。単純だがこれで種の移動はOKだ。
extends Area2D # Added @ Part 5
# 種の移動速度(px/second)
export var speed = 150
# 毎フレーム、種のx軸方向の位置が左に (spped * delta) 分だけ進む
func _physics_process(delta):
position.x -= speed * delta
続いて、以下のシグナルをスクリプトに接続する。
- 「Seed」ルートノードの「body_entered(body)」シグナルを接続する
- 「VisibilityNotifier2D」ノードの「viewport_exited(viewport)」シグナルを接続する
すると、スクリプトにそれぞれのシグナルによって呼ばれるメソッドが追加される。その中身を以下のようにコーディングする。
# 種が何らかの body と衝突したらシグナル発信により呼ばれるメソッド
func _on_Seed_body_entered(body):
# プレイヤーキャラクターに衝突したら出力
if body.name == "Player":
print("Player is hit!!!")
# 種自体消える
queue_free()
# 画面から消えたらシグナル発信により呼ばれるメソッド
func _on_VisibilityNotifier2D_viewport_exited(viewport):
print("viewport_exited method called")
# 種自体消える
queue_free()
今はまだプレイヤーキャラクターのダメージやゲームオーバーの部分が未実装なので、ひとまず種がプレイヤーキャラクターに当たったらprint
メソッドで「Player is hit!!!」とだけ出力されるようにしておく。その時、種も同時にqueue_free
メソッドが呼ばれて消える。
また、種が画面外に出た場合も「VisibilityNotifier2D」ノードのシグナルをきっかけにqueue_free
メソッドが呼ばれて消える。
Plant シーンに Seed シーンのインスタンスノードを追加する
種はプラントの「attack」アニメーション中に画面上に初めて現れるので、最初からシーンドックで追加しておくわけにはいかない。スクリプトで「Seed.tscn」ファイルを読み込んでおき、必要なタイミングでそれをインスタンス化して「Plant.tscn」シーンに追加する、というスクリプトを記述する必要がある。
では「Plant.gd」スクリプトを改めて編集していく。
まずはプロパティを2つ追加する。
var is_spawning = false # 追加
onready var spawned_seed = preload("res://Enemies/Plant/Seed.tscn") # 追加
attack
アニメーションは 8 FPS(毎秒8フレームの速さ)に設定したが、一方、_physics_process
メソッドの物理プロセスのフレームはデフォルトの設定が 60 FPS となっている。ということは、attack
アニメーションの 1 フレームが描画されている間に_physics_process
のフレームが複数回読み込まれる。そのため、「attack」アニメーションの 4 フレーム目で種を飛ばす、というコードにしているだけでは、4フレーム目が描画されている間に複数の種が飛んでしまうのだ。この問題を回避して、アニメーション一回につき種を一つだけ飛ばすために、このis_spawning
というステータスプロパティを使用する。
もう一つの追加したプロパティspawned_seed
は、「Seed.tscn」シーンファイルを事前に読み込んでおくためのものだ。preload
が事前にリソースファイルを読み込んでおくためのメソッドになっている。ちなみにプロパティ名をseed
としたいところだが、GDScript の予約語と被ってしまうので使用できない。
次にattack
メソッドを少し更新した。
func attack():
sprite.play("attack")
if sprite.frame == 4 and is_spawning == false:
spawn_bullet()
is_spawning = true
elif sprite.frame == 5 and is_spawning:
is_spawning = false
「attack」アニメーションの 4 フレーム目で、かつis_spawning
プロパティが false の場合にだけspawn_bullet
メソッドが呼ばれて種を飛ばす、というコードになっている。種を飛ばしたあとすぐにis_spawning
をtrue
に変更する。そのあとアニメーションの 5 フレーム目になって、is_spawning
がtrue
の場合は(絶対そうなるのだが)、次のアニメーションに備えてis_spawning
をもとのfalse
に戻す。これで、アニメーション一回につき種を一つだけ飛ばす動きになる。
最後にspawn_bullet
メソッドを更新して、種を飛ばすコードを実装した。
func spawn_bullet():
print("Spawn Bullet!!!")
var seed_instance = spawned_seed.instance()
add_child(seed_instance)
seed_instance.global_position = Vector2(position.x - 24, position.y)
事前にspawned_seed
プロパティで、「Seed.tscn」を読み込んでいるので、そのインスタンスを生成するのにinstance()
メソッドを実行している。これをseed_instance
というプロパティに代入した。
add_child
メソッドの引数にそのインスタンスを渡すことで、「Plant」ノードの子ノードとして「Seed.tscn」のインスタンスノードが追加される。
子ノードに追加されたらすぐに、画面上の位置を指定している。プラントの種を飛ばすときのテクスチャデザインに合わせて、プラントのposition.x
より左に24
px ずらした位置に配置した。その位置に種が現れるとすぐにそこから真っ直ぐ左方向へ飛んでいく。
それではプロジェクトを実行して、最終的なプラントの動きを確認しておこう。
- アニメーション一回につき一つだけ種を飛ばした
- 種はちょうど良い速度で飛んでいる
- プラントの口から種が飛んでいるように見えるので種の最初の位置も問題ない
- viewport(画面)から消えたら「Seed.tscn」のインスタンスノードは消えている
これでプラントは完成としよう。
Level1 シーンに敵キャラクターを複数配置する
ここまでで Part 4 で作ったマッシュルームを含めて四種類の敵キャラクターを用意した。これらを「Level1」シーンに適当に配置して、実際にプレイしてみよう。
すでにそれぞれの敵キャラクターの動作確認で「Level1」シーンに 1 つずつインスタンスノードを追加している。そのため、さらに敵キャラクターのインスタンスノードを追加するときは、シーンドックで追加したい同じ種類のインスタンスノードを選択してショートカットキー操作で複製(Windows: Ctrl + D / macOS: Cmd + D)すると簡単だ。ノードの名前も、例えば「Mushroom」ノードを複製したら「Mushroom2」という具合に、自動的に末尾に番号を追加してくれるので非常に便利だ。
今回はそれぞれの敵キャラクターのインスタンスノードを2つずつ「Level1」シーンに追加して配置した。
サンプルとして、以下のような配置にしている。これはもちろん、何度かプレイしてみて多少バランスを調整した後の配置だ。あなたもご自身で作ったタイルマップに合わせて、敵キャラクターの数や配置を調整してほしい。
納得のいく敵キャラクターの配置になるまで、プロジェクトを実行しながら調整してみよう。
上のGIF画像では、プラントの種やカメレオンの舌を何度かくらっているが、敵キャラクターの配置はひとまずこれで完了とする。
Part 5 で編集したスクリプトのコード
最後に今回の Part 5 で編集したスクリプトのコードを共有しておくので、必要に応じて確認してほしい。
Bunny.gd の全コード
extends "res://Enemies/Enemy.gd" # Added @ Part 5
export var jump_force = 200
var is_jumping = false
func _ready():
sprite.play("run") # Set default animation
func _physics_process(delta):
if is_on_wall():
speed *= -1
sprite.flip_h = !sprite.flip_h
if is_jumping:
if is_on_floor():
velocity.y = -jump_force
else:
if velocity.y < 0:
sprite.play("jump")
else:
sprite.play("fall")
velocity.x = -speed
velocity.y += gravity * delta
velocity = move_and_slide(velocity, Vector2.UP)
func _on_Area2D_body_entered(body):
if body.is_in_group("Players"):
is_jumping = true
func _on_Area2D_body_exited(body):
if body.is_in_group("Players"):
is_jumping = false
sprite.play("run")
Chameleon.gd の全コード
extends "res://Enemies/Enemy.gd" # Added @ Part 5
onready var raycast = $RayCast2D
func _ready():
sprite.play("idle")
func _physics_process(delta):
if raycast.is_colliding():
if raycast.get_collider().name == "Player":
if position.distance_to(raycast.get_collision_point()) > 80:
run(delta)
else:
attack()
else:
sprite.play("idle")
func run(delta):
sprite.play("run")
if is_on_wall():
speed *= -1
sprite.flip_h = !sprite.flip_h
raycast.cast_to.x *= -1
velocity.x = -speed
velocity.y += gravity * delta
velocity = move_and_slide(velocity, Vector2.UP)
func attack():
sprite.play("attack")
if sprite.frame == 6 or sprite.frame == 7:
if raycast.is_colliding() and raycast.get_collider().name == "Player":
if position.distance_to(raycast.get_collision_point()) < 50:
print("Player is hit")
Plant.gd の全コード
extends "res://Enemies/Enemy.gd" # Added @ Part 5
var is_spawning = false
onready var spawned_seed = preload("res://Enemies/Plant/Seed.tscn")
onready var raycast = $RayCast2D
func _ready():
sprite.play("idle")
func _physics_process(delta):
if raycast.is_colliding() and raycast.get_collider().name == "Player":
attack()
else:
sprite.play("idle")
func attack():
sprite.play("attack")
if sprite.frame == 4 and is_spawning == false:
spawn_bullet()
is_spawning = true
elif sprite.frame == 5 and is_spawning:
is_spawning = false
func spawn_bullet():
print("Spawn Bullet!!!")
var seed_instance = spawned_seed.instance()
add_child(seed_instance)
seed_instance.global_position = Vector2(position.x - 24, position.y)
Seed.gd の全コード
extends Area2D # Added @ Part 5
export var speed = 150
func _physics_process(delta):
position.x -= speed * delta
func _on_Seed_body_entered(body):
if body.name == "Player":
print("Player is hit!!!")
queue_free()
func _on_VisibilityNotifier2D_viewport_exited(viewport):
print("viewport_exited method called")
queue_free()
おわりに
以上で Part 5 は完了だ。今回は敵キャラクターを 3 種類追加した。なかなかハードだったと思うが、その分、作業手順には慣れていただけたのではないだろうか。やはり敵キャラクターの種類が増えて、それぞれ動きが違ってくると、ゲームの奥深さは断然違ってくる。今回のチュートリアルで要領を得ていただけたなら幸いだ。
ところで、今のままだと、ただただ敵キャラクターが待ち受けている地獄のような世界になっているため、プレイヤーキャラクターにとって嬉しい要素があまりない。
そこで次回のチュートリアルでは、レベルシーンにアイテム(スーパーマリオシリーズのコインのような)を追加して、ちょっとは嬉しい気持ちになれるようにゲームをアップデートする予定なので、お楽しみに。
UPDATE:
2022-02-25 「Chameleon.gd」スクリプトの_physics_process
メソッド、run
メソッドのコードを更新して、カメレオンが空中でも落下しない問題を修正