第15回目の今回はいよいよこのチュートリアルのシリーズの最終回だ。最終回はプラットフォーマーのレベルデザインに比較的よく使われるいくつかの仕掛けを追加していく。具体的には以下にリストアップしたトラップやギミックの類だ。

  • 動く床
  • 落ちる床
  • 高く飛べる床
  • 火が出る装置
  • 飛んでくる鉄球

今回は Part 1 でインポート済みのアセットからたくさんのスプライトシートを利用する。例によって、見た目にブラーがかかっている(ピクセルアート特有のエッジが効いた画像ではない)場合は、ファイルシステム上で利用するアセットファイルを選択し、インポートドックから「プリセット」>「2D Pixel」を選択し、「再インポート」をクリックして修正しよう。

Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るプラットフォーマー



動く床

名前の通り、プレイヤーキャラクターがそこに乗っているだけで移動できる、動く床を作成する。日本語で『床』としたが、英語でいうところのプラットフォームだ。まさにプラットフォーマーというジャンルに相応しい定番の仕掛けである。


シーンを作成する

  1. 「Node2D」をルートノードにして新規でシーンを作成する。
  2. ルートノードの名前は「MovingPlatform」とする。
  3. ファイルパスを「res://Traps/MovingPlatform/MovingPlatform.tscn」として保存する。
  4. 次に、以下のスクリーンショットと同じシーンツリーになるように、必要なノードを追加する。なお、ノードの名前変更はルートノード以外必要ない。
    MovingPlatformのシーンツリー

ノードのプロパティを編集する

シーンツリーに追加したノードうち、いくつかのプロパティを編集していこう。

Path2D ノード

このノードは、動く床の経路となるパスを作成する。ここではひとまず雛形としてシンプルな直線のパスを作成するが、インスタンス化した先のシーンでパスの形状は編集可能だ。

  1. インスペクターで「Resource」>「Local To Scene」にチェックを入れてオンにする。
  2. シーンツリードックで「Path2D」が選択されている状態だと、2D ワークスペース上にパスを描画するツール(以下のスクリーンショット)が表示される。それぞれのアイコンにカーソルを合わせると説明が見れるので確認しておこう。
    パス描画ツール
  3. 2D ワークスペース上に点を2つ以上打つことでパスを描いていく。左から3番目の「+」のついたアイコンをクリックして点を追加しよう。点は(0, 0)と、もう一つ真っ直ぐ上の適当な場所、(0, -128)あたりに打っておこう。これで縦に真っ直ぐのパスができたはずだ。
    Path2Dの2Dワークスペース

公式オンラインドキュメント:
ベジェ、曲線、パス
Path2D
Curve2D


PathFollow2D ノード

このノードは、親である「Path2D」ノードのパスを経路として、それに沿って子ノード(ここでは「KinematicBody2D」)を移動させる役割をする。経路に沿った移動に関わるプロパティについて少し解説しておく。

「Offset」プロパティでピクセル単位の値でパスの原点からの距離を指定するか、「Unit Offset」プロパティで原点からの距離を割合(0 ~ 1)で指定することで、経路上の位置を決めることができる。この位置を常に変化させることで、経路上をオブジェクトが移動する動きを作ることができるわけだ。この位置の変化はのちほど「AnimationPlayer」ノードのアニメーションとスクリプトで制御するので、ここで今直接編集する必要はない。

「Rotate」プロパティをオフにしておく。インスペクターで唯一変更するプロパティだ。

パスが y 軸に対して平行な直線ではない場合に、パスをたどるオブジェクト(ここでは動く床)の向きもデフォルトでは回転するが、このプロパティをオフにすることで固定できる。床がパスの角度に合わせて回転してしまうと、プレイヤーキャラクターはそこに乗っていられなくなる(敢えてそういうトラップとしてオンにしておくのもアイデアだが)。
PathFollow2Dのプロパティ編集


公式オンラインドキュメント:
PathFollow2D


AnimatedSprite ノード

親の「KinematicBody2D」ノードに「SpriteFrames」リソースを適用して見た目(テクスチャ)を作る。

  1. インスペクタードックで「Frames」プロパティに「新規 SpriteFrames」リソースを割り当てる。
  2. スプライトフレームパネルで以下のアニメーションを作成する。
  • アニメーション名: off

    • 速度: 1 FPS
    • ループ: オフ
    • アニメーションフレームリソース: res://Assets/Traps/Platforms/Brown Off.png
      AnimatedSpriteのアニメーション編集
  • アニメーション名: on

    • 速度: 16 FPS
    • ループ: オン
    • アニメーションフレームリソース: res://Assets/Traps/Platforms/Brown On (32x8).png
      AnimatedSpriteのアニメーション編集

AnimationPlayer ノード

このノードは重要だ。いつものような楽しい演出としての使用ではなく、このノードによって「PathFollow2D」ノードの「Unit Offset」プロパティの値を徐々に変更することで、「KinematicBody2D」が「Path2D」の経路を辿るようにする。

  1. アニメーションパネルで以下の内容のアニメーションを新規作成する
  • アニメーション名: move
  • 読み込み後自動再生: オン
  • アニメーションの長さ(秒): 4
  • リピート再生: オン
  • トラック:PathFollow2D ノード > unit_offset プロパティ
    • Time: 0 秒 / Value: 0 / Easing: 1.00
    • Time: 2 秒 / Value: 0.99 / Easing: 1.00
      *Value を 1 にすると経路を折り返すタイミングで一瞬弾けるような現象が生じるので 0.99 としている
      AnimationPlayerのアニメーション編集

チェーンのシーンを作成する

次は動く床の経路がわかるように、経路に沿ってチェーンを配置したい。そこで今回はチェーンのセグメント1つ分のシーンを作成し、動く床が読み込まれた時に、スクリプトで画面上に必要な数だけチェーンをインスタンス化する方向で進める。ではシーンを作成しよう。

  1. ルートノードに「Sprite」クラスを指定して新規でシーンを作成する。
  2. ルートノードの名前は「MovingPlatformChain」に変更する。
  3. 「MovingPlatformChain」ルートノードの「Texture」プロパティにリソース「res://Assets/Traps/Platforms/Chain.png」を適用する。
  4. ファイルパスを「res://Traps/MovingPlatform/MovingPlatformChain.tscn」としてシーンを保存する。
    MovingPlatformChain

スクリプトをアタッチして編集する

それではスクリプトで「MovingPlatform」が読み込まれたらチェーンを表示するようにコーディングしていこう。

「MovingPlatform」ルートノードにスクリプトをアタッチしてほしい。この時、ファイルパスを「res://Traps/MovingPlatform/MovingPlatform.gd」としてスクリプトを作成しよう。

アタッチできたら、「MovingPlatform.gd」スクリプトを以下のように編集してほしい。

extends Node2D

# Path2D の参照
onready var path = $Path2D
# MovingPlatformChainシーンをプリロード
onready var chain_scn = preload("res://Traps/MovingPlatform/MovingPlatformChain.tscn")

# ノードが読み込まれる時に実行
func _ready():
	make_chain()

# チェーンを Path2D の経路に合わせて必要数インスタンス化して配置するメソッド
func make_chain():
	# Path2D のキャッシュされた点(の位置)を取得
	var points = path.curve.get_baked_points()
  # Path2D の全てのキャッシュされた点にチェーンのインスタンスを配置
	for point in points:
		var chain = chain_scn.instance()
		add_child(chain)
		move_child(chain, 0)
		chain.z_index = -1
		chain.position = point

「Path2D」ノードのリソース「Curve2D」クラスのメソッドget_baked_pointsで、キャッシュされた点を取得できる。このキャッシュされた点はパスを作成した時の点とは別物である。パスをゲーム画面上に描画するときに、パスの形状を複数の点に置き換えてキャッシュ(一時保存)しているのだ。ちなみに、キャッシュされた点と点の間隔は、「Bake Interval」プロパティで設定でき、値が小さいほどパスのカーブなどの形状を滑らかに描画できるが、その分コンピュータのメモリを消費するので注意が必要だ。

今回はこのキャッシュされた点の位置を利用して、そこにチェーンを配置するようにしたというわけだ。

公式オンラインドキュメント:
Curve2D


Level3 シーンにインスタンスを追加する

今回のチュートリアル用に「Level3」シーンを作成しておこう。「Level1」シーンを複製(継承ではない)して、敵キャラクターやアイテムなどの不要なノードは一旦全て削除し、TileMapも簡単なものを新しく作成しよう。

続けて、先ほど作成した「MovingPlatformtscn」をインスタンス化して追加しよう。
Level3シーンツリー

以下のように確認しやすい配置にするだけでOKだ。
Level3シーン2Dワークスペース

「MovingPlatform」のインスタンスの「Path2D」のパスを以下の手順で編集してみよう。

  1. シーンツリードックで「MovingPlatform」を右クリックし、「編集可能な子」にチェックを入れよう。
    編集可能な子にチェック
  2. そのままシーンツリードックで「MovingPlatform」の子の「Path2D」ノードを選択する。
    シーンツリードックでPath2Dを選択
  3. インスペクターで「Curve」プロパティの「Curve2D」リソースをユニーク化する。
    インスペクターでリソースをユニーク化
  4. 2D ワークスペースでツールバーのパス描画ツールを切り替えながらパスを編集してみよう。例えば「点を空きスペースに追加」ツールで点を追加し、「曲線を閉じる」ツールでパスの端と端を繋いで、「コントロールポイントを選ぶ」ツールで既存の点をドラッグして滑らかなカーブにしてみよう。
    2Dワークスペースでパスを編集

最後にシーンを実行して動作確認をしておこう。
シーンを実行

床の動きが速すぎる場合は、「MovingPlatform」の子「AnimationPlayer」ノードを選択し、インスペクターで「Playback Options」>「Speed」プロパティを編集する。遅くしたい場合は値を 1 より小さくする。
AnimationPlayerノードのPlayback Options>Speedプロパティ


応用: 回転ノコギリ

動く床のロジックを応用して「回転ノコギリ」のトラップを作ることができる。

「Path2D」のパスを辿る回転するノコギリで、当たるとダメージを受ける、というトラップだ。

「AnimatedSprite」の「Frames」プロパティに適用する「SpriteFrames」リソースのアニメーション用アセットは「res://Assets/Traps/Saw/」にまとまっている。

「KinematicBody2D」の代わりに「Area2D」を使用し、そのシグナル「body_entered」をスクリプトに接続して、プレイヤーキャラクターがコリジョン形状に入ったらダメージを与えるように制御すればOKだ。

今回のチュートリアルでは、ボリュームの都合で詳細な解説は割愛させていただく。



落ちる床

落ちる床は、プロペラで空中に浮かんでおり、プレイヤーキャラクターが飛び乗ると、そのあとすぐにグラグラして落下するという仕掛けだ。飛び乗ったあとすぐに離れないと、画面下に落下してゲームオーバーになってしまう、というのがよくある使い方だ。


シーンを作成する

  1. ルートノードに「KinematicBody2D」クラスを指定してシーンを新規作成する。このクラスを選択する理由は、スクリプトのコードにより重力を加えて落下させるためだ。
  2. ルートノードの名前は「FallingPlatform」に変更する。
  3. ファイルパスを「res://Traps/FallingPlatform/FallingPlatform.tscn」としてシーンを保存する。
  4. 以下のスクリーンショットのようなシーンツリーになるように必要なノードを追加しよう。
    FallingPlatformシーンツリー
    なお、以下のノードはどちらも「CollisionShape2D」クラスのため、名前を変更している。
    • 「BodyCollision」ノード
    • 「AreaCollision」ノード

ノードのプロパティを編集する

AnimatedSprite ノード

このノードを編集して落ちる床のルートノードの見た目を作る。

  1. インスペクターで「Frames」プロパティに「新規 SpriteFrames」リソースを適用する。
  2. スプライトフレームパネルで以下のアニメーションを作成する
  • アニメーション名: Off
    • 速度: 1 FPS
    • ループ: オフ
    • アニメーションフレームリソース: res://Assets/Traps/Falling Platforms/Off.png
      SpriteFramesのoffアニメーション
  • アニメーション名: on
    • 速度: 16 FPS
    • ループ: オン
    • アニメーションフレームリソース: res://Assets/Traps/Falling Platforms/On (32x10).png
      SpriteFramesのonアニメーション
  1. インスペクターに戻って、「Animation」プロパティの値として「on」を選択する。
  2. 続けて「Playing」プロパティをオンにする。

BodyCollision ノード

このノードを編集して、ルートノードのコリジョン形状を設定する。ルートノードは「KinematicBody2D」(つまり物理ボディ)なので、コリジョン形状を作れば、プレイヤーキャラクターがそこに乗ることができるようになる。

  1. インスペクターで「Shape」プロパティに「新規 RectangleShape2D」リソースを適用する。
  2. 2D ワークスペース上でコリジョン形状を、「SpriteFrames」のテクスチャの床部分に合わせて調整する。
    BodyCollisionのRectangleShape2Dのコリジョン形状設定

「RectangleShape2D」リソースの「Extents」プロパティは (16, 2.5) になったはずだ。


AreaCollision ノード

このノードは、プレイヤーキャラクターが落ちる床に乗ったことを検知するために、親ノードの「Area2D」にコリジョン形状をを提供する。プレイヤーキャラクターの足が床に接したことを検知したいので、床のすぐ上に 1 px 程度の浅いコリジョン形状を配置する必要がある。なお、作業手順は先の「BodyCollision」ノードと同様だ。

  1. インスペクターで「Shape」プロパティに「新規 RectangleShape2D」リソースを適用する。
  2. 2D ワークスペース上でコリジョン形状を、床部分の上に乗せるような形で調整する。
    AreaCollisionのRectangleShape2Dのコリジョン形状設定

「RectangleShape2D」リソースの「Extents」プロパティは (15, 1) になったはずだ。


AnimationPlayer ノード

このノードのアニメーションで、プレイヤーキャラクターが乗った直後に落ちる床が左右にグラグラ震える演出を追加する。のちほど、このアニメーションが再生された後に床が落下するようにスクリプトで制御していく。

  1. アニメーションパネルを開き、以下の内容で新規アニメーションを作成する。
  • アニメーション名: shaking
  • 読み込み後、自動再生: オフ
  • アニメーションの長さ(秒): 0.4
  • アニメーションループ: オフ
  • トラック:
    • AnimatedSprite ノード > offset プロパティ
      • Time: 0 / Value: (-1, 0) / Easing: 1.00
      • Time: 0.05 / Value: (1, 0) / Easing: 1.00
      • Time: 0.1 / Value: (-1, 0) / Easing: 1.00
      • Time: 0.15 / Value: (1, 0) / Easing: 1.00
      • Time: 0.2 / Value: (-1, 0) / Easing: 1.00
      • Time: 0.25 / Value: (1, 0) / Easing: 1.00
      • Time: 0.3 / Value: (-1, 0) / Easing: 1.00
      • Time: 0.35 / Value: (1, 0) / Easing: 1.00
      • Time: 0.4 / Value: (-1, 0) / Easing: 1.00

shakingアニメーション


以下のGIF画像ように、一瞬震えるアニメーションになればOKだ。
shakingアニメーションGIF


Timer ノード

このタイマーは、落下してから一定時間経過後に落ちる床のインスタンスを解放するために利用する。

  1. インスペクターで「Wait Time」プロパティの値はデフォルトの 1 秒のままにしておく。この時間が落下開始してから解放するまでの時間になる。
  2. 「One Shot」プロパティをオンにする。タイマーは一回しか使わない。
  3. 「Autostart」プロパティはオフのままだ。のちほどスクリプトでプレイヤーが床に乗ってからスタートするよう調整する。

スクリプトをアタッチして編集する

ここからは、プレイヤーキャラクターが乗ったら落ちる床が本当に落ちるようにプログラミングしていく。ルートノード「FallingPlatform」にスクリプトをアタッチしてほしい。ファイルパスを「res://Traps/FallingPlatform/FallingPlatform.gd」として作成しよう。

アタッチできたら「FallingPlatform.gd」スクリプトを以下のように編集しよう。

extends KinematicBody2D

# 重力のプロパティ
var gravity = 512
# 速度のプロパティ、初期値は(0, 0)
var velocity = Vector2()
# AnimationPlayer ノードの参照
onready var anim_player = $AnimationPlayer
# Timer ノードの参照
onready var timer = $Timer


func _ready():
	# シーンが読み込まれたら物理プロセスを停止しておく
	set_physics_process(false)


func _physics_process(delta):
	# 速度のy軸方向の値に重力を加算する
	velocity.y += gravity * delta
	# 速度に合わせて KinematicBody2D のルートノードを動かす(落下させる)
	move_and_slide(velocity, Vector2.UP)

少し解説しておくと、まず_readyメソッドにより、set_physics_process(false)が呼ばれるので、シーンが読み込まれる際に物理プロセスは停止状態になる。

そのため、すぐ下の_physics_processメソッドで、落ちる床が重力により落下するように定義しているが、これは物理プロセスが次に再開された時に初めて実行されることになる。


次に「Area2D」ノードのシグナル「body_entered」をスクリプトに接続する。プレイヤーキャラクターが落ちる床に乗ったら発信されるシグナルだ。接続後、追加された_on_Area2D_body_enteredメソッドを以下のように編集しよう。

# 物理ボディが Area2D のコリジョン形状に入った時に発信されるシグナルで呼ばれるメソッド
func _on_Area2D_body_entered(body):
	# プレイヤーキャラクターが床に乗ったら
	if body.name == "Player":
		# AnimationPlayer ノードの shaking アニメーションを再生する
		anim_player.play("shaking")

これで落ちる床に乗ったら、「AnimationPlayer」の「shaking」アニメーションが再生されるようになった。さらに「AnimationPlayer」ノードのシグナル「animation_finished」をスクリプトに接続しよう。追加された_on_AnimationPlayer_animation_finishedメソッドを以下のように編集してほしい。

func _on_AnimationPlayer_animation_finished(anim_name):
	# 終了したアニメーションが shaking だったら
	if anim_name == "shaking":
		# 物理プロセスを再開する(自動的に重力で床が落下する)
		set_physics_process(true)
		# タイマーをスタートする
		timer.start()

これでアニメーション「shaking」が終了した直後、物理プロセスが再開され、重力により自動的に床が落下する。


最後に「Timer」ノードのシグナル「timeout」をスクリプトに接続して、_on_Timer_timeoutメソッドを以下のように編集しよう。

func _on_Timer_timeout():
	# FallingPlatform ノードを解放する
	queue_free()

これで完全にゲームの世界から落ちる床が消える。以上でスクリプトの編集は完了だ。


Level3 シーンにインスタンスを追加する

作成した「FallingPlatform.tscn」のインスタンスを「Level3」シーンに追加し、シーンを実行して動作を確認してみよう。
FallingPlatformインスタンス追加後Level3シーンを実行

問題なさそうだ。これを実際にレベルデザインする際に下に地面がないところに配置するとよりスリリングで効果的だ。



高く飛べる床

次は「高く飛べる床」を作る。トランポリンやバネのような仕掛けだ。タイルマップで、これを利用しないと辿り着けないような高い位置に地面を作ると効果的だ。


シーンを作成する

  1. 「StaticBody2D」クラスをルートノードにしてシーンを新規作成する。
  2. ルートノードの名前を「Spring」に変更する。
  3. ファイルパスを「res://Traps/Spring/Spring.tscn」としてシーンを保存する。
  4. 以下のスクリーンショットのようなシーンツリーになるように必要なノードを追加する。
    Springシーンツリー
    なお、以下のノードは同じ「CollisionShape2D」クラスのためノードの名前を変更している。
    • 「BodyCollision」ノード
    • 「AreaCollision」ノード

ノードのプロパティを編集する

AnimatedSprite ノード

ルートノードの「Spring」に見た目を提供する。

  1. インスペクターで「Frames」プロパティに「新規 SpriteFrames」リソースを適用する。
  2. スプライトフレームパネルで以下の2つのアニメーションを作成する。
  • アニメーション名: idle
    • 速度: 1 FPS
    • ループ: オフ
    • アニメーションフレームリソース: res://Assets/Traps/Trampoline/Idle.png
      idleアニメーション
  • アニメーション名: jump
    • 速度: 8 FPS
    • ループ: オフ
    • アニメーションフレームリソース: res://Assets/Traps/Trampoline/Jump (28x28).png
      jumpアニメーション
  1. インスペクターに戻って「Animation」プロパティを「idle」に設定する。
  2. 「Playing」プロパティをオンに設定する。

BodyCollision ノード

ルートノードにコリジョン形状を適用する。「AnimatedSprite」のテクスチャ画像の赤い台の位置に合わせてコリジョン形状を設定し、プレイヤーキャラクターがそこに乗れるようにする。

  1. インスペクターで「Shape」プロパティに「新規 RectangleShape2D」リソースを適用する。
  2. 2D ワークスペースで「AnimatedSprite」の「idle」のアニメーションフレームの赤い台の形に合わせてコリジョン形状を調整する。ピッタリ形を合わせた場合は「RectangleShape2D」>「Extents」プロパティは (11.5, 2.5) になる。
    jumpアニメーション

AreaCollision ノード

このノードは、プレイヤーキャラクターが床に乗ったかどうかを検知するための親ノード「Area2D」にコリジョン形状を適用する。プレイヤーキャラクターの足を検出するため、赤い台のすぐ上に浅く配置するのがポイントだ。

  1. インスペクターで「Shape」プロパティに「新規 RectangleShape2D」リソースを適用する。
  2. 2D ワークスペースで、「AnimatedSprite」の「idle」のアニメーションフレームの赤い台のすぐ上の部分に浅くコリジョン形状を配置する。「RectangleShape2D」>「Extents」プロパティは (10.5, 1.5) になる。
    jumpアニメーション

スクリプトをアタッチして編集する

ルートノード「Spring」にスクリプトをアタッチする。ファイルパスを「res://Traps/Spring/Spring.gd」として作成する。

スクリプトがアタッチできたら、例によってまずはプロパティから定義する。

extends StaticBody2D

# Player ノードの参照用(最初は名前のみ定義)
var player
# AnimatedSprite ノードの参照
onready var sprite = $AnimatedSprite

さてここで、プレイヤーキャラクターが「Area2D」ノードのコリジョン形状に入ったタイミング発信されるシグナルを利用したいので、「Area2D」ノードの「body_entered」シグナルを「Spring.gd」スクリプトに接続しておこう。

スクリプトに_on_Area2D_body_enteredメソッドが追加されたら以下のように編集する。

func _on_Area2D_body_entered(body):
	# AnimatedSpriteのアニメーションのフレームを 0 にする
	sprite.frame = 0
	# Player ノードを検知したら
	if body.name == "Player":
		# デバッグ用
		print("Player entered Spring area")
		# プロパティ player に body(Playerノード)を代入
		player = body
		# AnimatedSpriteの jump アニメーションを再生する
		sprite.play("jump")

これで、プレイヤーキャラクターが高く飛ぶ床に乗ったら、床がアニメーションするようになった。ちなみにメソッドの外で定義されているplayerプロパティにbodyを代入したのは、このメソッド以外で「Player」ノードを制御したいからだ。

では、肝心のプレイヤーを高く飛ばすためのコードを作っていこう。床のアニメーションの特定のタイミングに合わせてプレイヤーキャラクターを高く飛ばしたい。そこで「AnimatedSprite」ノードのシグナル「frame_changed」をスクリプトに接続して利用する。

このシグナルはアニメーションのフレームが変わるたびにシグナルが発信される。アニメーション「jump」のフレームが 1 の時に一番床が高くなるので、それを条件にif構文を使って、このフレームが 1 のタイミングでプレイヤーを高く飛ばそう。ではシグナルを接続して追加された_on_AnimatedSprite_frame_changedメソッドを以下のように編集しよう。

func _on_AnimatedSprite_frame_changed():
	# AnimatedSprite のアニメーションが jump でかつフレームが 1 でかつ player プロパティが空っぽでない場合
	if sprite.animation == "jump" and sprite.frame == 1 and player != null:
		# Player ノードの y 軸方向の速度をプレイヤーキャラクターのジャンプ力の2倍にする
		player.velocity.y = - player.jump_force * 2
		# Player ノードのダブルジャンプを可能にする
		player.can_double_jump = true

Level3 シーンにインスタンスを追加する

作成した「Spring.tscn」のインスタンスを「Level3」シーンに追加し、シーンを実行して動作を確認してみよう。
Springシーンのインスタンス追加後Level3シーンを実行

十分高く飛ばすことができた。高さを調整したい時はplayer.velocity.y = - player.jump_force * 2の最後の 2 を別の数値に変更して調整いただければと思う。これを使って、高い足場に移動するようなレベルのデザインをすると効果的だ。



火が出る装置

踏むと足元から火が出る装置を作る。例えば、この装置を足元にずらりと並べて、どこに着地しても火が出るようなスリリングなステージを構成することができる。


シーンを作成する

  1. 「StaticBody2D」クラスをルートノードにしてシーンを新規作成する。
  2. ルートノードの名前は「FirePod」に変更する。
  3. ファイルパスを「res://Traps/FirePod/FirePod.gd」としてシーンを保存する。
  4. 以下のスクリーンショットのようなシーンツリーになるように必要なノードを追加する(ほとんど落ちる床や高く飛べる床と同じ構成だ)。
    FirePodシーンツリー
    なお、以下のノードは同じ「CollisionShape2D」クラスのため名前を変更している。
    • 「BodyCollision」ノード
    • 「AreaCollision」ノード

ノードのプロパティを編集する

AnimatedSprite ノード

ルートノードの見た目を作る。

  1. インスペクターで「Frames」プロパティに「新規 SpriteFrames」リソースを適用する。
  2. スプライトフレームパネルで以下のアニメーションを作成する。
  • アニメーション名: hit
    • 速度: 8 FPS
    • ループ: オフ
    • アニメーションフレームリソース: res://Assets/Traps/Fire/Hit (16x32).png
      hitアニメーション
  • アニメーション名: off
    • 速度: 1 FPS
    • ループ: オフ
    • アニメーションフレームリソース: res://Assets/Traps/Fire/Off.png
      offアニメーション
  • アニメーション名: on
    • 速度: 8 FPS
    • ループ: オン
    • アニメーションフレームリソース: res://Assets/Traps/Fire/On (16x32).png
      onアニメーション
  1. インスペクターに戻って「Animation」プロパティで「off」を選択する。
  2. 「Playing」プロパティをオンにする。

BodyCollision ノード

ルートノードにコリジョン形状を適用する。

  1. インスペクターで「Shape」プロパティに「新規 RectangleShape2D」リソースを適用する。
  2. 2D ワークスペースで、「AnimatedSprite」の「off」のアニメーションフレームの装置のサイズに合わせてコリジョン形状を調整する。ピッタリ形を合わせた場合は「RectangleShape2D」>「Extents」プロパティは (8, 8) になる。
    jumpアニメーション

AreaCollision ノード

このノードは、以下の2つを検知するために親ノードの「Area2D」にコリジョン形状を適用する。

  • プレイヤーキャラクターが装置の上に乗ったこと
  • 火が出た時にプレイヤーキャラクターが火に当たったこと

いずれもプレイヤーキャラクターの足を検出する必要があるため、装置のすぐ上にコリジョン形状を配置する。

  1. インスペクターで「Shape」プロパティに「新規 RectangleShape2D」リソースを適用する。
  2. 2D ワークスペースで、「AnimatedSprite」の「off」アニメーションフレームの装置のすぐ上の部分にコリジョン形状を配置する。「RectangleShape2D」>「Extents」プロパティは (5, 3) になる。
    jumpアニメーション

Timer ノード

火が出る装置の火が出ている時間はこのノードのタイマーで管理する。

  1. インスペクターで「Wait Time」プロパティはデフォルトの 1 のままにしておく。火が出るのは 1 秒くらいがちょうど良いだろう。
  2. 「One Shot」プロパティをオンにしよう。
  3. 「Autostart」プロパティはデフォルトのオフのままにしておく。プレイヤーキャラクターが装置に乗ったことを検知してから火を出したいので、スクリプトでタイマーをスタートさせるようのちほどコーディングする。

スクリプトをアタッチして編集する

ルートノード「FirePod」にスクリプトをアタッチしよう。この時、ファイルパスを「res://Traps/FirePod/FirePod.gd」として作成する。

なお、火が出る装置には以下の基本動作を繰り返すように制御しつつ、「on」アニメーション時に装置の上にプレイヤーキャラクターが乗っていたら(火に当たったら)ダメージを与えるように制御していく。

  1. プレイヤーキャラクターが乗った時に「Area2D」のシグナル発信
  2. シグナルをトリガーに「off」アニメーションを「hit」アニメーションに切り替え
  3. 「hit」アニメーションが終了した時に「AnimatedSprite」のシグナル発信
  4. シグナルをトリガーに「on」アニメーションに切り替え
  5. 「on」アニメーション開始時に「Timer」のタイマー開始
  6. タイムアウトした時に「Timer」のシグナル発信
  7. シグナルをトリガーに「off」アニメーションに戻る

アタッチできたら、さっそく「FirePod.gd」スクリプトを編集していこう。まずはプロパティから定義する。

extends StaticBody2D

# プレイヤーキャラクターが受けるダメージ
var damage: float = 20.0
# AnimatedSprite ノードの参照
onready var sprite = $AnimatedSprite
# AreaCollision ノードの参照
onready var area_collision = $Area2D/AreaCollision
# Timer ノードの参照
onready var timer = $Timer

続いて「Area2D」のシグナル「body_entered」を「FirePod.gd」スクリプトに接続しよう。

接続できたら、追加された_on_Area2D_body_enteredメソッドを編集する。

# Area2D ノードが物理ボディを検知した時に発信されるシグナルで呼ばれるメソッド
func _on_Area2D_body_entered(body):
	# プレイヤーキャラクターが乗ったら
	if body.name == "Player":
		# AnimatedSprite ノードのアニメーションが現在「off」の場合
		if sprite.animation == "off":
			# デバッグ用
			print("Player entered when FirePod off.")
			# アニメーション「hit」を再生する
			sprite.play("hit")
			# AreaCollision ノードの Disabled プロパティをオン(衝突判定を無効)にする
			area_collision.set_deferred("disabled", true)
		# AnimatedSprite ノードのアニメーションが現在「on」の場合
		elif sprite.animation == "on":
			# デバッグ用
			print("Player entered when FirePod on.")
			# Player ノードの「enemy_hit」シグナルを引数にダメージを渡して発信
			body.emit_signal("enemy_hit", damage)
			# Player ノードの AnimatedSprite ノードのアニメーション「hit」を再生する
			body.sprite.play("hit")
			# Player ノードの DamageSFX ノードのサウンド(ダメージの効果音)を再生する
			body.damage_sfx.play()

次は「AnimatedSprite」ノードのアニメーションが終了した時に発信されるシグナル「animation_finished」をスクリプトに接続しよう。追加されたメソッド_on_AnimatedSprite_animation_finishedを以下のように編集する。

func _on_AnimatedSprite_animation_finished():
	# AnimatedSprite ノードのアニメーションが現在「hit」の場合
	if sprite.animation == "hit":
		# Timer ノードのタイマーをスタートさせる
		timer.start()
		# AnimatedSprite ノードのアニメーション「on」を再生する
		sprite.play("on")
		# AreaCollision ノードの Disabled プロパティをオフ(衝突判定を有効)に戻す
		area_collision.set_deferred("disabled", false)

最後に「Timer」ノードのタイマーがタイムアウトした時に発信されるシグナル「timeout」をスクリプトに接続しよう。追加されたメソッド_on_Timer_timeoutを以下のように編集する。

func _on_Timer_timeout():
	# AnimatedSprite ノードのアニメーションが現在「on」の場合
	if sprite.animation == "on":
		# AnimatedSprite ノードのアニメーション「off」を再生する
		sprite.play("off")

以上で「FirePod.gd」スクリプトのコーディングは完了だ。


Level3 シーンにインスタンスを追加する

作成した「FirePod.tscn」のインスタンスを「Level3」シーンに追加しよう。今回は5、6個のインスタンスをずらりと並べてその上にアイテムボックスを置いてみた。ダメージ判定も確認したいのでプロジェクトを実行して動作を確認してみよう。
Level3にFirePodインスタンス追加後プロジェクトを実行

概ね想定通りの挙動だ。実際にレベルデザインでも、この火が出る装置を連続的に複数並べると効果的だ。走り抜けたら何も怖くない装置だが、途中にアイテムボックスの誘惑を用意しておくと、このトラップの効果が発揮されるだろう。


応用: スパイク

もっと簡単なトラップとして、プラットフォーマーでよく用いられるのが、刺さると痛いスパイク(トゲ)だ。アニメーションも必要もないので、今回の火の当たり判定の部分を応用すれば簡単にシーンを作成できるだろう。

シーンツリーは、ルートノードを「Area2D」にして、それに「Sprite」と「CollisionShape2D」を追加するだけで済むはずだ。「Sprite」ノードの「Texture」プロパティには「res://Assets/Traps/Spikes/Idle.png」のリソースファイルを適用すると良いだろう。

詳細な説明は、このチュートリアルのボリュームの関係で割愛させていただく。



飛んでくる鉄球

今回のチュートリアルではこれが最後の仕掛けになる。難易度も一番高いので頑張ろう。レベル上の任意の場所にチェーンで繋がれたトゲトゲの鉄球だ。仕様としては以下の通りだ。

  1. 鉄球にプレイヤーキャラクターが一定距離まで近づくとプレイヤーキャラクターを検知する
  2. 検知した時点のプレイヤーキャラクターの位置を目掛けて鉄球が飛んでくる(追尾はしない)
  3. 鉄球はチェーンに繋がれているので一定の距離までしか飛んでこない
  4. チェーンが伸びきったら自動的に元の位置に戻る
  5. 元の位置に戻った時に 1 の動作を再開する
  6. 鉄球に当たるとプレイヤーキャラクターはダメージを受ける

シーンを作成する

  1. 「Node2D」クラスをルートノードにしてシーンを新規作成する。
  2. ルートノードの名前を「SpikedBall」に変更する。
  3. ファイルパスを「res://Traps/SpikedBall/SpikedBall.tscn」としてシーンを保存する。
  4. 以下のスクリーンショットのようなシーンツリーになるように必要なノードを追加していく。
    SpikedBallシーンツリー
    なお、名前を変更しているノードとそのクラスは以下の通りだ。
    • 「BallArea」ノード: 「Area2D」クラス
    • 「BallCollision」ノード: 「CollisionShape2D」クラス
    • 「DetectionArea」ノード: 「Area2D」クラス
    • 「DetectionCollision」ノード: 「CollisionShape2D」クラス

シーンツリーはここまでに作ってきた仕掛けの要素を色々まとめたような構造になっている。動く床と同様に「Path2D」と「PathFollow2D」を利用して鉄球を移動させている。動く床では「AnimationPlayer」を利用したが、鉄球では「Tween」を利用する。また、「Area2D」ブランチは、プレイヤーキャラクターを検知する範囲を設定するためのノードで、仕組みとしては、落ちる床や火が出る装置と同様だ。


ノードのプロパティを編集する


Path2D ノード

動く床の時は 2D ワークスペースでパスを描いたが、今回はスクリプトでパスを描くことになるので、ここでは作業不要だ。

  1. インスペクターで「Resource」>「Local To Scene」にチェックを入れてオンにする。

Sprite ノード

このノードは鉄球のテクスチャ用だ。「BallArea」ノードが鉄球そのものだと考えるとわかりやすいだろう。

  1. インスペクターで「Texture」プロパティにリソース「res://Assets/Traps/Spiked Ball/Spiked Ball.png」を適用する。
  2. 「Offset」の値を (-0.5, -0.5) とする。ドット絵の都合でテクスチャが 1 px だけ右下に寄っているため、中央に配置するよう補正した形だ。

BallCollision ノード

このノードは鉄球である「BallArea」ノードにコリジョン形状を適用する。

  1. インスペクターで「Shape」プロパティに「新規 CircleShape2D」リソースを適用する。
  2. 2D ワークスペースで「Sprite」のテクスチャに合わせてコリジョン形状を調整する。トゲの範囲までコリジョン形状を広げるか、内側のボール部分のみにするかで若干ゲームの難易度は変わる。このチュートリアルでは、以下のスクリーンショットのようにボール部分のみとした。「CircleShape2D」リソースのプロパティ「Radius」の値は 10 、「Custom Solver Bias」は 0 になる。
    BallCollisionのコリジョン形状

DetectionCollision ノード

こちらの「CollisionShape2D」クラスのノードは、プレイヤーキャラクターの検知用だ。いわば鉄球トラップのテリトリーと言っても良いだろう。半径 80 px の円で形成する。

  1. インスペクターで「Shape」プロパティに「新規 CircleShape2D」リソースを適用する。
  2. こちらは 2D ワークスペースでは作業せず、インスペクターにそのまま半径の値を入力する。「CircleShape2D」リソースの「Radius」プロパティの値を 80 にする。「Custom Solver Bias」は 0 のまま。2D ワークスペースでは以下のスクリーンショットのようになったはずだ。
    BallCollisionのコリジョン形状

チェーンのシーンを作成する

鉄球はチェーンで地面に繋がれている。この長いチェーンを構成するチェーンのセグメントをシーンとして作成する。ちょうど動く床と同じパターンだ。

  1. ルートノードに「Sprite」クラスを指定して新規でシーンを作成する。
  2. ルートノードの名前は「SpikedBallChain」に変更する。
  3. 「MovingPlatformChain」ルートノードの「Texture」プロパティにリソース「res://Assets/Traps/Spiked Ball/Chain.png」を適用する。
  4. ファイルパスを「res://Traps/SpikedBall/SpikedBallChain.tscn」としてシーンを保存する。

SpikedBallChainシーン


スクリプトをアタッチして編集する

ではスクリプトをルートノードの「SpikedBall」にアタッチしよう。ファイルパスを「res://Traps/SpikedBall/SpikedBall.gd」として作成する。

ここからは大まかに以下の仕様を形にしていく。

  • 鉄球のシーンが読み込まれた時にチェーンを作成する
  • 「Area2D」ノードでプレイヤーキャラクターを検知して発信するシグナルをトリガーにして
    • 「Path2D」パスを作成する
    • 鉄球を移動させる
  • 鉄球の位置に合わせて常にチェーン全体の長さ(各チェーンセグメントの位置)を変化させる
  • プレイヤーキャラクターが鉄球に当たったらダメージを与える

では「SpikedBall.gd」をスクリプトを編集していこう。まずはプロパティの定義からだ。

extends Node2D

# チェーンの最大の長さ
var chain_length = 80
# チェーンセグメントの数
var chain_num = 10
# プレイヤーキャラクターが受けるダメージ
var damage = 20.0

# Path2D ノードの参照
onready var path = $Path2D
# PathFollow2D ノードの参照
onready var path_follow = $Path2D/PathFollow2D
# BallArea ノードの参照
onready var ball_area = $Path2D/PathFollow2D/BallArea
# DetectionArea ノードの参照
onready var detection_area = $DetectionArea
# DetectionCollision ノードの参照
onready var detection_collision = $DetectionArea/DetectionCollision
# Tween ノードの参照
onready var tween = $Tween
# チェーンのシーンファイルのプリロードの参照
onready var chain_scn = preload("res://Traps/SpikedBall/SpikedBallChain.tscn")
# チェーンセグメントのコンテナとして利用する配列
onready var chain_container = []

次に鉄球シーンが読み込まれる際にチェーンを作成するコードを記述する。

# シーン読み込み時に呼ばれるメソッド
func _ready():
	# チェーンを作成するメソッドを呼ぶ
	make_chain()

# チェーンを作成するメソッドを定義
func make_chain():
	# チェーンのセグメントの数(10)だけループ処理
	for i in range(chain_num):
		# チェーンのシーンをインスタンス化
		var chain = chain_scn.instance()
		# チェーンのインスタンスを子ノードとして SpikedBall ルートノードに追加
		add_child(chain)
		# 子に追加したインスタンスノードを 0 番目に移動
		move_child(chain, 0)
		# インスタンスノードを背面に移動
		chain.z_index = -1
		# チェーンセグメントのコンテナ用配列に追加
		chain_container.append(chain)

次に_processメソッドを利用して毎フレーム、チェーンの長さ(チェーンセグメントの個々の位置)を更新するようにする。

# Tips:引数 delta を使用しない時は _delta とするとアラートが出ない
func _process(_delta):
	# チェーンを再配置するメソッドを呼ぶ
	relocate_chain()

# チェーンを再配置するメソッドを定義する
func relocate_chain():
	# ルートノードの位置から現在の鉄球の位置に対する方向を取得
	var direction = global_position.direction_to(ball_area.global_position)
	# ルートノードの位置から現在の鉄球の位置までの距離をチェーンセグメントの数で割って...
	# ...セグメント1つ分の長さを取得する
	var segment_length = global_position.distance_to(ball_area.global_position) / chain_num
	# チェーンセグメントの数(10)だけループ処理
	for i in range(chain_num - 1):
		# コンテナから順番に取り出したチェーンセグメントの位置を次の計算式で指定する...
		# ... 方向 × セグメントの長さ × コンテナの順番
		chain_container[i].position = direction * segment_length * i

次に「DetectionArea」ノードのコリジョン形状の範囲にプレイヤーキャラクターが入った時の挙動を作っていく。まず「Area2D」がプレイヤーキャラクターを検知した時に発信されるシグナル「body_entered」をスクリプトに接続しよう。

シグナルが接続できたら、追加されたメソッド_on_DetectionArea_body_enteredと、その中で呼ばれる2つのメソッドを記述していく。その2つのメソッドのうち、1つは「Path2D」ノードに2つの点を打って直線のパスを作成するメソッド。もう一つは「Path2D」ノードで作成されたパスの経路を辿って鉄球が移動するアニメーションを制御するメソッドだ。

# DetectionArea ノードのコリジョン形状に物理ボディが入った時に発信されるシグナルで呼ばれるメソッド
func _on_DetectionArea_body_entered(body):
	# デバッグ用
	print("_on_DetectionArea_body_entered called")
	# 検知した物理ボディが Player ノードだったら
	if body.name == "Player":
		# Path2Dのパスを作成するメソッドを呼ぶ
		set_path_points(body)
		# 鉄球をパスに沿って動かすメソッドを呼ぶ
		move_ball()
		# DetectionCollision ノードの衝突判定を無効化する
		detection_collision.set_deferred("disabled", true)

# Path2Dのパスを作成するメソッドを定義
func set_path_points(body):
	# デバッグ用
	print("set_path_points called")
	# 一度 Path2D のパスの点を全て消去してリセットする
	path.curve.clear_points()
	# まず鉄球のデフォルトの位置である原点(0, 0)にパスの点を追加する
	path.curve.add_point(Vector2.ZERO)
	# 鉄球の現在の位置から衝突した物理ボディ(「Player」ノードのこと)に対する方向を取得
	var direction = global_position.direction_to(body.global_position)
	# 鉄球から物理ボディへの方向にチェーンの最大長を乗算した位置を鉄球がこれから目指す位置とする
	var directed_point = direction * chain_length
	# 鉄球が目指す位置を Path2D ノードのパスの2つ目の点として追加する
	path.curve.add_point(directed_point)

# 鉄球をパスに沿って移動させるメソッドを定義
func move_ball():
	# デバッグ用
	print("move_ball called")
	# PathFollow2D の Unit Offset プロパティを 0 から 1 へ 0.5 秒かけて変化させる Tween の設定
	tween.interpolate_property(path_follow, "unit_offset", 0, 1, 0.5)
	# Tween ノードのアニメーションを開始する
	tween.start()
	# Tween ノードの tween_completed を利用してアニメーション(移動)が終わるのを待つ
	yield(tween,"tween_completed")
	# PathFollow2D の Unit Offset プロパティを 1 から 0 へ 1 秒かけて変化させる Tween の設定
	tween.interpolate_property(path_follow, "unit_offset", 1, 0, 1)
  # Tween ノードのアニメーションを開始する
	tween.start()
  # アニメーション(移動)が終わるのを待つ
	yield(tween,"tween_completed")
  # DetectionCollision ノードの衝突判定を有効化する
	detection_collision.set_deferred("disabled", false)

改めて解説すると、メソッドset_path_pointsは、「Path2D」ノードに原点 (0, 0) とプレイヤーキャラクターを検知した位置の2つの点を追加して直線のパスを作る。次にメソッドmove_ballで、そのパスの両端を「Tween」ノードを利用して鉄球が行って戻ってくる動作を処理している。

最後に鉄球にプレイヤーキャラクターが当たった時のダメージ処理をコーディングする。まず先に、鉄球「BallArea」ノードのコリジョン形状にプレイヤーキャラクターが入った時に発信されるシグナル「body_entered」をスクリプトに接続しよう。

接続できたら、追加された_on_BallArea_body_enteredメソッドを以下のように編集する。

# BodyArea に物理ボディが入った時に呼ばれるメソッド
func _on_BallArea_body_entered(body):
	# 物理ボディが Player ノードだったら
	if body.name == "Player":
		# Player ノードのシグナル「enemy_hit」を引数にダメージを渡して発信
		body.emit_signal("enemy_hit", damage)
		# Player シーンの AnimatedSprite ノードのアニメーション「hit」を再生
		body.sprite.play("hit")
		# Player シーンの DamageSFX ノードのサウンド(効果音)を再生する
		body.damage_sfx.play()

これで飛んでくる鉄球のコーディングは完了だ。


Level3 シーンにインスタンスを追加する

出来上がった飛んでくる鉄球のシーン「Spikes.tscn」のインスタンスを「Level3」シーンに追加して動作確認してみよう。今回もダメージ判定を含めて確認するため、プロジェクトを実行しよう。
飛んでくる鉄球の動作確認でプロジェクトを実行

ちょうどうまく操作すればかわせる程度のスピードで鉄球が飛んでくるように調整できた。チェーンが最大長まで伸びきったら鉄球は元の位置に戻る。飛んでくる時は0.5秒で移動するが、戻るときは1秒かかる。この辺りも丁度良い塩梅になったのではないだろうか。


Level3 のレベルデザインをする

ここまでに作った装置を全て使用して「Level3」シーンをデザインしてみよう。

「MovingPlatform」のインスタンスを追加した時は、そのノードを右クリックして「編集可能な子」にして「Path2D」のパスの形も色々作ってみよう。ただし、その前に「Curve」プロパティの「Curve2D」リソースをユニーク化することをお忘れなく。

また、応用編として紹介した回転ノコギリやスパイクのシーンも作成している場合は、それらのインスタンスも追加しよう。

このチュートリアルではサンプルとして以下のようなデザインにした。
飛んでくる鉄球の動作確認でプロジェクトを実行


タイルマップのデザインや今回作ったそれぞれの装置の配置をして「Level3」のデザインが完成したら、プロジェクトを実行してゲームを遊んでみよう。
Level3シーンを完成させてプロジェクトを実行



Part 15 で編集したスクリプトのコード

最後に今回の Part 15 で編集したスクリプトのコードを共有しておくので、必要に応じて確認してほしい。

.MovingPlatform.gd の全コード
extends Node2D


onready var path = $Path2D
onready var chain_scn = preload("res://Traps/MovingPlatform/MovingPlatformChain.tscn")


func _ready():
	make_chain()


func make_chain():
	var points = path.curve.get_baked_points()
	for point in points:
		var chain = chain_scn.instance()
		add_child(chain)
		move_child(chain, 0)
		chain.z_index = -1
		chain.position = point

FallingPlatform.gd の全コード
extends KinematicBody2D


var gravity = 512
var velocity = Vector2()
onready var anim_player = $AnimationPlayer
onready var timer = $Timer


func _ready():
	set_physics_process(false)


func _physics_process(delta):
	velocity.y += gravity * delta
	move_and_slide(velocity, Vector2.UP)


func _on_Area2D_body_entered(body):
	if body.name == "Player":
		anim_player.play("shaking")


func _on_AnimationPlayer_animation_finished(anim_name):
	if anim_name == "shaking":
		set_physics_process(true)
		timer.start()


func _on_Timer_timeout():
	queue_free()

Spring.gd の全コード
extends StaticBody2D

var player
onready var sprite = $AnimatedSprite


func _on_Area2D_body_entered(body):
	sprite.frame = 0
	if body.name == "Player":
		print("Player entered Spring area")
		player = body
		sprite.play("jump")


func _on_AnimatedSprite_frame_changed():
	if sprite.animation == "jump" and sprite.frame == 1 and player != null:
		player.velocity.y = - player.jump_force * 2
		player.can_double_jump = true

FirePod.gd の全コード
extends StaticBody2D


var damage: float = 20.0

onready var sprite = $AnimatedSprite
onready var area_collision = $Area2D/AreaCollision
onready var timer = $Timer


func _on_Area2D_body_entered(body):
	if body.name == "Player":
		if sprite.animation == "off":
			print("Player entered when FirePod off.")
			sprite.play("hit")
			area_collision.set_deferred("disabled", true)
		elif sprite.animation == "on":
			print("Player entered when FirePod on.")
			body.emit_signal("enemy_hit", damage)
			body.sprite.play("hit")
			body.damage_sfx.play()


func _on_AnimatedSprite_animation_finished():
	if sprite.animation == "hit":
		timer.start()
		sprite.play("on")
		area_collision.set_deferred("disabled", false)


func _on_Timer_timeout():
	if sprite.animation == "on":
		sprite.play("off")

SpikedBall.gd の全コード
extends Node2D


var chain_length = 80
var chain_num = 10
var damage = 20.0

onready var path = $Path2D
onready var path_follow = $Path2D/PathFollow2D
onready var ball_area = $Path2D/PathFollow2D/BallArea
onready var detection_area = $DetectionArea
onready var detection_collision = $DetectionArea/DetectionCollision
onready var tween = $Tween
onready var chain_scn = preload("res://Traps/SpikedBall/SpikedBallChain.tscn")
onready var chain_container = []


func _ready():
	make_chain()


func make_chain():
	for i in range(chain_num):
		var chain = chain_scn.instance()
		add_child(chain)
		move_child(chain, 0)
		chain.z_index = -1
		chain_container.append(chain)


func _process(_delta):
	relocate_chain()


func relocate_chain():
	var direction = global_position.direction_to(ball_area.global_position)
	var segment_length = global_position.distance_to(ball_area.global_position) / chain_num
	for i in range(chain_num - 1):
		chain_container[i].position = direction * segment_length * i


func _on_DetectionArea_body_entered(body):
	print("_on_DetectionArea_body_entered called")
	if body.name == "Player":
		set_path_points(body)
		move_ball()
		detection_collision.set_deferred("disabled", true)


func set_path_points(body):
	print("set_path_points called")
	path.curve.clear_points()
	path.curve.add_point(Vector2.ZERO)
	var direction = global_position.direction_to(body.global_position)
	var directed_point = direction * chain_length
	path.curve.add_point(directed_point)


func move_ball():
	print("move_ball called")
	tween.interpolate_property(path_follow, "unit_offset", 0, 1, 0.5)
	tween.start()
	yield(tween,"tween_completed")
	tween.interpolate_property(path_follow, "unit_offset", 1, 0, 1)
	tween.start()
	yield(tween,"tween_completed")
	detection_collision.set_deferred("disabled", false)


func _on_BallArea_body_entered(body):
	if body.name == "Player":
		body.emit_signal("enemy_hit", damage)
		body.sprite.play("hit")
		body.damage_sfx.play()


おわりに

以上で Part 15 は完了だ。

今回はステージ上の様々な仕掛けを作った。オーソドックスな仕掛けでも、タイルマップとの組み合わせ、敵キャラクターの組み合わせ、仕掛け同士の組み合わせによって、楽しく、そして難しいレベルデザインが可能になるだろう。プレイヤーにどうやって先に進めるのかを考える機会を与え、考えた結果、難しい場面を乗り越えられたときにある種の快感を提供することができるはずだ。

さて、今回の Part 15 にて「Godot で作るプラットフォーマー」のチュートリアルシリーズは終了だ。このシリーズを通して、プラットフォーマーのゲームを作るための基本的なところは網羅できただろう。チュートリアルを参考にしてくださった方々には感謝申し上げたい。また、このチュートリアルがこれからゲーム開発に挑戦したいと考えている方々に少しでもお役に立てたならば幸いだ。

ところで、面白いことに、自分でプラットフォーマーのゲームを作ってみたあと、改めて往年のスーパーマリオブラザーズシリーズなどをプレイしてみると、あらゆる部分でゲームが緻密に設計されているのがよくわかる。ジャンプひとつとっても絶妙のバランスだ。世界的にゲームが大ヒットする理由の一つはやはりゲームデザインなのだと思い知らされる。ゲームデザインは十分条件ではないが、必要条件であると言っても過言ではないだろう。

今後も Godot を使った別のチュートリアルシリーズや、Godot を使う上での Tips 的なブログを投稿していくので、これからも Peanuts-code.com のコンテンツをどうぞお楽しみに。