このチュートリアルでは、スマホゲームで大人気の ディズニーツムツムのような同じ色のドロップをなぞってつなげて消すタイプのゲームの作り方を説明する。ちなみにディズニーツムツムを知らない方は以下のリンク先を一度ご覧いただきたい。
Other Tutorials
「パズル&ドラゴンズ」のようなゲームを作ってみたい場合:
Godot で作る進化形マッチ 3 パズルゲーム
「キャンディークラッシュ」のようなゲームを作ってみたい場合:
Godot で作るマッチ 3 パズルゲーム
なお、このチュートリアルで最後にできあがるプロジェクトのファイルは GitHubリポジトリ
に置いている。.zipファイルをダウンロードしていただき、「End」フォルダ内の「project.godot」ファイルを Godot Engine でインポートしていただければ、直接プロジェクトを確認していただくことも可能だ。
Environment
このチュートリアルは以下の環境で作成しました。
・Godot のバージョン: 3.4.4
・コンピュータのOS: macOS 11.6.5
Memo:
ゲームを作り始めるのに以下の記事もお役立てください。
Godot をダウンロードする
Godot のプロジェクトマネージャー
Godot の言語設定
新規プロジェクトを作成する
まずは Godot Engine を立ち上げて、新規プロジェクトを作成してほしい。プロジェクトの名前は「Connect Colors Start」としておこう。
プロジェクト設定を更新する
エディタが表示されたら、先にプロジェクト全体に関わる設定を更新しておこう。
まずはゲームのディスプレイサイズを設定する。今回はスマホの縦向きの画面を想定して、縦横の比率を16:9とした。
- 「プロジェクト」メニュー>「プロジェクト設定」を開く。
- 「一般」タブで「window」で検索して、サイドバーの「Display」>「Window」を選択する。
- 「Size」セクションで以下の項目の値を変更する。
- Width: 144
- Height: 256
- Test Width: 288
- Test Height: 512
- 「Stretch」セクションで以下の項目の値を変更する。
- Mode: 2d
- Aspect: keep
そのまま「プロジェクト設定」ウインドウを開いた状態で、デバッグパネルでスマホのタッチ操作をマウスで代用するための設定をする。
- 「一般」タブで「mouse」で検索し、サイドバーの「Input Devices」>「Pointing」を選択する。
- 「Emulate Touch From Mouse」の On のチェックを入れる。
さらに「プロジェクト設定」ウインドウを開いた状態で、インプットマップにスマホのタッチ操作の相当するアクションを追加しよう。
- 「インプットマップ」タブに切り替え、アクションに「tap」を追加する。
- 「tap」の操作にマウスの左クリックを追加する。
アセットをダウンロードしてインポートする
次に、KENNEYのサイトからアセットをダウンロードして利用させてもらおう。今回利用するのは Pixel Platformer というアセットパックだ。この素晴らしすぎる無料の素材に感謝せずにはいられない。
ダウンロードしたら「Tilemap」フォルダ内の「characters_packed.png」ファイルをエディタのファイルシステムドックへドラッグ&ドロップしてプロジェクトにインポートしよう。
ファイルをインポートした直後は画像がぼやけた感じになっているので、これを以下の手順で修正しておく。
- ファイルシステムドックでインポートしたアセットファイルを選択した状態にする
- インポートドックで「プリセット」>「2D Pixel」を選択する
- 一番下にある「再インポート」ボタンをクリックする。
これでピクセルアート特有のエッジの効いた画像になる。
World シーンを作る
まず最初のシーンとして、ゲームの舞台を用意する。「World」という名前のシーンを作成しよう。
- 「シーン」メニュー>「新規シーン」を選択する。
- 「ルートノードを生成」にて「その他のノード」を選択する。
- 「Node2D」クラスのノードをルートノードとして選択。
- ルートノードの名前を「World」に変更する。
- 一旦ここでシーンを保存しておこう。フォルダを作成して、ファイルパスを「res://World/World.tscn」としてシーンを保存する。
World シーンにノードを追加する
World シーンが以下のシーンツリーになるようにノードを追加しよう。なお、それぞれのプロパティ編集は後で順番にやっていくので一旦そのままで良い。
- World (Node2D)
- Bin (StaticBody2D)
- CollisionPolygon2D
- SpawnPath (Path2D)
- Spawner (PathFollow2D)
- AnimationPlayer
- Drops (Node2D)
- DropsLine (Line2D)
- Pointer (Area2D)
- CollisionShape2D
- Bin (StaticBody2D)
シーンツリードックの表示は以下のようになったはずだ。
World シーンのノードを編集する
Bin (StaticBody2D) ノード
このノードの編集は不要だ。「StaticBody2D」は 2D ゲームにおいて、移動しない障害物や壁などに利用される。今回は落ちてくるドロップ(なぞって消すオブジェクト)を画面内にとどめるための入れ物(ビン)として利用する。
CollisionPolygon2D ノード
このノードは親ノードの「Bin」にコリジョン形状を付与するための利用する。コリジョン形状は 2D ワークスペース上で点を打って作る。
2D ワークスペースのツールバーでグリッドスナップを有効にする。
基本的に、ウインドウ枠の外側を囲むように点を打ってコリジョン形状を成形する。ただし、上部は、ディスプレイサイズの y 座標 0 より -64px ずらして成形する。理由は、ウインドウ枠上部外側でドロップを生成して落下させるためだ。また、コリジョンポリゴン下部はディスプレイサイズよりやや内側まで配置し、ドロップが転がるように少し斜めにしている。
SpawnPath (Path2D) ノード
このノードはドロップの生成位置を x 軸方向に常に移動させるために使う。これは、ドロップが毎回画面上部の同じ位置から落ちてくるのを防ぐ役割だ。これをウインドウ枠の上部外側に配置する。
- 2D ワークスペース上で (16, -32) と (128, -32) の2つの点を打って x 軸に並行な直線のパスを作る。
Spawner (PathFollow2D) ノード
このノードは先に編集した「SpawnPath」ノードのパス上を移動するノードだ。このノードを常にパスに沿って往復させ、このノードの位置からドロップを生成するようにする。これによりドロップの落下位置が「SpawnPath」ノードのパスの範囲で常に変化する
- プロパティ「Rotate」をオフにする。
AnimationPlayer ノード
このノードは「Spawner」を「SpawnPath」のパスに沿って常に往復移動させるために利用する。「Spawner」のプロパティ「Unit Offset」は、親ノード「SpawnPath」のパスの開始位置を 0、終点を 1 で表す。つまりこのプロパティを常に 0 ⇄ 1 で変化させれば、パス上を往復させることができる。
- 以下の通りにアニメーションを一つ作成する。
- アニメーション名: move_spawn_pos
- 読み込み後、自動再生: 有効
- アニメーションの長さ(秒): 0.4
*0.4 秒でパスを往復する - アニメーションループ: 有効
- トラック:
- Spawner ノード - unit_offset プロパティ
- Time: 0 / Value: 0 / Easing: 1.00
- Time: 0.2 / Value: 1 / Easing: 1.00
*0.2秒時点でパスの終点から折り返す
- Spawner ノード - unit_offset プロパティ
Drops (Node2D) ノード
このノードのプロパティ編集は不要だ。役割は、ドロップシーン(後ほど作成)から複数生成するインスタンスをまとめるただの入れ物だ。
DropLine (Line2D) ノード
このノードは同じ色のドロップをなぞった時に、そのドロップとドロップをつなぐ線を描画するために利用する。これにより、どのドロップをなぞってきたのか、いくつつながっているのか、が視覚的に確認しやすくなる。
プロパティ「Width」を 2 に変更する。これは線の太さだ。
プロパティ「Capping」>「Joint Mode」、「Begin Cap Mode」、「End Cap Mode」をそれぞれ「Round」に変更する。これにより、線の繋ぎ目、先端、終端の形状を丸くすることができる。
Pointer (Area2D) ノード
このノードの編集は不要だ。目的としては、スマホなら指、PCならマウスカーソルにこのノードを追随させて、ドロップに触っていることを検知するために利用する。のちほど、このノードの位置を常に指またはマウスカーソルの位置と同じにするためのコードをスクリプトに記述する。
CollisionShape2D ノード
このノードは、親ノード「Pointer」にコリジョン形状を付与する。指やマウスカーソルでのドロップをタッチする操作を考慮して、コリジョンはできるだけ小さい形状にする。
- プロパティ「Shape」にリソース「新規 CircleShape2D」を適用する。
- 適用したリソース「CircleShape2D」のプロパティ「Radius」の値を 1 にする。
以上で各ノードの編集は完了だ。
Drop シーンを作る
ここからは同じ色をなぞって消す対象となる「Drop」シーンを作っていく。
- 「シーン」メニュー>「新規シーン」を選択する。
- 「ルートノードを生成」にて「その他のノード」を選択する。
- 「RigidBody2D」クラスのノードをルートノードとして選択。
- ルートノードの名前を「Drop」に変更する。
- 一旦ここでシーンを保存しておこう。フォルダを作成して、ファイルパスを「res://Drops/Drop.tscn」としてシーンを保存する。
Drop シーンにノードを追加する
Drop シーンが以下のシーンツリーになるようにノードを追加しよう。なお、それぞれのプロパティ編集は後で順番にやっていくので一旦そのままで良い。
- Drop (RigidBody2D)
- Sprite
- CollisionShape2D
- PointableArea (Area2D)
- CollisionShape2D
- AnimationPlayer
- StickableArea (Area2D)
- CollisionShape2D
シーンツリードックの表示は以下のようになったはずだ。
Drop シーンのノードを編集する
Drop (RigidBody2D) ルートノード
「Drop」シーンには、そのインスタンスを「World」シーンに追加した時、自動的に重力に従って落下したり、バウンドしたりしてもらいたい。そのような物理演算による動きをノードのプロパティに合わせて自動的に再現してくれるのが RigidBody2D クラスだ。
- インスペクタードックにて、プロパティ「Physics Material Override」に新規「PhysicsMaterial」リソースを適用する。
- プロパティ「Gravity Scale」の値を 2 にする。ドロップの落下速度を少し速くするのが目的だ。
- ノードドック>グループタブを開き、「Drops」という名前のグループを作って追加する。これはスクリプトでの条件分岐処理で重要だ。
Sprite ノード
このノードで「Drop」にテクスチャ(見た目)を付与する。冒頭でインポートしたたくさんのテクスチャをまとめた1枚のスプライトシートから使いたいテクスチャの範囲を指定してスプライトのテクスチャを設定する方法を採用する。
- インスペクターにて、プロパティ「Texture」にファイルシステムからリソース「res://characters_packed.png」をドラッグして適用する。
- 「Region」>「Enabled」をオンにする。
- エディタ下部のテクスチャ領域パネルを開く。ここで行うのはスプライトシートの中の利用したいテクスチャの領域を指定する作業だ。
- 作業しやすいように、展開アイコンをクリックしてパネルを広げる。
- パネル上部の「snapモード」で「グリッドスナップ」を選択する。
- パネル上部の「ステップ」を 24px 24px にする。これでグリッドのサイズがスプライトシートのテクスチャ 1 つ分と同じサイズになる。
- スプライトシート上でドラッグ操作により緑色のドロップ(見た目はエイリアンだが)の2種類のテクスチャを範囲選択する。
- 作業しやすいように、展開アイコンをクリックしてパネルを広げる。
- インスペクターに戻り、「Animation」>「Hframes」プロパティの値を 2 に変更する。
CollisionShape2D ノード (ルートノード Drop の子)
このノードはルートノード「Drop」にコリジョン形状を提供する。ルートノードは「RigidBody2D」クラスで、物理ボディの1つだ。物理ボディ同士の衝突判定にはコリジョン設定が必須だ。このコリジョン形状により、複数の「Drop」シーンのインスタンスが画面上でお互いにぶつかり合って、積み上がっていくことを想定している。
- プロパティ「Shape」に新規「CircleShape2D」リソースを適用する。
- さらにそのリソースのプロパティ「Radius」の値を 12 に変更する。これで半径 12 px の円形のコリジョン形状ができた。2D ワークスペースで直感的にサイズ調整しても構わない。
PointableArea (Area2D) ノード
このノードは、指またはマウスカーソルがそのドロップに触れたことや離れたことを検知するために利用する。プロパティの編集は不要だが、グループの追加が必要だ。
- ノードドック>グループタブを開き、「Pointable」という名前のグループを作って追加する。これは指またはマウスカーソルがドロップに触れているかどうかを判定する際に利用する。
CollisionShape2D (PointableArea の子)ノード
このノードは、親の「PointableArea」にコリジョン形状を提供する。指やマウスカーソルがドロップの端に触っただけでは反応しないように、ルートノード「Drop」のコリジョン形状よりやや内側に収まるようにする。
- プロパティ「Shape」に新規「CircleShape2D」リソースを適用する。
- さらにそのリソースのプロパティ「Radius」の値を 10 に変更する。ルートノード「Drop」のコリジョン形状よりひと回り小さくした。隣接するドロップ同士でこのコリジョン形状と重なってしまうと、ドロップから指が離れたことを検出される前に隣のドロップとの接触が検出されるため、スクリプトでの制御が難しくなってしまうのだ。2D ワークスペースで直感的にサイズ調整しても構わない。
*このノードのコリジョン形状は内側の円
AnimationPlayer ノード
ここではまず先に、指やマウスカーソルが触れていない時のドロップの待機中のアニメーションと、触れた後にそれがわかるように点滅するアニメーションを作成する。このノードは、作成したそれらのアニメーションリソースを再生するのに利用する。
- 以下の通りに、ドロップの待機中のアニメーションを作成する。
- アニメーション名: idle
- 読み込み後、自動再生: 有効
- アニメーションの長さ(秒): 1
- アニメーションループ: 有効
- トラック:
- Sprite ノード - frame プロパティ
- Time: 0 / Value: 0 / Easing: 1.00
- Time: 0.5 / Value: 1 / Easing: 1.00
- Sprite ノード - modulate プロパティ
- Time: 0 / Value: #ffffff / Easing: 1.00
*「flash」アニメーションでmodulateが変更された後、「idle」アニメーション再生時に確実に初期値に戻すためのトラック
- Time: 0 / Value: #ffffff / Easing: 1.00
- Sprite ノード - frame プロパティ
- 以下の通りに、ドロップの待機中のアニメーションを作成する。
- アニメーション名: flash
- 読み込み後、自動再生: 無効
- アニメーションの長さ(秒): 0.2
- アニメーションループ: 有効
- トラック:
- Sprite ノード - frame プロパティ
- Time: 0 / Value: 0 / Easing: 1.00
- Time: 0.1 / Value: 1 / Easing: 1.00
- Sprite ノード - modulate プロパティ
- Time: 0 / Value: #ffffff / Easing: 1.00
- Time: 0.1 / Value: #64ffffff / Easing: 1.00
- Sprite ノード - frame プロパティ
StickableArea (Area2D) ノード
このノードは、ドロップが他のドロップと接触しているかどうかを検知するために利用する。プロパティの編集は不要だが、グループの追加が必要だ。
- ノードドック>グループタブを開き、「Stickable」という名前のグループを作って追加する。これはドロップを指でなぞった時に「隣接している」 = 「つなげるか」かどうかを判定するのに重要だ。
CollisionShape2D ノード
このノードは、親の「StickableArea」にコリジョン形状を提供する。隣接しているドロップを検知するのに利用する。隣り合っているドロップ同士の接触を検知させるために、ルートノード「Drop」のコリジョン形状よりやや大きめにする。
- プロパティ「Shape」に新規「CircleShape2D」リソースを適用する。
- さらにそのリソースのプロパティ「Radius」の値を 18 に変更する。これで半径 18 px の円形のコリジョン形状ができた。2D ワークスペースで直感的にサイズ調整しても構わない。
*このノードのコリジョン形状は一番外側の円
以上で各ノードの編集は完了だ。
Drop シーンをスクリプトで制御する
それではルートノード「Drop」に新規スクリプトをアタッチしよう。ファイルパスを「res://Drops/Drop.gd」としてスクリプトファイルを作成する。
ひとまずスクリプトを以下のように編集してほしい。
###Drop.gd###
extends RigidBody2D
# Drop シーンを継承するシーンにそれぞれの色の名前を割り当てるプロパティ
# 今はブランクの文字列
export var color = ""
# 隣接するドロップを入れる配列を stuck_drop と定義
var stuck_drops = []
# AnimationPlayerノードへの参照
# Drop のインスタンスを World シーンに追加してから利用する
onready var anim_player = $AnimationPlayer
次に、「StickableArea」ノードの Area2D ノードのシグナルを利用する。隣接するドロップと接触した時に発信されるシグナル「area_entered」と、接触していたドロップが離れた時に発信されるシグナル「area_exited」をスクリプトに接続しよう。
それぞれのシグナルを接続したときに生成されるメソッドを以下のように編集してほしい。
###Drop.gd###
# StickableArea に他の area (Area2D クラスのノード)が当たった時に呼ばれるメソッド
func _on_StickableArea_area_entered(area):
# もし当たった area が Stickable グループのノードだったら
if area.is_in_group("Stickable"):
# その親ノードを drop と定義
var drop = area.get_parent()
# 配列 stuck_drops に drop を追加
stuck_drops.append(drop)
# StickableArea から他の area (Area2D クラスのノード) が離れた時に呼ばれるメソッド
func _on_StickableArea_area_exited(area):
# もし当たった area が Stickable グループのノードだったら
if area.is_in_group("Stickable"):
# その親ノードを drop と定義
var drop = area.get_parent()
# 配列 stuck_drops の中の drop の index を調べる
var index = stuck_drops.find(drop)
# 配列 stuck_drops から index に該当する要素(隣接していた Drop)を削除
stuck_drops.remove(index)
これで「Drop.gd」の編集は完了だ。
Drop シーンを継承したシーンを作る
さっき作った「Drop」シーンはこれから作るシーンの雛形だ。これから「Drop」シーンを継承したシーンをドロップの色の数だけ作成する。ドロップの色は、青、緑、オレンジ、赤、黄の 5 色だ。まずは青のドロップを例に手順を進めてみよう。
- 「シーン」メニュー>「新しい継承シーン」を選択する。
- 継承元のシーンとして「Drops.tscn」を選択する。
- シーンが生成されたら、ルートノードの名前を「Blue」に変更する。
*このルートノードの名前はそれぞれのドロップの色に合わせること。 - シーンを一旦保存しておこう。ファイルパスを「res://Drops/BlueDrop.tscn」として保存する。
- シーンツリードックでルートノード「BlueDrop」を選択した状態で、インスペクターで Script Variables の「Color」の値を「Blue」とする。
- シーンツリードックで「Sprite」ノードを選択する。エディタ下部の「テクスチャ領域」パネルを開き、青いエイリアンのテクスチャ2つ分を選択しよう。
以上で、「BlueDrop」シーンは完成だ。同じ手順で残りの 4 色のシーンも作成してほしい。なお、シーンのルートノードの名前とそのプロパティ「Color」の値は以下の通りだ。
- ルートノード: GreenDrop / Color: Green
- ルートノード: OrangeDrop / Color: Orange
- ルートノード: RedDrop / Color: Red
- ルートノード: YellowDrop / Color: Yellow
全部で 5 色のドロップの継承シーンができたら作業完了だ。
World シーンをスクリプトで制御する
いよいよ今回のチュートリアルも終盤に差し掛かった。では「World」シーンのルートノードにスクリプトをアタッチしよう。ファイルパスは「res://World/World.tscn」として作成する。
スクリプトエディタが開いたら、まずは以下の通りにプロパティを定義する。
###World.gd###
extends Node2D
# preloadした5色のドロップシーンを要素にもつ配列を drop_scenes として定義
const drop_scenes = [
preload("res://Drops/BlueDrop.tscn"),
preload("res://Drops/GreenDrop.tscn"),
preload("res://Drops/OrangeDrop.tscn"),
preload("res://Drops/RedDrop.tscn"),
preload("res://Drops/YellowDrop.tscn")
]
# つなげて消せる最小ドロップ数
export (int) var min_erasable = 3
# プレイ画面に表示される最大ドロップ数
export (int) var max_drops = 50
# 現在ゲームをプレイ中の場合は true になる
var is_playing = false
# 指を画面に当てたまま or マウス左クリックを押したままの場合 true になる
var is_holding = false
# 現在指またはマウスカーソルが当たっているドロップの参照用
var pointed_drop
# 現在つなげているドロップの色
var active_color = ""
# ホールド中の(なぞってつながっている)ドロップのリスト用の配列
var held_drops = []
# Spawnerノードへの参照
onready var spawner = $SpawnPath/Spawner
# Dropsノードへの参照
onready var drops = $Drops
# DropsLineノードへの参照
onready var drops_line = $DropsLine
# Pointerノードへの参照
onready var pointer = $Pointer
次にゲーム開始直後に画面外上部からドロップが最大ドロップ数の 50 個落ちてくるようコーディングしていこう。
###World.gd###
# World シーンのノードが全て読み込まれたら呼ばれるメソッド
func _ready():
# ランダム系のメソッドの出力結果を毎回ランダムにしてくれる組み込みメソッド
randomize()
# max_drops の数 (50) だけループする
for _i in range(max_drops):
# ドロップを生成するメソッド(このあと定義)を呼び出す
spawn_drop()
# 一つのドロップが生成されたら0.025秒待機して次のドロップ生成
yield(get_tree().create_timer(0.025), "timeout")
# ドロップを生成するメソッド
func spawn_drop():
# 配列 drop_scenes からランダムで選ばれた色のドロップのシーンファイルの参照
var drop_scene = drop_scenes[randi() % drop_scenes.size()]
# 選択された色のドロップシーンをインスタンス化する
var drop = drop_scene.instance()
# ドロップのインスタンスの位置を Spawner ノードの位置と同じにする
drop.position = spawner.global_position
# ドロップのインスタンスを World シーンに追加する
drops.add_child(drop)
それではプロジェクトを実行して、ゲーム開始時のランダムに決定された色のドロップが50個降ってくる挙動を見ておこう。なお、初めてプロジェクトを実行する際はメインシーンを「World.tscn」としておこう。
「Pointer (Area2D)」ノードの位置は、指、またはマウスカーソルの位置に追随させるようにこのあとコーディングするのだが、その時、指やマウスカーソルが「Drop」インスタンスの「PointableArea (Area2D)」と重なった時とそのあと離れた時に「Pointer」ノードがそれを検知してシグナルを発信する。それを利用して、ドロップをなぞった時の処理をコーディングしていこう。
シーンツリードックで「Pointer」を選択し、ノードドック>シグナルタブにて、シグナル「area_entered(area: Area2D)」とシグナル「area_exited(area: Area2D)」をスクリプトに接続する。
自動的に生成されたメソッドを以下のように編集しよう。
###World.gd###
# Pointer ノードが他の area(Area2D オブジェクト)に触れたら呼ばれるメソッド
func _on_Pointer_area_entered(area):
# area が「Pointable」グループのノードだったら
if area.is_in_group("Pointable"):
# pointed_drop に area の親ノード(Drop ノード)の参照を渡す
pointed_drop = area.get_parent()
# ホールド中のドロップが 0 ではなく..
# かつホールド中の最後のドロップが pointed_drop と隣接していたら
if not held_drops.empty() and held_drops[-1] in pointed_drop.stuck_drops:
# ドロップのつながりを更新するメソッドを呼ぶ(あとで定義)
update_drops_connection()
# Pointer ノードが触れていた area(Area2D オブジェクト)が離れたら呼ばれるメソッド
func _on_Pointer_area_exited(area):
# area が「Pointable」グループのノードだったら
if area.is_in_group("Pointable"):
# pointed_drop を null にする
pointed_drop = null
次は組み込み関数_process
を利用して、毎フレーム(60FPS)呼び出したいメソッドを実行する。
###World.gd###
# 組み込み関数: 60FPSで呼び出される
func _process(_delta):
# DropsLine ノードの Points プロパティを更新する
update_drops_line()
# 指またはマウスカーソルの操作を受けつる
get_input()
# DropsLine ノードの Points プロパティを更新するメソッド
# ドロップが転がったり落ちたりして位置が変わるため
func update_drops_line():
# ホールド中のドロップが 1 つでもある場合
if not held_drops.empty():
# テンポラリのVector2配列を作成
var temp_array = PoolVector2Array()
# ホールド中のドロップに対してループ
for drop in held_drops:
# テンポラリの配列にホールド中のドロップの位置を追加
temp_array.append(drop.position)
# DropsLine ノードの points プロパティを現在ホールド中のドロップの位置に更新
drops_line.points = temp_array
# 指またはマウスの入力があったら処理するメソッド
func get_input():
# Pointer ノードの位置を常に指またはマウスカーソルの位置にする
pointer.position = get_global_mouse_position()
# もし指で画面を押したら、もしくはマウス左ボタンを押したら
if Input.is_action_just_pressed("tap"):
# ドロップをホールドするメソッド(あとで定義)を呼び出す
hold_drop()
# ホールド中のドロップのつながりを更新するメソッド(あとで定義)を呼び出す
update_drops_connection()
# もし指が画面から離れたら、もしくはマウス左ボタンが上がったら
if Input.is_action_just_released("tap"):
# ホールド中のドロップを消すメソッド(あとで定義)を呼び出す
erase_drops()
# ホールドを解除するメソッド(あとで定義)を呼び出す
release_drops()
上のコードで定義したメソッドget_input
内で呼び出している以下のメソッドはこの後順番に定義していく。
hold_drop
update_drops_connection
erase_drops
release_drops
まずはドロップを押さえた時に呼び出されるメソッドhold_drop
とupdate_drops_connection
を定義していこう。
###World.gd###
# ドロップをホールド中にするメソッド
func hold_drop():
# もし指またはマウスカーソルがドロップに触れていたら
if pointed_drop:
# ホールド中とする
is_holding = true
# ドロップのつながりを更新するメソッド
func update_drops_connection():
# もしドロップをホールド中かつ..
# 指またはマウスカーソルがドロップに重なっていたら
if is_holding and pointed_drop:
# もしホールド中のドロップが 0 だったら
if held_drops.empty():
# これからつなぐドロップの色を現在指または..
# マウスカーソルが触れているドロップの色とする
active_color = pointed_drop.color
# ドロップをつなぐメソッド(あとで定義)を呼ぶ
connect_drop()
# もし現在指またはマウスカーソルが触れているドロップの色が..
# つないでいるドロップの色と同じだったら
elif pointed_drop.color == active_color:
# ホールド中のドロップの数が 2 以上かつ現在触れている..
# ドロップがホールド中のドロップの最後から2番目と同じだったら
if held_drops.size() >= 2 and pointed_drop == held_drops[-2]:
# つながりを解除するメソッド(あとで定義)を呼ぶ
disconnect_drop()
# ホールド中のドロップの中に現在指またはマウスカーソルが..
# 触れているドロップがなければ
elif not pointed_drop in held_drops:
# ドロップをつなぐメソッド(あとで定義)を呼ぶ
connect_drop()
ここで定義したメソッドupdate_drops_connection
内を見ると、さらに未定義のconnect_drop
メソッドとdisconnect_drop
メソッドが呼ばれている。
続けてこれらのメソッドを定義しよう。
###World.gd###
# ドロップをつなぐメソッド
func connect_drop():
# 現在指またはマウスカーソルが触れているドロップのAnimationPlayerで..
# アニメーション"flash"を再生する
pointed_drop.anim_player.play("flash")
# ホールド中のドロップリストに現在触れているドロップを追加する
held_drops.append(pointed_drop)
# DropsLine ノードの Points プロパティに現在触れているドロップの位置を追加する
drops_line.add_point(pointed_drop.position)
# ドロップのつながりを解除するメソッド
func disconnect_drop():
# ホールド中のドロップのリストの最後のドロップを canceled_drop とする
var canceled_drop = held_drops.pop_back()
# canceled_drop の AnimationPlayer でアニメーション("flash")を停止する
canceled_drop.anim_player.stop()
# canceled_drop の AnimationPlayer でアニメーション("idle")を再生する
canceled_drop.anim_player.play("idle")
# DropsLineノードの Points プロパティから最後の点を削除する
drops_line.remove_point(drops_line.get_point_count() - 1)
get_input
メソッド内で、指が画面から離れたら、またはマウス左ボタンが上がった時に呼び出される2つのメソッドerase_drops
、release_drops
をこれから定義していく。
###World.gd###
# ドロップを消すメソッド
func erase_drops():
# もしホールド中のドロップの数がつなげて消せる最小ドロップ数未満だったら
if held_drops.size() < min_erasable:
# メソッドを即時終了する
return
# ホールド中のドロップの配列を変数 erased の値として複製
var erased = held_drops.duplicate()
# 配列 erased の要素に対してループ処理
for drop in erased:
# 配列 erased に含まれるドロップを解放する
drop.queue_free()
# 消した分、新しいドロップを生成する
spawn_drop()
# 0.1 秒待機(それから次のループ)
yield(get_tree().create_timer(0.1), "timeout")
# ドロップからホールドを解放するメソッド
func release_drops():
# ホールド中ステータスを解除
is_holding = false
# ホールド中のドロップの配列の要素対してループ
for drop in held_drops:
# 配列から取り出したドロップの AnimationPlayer で..
# アニメーションを停止する
drop.anim_player.stop()
# 配列から取り出したドロップの AnimationPlayer で..
# アニメーション"idle"を再生する
drop.anim_player.play("idle")
# ホールド中のドロップの配列を空っぽにする
held_drops.clear()
# DropsLine ノードの Points プロパティを空っぽにする
drops_line.clear_points()
以上で「World.gd」スクリプトの編集は完了だ。
シーンを実行して動作確認する
最後にシーンを実行して思った通りの動きが再現できるか確認してみよう。
以下について想定通りであることが確認できただろうか。
- マウスカーソルがドロップの中央付近にある状態でマウス左ボタンを押すとドロップがホールド状態になり「flash」アニメーションが再生される
- マウス左ボタンを押したまま、隣接するドロップをなぞっていくと「DropsLine」の線がつながっていく
- なぞってきたドロップを戻ってなぞり直すとホールドが解除され「idle」アニメーションの再生に戻る
- なぞったドロップが3つ以上だとマウス左ボタンを離したときにホールド中のドロップが全て消える
- なぞったドロップが3つ未満だとマウス左ボタンを離してもドロップは消えずホールド解除のみされる
サンプルゲーム
今回のチュートリアルで作成したプロジェクトをさらにブラッシュアップしたサンプルゲームを用意した。
プロジェクトファイルは、GitHubリポジトリ に置いているので、そこから .zip ファイルをダウンロードしていただき、「Sample」フォルダ内の「project.godot」ファイルを Godot Engine でインポートすれば確認していただけるはずだ。
おわりに
今回のチュートリアルでは同じ色をなぞって消すパズルゲームを作った。ゲームの中毒性を感じずにはいられない種類のゲームだ。最後に作成におけるポイントをまとめておこう。
- ドロップは RigidBody2D にして、エンジンに物理演算を任せる。
- ドロップに、指やカーソルを検知するための Area2D クラスのノードと、隣接するドロップを検知するための Area2D クラスのノードを追加して、それらのシグナルを利用する。
- 指やマウスカーソルには Area2D クラスのノードを常に追随させ、ドロップとの接触にはこのノードのシグナルを利用する。
参照
- KENNEY
- Godot Docs: 物理の紹介
- Godot Docs: RigidBody2D
- Godot Docs: Area2Dの使用
- Godot Docs: Area2D
- Godot Docs: CollisionShape2D
- Godot Docs: CollisionPolygon2D
- Godot Docs: Path2D
- Godot Docs: PathFollow2D
- Zenn:【Godot Engine】Path2D/PathFollow2Dを使って決まった経路で動かす方法 | syun77
- Godot Docs: Line2D
- Qiita:【Godot】Line2Dを使ったトレイルの実装方法 | @2dgames_jp
- Godot Docs: Introduction to the animation features
- Godot Docs: AnimationPlayer
UPDATE
2022/06/20 変数holded_drops
のスペルをheld_drops
に修正(GitHubリポジトリ上のコードも修正)