第6回目の今回は、アイテムを作る。アイテムというのは、ちょうどスーパーマリオシリーズのコインのような、当たるとポイントを獲得できてちょっと嬉しい存在だ。さらに、下から小突くとアイテムが出てくる箱、その名もアイテムボックスを作る。それぞれスクリプトで動作を制御し、最後はレベルシーンに配置するところまでやっていこう。

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



Item シーンを作る

Item シーンを作成してノードを追加する

敵キャラクターの作り方と同じく、アイテムについても、雛形となるシーンを作成して、それを継承する形で複数種類のアイテムのシーンを作っていく。

まずは雛形として「Item」シーンを作成しよう。

  1. 「シーン」メニュー>「新規シーン」を選択する
  2. 「ルートノードを生成」で「その他のノード」を選択する
  3. 「Area2D」クラスのノードを選択する
  4. 「Area2D」ルートノードの名前を「Item」に変更する
  5. 「Items」フォルダを作成して、パスを「res://Items/Item.tscn」としてシーンを保存する

続いて、ルートノードに必要な子ノードを追加していく。

  1. 「Item」ルートノードに「AnimatedSprite」ノードを追加する
  2. 「Item」ルートノードに「CollisionShape2D」ノードを追加する
  3. 「Item」ルートノードに「Label」ノードを追加する
  4. 「Item」ルートノードに「AnimationPlayer」ノードを追加する

Itemシーンのシーンツリー


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

各ノードのプロパティをインスペクターで調整していく。

AnimatedSprite の編集

  1. 「Frames」プロパティに「新規SpriteFrames」を割り当てる

  2. スプライトフレームパネルを開き、以下の手順でアニメーションを作成する

    1. 「default」アニメーションの名前を「idle」に変更する
    2. 「idle」アニメーションに、仮のアニメーションフレームとして「res://Assets/Items/Fruits/Apple.png」のスプライトシートを割り当てる(フレーム 0 ~ 16)
    3. 速度を 24 FPS にする
    4. ループをオンにしておく
      AnimatedSpriteのidleアニメーション

  3. インスペクターに戻って、「Animation」プロパティの値を「idle」にする

  4. 「Playing」プロパティをオンにする

CollisionShape2D の編集

  1. 「Shape」プロパティに「新規CapsuleShape2D」を割り当てる
  2. 2Dワークスペースでコリジョン形状をスプライトテクスチャに合わせて調整する
    • Radius: 8
    • Height: 0

CollisionShape2Dのコリジョン形状


Label の編集

  1. 「Text」プロパティの値に、デフォルトのテキストとして「100」と入力する
  2. 「Align」プロパティを「Center」にする
  3. 「Valign」プロパティを「Center」にする
  4. 「Position」プロパティを(-32, -20)にする
  5. 「Size」プロパティを (64, 14) にする

Labelノードの配置


AnimationPlayer の編集

「AnimationPlayer」は「AnimatedSprite」のようなスプライトテクスチャのアニメーション以外にも、他のあらゆるプロパティの値をアニメーションさせながら変化させることができる。

今回は、プレイヤーキャラクターがアイテムに当たった時に獲得ポイントを表示しながらアイテムが消えるアニメーションを新規作成する。以下の手順に沿って作ってみよう。

  1. アニメーションパネルを開いたら、パネル上部中央の「アニメーション」をクリックする
    アニメーションをクリック
  2. 「新規」を選択する
    新規を選択
  3. アニメーションの名前を「hit」にする
    新規を選択
  4. アニメーションの長さを「0.5」秒にする
    アニメーションの長さを0.5秒にする

ここからは(アニメーションさせるプロパティの)トラックを追加し、トラック上にいくつかキーを挿入し、キーごとに値を設定して、トラック上の値の変化によりアニメーションさせていく。キーを挿入するまでの手順は2パターンあるので、お好みの方でやってみてほしい。

● パターン 1 : アニメーションパネル上でトラックを追加してそこにキー挿入する方法

  1. アニメーションパネルの左上「トラックを追加」をクリックし、トラックの種類から「プロパティトラック」を選択する。
    トラックを追加>プロパティトラック
  2. どのノードのプロパティなのかを選択する。ここでは「AnimatedSprite」を選択する
    ノードを選択
  3. どのプロパティのトラックを追加するのかを選択する。見つかりにくい時は上の検索ボックスを利用する。ここでは「modulate」プロパティを選択して開く
    プロパティを選択
  4. アニメーションパネルに「AnimatedSprite」ノードの「modulate」プロパティのトラックが追加された。ここで上部タイムライン上の青い縦のバーを 0 秒の位置に持ってこよう。
    タイムライン上の青いバー
  5. この青いバーの位置にキーを挿入する。挿入するには「modulate」トラックの青いバーより少し横を右クリックし、「キーを挿入」メニューを選択しよう。
    キーを挿入

● パターン 2 : インスペクター上の鍵アイコンをクリックし、トラックとキーを同時に挿入する

  1. 事前にアニメーションパネルのタイムラインでキーを挿入したいタイミング(ひとまず最初は一番左の 0 秒の位置)に青い縦のバーを合わせておく
  2. シーンドックで「AnimatedSprite」を選択する
    AnimatedSpriteを選択
  3. 「AnimatedSprite」ノードを選択した場合、エディター下部で自動的にスプライトフレームパネルが開くが、アニメーションパネルに切り替える
    アニメーションパネルに切り替え
  4. インスペクターで、今回アニメーションのトラックとして追加したい「Modulate」プロパティの右側にある鍵アイコンをクリックする
    Modulateプロパティのキーアイコン
  5. 新規トラックを作成してキーを挿入するかどうかの確認のダイアログが表示されるので「作成」をクリックする
    確認のダイアログ

ここからの手順は共通。

  1. 0.5秒の位置にも「modulate」トラックにキーを挿入しよう。
  2. それぞれのキーの値を編集する。挿入したキーは四角いアイコンで表示されているので、それをクリックして選択する。
    キーを選択
  3. インスペクターで「AnimationTrackKeyEdit」の「Value」の値を編集する。
    • 0 秒: (r, g, b, a) = (255, 255, 255, 0)
    • 0.5秒: (r, g, b, a) = (255, 255, 255, 255)

キーの値を編集1

キーの値を編集2

ここまでの設定でアニメーションがどうなったのか見ていこう。アニメーションパネル左上にあるプレイバックアイコンを操作することで2Dワークスペース上で簡単に確認ができる。
プレイバックアイコン

GIF画像のリピート再生だとわかりにくいかもしれないが、「Modulate」プロパティの値の透明度を 255 から 0 に変化させることによって、フェードアウトするアニメーションになった。
アニメーションの確認


同様にして、他にもいくつかのトラックを追加し、それぞれのトラックに対して、キーを挿入してアニメーションをより複雑にしていこう。

● AnimatedSprite ノードのトラック

  • modulate プロパティ: アイテムの不透明度を MAX から 0 へ *作成済み
    • Time: 0 秒 / Value: (255, 255, 255, 255)
    • Time: 0.5 秒 / Value: (255, 255, 255, 0)
  • position プロパティ: アイテムの位置を上方向に 10 px 移動
    • Time: 0 秒 / Value: (0, 0)
    • Time: 0 秒 / Value: (0, -10)
  • scale プロパティ: アイテムの大きさを横方向に 5 倍広げて縦方向は 0 に
    • Time: / Value: (1, 1)
    • Time: / Value: (5, 0)

● Label ノードのトラック

  • modulate プロパティ: 獲得ポイントの不透明度を最初の 0.1 秒で Max(表示)、最後の 0.1 秒で 0(非表示)へ
    • Time: 0 秒 / Value: (255, 255, 255, 0)
    • Time: 0.1 秒 / Value: (255, 255, 255, 255)
    • Time: 0.4 秒 / Value: (255, 255, 255, 255)
    • Time: 0.5 秒 / Value: (255, 255, 255, 0)
  • rect_position プロパティ: 獲得ポイント表示をアイテムのやや上からさらに 20 px 上へ移動
    • Time: 0 秒 / Value: (-32, -20)
    • Time: 0.5 秒 / Value: (-32, -40)

アニメーションパネルは以下のスクリーンショットのようになったはずだ。
プレイバックアイコン

では、2Dワークスペース上でアニメーションを確認してみよう。
アニメーションの確認

だいたい意図した形になったので、アイテムのアニメーションは完成としよう。


Item シーンにスクリプトをアタッチする

それではアイテムシーンをスクリプトで制御していこう。

さっそく、シーンドックで「Item」ルートノードを選択したら、スクリプトをアタッチする。継承元を「Area2D」、ファイルパスを「res://Items/Item.gd」としてスクリプトを作成しよう。

スクリプトエディタが開いたら、いつも通り、まずはプロパティから定義していく。

extends Area2D


export var point = 100 # アイテムに当たった時の獲得ポイント
onready var sprite = $AnimatedSprite # AnimatedSprite ノードの参照
onready var label = $Label # Label ノードの参照
onready var anim_player = $AnimationPlayer # AnimationPlayer ノードの参照

ここでpointというプロパティをデフォルトの値を100として定義した。これはプレイヤーキャラクターがアイテムに当たった時に獲得できるポイントだ。exportキーワード付きなので、継承後のシーンではインスペクターで気軽にポイントを変更できる。この値はアイテムの種類によって変える予定だ。取るのが難しいアイテムほど高得点にしたい。また、今後、スコア機能を実装する時にポイントがスコアに加算されるようにもしていく予定だ。また、このpointプロパティを画面上に表示させるため、_readyメソッドで「Label」ノードの「Text」プロパティにpointの値を適用するよう、このあと設定していく。

では_readyメソッドを定義していこう。

func _ready():
	sprite.modulate = Color(1, 1, 1, 1)
	sprite.position = Vector2.ZERO
	sprite.scale = Vector2.ONE
	label.modulate = Color(1, 1, 1, 0)
	label.rect_position = Vector2(-32, -20)
	label.text = str(point)

「AnimatedSprite」ノードのmodulatepositionscaleおよび「Label」ノードのmodulaterect_positionは全て、さきほどアニメーションパネルで「hit」アニメーションに用いたプロパティだ。デバッグ作業をしていると、各プロパティがアニメーションを実行した後の値のままになってしまうので、最初に_readyメソッドで初期値を設定するようにした。

最後の「Label」ノードのtextプロパティは、インスペクターで 100 と入力しておいた「Text」プロパティとイコールだ。上で定義したpointプロパティの値は int 型なのでstr関数により string 型(文字列型)に変換して、textプロパティの値として設定している。

あとはプレイヤーキャラクターが当たったらアイテムが消える動きを作っていこう。

ここはまずシグナルを利用したいので、シーンドックに戻って「Item」ルートノードを選択し、ノードドック>「シグナル」タブから「body_entered(body)」を接続しよう。接続先はもちろん「Item.gd」スクリプトだ。
body_enteredシグナルを接続

接続できたら_on_Item_body_enteredメソッドが追加されたはずだ。このメソッドを以下のように編集する。

func _on_Item_body_entered(body):
	# もし衝突した body の名前が Player だったら
	if body.name == "Player":
		print("Player hit Item")
		# AnimationPlayer ノードの「hit」アニメーションを再生する
		anim_player.play("hit")
		# 「hit」アニメーションが終了するまで待機
		yield(anim_player, "animation_finished")
		# この「Item」ノードを開放する(消す)
		queue_free()

ここで一旦テストとして「Item」シーンのインスタンスを「Level1」シーンに追加して、動作確認しておこう。確認できたら追加した「Item」ノードは不要なので削除すること。
Itemシーンの動作確認

動作も概ね期待通りなので、雛形となる「Item.tscn」シーンはこれで完成としておこう。次はこの「Item.tscn」を継承して個々のアイテムのシーンを作っていく。



Item シーンを継承して複数種類のアイテムのシーンを作る

では次に「Item.tscn」シーンを継承して、個々のアイテムを作っていく。作業は基本的にスプライトシートを差し替えて、獲得ポイントを変更するだけだ。

作成する個々のアイテムの種類と獲得ポイントは以下のように設定する(完全に適当なので、アイテムの種類数や割り当てるポイントの大きさはご自由にしていただいて構わない)。

  • Apple: 100
  • Bananas: 300
  • Cherries: 500
  • Kiwi: 700
  • Melon: 1000
  • Orange: 1200
  • Pineapple: 1500
  • Strawberry: 2000

上述の個々のアイテムのシーンを以下の手順で一つずつ順番に作成していこう。

  1. 「シーン」メニュー>「新しい継承シーン」を選択する。
  2. 継承元に「res://Items/Item.tscn」を選択する。
  3. ルートノードの名前を「Item」から「Bananas」など個々のアイテムの名前に変更する
  4. ファイルパスを「res://Items/Bananas.tscn」などとして、シーン保存する
  5. ルートノードを選択し、インスペクターから「Point」プロパティの値を 300 など各アイテムに割り当てる予定のポイントに変更する( Apple の場合は変更なし)
    Script VariablesのPoint
  6. 「AnimatedSprite」ノードの「Frames」プロパティの値「SpriteFrames」の右横の をクリックし、「ユニーク化」を選択する *お忘れなきよう!
  7. スプライトフレームパネルを開き、「idle」アニメーションのアニメーションフレームを全て削除して、新たに「res://Assets/Items/Fruits/Bananas.png」など各アイテムのスプライトシートを割り当てる( Apple の場合は変更なし)
    AnimatedSpriteのアニメーションフレームを差し替え

必要な数だけ個々のアイテムのシーンを作成していただけただろうか。次は「Level1」シーンに作成したアイテムのインスタンスノードを追加して、マップに配置していこう。



Level1 シーンにアイテムを追加する

シーンドックで「Level1」シーンを開こう。そして、作成した個々のアイテムシーンをインスタンス化して、「Level1」ルートノードの子ノードとして追加しよう。

ここではサンプルとして合計8種類のフルーツアイテムを一つずつ追加した。確認しやすくするため、一時的に敵キャラクターのノードは全て非表示にした。
Level1シーンにアイテムシーン追加

シーンドックで各アイテムを追加したら(もしくはしながら)、2Dワークスペース上で、タイルマップのどの位置にアイテムを置いたら面白いかを検討しつつ、一つずつ配置していこう。サンプルの配置場所は以下のとおりだ。
2Dワークスペースでアイテムノードを配置

ではプロジェクトを実行して、プレイヤーキャラクターがアイテムに当たった時の挙動を確認しつつ、実際のプレイ画面上でも念のためにアイテムの配置を確認しておこう。なお、以下のGIF画像は 2 倍速にしている。
2Dワークスペースでアイテムノードを配置

各アイテムの「AnimatedSprite」の「idle」アニメーションがループ再生されており、プレイヤーキャラクターがアイテムに当たったら「AnimationPlayer」のアニメーションを実行してオブジェクトが削除された。概ね問題ないだろう。



ItemBox シーンを作る

次は下から小突くとフルーツアイテムが順番に出てくるアイテムボックスを作ってみよう。スーパーマリオシリーズの下から小突くとコインが繰り返し出てくるブロックのようなイメージだ。アイテムボックスの仕様は以下のようにする。

  1. 衝突判定があり、プレイヤーキャラクターや敵キャラクターはボックスを透過しない
  2. 下からプレイヤーキャラクターが衝突した時しかアイテムは出ない
  3. 最初に下から衝突してから 4 秒経過すると、アイテムボックスは空っぽになる
  4. 最後の一つのアイテムが出ると、アイテムボックスは壊れて無くなる。

ItemBox シーンを作成してノードを追加する

では以下の手順でシーンを作ろう。

  1. 「シーン」メニュー>「新規シーン」を選択
  2. 「ルートノードを生成」で「その他のノード」を選択する
  3. 「StaticBody2D」クラスをルートノードに設定する
  4. ルートノードの名前を「ItemBox」に変更する
  5. ファイルパスを「res://Items/ItemBox.tscn」として保存する

なお、自ら動くことのない物理ボディには「StaticBody2D」が最適だ。


続いて、上述の仕様を満たすために必要なノードを追加していく。
  1. 「ItemBox」ルートノードに「AnimatedSprite」ノードを追加する
  2. 「ItemBOx」ルートノードに「CollisionShape2D」ノードを追加する
  3. 「ItemBox」ルートノードに「Timer」ノードを追加する
  4. 「ItemBox」ルートノードに「Area2D」ノードを追加する
  5. 「Area2D」ノードに「CollisionShape2D」を追加する

シーンドックは以下のようになっただろうか。

ItemBoxのシーンドック

次はインスペクターでそれぞれのノードのプロパティを編集していく。


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

AnimatedSprite の編集

アセットにアニメーション用のスプライトシートが用意してあるので、それらを利用してアニメーションを設定する。

  1. インスペクターで「Frames」プロパティに「新規SpriteFrames」を割り当てる

  2. スプライトフレームパネルを開き、以下の2つのアニメーションを作成する

    • アニメーション名: hit
      • スプライトシート: res://Assets/Items/Boxes/Box2/Hit (28x24).png
      • 速度: 24 FPS
      • ループ: オフ

    hitアニメーション

    • アニメーション名: idle
      • スプライトシート: res://Assets/Items/Boxes/Box2/Idle.png
      • 速度: 24 FPS
      • ループ: オン

    idleアニメーション

  3. インスペクターに戻り「Animation」プロパティを「idle」に設定する


ルートノード直下の CollisionShape2D の編集

プレイヤー / 敵キャラクターとの衝突を検知するようにコリジョン形状を調整していく。

  1. インスペクターで「Shape」プロパティに「新規RectangleShape2D」を割り当てる
  2. 2Dワークスペースでコリジョン形状を編集し、「idle」アニメーションのスプライトテクスチャの一番下を 1 px だけ空けて、他はスプライトテクスチャにピッタリ合わせる
    コリジョン形状の編集
  3. インスペクター関連プロパティが以下のようになっているか確認する
  • Extents: (10, 9.5)
  • Position: (0, -0.5)

Timer の編集

最初にアイテムボックスを下から小突いてから 4 秒経過後にアイテムボックスを空っぽにするにはタイマーの設定が必要だ。「Timer」ノードを以下のように設定しよう。

  1. 「Wait Time」プロパティの値を 4 にする
  2. 「One Shot」プロパティを オン にする

Area2D 直下の CollisionShape2D を編集する

下からプレイヤーキャラクターが衝突した時しかアイテムが出ないように、ボックスの下部にだけ、本体とは別のコリジョン形状を設定する。

  1. インスペクターで「Shape」プロパティに「新規RectangleShape2D」を割り当てる
  2. 2Dワークスペースでコリジョン形状を編集し、「idle」アニメーションのスプライトテクスチャの一番下から高さ 1 px の範囲に合わせる
    コリジョン形状の編集

ここまでで各ノードのプロパティ編集はひとまず完了だ。「ItemBox」ルートノードにスクリプトをアタッチしたいところだが、その前にもう一つ別のオマケのシーンを作成する。



BrokenBox シーンを作成する

アイテムボックスからアイテムが出尽くした時に箱が壊れる演出を入れたい。アイテムボックスが壊れる時に、BrokenBox シーンを ItemBox と同じ場所に配置したら、ItemBox は消しつつ BrokenBox シーンによって木片が飛び散るようにする。このような細かい複数の物体や流体を表現するにはパーティクルシステムを利用する。パーティクルとは小片とか粒という意味だ。

では手早く以下の手順で BrokenBox シーンを作成しよう。

  1. 「シーン」メニュー>「新規シーン」を選択
  2. 「ルートノードを生成」で「その他のノード」を選択
  3. 「Particles2D」クラスのノードを選択
  4. 「Particles2D」ノードの名前を「BrokenBox」に変更する
  5. ファイルパスを「res://Items/BrokenBox.tscn」としてシーンを保存する


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

ルートノードに設定した「Particles2D」ノードだが、このプラットフォーマーのチュートリアルでは初登場だ。「Paarticle2D」は2Dゲームで、例えば炎や雨、雪、煙、血飛沫など、細かな粒子の動きを表現するのによく利用される。なお、「Particles2D」クラスのノードについては、Godot の公式オンラインドキュメントも説明が比較的わかりやすいので、参考にしていただくのが良いだろう。

公式オンラインドキュメント
パーティクル・システム(2D)

「Particles2D」ノードにはプロパティの項目がたくさんある。最初は思い通りの動きにするのにやや骨が折れるが、慣れてしまえば本当に多くの表現に利用できる。インスペクターで必要なプロパティを上から順番に編集していこう。

インスペクターで が値の左横にあるプロパティは、初期値から変更を加えているので目印にしてほしい。

  • Emitting: オフ
    パーティクルを実行するかどうか。下の「One Shot」がオンなので、一度実行されたらオフになる。ボックスが破壊されたタイミングでスクリプトによってオンにする予定だ。
  • Amount: 16
    パーティクルの数。今回は16個

Time セクション:

  • Lifetime: 4
    パーティクルが存在する時間(秒)。今回は 4 秒後に消える設定。
  • One Shot: オン
    一回限りの実行かどうか。今回はボックスが壊れたらボックスは消えるのでオンにしておく。
  • Speed Scale
    パーティクルシステム実行時の速度
  • Explosiveness: 0.8
    1 に近づくほど、次回の実行までの時間が長くなる。したがって 1 はすべてのパーティクルが同時に表示される。今回は同時まではいかないが、同時にやや近い 0.8 に設定。

Process Material セクション:

  • Material: 新規ParticlesMaterial
    パーティクルを処理するための素材。セットした「ParticlesMaterial」のプロパティを設定していくので、インスペクターで「ParticlesMaterial」をクリックする。

    ParticlesMaterial のプロパティ:

    • Emission Shape セクション

      • Shape: Box
        デフォルトの Point だと全てのパーティクルの開始位置が点になるが、Boxだと、四角い範囲内からの開始になる。
    • Flags セクション

      • Disable Z: オン
        2Dゲームなので、Z軸方向の位置は無効にする。
    • Direction セクション

      • Direction: (0, -1, 0)
        パーティクルの移動する方向、真っ直ぐ上方向にするため y の値を -1 とした。
      • Spread: 30
        パーティクルが広がる角度の設定。デフォルトは 45° だが、箱の破片があまり広く飛び散ってほしくないので、少し狭めの 30° にした。
    • Gravity セクション

      • Gravity: (0, 10, 0)
        重力の設定。真っ直ぐ下方向に重力をかけるため y の値を 10 とした。
    • Initial Velocity セクション

      • Velocity: 20
        パーティクルの速度の設定。下から小突いて壊れた時に飛び散る破片として違和感ない 20 とした。
    • Angular Velocity セクション

      • Velocity: 15
        パーティクルの 1 秒あたりの回転角度の設定。少しだけ回転するように 15° とした。
    • Scale セクション

      • Scale: 0.5
        箱の破片の大きさを実際のテクスチャの半分にして、細かな破片になるようにした。
      • Scale Random: 1
        0.5 ~ 1 の Scalse の間で完全にランダムになるよう値を 1 とした(1 は 100%)。
    • Animation セクション

      • Offset: 1
        パーティクルのアニメーションのオフセット設定。アニメーションの開始フレームをずらすことができる。しかし今回は「Animation」セクションの「Speed」プロパティを 0 にしているため、そのままだとアニメーションはせず、全てのパーティクルがスプライトシートの 1 フレーム目のテクスチャになってしまう。それを回避するためにまずはこのプロパティの値を 1 とした。これにより、アニメーションの開始位置をスプライトシートの最後のフレームにずらすことができる。しかしこのままでは、全てのパーティクルがスプライトシートの最後のフレームのテクスチャになってしまうだけだ。そこで次の「Offset Random」を設定する。
      • Offset Random: 1
        アニメーションのオフセットのランダム率を 1(つまり 100%)とした。これにより、例えば 4 フレームのスプライトシートによるアニメーションなら、1 ~ 4 フレームのうちいずれかのテクスチャがアニメーションの開始位置として適用される。今回「Speed」プロパティが 0 なので、アニメーションせず、各パーティクルのテクスチャはランダムで決定され、かつ生成されてから消失するまで同じテクスチャが維持される。
  • Textures セクション:
    ファイルシステムドックから「res://Assets/Items/Boxes/Box2/Break.png」のスプライトテクスチャファイルをドラッグ&ドロップで適用する。

  • Material セクション:

    • Material: 新規CanvasItemMaterial を適用
    • Particle Animation: オン
    • Particle Animation H Frames: 4
    • Particle Animation V Frames: 1
    • Particle Anim Loop: オフ

これでインスペクターでのプロパティの編集は完了だ。「Particles2D」クラスは「ParticlesMaterial」も含めて、プロパティが非常に多く、最初は難しく感じるだろう。どのプロパティが何に影響するのかは、いろいろ触っているうちにわかってくるところがある。このチュートリアルに限らず、是非あなた自身の手でパーティクルシステムにより様々な表現ができることを試してみて欲しい。


BrokenBox にスクリプトをアタッチする

「BrokenBox」ルートノードに新規でスクリプトをアタッチしよう。ファイルパスを「res://Items/BrokenBox.gd」として作成しよう。

このスクリプトでやりたいことはシンプルに以下の2つだけだ。

  • シーンツリーに追加されたらまず「Emitting」プロパティをオンにしてパーティクルシステムを最初から自動的に実行する
  • パーティクルシステムが終了して「Emitting」プロパティがオフになったらこの「BrokenBox」ノードを解放(削除)する

ということで、上記の2つを制御するための具体的なスクリプトは以下の通りだ。

extends Particles2D

# シーンツリーに追加されたらパーティクルシステムを実行
func _ready():
	emitting = true

# パーティクルシステムが終了したら BrokenBox ノードを解放
func _process(delta):
	if not emitting:
		queue_free()
		# デバッグ用
		print("BrokenBox removed.")


ItemBox シーンにスクリプトをアタッチする

当初予定していた仕様を満たすため、「ItemBox.gd」スクリプトに戻って、コードでアイテムボックスを制御していく。

まずは「ItemBox」ルートノードを選択して、新規でスクリプトをアタッチする。スクリプトのファイルパスは「res://Items/ItemBox.gd」として作成しよう。

「ItemBox.gd」スクリプトを開いたらさっそく編集していこう。まずはプロパティの定義からしていく。

extends StaticBody2D

# Timer ノードのタイマーを利用したことがなければ true
var timer_unused = true
# AnimatedSprite ノードの参照
onready var sprite = $AnimatedSprite
# Timer ノードの参照
onready var timer = $Timer
# 親ノードの参照
onready var parent = get_parent()
# 事前に読み込んだ BrokenBox シーンファイル
onready var broken_box_tscn = preload("res://Items/BrokenBox.tscn")
# 事前に読み込んだ個々のアイテムのシーンファイルを配列(Array)に格納
onready var items = [
	preload("res://Items/Apple.tscn"),
	preload("res://Items/Bananas.tscn"),
	preload("res://Items/Cherries.tscn"),
	preload("res://Items/Kiwi.tscn"),
	preload("res://Items/Melon.tscn"),
	preload("res://Items/Orange.tscn"),
	preload("res://Items/Pineapple.tscn"),
	preload("res://Items/Strawberry.tscn"),
]

今回、アイテムボックスの仕様を満たすには、「Timer」ノードのタイマーが発動済みか未使用かの区別が必要になる。そのステータス管理用のプロパティがtimer_unusedだ。

onreadyキーワード付きのitems(~s と複数形になっていることに注意)プロパティは、このシリーズのチュートリアルでは初登場の 配列(Array) 型の値をとる。配列というのは、複数のオブジェクトを格納することができるデータ型だ。今回は、事前に読み込んだ個々のアイテムのシーンファイル .tscn を格納している。つまり、アイテムボックスから出現するアイテムの元となるシーンファイルをこの配列の要素にしている。要素の順番も重要だ。配列の前からポイントの低いアイテムの順番になっている。前から順に要素を取り出すようにすれば、ポイントの低いアイテムから順番に出現させることができる。

配列(Array)は非常に便利なので、今後もできるだけ慣れ親しんでいきたいところだ。お時間があれば、ぜひ公式オンラインドキュメントもご参照いただきたい。

公式オンラインドキュメント
Array


_readyメソッドでゲーム開始前に必要な準備をコーディングしよう。

func _ready():
	# AnimatedSprite ノードが idle アニメーションを再生
	sprite.play("idle")

ここで「Timer」ノードの「timeout()」シグナルをこのスクリプトに接続しておく。

ちなみに、先にインスペクターで「Timer」ノードの「Wait Time」プロパティを 4 秒に設定したので、タイマーがスタートしてから 4 秒でこのシグナルが発信される。

シグナルを接続したら、以下の_on_Timer_timeoutメソッドが追加されたはずだ。メソッド内を編集していく。

func _on_Timer_timeout():
	# デバッグ用
	print("Item box timer time out")
	# 配列 items が空っぽでなければ
	if not items.empty():
		# 配列 items から全ての要素を消去
		items.clear()
		# デバッグ:配列 items の要素数を確認
		print("items size: ", items.size())

配列クラスには色々便利なメソッドが用意されているが、ここではひとまず、今回利用したメソッドに関してのみ説明しておこう。

if構文で利用しているemptyメソッドでは、配列itemsが空っぽかどうかを確認できる。空っぽだったらtrue、空っぽでなければfalseを返す。

ifブロック内で実行しているclearメソッドは、配列内の要素を全て消去する。制限時間 4 秒が経過したらアイテムボックスは空っぽになる、というイメージだ。

デバッグ用のprint関数内で使用されているsizeメソッドは、配列の要素数を返す。


次に「Area2D」ノードの「body_entered(body)」シグナルをこのスクリプトに接続する。

接続したらスクリプトに_on_Area2D_body_enteredメソッドが追加されたはずだ。ところで「Area2D」ノードのコリジョン形状はアイテムボックスの下部 1 px の部分にだけ配置したことは覚えているだろうか。そこにプレイヤーキャラクターが衝突した時に実行すべきコードをこの_on_Area2D_body_enteredメソッド内に記述していくというわけだ。

具体的にコードは以下のようになった。

func _on_Area2D_body_entered(body):
	# 衝突した body が Player という名前のノードだったら
	if body.name == "Player":
		# timer_unused プロパティが true だったら
		if timer_unused:
			# Timer ノードのタイマーをスタートする
			timer.start()
			# timer_unused を false にする
			timer_unused = false
		
		# AnimatedSprite の play メソッドで hit アニメーションを再生
		sprite.play("hit")
		# AnimatedSprite のアニメーションが終了するまで待機
		yield(sprite, "animation_finished")
		# AnimatedSprite の Play メソッドで idle アニメーションに戻して再生
		sprite.play("idle")
		
		# もし配列 items が空っぽだったら
		if items.empty():
			# デバッグ用
			print("Item box is empty.")
			# AnimatedSpriteノードを非表示にしてアイテムボックスを画面から消す
			sprite.visible = false
			# BrokenBox シーンのインスタンスを変数に代入
			var broken_box = broken_box_tscn.instance()
			# 親ノードの子ノード(ItemBox と同じ階層)として BrokenBox インスタンスを追加
			parent.add_child(broken_box)
			# BrokenBox の位置を ItemBOx の位置と同じにする
			broken_box.position = position
			# ItemBox を解放(削除)する
			queue_free()
			# デバッグ用
			print(self.name, " removed.")
		# 配列 items が空っぽではなかったら
		else:
			# デバッグ用
			print("Item box is not empty.")
			# 変数 item を定義 > 配列 items の最初の要素を取り出してインスタンス化
			var item = items.pop_front().instance()
			# インスタンス化したアイテムのシーンを ItemBox の子ノードにする
			add_child(item)
			# アイテムのインスタンスノードの position の y の値を -12 する(箱より少し上にアイテムを出現させる)
			item.position.y -= 12
			# アイテムのインスタンスノードの hit メソッドを実行
			item.hit()

少し補足説明しておく。

if timer_unused:の構文で、timer_unusedプロパティがtrueの場合(つまりタイマーが使われた事がない)だけ、タイマーをスタートさせている。その直後にこのtimer_unusedfalse(タイマーが使われた事がある)に変更するので、初めてアイテムボックスを下から小突いた時にしかタイマーは開始しない仕組みになっている。

次に「AnimatedSprite」の「hit」アニメーションを再生させている。

さらにそのあとはif / else構文で条件分岐する。

ここでもまた配列itemsemptyメソッドを利用している。ifelseの条件が逆の方がわかりやすいかもしれないが、今回はif構文にnotを入れなくて済むようにした。

if items.empty():の条件である『配列itemsが空っぽの場合』を満たしたらブロックの中に進む。これはつまりアイテムボックスからアイテムが出尽くした場合だ。その場合、以下のような順番で処理が進む。

  1. 先に「AnimatedSprite」ノードを非表示にして、画面からアイテムボックスを消す。
  2. 「BrokenBox」シーンをインスタンス化する
  3. 「BrokenBox」のインスタンスを、親ノード に追加する(この時点でパーティクルシステムが実行される)
  4. 「BrokenBox」ノードの位置を「ItemBox」ノードの位置と同じにする
  5. 「ItemBox」ノードは解放(削除)する(解放しても「BrokenBox」は親ノードに紐づいているためパーティクル実行中でも影響されない)

順番が逆でややこしいかもしれないが、else:の条件は『配列itemsが空っぽでなければ』となる。アイテムボックスにまだアイテムが入っている場合だ。この場合は以下の順序でコードが実行される。

  1. pop_frontメソッドにより、配列itemsの先頭の要素(アイテムのシーン)を取り出して、それをインスタンス化する(この時取り出した要素はitems内からは削除される)
  2. 取り出したシーンのインスタンスを「ItemBox」自身の子ノードとして追加する
  3. 子にしたアイテムのノードの位置を、「ItemsBox」自身の位置より y 軸上で -12ずらした位置に設定する
  4. アイテムのノードのhitメソッドを実行する

これで「ItemBox.gd」スクリプトのコーディングは完成だ。

「Level1」シーンにインスタンスを追加して、アイテムボックスが正しく動作するか確認しておこう。

プロジェクトを実行して当初想定していた以下の仕様を満たしているか確認してみよう。

  1. 衝突判定があり、プレイヤーキャラクターや敵キャラクターはボックスを透過しない
  2. 下からプレイヤーキャラクターが衝突した時しかアイテムは出ない
  3. 最初に下から衝突してから 4 秒経過すると、アイテムボックスは空っぽになる
  4. 最後の一つのアイテムが出ると、アイテムボックスは壊れて無くなる。

概ね問題ないが、アイテムボックスからアイテム(フルーツ)が出てきてすぐにぺちゃんこに変形してしまうので、何が出てきたのかわかりにくい。そこで「Item.tscn」シーンを開き、「AnimationPlayer」の「hit」アニメーションを編集しよう。

「AnimatedSprite」>「modulate」トラック:0 秒の位置にあるキーの「Easing」プロパティを 1 から 3 に変更

「AnimatedSprite」>「scale」トラック:0 秒の位置にあるキーの「Easing」プロパティを 1 から 4 に変更

もう一度確認だ。

さっきよりはフルーツの形状がわかりやすくなっただろう。これでアイテムボックスは完成としよう。



Level1 シーンにアイテムボックスを追加する

では Part 6 最後の仕上げとして、完成したアイテムボックスを「Level1」シーンに追加しよう。どこに配置するかはあなたの自由なので、適当な場所に配置してほしい。

あくまでサンプルだが、以下のスクリーンショットのような感じで、マップ上に3つ配置した。

最後にプロジェクトを実行して、「Level1」シーン全体を確認してみよう。なお以下のGIF画像は 2.5 倍速にしている。



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

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

Item.gd の全コード
extends Area2D # Added @ Part 6


export var point = 100
onready var sprite = $AnimatedSprite
onready var label = $Label
onready var anim_player = $AnimationPlayer


func _ready():
	sprite.modulate = Color(1, 1, 1, 1)
	sprite.position = Vector2.ZERO
	sprite.scale = Vector2.ONE
	label.modulate = Color(1, 1, 1, 0)
	label.rect_position = Vector2(-32, -20)
	label.text = str(point)


func _on_Item_body_entered(body):
	if body.name == "Player":
		print("Player hit Item")
		hit()


func hit():
	print("Got ", point, " point.")
	anim_player.play("hit")
	yield(anim_player, "animation_finished")
	queue_free()
ItemBox.gd の全コード
extends StaticBody2D # Added @ Part 6

var timer_unused = true
onready var sprite = $AnimatedSprite
onready var timer = $Timer
onready var parent = get_parent()
onready var broken_box_tscn = preload("res://Items/BrokenBox.tscn")
onready var items = [
	preload("res://Items/Apple.tscn"),
	preload("res://Items/Bananas.tscn"),
	preload("res://Items/Cherries.tscn"),
	preload("res://Items/Kiwi.tscn"),
	preload("res://Items/Melon.tscn"),
	preload("res://Items/Orange.tscn"),
	preload("res://Items/Pineapple.tscn"),
	preload("res://Items/Strawberry.tscn"),
]


func _ready():
	sprite.play("idle")


func _on_Timer_timeout():
	print("ItemBox > Timer timeout")
	if not items.empty():
		items.clear()
		print("items size: ", items.size())


func _on_Area2D_body_entered(body):
	if body.name == "Player":
		print("ItemBox > Area2D Player entered")
		
		if timer_unused:
			timer.start()
			timer_unused = false
			
		sprite.play("hit")
		yield(sprite, "animation_finished")
		sprite.play("idle")
		
		if items.empty():
			print("ItemBox is empty.")
			sprite.visible = false
			var broken_box = broken_box_tscn.instance()
			parent.add_child(broken_box)
			broken_box.position = position
			queue_free()
			print(self.name, " removed.")
		else:
			print("ItemBox is not empty.")
			var item = items.pop_front().instance()
			add_child(item)
			item.position.y -= 12
			item.hit()
BrokenBox.gd の全コード
extends Particles2D # Added @ Part 6


func _ready():
	emitting = true


func _process(delta):
	if not emitting:
		queue_free()
		print("BrokenBox removed.")


おわりに

以上で Part 6 は完了だ。今回はアイテムの雛形シーンを作り、それを継承して個々のアイテムシーンを作った。さらに、それらのアイテムが出てくるアイテムボックスを作り、アイテムボックスが壊れる時の演出をパーティクルシステムで実装した。ということで、今回もなかなかボリューミーな回になった。しかし、やった分だけゲームの楽しさは確実に増したはずだ。

次回 Part 7 では、レベルクリア条件となるものを用意して、次のレベルへの遷移を実装する予定だ。それでは次回もお楽しみに。


UPDATE:
2022-02-25 「おわりに」の次回の内容を「HUD」から「次のレベルへの遷移」に変更