このチュートリアルでは、スマホゲームで大人気の ディズニーツムツムのような同じ色のドロップをなぞってつなげて消すタイプのゲームの作り方を説明する。ちなみにディズニーツムツムを知らない方は以下のリンク先を一度ご覧いただきたい。

LINE:ディズニー ツムツム


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とした。

  1. 「プロジェクト」メニュー>「プロジェクト設定」を開く。
  2. 「一般」タブで「window」で検索して、サイドバーの「Display」>「Window」を選択する。
  3. 「Size」セクションで以下の項目の値を変更する。
    • Width: 144
    • Height: 256
    • Test Width: 288
    • Test Height: 512
      Display - Window - Size
  4. 「Stretch」セクションで以下の項目の値を変更する。
    • Mode: 2d
    • Aspect: keep
      Display - Window - Stretch

そのまま「プロジェクト設定」ウインドウを開いた状態で、デバッグパネルでスマホのタッチ操作をマウスで代用するための設定をする。

  1. 「一般」タブで「mouse」で検索し、サイドバーの「Input Devices」>「Pointing」を選択する。
  2. 「Emulate Touch From Mouse」の On のチェックを入れる。
    Input Devices - Pointing - Emulate Touch From Mouse

さらに「プロジェクト設定」ウインドウを開いた状態で、インプットマップにスマホのタッチ操作の相当するアクションを追加しよう。

  1. 「インプットマップ」タブに切り替え、アクションに「tap」を追加する。
  2. 「tap」の操作にマウスの左クリックを追加する。
    Inputmap - action - tap

アセットをダウンロードしてインポートする

次に、KENNEYのサイトからアセットをダウンロードして利用させてもらおう。今回利用するのは Pixel Platformer というアセットパックだ。この素晴らしすぎる無料の素材に感謝せずにはいられない。

ダウンロードしたら「Tilemap」フォルダ内の「characters_packed.png」ファイルをエディタのファイルシステムドックへドラッグ&ドロップしてプロジェクトにインポートしよう。

ファイルをインポートした直後は画像がぼやけた感じになっているので、これを以下の手順で修正しておく。

  1. ファイルシステムドックでインポートしたアセットファイルを選択した状態にする
    select the asset
  2. インポートドックで「プリセット」>「2D Pixel」を選択する
    select 2D Pixel
  3. 一番下にある「再インポート」ボタンをクリックする。
    click reinport

これでピクセルアート特有のエッジの効いた画像になる。


World シーンを作る

まず最初のシーンとして、ゲームの舞台を用意する。「World」という名前のシーンを作成しよう。

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

World シーンにノードを追加する

World シーンが以下のシーンツリーになるようにノードを追加しよう。なお、それぞれのプロパティ編集は後で順番にやっていくので一旦そのままで良い。

  • World (Node2D)
    • Bin (StaticBody2D)
      • CollisionPolygon2D
    • SpawnPath (Path2D)
      • Spawner (PathFollow2D)
    • AnimationPlayer
    • Drops (Node2D)
    • DropsLine (Line2D)
    • Pointer (Area2D)
      • CollisionShape2D

シーンツリードックの表示は以下のようになったはずだ。
scene tree dock

World シーンのノードを編集する

Bin (StaticBody2D) ノード

このノードの編集は不要だ。「StaticBody2D」は 2D ゲームにおいて、移動しない障害物や壁などに利用される。今回は落ちてくるドロップ(なぞって消すオブジェクト)を画面内にとどめるための入れ物(ビン)として利用する。

CollisionPolygon2D ノード

このノードは親ノードの「Bin」にコリジョン形状を付与するための利用する。コリジョン形状は 2D ワークスペース上で点を打って作る。

  1. 2D ワークスペースのツールバーでグリッドスナップを有効にする。
    enable grid snap

  2. 基本的に、ウインドウ枠の外側を囲むように点を打ってコリジョン形状を成形する。ただし、上部は、ディスプレイサイズの y 座標 0 より -64px ずらして成形する。理由は、ウインドウ枠上部外側でドロップを生成して落下させるためだ。また、コリジョンポリゴン下部はディスプレイサイズよりやや内側まで配置し、ドロップが転がるように少し斜めにしている。
    create collisoin shape

SpawnPath (Path2D) ノード

このノードはドロップの生成位置を x 軸方向に常に移動させるために使う。これは、ドロップが毎回画面上部の同じ位置から落ちてくるのを防ぐ役割だ。これをウインドウ枠の上部外側に配置する。

  1. 2D ワークスペース上で (16, -32) と (128, -32) の2つの点を打って x 軸に並行な直線のパスを作る。
    create path2d path

Spawner (PathFollow2D) ノード

このノードは先に編集した「SpawnPath」ノードのパス上を移動するノードだ。このノードを常にパスに沿って往復させ、このノードの位置からドロップを生成するようにする。これによりドロップの落下位置が「SpawnPath」ノードのパスの範囲で常に変化する

  1. プロパティ「Rotate」をオフにする。
    disable property rotate

AnimationPlayer ノード

このノードは「Spawner」を「SpawnPath」のパスに沿って常に往復移動させるために利用する。「Spawner」のプロパティ「Unit Offset」は、親ノード「SpawnPath」のパスの開始位置を 0、終点を 1 で表す。つまりこのプロパティを常に 0 ⇄ 1 で変化させれば、パス上を往復させることができる。

  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秒時点でパスの終点から折り返す
          create animation

Drops (Node2D) ノード

このノードのプロパティ編集は不要だ。役割は、ドロップシーン(後ほど作成)から複数生成するインスタンスをまとめるただの入れ物だ。

DropLine (Line2D) ノード

このノードは同じ色のドロップをなぞった時に、そのドロップとドロップをつなぐ線を描画するために利用する。これにより、どのドロップをなぞってきたのか、いくつつながっているのか、が視覚的に確認しやすくなる。

  1. プロパティ「Width」を 2 に変更する。これは線の太さだ。
    Line2D Width property

  2. プロパティ「Capping」>「Joint Mode」、「Begin Cap Mode」、「End Cap Mode」をそれぞれ「Round」に変更する。これにより、線の繋ぎ目、先端、終端の形状を丸くすることができる。
    Line2D Capping each property

Pointer (Area2D) ノード

このノードの編集は不要だ。目的としては、スマホなら指、PCならマウスカーソルにこのノードを追随させて、ドロップに触っていることを検知するために利用する。のちほど、このノードの位置を常に指またはマウスカーソルの位置と同じにするためのコードをスクリプトに記述する。

CollisionShape2D ノード

このノードは、親ノード「Pointer」にコリジョン形状を付与する。指やマウスカーソルでのドロップをタッチする操作を考慮して、コリジョンはできるだけ小さい形状にする。

  1. プロパティ「Shape」にリソース「新規 CircleShape2D」を適用する。
  2. 適用したリソース「CircleShape2D」のプロパティ「Radius」の値を 1 にする。
    CollisionShape2D Shape, Radius

以上で各ノードの編集は完了だ。


Drop シーンを作る

ここからは同じ色をなぞって消す対象となる「Drop」シーンを作っていく。

  1. 「シーン」メニュー>「新規シーン」を選択する。
  2. 「ルートノードを生成」にて「その他のノード」を選択する。
  3. 「RigidBody2D」クラスのノードをルートノードとして選択。
  4. ルートノードの名前を「Drop」に変更する。
  5. 一旦ここでシーンを保存しておこう。フォルダを作成して、ファイルパスを「res://Drops/Drop.tscn」としてシーンを保存する。

Drop シーンにノードを追加する

Drop シーンが以下のシーンツリーになるようにノードを追加しよう。なお、それぞれのプロパティ編集は後で順番にやっていくので一旦そのままで良い。

  • Drop (RigidBody2D)
    • Sprite
    • CollisionShape2D
    • PointableArea (Area2D)
      • CollisionShape2D
    • AnimationPlayer
    • StickableArea (Area2D)
      • CollisionShape2D

シーンツリードックの表示は以下のようになったはずだ。
Drop scene tree


Drop シーンのノードを編集する

Drop (RigidBody2D) ルートノード

「Drop」シーンには、そのインスタンスを「World」シーンに追加した時、自動的に重力に従って落下したり、バウンドしたりしてもらいたい。そのような物理演算による動きをノードのプロパティに合わせて自動的に再現してくれるのが RigidBody2D クラスだ。

  1. インスペクタードックにて、プロパティ「Physics Material Override」に新規「PhysicsMaterial」リソースを適用する。
    Physics Material Override
  2. プロパティ「Gravity Scale」の値を 2 にする。ドロップの落下速度を少し速くするのが目的だ。
    Gravity Scale
  3. ノードドック>グループタブを開き、「Drops」という名前のグループを作って追加する。これはスクリプトでの条件分岐処理で重要だ。
    Node dock - groups - Drops

Sprite ノード

このノードで「Drop」にテクスチャ(見た目)を付与する。冒頭でインポートしたたくさんのテクスチャをまとめた1枚のスプライトシートから使いたいテクスチャの範囲を指定してスプライトのテクスチャを設定する方法を採用する。

  1. インスペクターにて、プロパティ「Texture」にファイルシステムからリソース「res://characters_packed.png」をドラッグして適用する。
    Sprite node Texture property
  2. 「Region」>「Enabled」をオンにする。
    Region > Enabled = on
  3. エディタ下部のテクスチャ領域パネルを開く。ここで行うのはスプライトシートの中の利用したいテクスチャの領域を指定する作業だ。
    Region panel
    1. 作業しやすいように、展開アイコンをクリックしてパネルを広げる。
      Expand Region pannel
    2. パネル上部の「snapモード」で「グリッドスナップ」を選択する。
      Region pannel > choose grid snap
    3. パネル上部の「ステップ」を 24px 24px にする。これでグリッドのサイズがスプライトシートのテクスチャ 1 つ分と同じサイズになる。
      Region pannel > input grid step
    4. スプライトシート上でドラッグ操作により緑色のドロップ(見た目はエイリアンだが)の2種類のテクスチャを範囲選択する。
      Select region
  4. インスペクターに戻り、「Animation」>「Hframes」プロパティの値を 2 に変更する。
    animation - hframes property

CollisionShape2D ノード (ルートノード Drop の子)

このノードはルートノード「Drop」にコリジョン形状を提供する。ルートノードは「RigidBody2D」クラスで、物理ボディの1つだ。物理ボディ同士の衝突判定にはコリジョン設定が必須だ。このコリジョン形状により、複数の「Drop」シーンのインスタンスが画面上でお互いにぶつかり合って、積み上がっていくことを想定している。

  1. プロパティ「Shape」に新規「CircleShape2D」リソースを適用する。
  2. さらにそのリソースのプロパティ「Radius」の値を 12 に変更する。これで半径 12 px の円形のコリジョン形状ができた。2D ワークスペースで直感的にサイズ調整しても構わない。
    CollisionShape2D - Shape - CircleShape2D CollisionShape2D - 2DWrokSpace

PointableArea (Area2D) ノード

このノードは、指またはマウスカーソルがそのドロップに触れたことや離れたことを検知するために利用する。プロパティの編集は不要だが、グループの追加が必要だ。

  1. ノードドック>グループタブを開き、「Pointable」という名前のグループを作って追加する。これは指またはマウスカーソルがドロップに触れているかどうかを判定する際に利用する。
    Node dock - Groups - Pointable

CollisionShape2D (PointableArea の子)ノード

このノードは、親の「PointableArea」にコリジョン形状を提供する。指やマウスカーソルがドロップの端に触っただけでは反応しないように、ルートノード「Drop」のコリジョン形状よりやや内側に収まるようにする。

  1. プロパティ「Shape」に新規「CircleShape2D」リソースを適用する。
  2. さらにそのリソースのプロパティ「Radius」の値を 10 に変更する。ルートノード「Drop」のコリジョン形状よりひと回り小さくした。隣接するドロップ同士でこのコリジョン形状と重なってしまうと、ドロップから指が離れたことを検出される前に隣のドロップとの接触が検出されるため、スクリプトでの制御が難しくなってしまうのだ。2D ワークスペースで直感的にサイズ調整しても構わない。
    CollisionShape2D - Shape - CircleShape2DCollisionShape2D - 2DWrokSpace*このノードのコリジョン形状は内側の円

AnimationPlayer ノード

ここではまず先に、指やマウスカーソルが触れていない時のドロップの待機中のアニメーションと、触れた後にそれがわかるように点滅するアニメーションを作成する。このノードは、作成したそれらのアニメーションリソースを再生するのに利用する。

  1. 以下の通りに、ドロップの待機中のアニメーションを作成する。
    • アニメーション名: 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」アニメーション再生時に確実に初期値に戻すためのトラック
          2D Workspace - idle animationAnimation panel - idle animation
  2. 以下の通りに、ドロップの待機中のアニメーションを作成する。
    • アニメーション名: 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
          2D Workspace - flash animationAnimation panel - flash animation

StickableArea (Area2D) ノード

このノードは、ドロップが他のドロップと接触しているかどうかを検知するために利用する。プロパティの編集は不要だが、グループの追加が必要だ。

  1. ノードドック>グループタブを開き、「Stickable」という名前のグループを作って追加する。これはドロップを指でなぞった時に「隣接している」 = 「つなげるか」かどうかを判定するのに重要だ。
    Node dock - Groups - Stickable

CollisionShape2D ノード

このノードは、親の「StickableArea」にコリジョン形状を提供する。隣接しているドロップを検知するのに利用する。隣り合っているドロップ同士の接触を検知させるために、ルートノード「Drop」のコリジョン形状よりやや大きめにする。

  1. プロパティ「Shape」に新規「CircleShape2D」リソースを適用する。
  2. さらにそのリソースのプロパティ「Radius」の値を 18 に変更する。これで半径 18 px の円形のコリジョン形状ができた。2D ワークスペースで直感的にサイズ調整しても構わない。
    CollisionShape2D - Shape - CircleShape2DCollisionShape2D - 2DWrokSpace*このノードのコリジョン形状は一番外側の円

以上で各ノードの編集は完了だ。


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」をスクリプトに接続しよう。
StickableArea - signals

それぞれのシグナルを接続したときに生成されるメソッドを以下のように編集してほしい。

###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 色だ。まずはのドロップを例に手順を進めてみよう。

  1. 「シーン」メニュー>「新しい継承シーン」を選択する。
  2. 継承元のシーンとして「Drops.tscn」を選択する。
  3. シーンが生成されたら、ルートノードの名前を「Blue」に変更する。
    *このルートノードの名前はそれぞれのドロップの色に合わせること。
  4. シーンを一旦保存しておこう。ファイルパスを「res://Drops/BlueDrop.tscn」として保存する。
  5. シーンツリードックでルートノード「BlueDrop」を選択した状態で、インスペクターで Script Variables の「Color」の値を「Blue」とする。
    BlueDrop - Color property
  6. シーンツリードックで「Sprite」ノードを選択する。エディタ下部の「テクスチャ領域」パネルを開き、青いエイリアンのテクスチャ2つ分を選択しよう。
    Sprite - Texture Region

以上で、「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」としておこう。
run project


「Pointer (Area2D)」ノードの位置は、指、またはマウスカーソルの位置に追随させるようにこのあとコーディングするのだが、その時、指やマウスカーソルが「Drop」インスタンスの「PointableArea (Area2D)」と重なった時とそのあと離れた時に「Pointer」ノードがそれを検知してシグナルを発信する。それを利用して、ドロップをなぞった時の処理をコーディングしていこう。

シーンツリードックで「Pointer」を選択し、ノードドック>シグナルタブにて、シグナル「area_entered(area: Area2D)」とシグナル「area_exited(area: Area2D)」をスクリプトに接続する。
connect signals

自動的に生成されたメソッドを以下のように編集しよう。

###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_dropupdate_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_dropsrelease_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」スクリプトの編集は完了だ。



シーンを実行して動作確認する

最後にシーンを実行して思った通りの動きが再現できるか確認してみよう。
run project finally

以下について想定通りであることが確認できただろうか。

  • マウスカーソルがドロップの中央付近にある状態でマウス左ボタンを押すとドロップがホールド状態になり「flash」アニメーションが再生される
  • マウス左ボタンを押したまま、隣接するドロップをなぞっていくと「DropsLine」の線がつながっていく
  • なぞってきたドロップを戻ってなぞり直すとホールドが解除され「idle」アニメーションの再生に戻る
  • なぞったドロップが3つ以上だとマウス左ボタンを離したときにホールド中のドロップが全て消える
  • なぞったドロップが3つ未満だとマウス左ボタンを離してもドロップは消えずホールド解除のみされる


サンプルゲーム

今回のチュートリアルで作成したプロジェクトをさらにブラッシュアップしたサンプルゲームを用意した。


プロジェクトファイルは、GitHubリポジトリ に置いているので、そこから .zip ファイルをダウンロードしていただき、「Sample」フォルダ内の「project.godot」ファイルを Godot Engine でインポートすれば確認していただけるはずだ。



おわりに

今回のチュートリアルでは同じ色をなぞって消すパズルゲームを作った。ゲームの中毒性を感じずにはいられない種類のゲームだ。最後に作成におけるポイントをまとめておこう。

  • ドロップは RigidBody2D にして、エンジンに物理演算を任せる。
  • ドロップに、指やカーソルを検知するための Area2D クラスのノードと、隣接するドロップを検知するための Area2D クラスのノードを追加して、それらのシグナルを利用する。
  • 指やマウスカーソルには Area2D クラスのノードを常に追随させ、ドロップとの接触にはこのノードのシグナルを利用する。


参照


UPDATE
2022/06/20 変数holded_dropsのスペルをheld_dropsに修正(GitHubリポジトリ上のコードも修正)