第7回目の今回は、レベルのマップ上にスタートポイント、中間のチェックポイント、そしてエンドポイントを配置し、エンドポイントに到達した時に次のレベルに遷移する仕組みを実装していく。併せて、現在レベルシーンは「Level1」だけなので、次の「Level2」シーンも作成していく。
Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るプラットフォーマー
スタートポイント、 チェックポイント、 エンドポイントを作る
今回のチュートリアルのテーマはレベルクリアとそのあとの次のレベルへの遷移なので、必要なのは各レベルにゴールを用意することだが、せっかくなので以下の3つを作っていく。
- スタートポイント(ゲーム開始地点)
- チェックポイント(通過点)
- エンドポイント(レベルのゴール)
それぞれのオブジェクトをスプライトテクスチャとコリジョン形状をつけて用意していく。特にエンドポイントについては、衝突判定をトリガーにして次のレベルに遷移するように、後ほどスクリプトで制御する予定だ。
StartPoint シーンを作る
シーンを新規作成してノードを追加する
以下の手順で「StartPoint」シーンを作成して、ノードを追加して保存しよう。
- 「シーン」メニュー>「新規シーン」を選択する
- 「ルートノードを生成」で「その他のノード」を選択する
- 「Area2D」クラスのノードをルートノードに選択する
- 「Area2D」ノードの名前を「StartPoint」に変更する
- 「StartPoint」ルートノードに以下の子ノードを追加する
- 「AnimatedSprite」ノード
- 「CollisionShape2D」ノード
- 「StaticBody2D」ノード
- 「StaticBody2D」ノードに「CollisionShape2D」ノーをを追加する
- ファイルパスが「res://Points/StartPoint.tscn」となるように、「Points」フォルダを作成しつつ、そこにファイルを作成する形でシーンを保存する
シーンツリーに必要なノードは揃ったので、次はそれぞれのノードのプロパティを編集する。
各ノードのプロパティを編集する
まずは「AnimatedSprite」ノードのプロパティから編集していく。
- インスペクターで「Frames」プロパティに「新規SpriteFrames」を割り当てる
- 「SpriteFrames」をクリックし、スプライトフレームパネルを開く
- 以下の2つのアニメーションを作成する(片方は default の名前を変更して作成)
アニメーション名: idle
- スプライトシー: res://Assets/Items/Checkpoints/Start/Start (Idle).png
- 速度: 1 FPS
- ループ: オフ
アニメーション名: moving
- スプライトシート: res://Assets/Items/Checkpoints/Start/Start (Moving) (64x64).png
- 速度: 24 FPS
- ループ: オン
- インスペクターに戻り「Animation」プロパティを「idle」にする
- 「Playing」プロパティをオンにする
次に「CollisionShape2D」ノードのプロパティを編集する。このノードはプレイヤーキャラクターが当たったときに「moving」アニメーションを再生するための仕掛けだ。「Area2D」ルートノードは物理ボディではないので、コリジョン形状と接触しても透過する。
- インスペクターで「Shape」プロパティに「新規RectangleShape2D」を割り当てる
- コリジョン形状をスプライトテクスチャの市松模様の台の上に載せるような感じで配置する
- コリジョン形状の調整により関連プロパティの値が以下のようになっているか確認する
- Extents: (16, 1)
- Position: (11, 23)
最後に「StaticBody2D」ノードの子になっている方の「CollisionShape2D」のプロパティを編集しよう。こちらは台の上にプレイヤーキャラクターが乗れるようにするためのコリジョン形状だ。
- インスペクターで「Shape」プロパティに「新規RectangleShape2D」を割り当てる
- コリジョン形状をスプライトテクスチャの市松模様の台にピッタリ合わせるようにして配置する
- コリジョン形状の調整により関連プロパティの値が以下のようになっているか確認する
- Extents: (17, 4)
- Position: (11, 28)
スクリプトをアタッチする
「StartPoint」ルートノードに新規でスクリプトをアタッチする。ファイルパスを「res://Points/StartPoint.gd」として開こう。
今回のスクリプトはいたってシンプルだ。しかしシグナルの力を借りる必要がある。「StartPoint」ノードのシグナル「body_entered(body)」と「_body_exited(body)」を先にスクリプトに接続しておこう。
では具体的なスクリプトを確認していこう。今回は以下のようになった。
extends Area2D # Added @ Part 7
# AnimatedSprite ノードの参照
onready var sprite = $AnimatedSprite
func _ready():
# ゲーム開始時は idle アニメーションを再生
sprite.play("idle")
# ボディが Area2D のコリジョン形状に入ったらシグナルでこのメソッドが呼ばれる
func _on_StartPoint_body_entered(body):
# もしボディの名前が Player だったら moving アニメーションを再生する
if body.name == "Player":
sprite.play("moving")
# ボディが Area2D のコリジョン形状から出たらシグナルでこのメソッドが呼ばれる
func _on_StartPoint_body_exited(body):
# もしボディの名前が Player だったら idle アニメーションを再生する
if body.name == "Player":
sprite.play("idle")
これで「StartPoint」シーンは完成だ。このあとさっそくインスタンス化して「level1」シーンに追加してみよう。
Level1 シーンにインスタンスを追加する
では完成した「StartPoint」シーンをインスタンス化して「Level1」シーンに追加しよう。
追加できたら、プロジェクトを実行して動作確認をしておこう。
プレイヤーキャラクターが市松模様の台に乗った時だけ矢印の標が「moving」アニメーションにより動いた。台から離れると「idle」アニメーションで停止した。問題ないので次のチェックポイント作成の作業へ移ろう。
Checkpoint シーンを作る
シーンを新規作成してノードを追加する
以下の手順で「Checkpoint」シーンを作成して、ノードを追加して保存しよう。
- 「シーン」メニュー>「新規シーン」を選択する
- 「ルートノードを生成」で「その他のノード」を選択する
- 「Area2D」クラスのノードをルートノードに選択する
- 「Area2D」ノードの名前を「Checkpoint」に変更する
- 「Checkpoint」ルートノードに「AnimatedSprite」ノードと「CollisionShape2D」ノードを追加する
- ファイルパスを「res://Points/Checkpoint.tscn」としてシーンを保存する
シーンツリーに必要なノードが揃った。さっきの「StartPoint」よりノードが少なくて簡単だ。
各ノードのプロパティを編集する
「AnimatedSprite」ノードのプロパティを編集する。
- インスペクターで「Frames」プロパティに「新規SpriteFrames」を割り当てる
- 「SpriteFrames」をクリックし、スプライトフレームパネルを開く
- 以下の2つのアニメーションを作成する(片方は default の名前を変更して作成)
アニメーション名: flag_idle
- スプライトシー: res://Assets/Items/Checkpoints/Checkpoint/Checkpoint (Flag Idle)(64x64).png
- 速度: 24 FPS
- ループ: オン
アニメーション名: flag_out
- スプライトシート: res://Assets/Items/Checkpoints/Checkpoint/Checkpoint (Flag Out) (64x64).png
- 速度: 24 FPS
- ループ: オフ
アニメーション名: no_flag
- スプライトシート: res://Assets/Items/Checkpoints/Checkpoint/Checkpoint (No Flag).png
- 速度: 24 FPS
- ループ: オフ
- インスペクターに戻り「Animation」プロパティを「flag_idle」にする
- 「Playing」プロパティをオンにする
次に「CollisionShape2D」ノードのプロパティを編集する。このノードはプレイヤーキャラクターが当たったときに「flag_out」アニメーションを再生したあと、そのまま「flag_idle」アニメーションを再生するために必要だ。「Area2D」ルートノードは物理ボディではないので、コリジョン形状と接触しても透過する。道中に立てるフラグとしては最適だ。
- インスペクターで「Shape」プロパティに「新規RectangleShape2D」を割り当てる
- コリジョン形状をスプライトテクスチャのフラグのポーツ部分にピッタリ合わせる感じで配置する
- コリジョン形状の調整により関連プロパティの値が以下のようになっているか確認する
- Extents: (1.5, 17.5)
- Position: (-8.5, 11)
スクリプトをアタッチする
ではチェックポイントを通過した時の「Checkpoint」のアニメーションをスクリプトで制御しよう。
まずは「Checkpoint」ルートノードに新規スクリプトをアタッチする。パスを「res://Points/Checkpoint.gd」としてファイルを開こう。開いたら、以下のコード差し替えよう。コードはシンプルなので、解説はコード内のコメントをご覧いただきたい。
extends Area2D
# AnimatedSprite ノードの参照
onready var sprite = $AnimatedSprite
# ゲーム開始時は no_flag アニメーションを再生
func _ready():
sprite.play("no_flag")
func _on_Checkpoint_body_entered(body):
# チェックポイントにプレイヤーキャラクターが当たったら
if body.name == "Player":
# flag_out アニメーションを再生
sprite.play("flag_out", true)
# アニメーションが終了するのを待つ
yield(sprite, "animation_finished")
# flag_idle アニメーションを再生
sprite.play("flag_idle")
Level1 シーンにインスタンスを追加する
では「Checkpoint」シーンをインスタンス化して、「Level1」シーンに追加して動作を確認しよう。
最初は「no_flag」アニメーションが再生されているので、ただポールが立っているだけだ。そのあと最初にプレイヤーキャラクターがチェックポイントに当たった時に、「flag_out」アニメーションが再生され、アニメーションが終了するとそのまま「flag_idle」アニメーションに切り替わった。そのあとはプレイヤーキャラクターがチェックポイントに当たっても何も起こらない。想定通りの動作が確認できたので、(少し休憩して)次のエンドポイント作成の手順に移ろう。
EndPoint シーンを作る
シーンを新規作成してノードを追加する
以下の手順で「EndPoint」シーンを作成して、ノードを追加して保存しよう。
- 「シーン」メニュー>「新規シーン」を選択する
- 「ルートノードを生成」で「その他のノード」を選択する
- 「Area2D」クラスのノードをルートノードに選択する
- 「Area2D」ノードの名前を「EndPoint」に変更する
- 「EndPoint」ルートノードに「AnimatedSprite」ノードを追加する
- 「EndPoint」ルートノードに「CollisionShape2D」ノードを追加する
- 「EndPoint」ルートノードに「StaticBody2D」ノードを追加する
- 「StaticBody2D」ノードに「CollisionPolygon2D」ノードを追加する
- 「Endpoint」ルートノードに「AnimationPlayer」ノードを追加する
- 「Endpoint」ルートノードに「Particles2D」ノードを追加する
- ファイルパスを「res://Points/EndPoint.tscn」としてシーンを保存する
シーンツリーに必要なノードが揃ったので、次はそれぞれのノードを編集していく。
各ノードのプロパティを編集する
「AnimatedSprite」ノードのプロパティを編集する。
インスペクターで「Frames」プロパティに「新規SpriteFrames」を割り当てる
「SpriteFrames」をクリックし、スプライトフレームパネルを開く
以下の2つのアニメーションを作成する(片方は default の名前を変更して作成)
アニメーション名: idle
- スプライトシー: res://Assets/Items/Checkpoints/End/End (Idle).png
- 速度: 24 FPS
- ループ: オフ
アニメーション名: pressed
- スプライトシート: res://Assets/Items/Checkpoints/End/End (Pressed) (64x64).png
- 速度: 12 FPS
- ループ: オフ
インスペクターに戻り「Animation」プロパティを「idle」にする
なお、この「EndPoint」シーンの「AnimatedSprite」の「Playing」プロパティはオフのままにしておく。理由は「AnimatedSprite」のフレームをキーとして利用する形で、「AnimationPlayer」ノードの方でアニメーションさせたいからだ。
次に「CollisionShape2D」ノードのプロパティを編集する。このノードはプレイヤーキャラクターが当たったときに「pressed」アニメーションを再生するために必要だ。といっても、ここでいう「pressed」というのは後ほど「AnimationPlayer」ノードで作成するアニメーションの方を指している(ややこしくて申し訳ない)。
- インスペクターで「Shape」プロパティに「新規RectangleShape2D」を割り当てる
- コリジョン形状をスプライトテクスチャのカップの真ん中より少し下あたりに合わせるようにして配置する
- コリジョン形状の調整により関連プロパティの値が以下のようになっているか確認する
- Extents: (12, 1)
- Position: (0, 8)
続けて「StaticBody2D」ノードの子である「CollisionPolygon2D」ノードのコリジョン形状を作っていく。いつもは「CollisionShape2D」の方を使用していたが、この優勝カップのような曲線や凹型の形状には合わせにくい。「CollisionPolygon2D」ならば、点を打って、点と点を線で繋げていくことで形状を作っていくので、複雑な形のスプライトテクスチャにも当てはめやすいのだ。
公式オンラインドキュメント:
CollisionPolygon2D
作業前にツールバーのアイコンを確認しておこう。
- 緑の + がついたアイコン:「点を作成する」ツール。形状を作成するときに主に使用する。
- 青の ↑ がついたアイコン:「点を編集する」ツール。左クリックで点を移動、右クリックで点を削除する。
- 赤の × がついたアイコン:「点を消す」ツール。青のアイコンで右クリックするのと同じで、点を削除する。
併せて、点を打ちやすいようにツールバーでグリッドにスナップを有効にしておこう。またスナッピングオプション(縦に点が三つ並んだアイコン)>スナップの設定を開き、グリッドのステップを x、y ともに 4 px に設定する。
「AnimatedSprite」で「Animation」プロパティを「idle」にしておき、この時のスプライトテクスチャに合わせてコリジョン形状を作成していく。
2Dワークスペースでカップの断面を作るようにして点を打って線を繋いでいこう。最後の点を打ったあと、最初に打った点をクリックすれば最初と最後の点が繋がり、コリジョン形状の完成だ。以下のスクリーンショットでは、打った点の順番を記しているので参考にしてほしい。
次は「AnimationPlayer」ノードでアニメーションを作成する。「AnimatedSprite」ノードとは別でこの「AnimationPlayer」を用意する理由を説明しておこう。今回のこのエンドポイントのシーンでは、「AnimatedSprite」の「pressed」アニメーション再生中に、プレイヤーキャラクターが乗っている「StaticBody2D」のコリジョン形状も動かす必要がある。なぜなら、画面上でプレイヤーキャラクターがカップの中に入っている状態でカップが上下動するので、キャラクターも一緒に上下動しないと、ゲームをプレイする人は違和感を感じてしまうからだ。
では「AnimationPlayer」ノードを選択してアニメーションパネルを開こう。
- 新規アニメーションを「clear」という名前で作成する
- アニメーションの長さを 0.4 秒にする
- 作業しやすいようにタイムラインのスナップを 0.05 秒にする
- 作業しやすいようにタイムラインの拡大率を調整する
- 以下の内容でそれぞれのトラックを追加・編集していき、アニメーションを作成する
- トラック 1: AnimatedSprite > animation プロパティ
- Time: 0 / Value: pressed / Easing: 1.00
- Time: 0.4 / Value: idle / Easing: 1.00
- トラック 2: AnimatedSprite > frame プロパティ
- Time: 0 / Value: 0 / Easing: 1.00
- Time: 0.05 / Value: 1 / Easing: 1.00
- Time: 0.1 / Value: 2 / Easing: 1.00
- Time: 0.15 / Value: 3 / Easing: 1.00
- Time: 0.2 / Value: 4 / Easing: 1.00
- Time: 0.25 / Value: 5 / Easing: 1.00
- Time: 0.3 / Value: 6 / Easing: 1.00
- Time: 0.35 / Value: 7 / Easing: 1.00
- Time: 0.4 / Value: 0 / Easing: 1.00
- トラック 3: CollisionPolygon2D > position プロパティ
- Time: 0 / Value: (0, 0) / Easing: 1.00
- Time: 0.15 / Value: (0, -1) / Easing: 1.00
- Time: 0.2 / Value: (0, -4) / Easing: 1.00
- Time: 0.3 / Value: (0, -5) / Easing: 1.00
- Time: 0.4 / Value: (0, -4) / Easing: 1.00
- 最後に2Dワークスペース上でアニメーションを再生してみて動作確認する
最後に「Particles2D」ノードを編集していこう。
エンドポイントのカップにプレイヤーキャラクターが入ったらレベルクリアとなって、次のレベルに遷移させる。この時、多少かっこいい演出をしたいところだ。そこで、プレイヤーキャラクターがカップに入ったあと、カップからふわぁと光の粒子のようなものが舞い上がるような演出を作っていく。
公式オンラインドキュメント:
Particles2D
ではプロパティの多い「Particles2D」だが、プロパティの設定を以下のようにしてほしい。
- Amount: 300
- Time セクション
- Lifetime: 1.5
- Process Material セクション
Material: 新規ParticlesMaterial
- Emission Shape セクション
- Shape: Box
- Box Extents: (14, 1, 1)
- Direction セクション
- Direction: (0, -1, 0)
- Spread: 0
- Gravity セクション
- Gravity: (0, -200, 0)
- Initial Velocity セクション
- Velocity: 50
- Velocity Random: 0.5
- Scale セクション
- Scale: 2
- Scale Curve: 新規CurveTexture
- Curve: 新規Curve
- Min Value: 0
- Max Value: 1
- Bake Resolution: 100
- Curve: 新規Curve
- Color セクション
- Color Ramp: 新規GradientTexture
- Gradient: 新規Gradient
- Colors: PoolColorArray (size 2)
- 0: #99e5ff
- 1: #5400ff
- Colors: PoolColorArray (size 2)
- Gradient: 新規Gradient
- Color Ramp: 新規GradientTexture
- Emission Shape セクション
- Transform セクション
- Position: (0, -8)
ここまでのプロパティでどのようなパーティクルになったか確認しておこう。
パーティクルがスプライトテクスチャより前に表示されているので、「Particles2D」ノードを最背面に移動させてカップの中から粒子が上昇しているようにしよう。具体的にはシーンドックで「EndPoint」の子ノードのうち「Particles2D」を一番上に移動するだけだ。
ところで、「Particles2D」はプロパティの値を触れば触るほど慣れてきて、思い通りの表現ができるようになる。少し時間をかけて、一つ一つの変更で何がどう変わるのかをあなた自身の目で確かめてほしい。また、もっとこの方が良いと思うプロパティの設定があれば、積極的にアレンジいただければと思う。
Player シーンにクリア時のアニメーションを実装する
さて、ここでもう一つ素敵な演出を入れようではないか。エンドポイントのカップに入ったプレイヤーキャラクターが次のレベルにワープするような演出だ。パーティクルの粒子を浴びたあと、プレイヤーキャラクターがふわりと浮かび上がり、そのまま透明になりながら最後は一気に x 軸方向の幅が 0 、 y 軸方向には 20 倍に拡大して消える、というのを「AnimationPlayer」を利用して実装する。
では久しぶりに「Player.tscn」シーンのファイルを開こう。開いたら、「Player」ルートノードに「AnimationPlayer」ノードを追加しよう。
「AnimationPlayer」ノードを選択して、アニメーションパネルを開いたら、新規で「clear」という名前でアニメーションを作成する。作成できたら、以下の手順でアニメーションを編集しよう。
- アニメーションの長さを 1 秒に設定する
- タイムラインのスナップを 1 秒に設定する
- 以下の内容でトラックを追加、編集する
- トラック1: AnimatedSprite ノード > position プロパティ
- Time: 0 / Value: (0, 0) / Easing: 1.00
- Time: 1 / Value: (0, -64) / Easing: 1.00
- トラック2: AnimatedSprite ノード > scale プロパティ
- Time: 0 / Value: (1, 1) / Easing: 30.0
- Time: 1 / Value: (0, 20) / Easing: 1.00
- トラック3: AnimatedSprite ノード > modulate プロパティ
- Time: 0 / Value: #ffffff / Easing: 3.0
- Time: 1 / Value: #00ffffff / Easing: 30.0
アニメーションを再生してみよう。
アニメーションを再生したあと、プロパティの値が変更されたままになってしまう。そこで、ゲーム開始時に初期値を設定するように、「Player.gd」スクリプトに以下のコードを追加しておこう。_ready
メソッドなので、挿入箇所は各種プロパティを定義するコードのあとが一般的だ。
func _ready():
sprite.position = Vector2(0, 0)
sprite.scale = Vector2(1, 1)
sprite.modulate = Color(1, 1, 1, 1)
あとは、エンドポイント側にこのあとアタッチする予定のスクリプトで、「Player」シーンの「AnimationPlayer」のアニメーションを再生させたいので、以下のプロパティも「Player.gd」スクリプトに追加しておこう。
onready var anim_player = $AnimationPlayer
さてここまでで各ノードのプロパティの編集は完了だ。次はスクリプトでプレイヤーキャラクターがエンドポイントのカップの中に入った時の動作を制御していく。
スクリプトをアタッチする
「EndPoint」ルートノードに新規でスクリプトをアタッチする。ファイルパスを「res://Points/EndPoint.gd」として作成しよう。
スクリプトが作成できたら、以下のコードを記述しよう。
extends Area2D
# Paricle2D ノードの参照
onready var particle = $Particles2D
# AnimatedSprite ノードの参照
onready var sprite = $AnimatedSprite
# CollisionPolygon2D ノードの参照
onready var polygon = $StaticBody2D/CollisionPolygon2D
# AnimationPlayer ノードの参照
onready var anim_player = $AnimationPlayer
# ゲーム開始時にパーティクルの停止とアニメーションで変更されるプロパティの初期値を設定
func _ready():
particle.emitting = false
sprite.play("idle")
polygon.position = Vector2(0, -4)
ここで例によって、「Area2D」クラスである「EndPoint」のシグナルを接続して、プレイヤーキャラクターがエンドポイントのカップの中に入った後の動作をコーディングしていく。
まずは「body_entered(body)」シグナルをこのスクリプトに接続しよう。以下の_on_EndPoint_body_entered
メソッドが追加されたはずなので、そのメソッドのブロック内にコーディングしていこう。
# プレイヤーキャラクターがカップの中に入ったら
func _on_EndPoint_body_entered(body):
if body.name == "Player":
# AnimationPlayer ノードの clear アニメーションを再生
anim_player.play("clear")
# clear アニメーションが終了するまで待機
yield(anim_player, "animation_finished")
# Particles2D ノードの emitting プロパティを有効にしてパーティクルシステム発動
particle.emitting = true
# Player ノードの AnimationPlayer の clear アニメーションを再生
body.anim_player.play("clear")
# clear アニメーションが終了するまで待機
yield(body.anim_player, "animation_finished")
# 次のレベルへ遷移(というのを一旦文字で出力し、Level2 シーン作成後に更新する)
print("Moving to the next level!")
これで「EndPoint.gd」スクリプトの編集は完了だ。少し休憩しよう。このあと、いよいよ「Level1」シーンに、先に用意したスタートポイント、チェックポイント、エンドポイントのインスタンスを追加していく。
Level1 シーンにインスタンスを追加する
「Level1.tscn」シーンを開いて、「EndPoint.tscn」をインスタンス化してルートノードに追加しよう。
追加できたら、さっそくプロジェクトを実行して動作を確認してみよう。
見ての通り、プレイヤーキャラクターのスプライトがエンドポイントのスプライトより手前に表示されているので、カップの中に入った状態を表現できていない。ここで、シーンドックで「Player」ノードを「EndPoint」ノードより上に配置すれば済むと思うだろう。確かにそれは正しい。しかし、今後毎回新しいレベルシーンを作るたびにこの順序を気にしなければならないのは少し面倒ではないか。そこで「EndPoint.tscn」シーンに戻って、インスペクターから「EndPoint」ルートノードの「Z Index」プロパティをデフォルトの 0 から 1 に変更してみよう。
なんと、このプロパティは奥行きとしての順序を設定できるのだ。各ノードが Z 軸方向(奥行き)にレイヤー状に配置されているとして、全てのノードは最初 0 番目のレイヤーにある。0 番目のレイヤーの中の順序はシーンツリーの順序に従う。今回「EndPoint」ノードを 1 番目のレイヤーに設定したので、他のノードよりも必ず全面に配置される。これならば、シーンツリー上のノードの順番を気にせずに済むだろう。ちなみにインスペクターで「Z Index」の一つ下にある「Z As Relative」はデフォルトでオンになっているが、この場合、親ノードの「Z Index」の値にそのノード自身の「Z Index」の値を加算した値が、シーンツリー上の奥行きの順序となる。
では改めてプレイヤーキャラクターをエンドポイントのカップに入れてみよう。
カップの中に入ってからふわりと浮かび上がって消えるまでの動きが確認できた。概ね問題なさそうなので、次の手順に進もう。
Level2 シーンを作る
ひとまず、あまり凝ったマップデザインにせず、できるだけ短時間で「Level2」シーンを作ってしまおう。タイルマップのデザインや、敵キャラクター、アイテムの配置はあなたの思う通りにご自由にやってただいて構わない。
Level1 シーンを複製する
Level2 を一から作るとなると、一通りの作業を経験しているとはいえ、なかなか手間がかかるだろう。そこで、すでに作成済みの「Level1」シーンを複製して、それを編集する方法を採用する。ちなみに、複製は単なるファイルのコピーでしかないので、継承とは別物だ。継承元の変更が反映される心配がないので気楽にやってほしい。以下の手順で複製できるのでやってみよう。
- ファイルシステムドックで「res://Levels/Level1.tscn」を右クリックする
- 「複製」をクリックする
- 「Level2.tscn」と名付けて複製する。
- ルートノードの名前を「Level2」に変更する
タイルマップをデザインする
タイルマップをデザインする前に、敵キャラクターやアイテムのノードが邪魔になりそうだったら先に削除しておこう。後からでもシーンをインスタンス化して簡単に追加できる。
「TileMap」ノードを選択したら、まず「Level1.tscn」で配置したタイルを全て削除する。これは選択ツールで選択してカットを繰り返しても良いし、バケツツールで選択した範囲を右クリックで削除しても構わない。効率が良いと思う方法で削除しよう。
Part 2 のチュートリアルでタイルセットを用意した時に、石のブロックタイルのアトラスをせっかく作ったので、今回はそれを使ってダンジョンっぽいタイルマップをデザインするのも良いだろう。このチュートリアルではサンプルとして以下のようなタイルマップをデザインした。
それぞれのノードをマップ上に配置する
タイルマップがデザインできたら、スタートポイント、チェックポイント、エンドポイントに加え、お好みの敵キャラクター、アイテム、アイテムボックスを配置しよう。「Level1」のように全ての種類のインスタンスを追加する必要はない。サンプルとして作ったタイルマップには、以下のようにマッシュルームばかりを配置した。マッシュルームの生息するジメジメしたダンジョンというイメージで作った(一匹だけプラントがいる…)。
注意しなければならないのは、敵キャラクターは基本的に左向きに作成していることだ。プレイヤーキャラクターが敵キャラクターの右側にいるからといって振り向くような動作は実装していない。壁にぶつかると左右反転する動作は実装しているが、少し違う。この点を考慮して配置していこう。
シーンを実行しながら、おかしなところを都度修正しつつ、完成を目指そう。シーンを実行して最後まで特に問題がなければ「Level2」シーンの作成は完了だ。ちなみに下のGIF画像は尺が長いので3倍速にしている。
今回のデバッグで、ブロックの端に立った時にプレイヤーキャラクターが滑って落ちてしまうことが多く、難易度が非常に高いと感じたので、「Player.tscn」の「Player」ルートノードの摩擦抵抗を表す「Friction」プロパティの値を 0.5 に変更した。
次のレベルへの遷移を実装する
ついに、今回のチュートリアルの最大のテーマである次のレベルへの遷移を実装していく。
Level1、Level2 さらに今後レベルシーンが増えることを前提にするならば、それらを管理する「Game」シーンがあったほうが制御しやすいだろう。
ではさっそく以下の手順で「Game」シーンを作成していこう。
- 「シーン」メニュー>「新規シーン」を選択
- 「Node」クラスをルートノードにしてシーンを作成
- 「Game」フォルダを「res://」の直下に作り、パスを「res://Game/Game.tscn」として保存
ということで、今のところルートノードだけのシーンツリーだ。
いつものように「Level1」シーンをインスタンス化して「Game」ルートノードに追加したいところだが、今回それはシーンドック上では行わない。今回は「Level1」などのレベルシーンの追加(または削除)は全てスクリプト上で行う。
次に「Game」ルートノードにスクリプトを新規でアタッチする。ファイルパスは「res://Game/Game.gd」として作成する。スクリプトのコードは以下のように記述しよう。
extends Node # Added @ Part 7
# あとでレベルシーンのインスタンスノードの参照を代入予定のプロパティ、型だけ定義
var level: Node2D
# 現在のレベル
export var current_level = 1
# 最後のレベル(今のところ Level2 が最後なので 2)
export var final_level = 2
func _ready():
# ゲーム開始時にこのメソッドを実行、メソッドはこのあと定義する
add_level()
# 現在のレベル数に応じたレベルシーンのインスタンスを Game ルートノードの子として追加するメソッド
func add_level():
# レベルシーンのインスタンスを参照
level = load("res://Levels/Level" + str(current_level) + ".tscn").instance()
# レベルシーンのインスタンスの tree_exited シグナルをこのあと定義する change_level メソッドに接続
level.connect("tree_exited", self, "change_level")
# Game ルートノードにレベルシーンのインスタンスノードを追加
add_child(level)
# レベルシーンのインスタンスノード解放時に発信される tree_exited シグナルによって呼ばれるメソッド
func change_level():
print("change_level() called.")
# もし現在のレベルが最後のレベルより低かったら
if current_level < final_level:
# デバッグ用
print("change to next level.")
# 現在のレベルシーンを解放
level.queue_free()
# 現在のレベルを一つ上げる
current_level += 1
# 次のレベルシーンを子ノードとして Game ルートノードに追加
add_level()
# もし現在のレベルが最後のレベルだったら
else:
# デバッグ用
print("Game Clear! Congrats!")
# ゲームを終了
get_tree().quit()
最後に定義しているchange_level
メソッドの内容をざっくり説明すると、まだ最後のレベルに達していなければ次のレベルに遷移し、最後のレベルをクリアした場合はゲームを終了する、という内容になっている。
ところで「Level1.gd」スクリプトはもはや「Level1」ノードだけのものではなくなった。ということで、このタイミングでついでに「Level1.gd」スクリプトの名前を変更しておこう。ファイルシステムドックで「res://Levels/Level1.gd」を右クリックし、「名前を変更」を選択して変更する。わかりやすく「Level.gd」にしておこう。
次に、プレイヤーキャラクターがエンドポイントのカップに入ったら次のレベルに遷移するコードを記述する。「Endpoint.gd」スクリプトを開いて、以下の「# 削除」と「# 追加」の行を更新しよう。
extends Area2D # Added @ Part 7
# 途中省略
func _on_EndPoint_body_entered(body):
if body.name == "Player":
anim_player.play("clear")
yield(anim_player, "animation_finished")
particle.emitting = true
body.anim_player.play("clear")
yield(body.anim_player, "animation_finished")
#print("Moving to the next level!") # 削除
# 親ノードである EndPoint ルートノードを解放する(この時 tree_exited シグナルが呼ばれる)
get_parent().queue_free() # 追加
get_parent
メソッドは、このスクリプトがアタッチされいるノードの親ノードを取得する。ここではすなわち「EndPoint」ノードの親なので「Level1」などのレベル数に応じたレベルシーンのルートノードがそれに当たる。その親ノードのqueue_free
メソッドを呼び出しているわけだ。
ということで、レベルクリアした時の最後の処理をまとめておこう。
- プレイヤーキャラクターがエンドポイントのカップに入る
- 「Level1」などその時のレベル数に応じたレベルシーンのルートノードが解放(削除)される。
- 「Game」シーンツリーから「Level_」ノードが消える
- レベルシーンの「tree_exited」シグナルが発信される
- シグナルが接続されている「Game.gd」スクリプトの
change_level
メソッドが呼ばれる - まだ最後のレベルでなければ次のレベルに遷移し、最後のレベルならゲームを終了する
これでようやく今回のチュートリアルのテーマである「次のレベルへの遷移」が実装できた。遷移の動作確認だけしやすいように、「EndPoint」ノードをプレイヤーキャラクターが登場する位置の近くに追加で配置しておこう。
動作確認の前にプロジェクトのメインシーンの設定を変更する。「プロジェクト」メニュー>「プロジェクト設定」を開き、「一般」タブ>「Application>Run」で、「Main Scene」を「res://Game/Game.tscn」に変更して閉じよう。
メインシーンを変更できたら、プロジェクトを実行してみよう。
「Level1」から「Level2」への遷移は想定通りの動きが確認できた。また「Level2」をクリアした時は最後のレベルなので、ゲームが終了してデバッグパネルが閉じた。これも問題ない。デバッグ用に配置したチェックポイントとエンドポイントは本番用の状態に戻しておこう。
なお「Level1.tscn」は本番用として以下の配置にしている。
レベルの遷移の動作確認は終えているので、通しでの確認をするかどうかはお好みだが、最初から最後まで問題ないか確認したい方は最後に改めてプロジェクトを実行してみよう。以下のGIF画像は尺が長いので5倍速にしている。
Part 7 で編集したスクリプトのコード
最後に今回の Part 7 で編集したスクリプトのコードを共有しておくので、必要に応じて確認してほしい。
StartPoint.gd の全コード
extends Area2D # Added @ Part 7
onready var sprite = $AnimatedSprite
func _ready():
sprite.play("idle")
func _on_StartPoint_body_entered(body):
if body.name == "Player":
sprite.play("moving")
func _on_StartPoint_body_exited(body):
if body.name == "Player":
sprite.play("idle")
Checkpoint.gd の全コード
extends Area2D # Added @ Part 7
var is_checked = false
onready var sprite = $AnimatedSprite
func _ready():
sprite.play("no_flag")
func _on_Checkpoint_body_entered(body):
if body.name == "Player" and not is_checked:
sprite.play("flag_out")
yield(sprite, "animation_finished")
sprite.play("flag_idle")
is_checked = true
EndPoint.gd の全コード
extends Area2D # Added @ Part 7
onready var particle = $Particles2D
onready var sprite = $AnimatedSprite
onready var polygon = $StaticBody2D/CollisionPolygon2D
onready var anim_player = $AnimationPlayer
func _ready():
particle.emitting = false
sprite.play("idle")
polygon.position = Vector2(0, -4)
func _on_EndPoint_body_entered(body):
if body.name == "Player":
anim_player.play("clear")
yield(anim_player, "animation_finished")
particle.emitting = true
body.anim_player.play("clear")
yield(body.anim_player, "animation_finished")
print("Moving to the next level!")
get_parent().queue_free()
Game.gd の全コード
extends Node # Added @ Part 7
var level: Node2D
export var current_level = 1
export var final_level = 2
func _ready():
add_level()
func add_level():
level = load("res://Levels/Level" + str(current_level) + ".tscn").instance()
level.connect("tree_exited", self, "change_level")
add_child(level)
func change_level():
print("change_level() called.")
if current_level < final_level:
print("change to next level.")
level.queue_free()
current_level += 1
add_level()
else:
print("Game Clear! Congrats!")
get_tree().quit()
おわりに
以上で Part 7 は完了だ。今回は次のレベルへの遷移を実装した。その準備として(一部ついでだが)StartPoint、Checkpoint、EndPoint という新しいシーンを作成した。復習も兼ねて、 Particles2D や AnimationPlayer を使ったレベルクリア時の演出もちょっと凝ってみた。Godot の取り扱いに少しでも慣れていただけたなら嬉しい限りだ。
次回のチュートリアルでは、HUD(ヘッズアップディスプレイ)を実装していく予定だ。これを実装するということは、ライフやスコアの仕組みも実装することになるので、また次も気合を入れて取り組んでいこう。では次回もお楽しみに。
UPDATE:
2022-03-04 プロジェクトのメインシーンを変更する手順を追加