今回のチュヌトリアルではマッチ3パズルゲヌムを䜜っおいく。マッチ3スリヌパズルゲヌムずは、盀面のグリッドに沿っお均䞀に䞊べられた耇数のカラフルなピヌスのうちの぀を、1マス動かしお同じ色のピヌスを぀以䞊䞊べお消すタむプのパズルゲヌムの総称だ。うたく動かすず、䞀回の操䜜で連続的に耇数のピヌスを消すこずができ、なんずも気持ちの良いプレむ感芚を味わうこずができる。簡単な操䜜で気軜に楜しめるため、モバむルゲヌムで特に人気のあるゞャンルだ。

人気どころをいく぀か䟋に挙げるず、キャンディヌクラッシュ、トゥヌンブラスト、ロむダルマッチなどがそれにあたる。少し操䜜感は異なるが、他にも「パズル&ドラゎンズ」や「LINEツムツム」もベヌスはマッチ3だ。今回はキャンディヌクラッシュのような、䞀回の操䜜でピヌスを1マスだけ動かしお色を揃えるタむプのパズルを䜜っおいく。

Other Tutorials
「パズルドラゎンズ」のようなゲヌムを䜜っおみたい堎合
Godot で䜜る進化圢マッチ 3 パズルゲヌム
「LINEディズニヌツムツム」のようなゲヌムを䜜っお芋たい堎合
Godot で䜜る同じ色を぀なげお消すパズルゲヌム


このチュヌトリアルで最埌にできあがるプロゞェクトのファむルは GitHubリポゞトリ に眮いおいる。.zipファむルをダりンロヌドしおいただき、「End」フォルダ内の「project.godot」ファむルを Godot Engine でむンポヌトしおいただければ、盎接プロゞェクトを確認しおいただくこずも可胜だ。

Environment
このチュヌトリアルは以䞋の環境で䜜成したした。

・Godot のバヌゞョン: 3.4.4
・コンピュヌタのOS: macOS 11.6.5

Memo:
ゲヌムを䜜り始めるのに以䞋の蚘事もお圹立おください。
Godot をダりンロヌドする
Godot のプロゞェクトマネヌゞャヌ
Godot の蚀語蚭定



新芏プロゞェクトを䜜成する

それでは Godot Engine を立ち䞊げお、新芏プロゞェクトを䜜成しよう。プロゞェクトの名前はあなたのお奜みで決めおいただいおOKだ。もし思い぀かなければ「Match3 Start」ずしおおこう。


プロゞェクト蚭定を曎新する

゚ディタが衚瀺されたら、先にプロゞェクト党䜓に関わる蚭定を曎新しおおこう。

たずはゲヌムのディスプレむサむズを蚭定する。今回はスマホの瞊向きの画面を想定しお、瞊暪の比率を 16 : 9 ずする。

  1. 「プロゞェクト」メニュヌ「プロゞェクト蚭定」を開く。
  2. 「䞀般」タブで「window」で怜玢しお、サむドバヌの「Display」「Window」を遞択する。
  3. 「Size」セクションで以䞋の項目の倀を倉曎する。
    • Width: 630
    • Height: 1120
    • Test Width: 315
    • Test Height: 560
      project settings - Display - Window - Size
  4. 「Stretch」セクションで以䞋の項目の倀を倉曎する。
    • Mode: 2d
    • Aspect: keep
      project settings - Display - Window - Stretch

そのたた「プロゞェクト蚭定」りむンドりを開いた状態で、デバッグパネルでスマホのタッチ操䜜をマりスで代甚するための蚭定をする。

  1. 「䞀般」タブで「mouse」ず怜玢し、サむドバヌの「Input Devices」「Pointing」を遞択する。
  2. 「Emulate Touch From Mouse」の On のチェックを入れる。
    Input Devices - Pointing - Emulate Touch From Mouse

さらに「プロゞェクト蚭定」りむンドりを開いた状態で、むンプットマップにスマホのタッチ操䜜の盞圓するアクションを远加しよう。

  1. 「むンプットマップ」タブに切り替え、アクションに「touch」を远加する。
  2. 「touch」の操䜜にマりスの巊クリックを远加する。
    Inputmap - action - tap

アセットをダりンロヌドしおむンポヌトする

次に、KENNEYのサむトからアセットをダりンロヌドしお利甚させおもらおう。今回利甚するのは「Physics Assets 」 ずいうアセットパックだ。このアセットに含たれるなんずもかわいい゚むリアンの顔の画像を、ゲヌムの盀面に䞊べるピヌスのテクスチャずしお䜿甚する。この玠晎らしすぎる無料の玠材に感謝せずにはいられない。

ダりンロヌドしたら「/physicspack/PNG/Aliens」フォルダの䞭のファむル名が「~_round.png」の画像だけを残しお他は削陀し、「Aliens」フォルダごず゚ディタのファむルシステムドックぞドラッグしおプロゞェクトにむンポヌトしよう。



Grid シヌンを䜜る

たずはマッチ3パズルゲヌムでピヌスが配眮される盀面ずしお、「Grid」シヌンを䜜成しよう。

  1. 「シヌン」メニュヌ「新芏シヌン」を遞択する。
  2. 「ルヌトノヌドを生成」にお「2D シヌン」を遞択する。
    1.「Node2D」クラスのルヌトノヌドが生成されたら、その名前を「Grid」に倉曎する。
  3. シヌンを保存する。フォルダを䜜成しお、ファむルパスを「res://Grid/Grid.tscn」ずしおシヌンを保存する。

Grid シヌンにノヌドを远加する

「Grid」ルヌトノヌドに、さらに「Node2D」クラスの子ノヌドを远加し、名前を「PiecesContainer」に倉曎しよう。このノヌドは盀面に配眮されるピヌスをたずめるためのノヌドだ。ゲヌム䞭は、スクリプトでピヌスのむンスタンスが生成されたら党おこの「PiecesContainer」ノヌドの子ずしお远加するこずになる。

シヌンツリヌドックの衚瀺は以䞋のようになったはずだ。
scene tree dock

なお、「Grid」シヌンのノヌドのプロパティ線集は特に必芁ない。



Piece シヌンを䜜る

次に、盀面に䞊べるピヌスずしお「Piece」シヌンを䜜成する。ただし、この「Piece」シヌンはあくたで雛圢で、実際にゲヌム䞭で利甚する各色のピヌスは、この「Piece」シヌンを継承する圢でのちほど甚意する。

  1. 「シヌン」メニュヌ「新芏シヌン」を遞択する。
  2. 「ルヌトノヌドを生成」にお「2D シヌン」を遞択する。
  3. 「Node2D」クラスのルヌトノヌドが生成されたら、その名前を「Piece」に倉曎する。
  4. シヌンを保存しおおこう。フォルダを䜜成しお、ファむルパスを「res://Pieces/Piece.tscn」ずしおシヌンを保存する。

Piece ノヌドに子ノヌドを远加する

「Piece」ルヌトノヌドに「Sprite」クラスの子ノヌドを远加しよう。シヌンツリヌドックの衚瀺は以䞋のようになったはずだ。
Piece scene tree


Sprite ノヌドのプロパティを線集する

「Sprite」ノヌドのプロパティを少し線集しおおこう。先述の通り、「Piece」はあくたで継承元雛圢なので、このシヌンでは「Sprite」ノヌドの「Texture」プロパティにはリ゜ヌスを敢えお適甚せずそのたたにしおおく。継承先のシヌンでそれぞれのピヌスの色にあった画像を適甚する予定だ。

「Offset」>「Offset」プロパティの倀を「(x: 35, y: -35)」に倉曎しおおこう。このシヌンを継承する各色のピヌスのシヌンでは「Texture」プロパティに先にむンポヌトした KENNEY の画像を適甚するが、その画像の瞊暪のサむズが 70 px なので、その画像の䞭心を右䞊にずらしお画像の巊䞋の角を(x: 0, y: 0)に合わせた。
Piece scene - Sprite - offset

ピヌスを配眮する盀面のグリッドは x 軞は巊から右ぞ、y 軞は䞋から䞊ぞカりントする仕様で、か぀盀面の 1 グリッドのサむズもテクスチャのサむズに合わせお 70 px ずする。ピヌスのテクスチャ画像の巊䞋の角を (x: 0, y: 0) に合わせれば、「Piece」ルヌトノヌドの䜍眮 (x: 0, y: 0) をグリッドに合わせお配眮したずきに、ちょうど「Sprite」の画像がグリッドに沿っお配眮されるずいうわけだ。
Diagram - Sprite offset ajusted to grid


Piece ノヌドにスクリプトをアタッチしお線集する

「Piece」ルヌトノヌドに新芏スクリプトをアタッチしよう。ファむルパスを「res://Pieces/Piece.gd」ずしおスクリプトファむルを䜜成する。

ひずたずスクリプトを以䞋のように線集しおほしい。

###Piece.gd###
extends Node2D

# ピヌスの色を文字列デヌタずしお蚭定するためのプロパティ
export (String) var color
# マッチした3぀以䞊同じ色が䞊んだ状態を瀺すプロパティ
var matched = false
# Sprite ノヌドの参照
onready var sprite = $Sprite

# ピヌスを移動させるメ゜ッド
# 匕数 target に枡した䜍眮に Piece むンスタンスを移動する
func move(target):
	position = target

# マッチした3぀以䞊同じ色が䞊んだずきに呌ばれるメ゜ッド
# matched プロパティを true にし、色を半透明にする
func make_matched():
	matched = true
	sprite.modulate = Color(1,1,1,.5)

これで「Piece.gd」スクリプトの線集は完了だ。



Piece シヌンを継承した各色のシヌンを䜜る

雛圢ずなる「Piece」シヌンは完成したので、それを継承したシヌンをピヌスの色の数だけ䜜成しおいこう。ピヌスの色は、ベヌゞュ、青、緑、ピンク、黄の 5 色だ。たずは「ベヌゞュ」のドロップを䟋に手順を進めおみよう。

  1. 「シヌン」メニュヌ「新しい継承シヌン」を遞択する。
  2. 継承元のシヌンずしお「Piece.tscn」を遞択する。
  3. 継承シヌンが生成されたら、ルヌトノヌドの名前を「PieceBeige」に倉曎する。
    *このルヌトノヌドの名前はそれぞれのドロップの色に合わせるこず。
  4. シヌンを䞀旊保存しおおく。ファむルパスを「res://Pieces/PieceBeige.tscn」ずしお保存する。
  5. シヌンツリヌドックでルヌトノヌド「PieceBeige」を遞択した状態で、むンスペクタヌで「Script Variables」の「Color」プロパティの倀を「beige」ずする。
    BlueDrop - Color property
  6. シヌンツリヌドックで「Sprite」ノヌドを遞択し、「Texture」プロパティに先にむンポヌトしおおいたリ゜ヌス「res://Aliens/alienBeige_round.png」を適甚するファむルシステムドックからドラッグすればOK。
    Sprite - Texture Region
    2D ワヌクスペヌス䞊では以䞋のスクリヌンショットのようになったはずだ。
    Sprite - Texture Region
    以䞊で、「PieceBeige」シヌンは完成だ。残りの 4 色のピヌスに぀いおも、同様の手順でシヌンを䜜成しおほしい。なお、シヌンごずに異なる郚分に぀いおは、以䞋を参考にしおほしい。
  • 青のピヌス
    • ルヌトノヌド名: PieceBlue
    • Color プロパティ: blue
    • Sprite > Texture プロパティ: res://Aliens/alienBlue_round.png
    • シヌン保存時のファむルパス: res://Pieces/PieceBlue.tscn
  • 緑のピヌス
    • ルヌトノヌド名: PieceGreen
    • Color プロパティ: green
    • Sprite > Texture プロパティ: res://Aliens/alienGreen_round.png
    • シヌン保存時のファむルパス: res://Pieces/PieceGreen.tscn
  • ピンクのピヌス
    • ルヌトノヌド名: PiecePink
    • Color プロパティ: green
    • Sprite > Texture プロパティ: res://Aliens/alienPink_round.png
    • シヌン保存時のファむルパス: res://Pieces/PiecePink.tscn
  • 黄のピヌス
    • ルヌトノヌド名: PieceYellow
    • Color プロパティ: yellow
    • Sprite > Texture プロパティ: res://Aliens/alienYellow_round.png
    • シヌン保存時のファむルパス: res://Pieces/PieceYellow.tscn

党郚で 5 色のピヌスの継承シヌンができたら䜜業完了だ。



Grid シヌンをスクリプトで制埡する

各色のピヌスのシヌンができあがったので、ここからはプログラミングしおゲヌムを制埡しおいく。今回はコヌド量がやや倚めなので頑匵ろう。

Godot ゚ディタで「Grid.tscn」シヌンに切り替えたら、「Grid」ルヌトノヌドに新芏スクリプトをアタッチしよう。ファむルパスは「res://Grid/Grid.gd」ずしお䜜成する。

なお、スクリプト内のコメントには「指が觊れた」たたは「指が離れた」ず蚘茉しおいるが、Godot デバッグパネル䞊では「マりス巊ボタンを抌した」たたは「マりス巊ボタンを離した」ず眮き換えおほしい。

たた、同じ色が぀以䞊揃った状態のこずを「マッチ」ず衚珟しおいるので、こちらもご留意いただきたい。

では、スクリプト゚ディタが開いたら、たずは必芁なプロパティを定矩しおおこう。

###Grid.gd###

extends Node2D

# 各色のピヌスのシヌンファむルを芁玠ずした配列
const pieces_scn = [
	preload("res://Pieces/PieceBeige.tscn"),
	preload("res://Pieces/PieceBlue.tscn"),
	preload("res://Pieces/PieceGreen.tscn"),
	preload("res://Pieces/PiecePink.tscn"),
	preload("res://Pieces/PieceYellow.tscn")
]

# x軞方向のグリッド数
var width: = 7
# y軞方向のグリッド数
var height: = 10
# x軞方向のグリッド開始䜍眮pixel
var x_start: = 70
# y軞方向のグリッド開始䜍眮pixel
var y_start: = 910
# 1グリッドのサむズPieceのSpriteのTextureず同じにする
var grid_size: = 70

# 盀面のピヌスの配眮を衚す配列二次元配列
var all_pieces = []

# 画面に指を觊れた䜍眮
var touched_pos = Vector2()
# 画面から指を離れた䜍眮
var released_pos = Vector2()

# 画面に指が觊れおいるステヌト、觊れおいる: true / 離れおいる: false
var is_touching = false
# マッチしたピヌスの自動凊理ステヌト、凊理䞭: true / 停止䞭: false
var is_waiting = false

# PiecesContainer ノヌドの参照
onready var pieces_container = $PiecesContainer

続いお以䞋のメ゜ッドを远加しよう。なお、以䞋のコヌド内に出おくる 二次元配列 ずは、芁玠ずしお配列を栌玍する配列、぀たり配列の配列のこずだ。

今回のスクリプトで利甚しおいる二次元配列の堎合、䞀階局目の配列では、盀面の x 軞方向のグリッドの数だけ空の配列を芁玠ずし、二階局目ずしおそれぞれの配列内に瞊方向のグリッド数だけ芁玠を栌玍する。その芁玠ずしお、ピヌスオブゞェクトを栌玍するこずで、それぞれのピヌスが盀面のどこに䜍眮しおいるかx 軞方向に䜕番目のグリッドで、y 軞方向に䜕番目のグリッドかを管理するこずができるずいうわけだ。

###Grid.gd###

# シヌンが読み蟌たれたら呌ばれる関数
func _ready():
    # ランダムな数を生成する関数の出力結果を毎回ランダムにするための組み蟌み関数を呌ぶ
	randomize()
    # all_pieces を盀面のグリッドを構成する二次元配列にする
	all_pieces = make_2d_array() # このあず定矩
    # ピヌスを生成しお盀面に配眮しお盀面情報を all_pieces に反映するメ゜ッドを呌ぶ
	spawn_pieces() # このあず定矩

# 盀面のグリッドを構成する二次元配列を生成するメ゜ッド
func make_2d_array():
	# array ずいう名前の配列を甚意
    var array = []
    # array に x 軞方向のグリッド数だけ空の配列を入れる
	for i in width:
		array.append([])
        # さらにそれぞれの配列に y 軞方向のグリッド数だけ暫定的に null を入れる
		for j in height:
			array[i].append(null)
    # できあがった二次元配列を返す
	return array

# ピヌスを生成しお盀面に配眮しお盀面情報を board に反映するメ゜ッド
func spawn_pieces():
    # x軞方向のグリッド数だけルヌプ
	for i in width:
        # y軞方向のグリッド数だけルヌプ
		for j in height:
			# 党ピヌスの二次元配列䞊で該圓グリッドにピヌスが存圚しない堎合
            #ゲヌム開始時は党郚 null
            if all_pieces[i][j] == null:
                # 各色のピヌスのシヌンからランダムで぀遞択しおむンスタンス化
				var index = floor(rand_range(0, pieces_scn.size()))
				var piece = pieces_scn[index].instance()
                # マッチしおしたった堎合は、ピヌスのむンスタンスを削陀しおやり盎し
				while match_at(i, j, piece.color): # このあず定矩
					piece.queue_free()
					index = floor(rand_range(0, pieces_scn.size()))
					piece = pieces_scn[index].instance()
                # ピヌスのむンスタンスをPiecesContainerノヌドの子にする
				pieces_container.add_child(piece)
                # グリッドからピクセルに倉換した䜍眮にピヌスのむンスタンスを配眮
				piece.position = grid_to_pixel(i, j) # このあず定矩
                # 党ピヌスの二次元配列を曎新
				all_pieces[i][j] = piece

䞊蚘コヌドの䞭で未定矩のmatch_atメ゜ッドずgrid_to_pixelメ゜ッドを定矩しおおこう。

###Grid.gd###

# 指定したグリッド䜍眮で同じ色ピヌスが぀以䞊䞊んでいるか確認するメ゜ッド
# 匕数columnはx軞のグリッド䜍眮、rowはy軞のグリッド䜍眮、colorはピヌスの色
func match_at(column, row, color):
    # 指定したグリッドの x 軞方向の䜍眮が3以䞊の堎合
	if column >= 2:
        # 指定したグリッド䜍眮の巊隣ずもう䞀぀巊隣にピヌスがある堎合
		if all_pieces[column-1][row] != null \
		and all_pieces[column-2][row] != null:
            # 巊隣ずもう䞀぀巊隣のピヌスの色が指定したピヌスの色ず同じ堎合
			if all_pieces[column-1][row].color == color \
			and all_pieces[column-2][row].color == color:
                # true を返す
				return true
    # 指定したグリッドの y 軞方向の䜍眮が3以䞊の堎合
	if row >= 2:
        # 指定したグリッド䜍眮の䞋ずもう䞀぀䞋にピヌスがある堎合
		if all_pieces[column][row-1] != null \
		and all_pieces[column][row-2] != null:
            # 䞋ずもう䞀぀䞋のピヌスの色が指定したピヌスの色ず同じ堎合
			if all_pieces[column][row-1].color == color \
			and all_pieces[column][row-2].color == color:
                # true を返す
				return true

# グリッドの䜍眮をピクセルの䜍眮に倉換するメ゜ッド
func grid_to_pixel(column, row):
    # 先にピクセル䜍眮出力甚に Vector2 型の倉数 pixel_pos を定矩
	var pixel_pos = Vector2()
    # ピクセル x 座暙 = x 軞方向のグリッド開始䜍眮 + グリッドサむズ x グリッド x 座暙
	pixel_pos.x = x_start + grid_size * column
    # ピクセル y 座暙 = y 軞方向のグリッド開始䜍眮 - グリッドサむズ x グリッド y 座暙
	pixel_pos.y = y_start - grid_size * row
    # ピクセル座暙を返す
	return pixel_pos

これで、ゲヌム開始時に各色のピヌスが盀面にランダムで䞊べられるはずだ。䞀床プロゞェクトを実行しお確認しおみよう。なお、初めおプロゞェクトを実行する堎合は、メむンシヌン遞択のダむアログが衚瀺されるので、「Grid.tscn」をメむンシヌンずしお遞択しよう。
run project - distribute pieces on the grid board


ちょうどgrid_to_pixelメ゜ッドを定矩したので、぀いでにこのあず䜿甚するpixel_to_gridメ゜ッドも定矩しおおこう。名前の通り、先に定矩したgrid_to_pixelずは逆で、ピクセルの䜍眮をグリッドの䜍眮に倉換するメ゜ッドだ。

###Grid.gd###

# ピクセルの䜍眮をグリッドの䜍眮に倉換するメ゜ッド
func pixel_to_grid(pixel_x, pixel_y) -> Vector2:
	var grid_pos = Vector2()
	grid_pos.x = floor((pixel_x - x_start) / grid_size)
	grid_pos.y = floor((pixel_y - y_start) / -grid_size)
	return grid_pos

さらに、もう䞀぀このあず䜿甚するis_in_gridメ゜ッドを定矩しおおく。これは匕数に枡した䜍眮が盀面グリッドの範囲内かどうかを刀定しおその結果を返すメ゜ッドだ。

###Grid.gd###

# 指定した䜍眮が盀面グリッドの範囲内かどうかを返すメ゜ッド
func is_in_grid(grid_position: Vector2):
	if grid_position.x >= 0 and grid_position.x < width \
	and grid_position.y >= 0 and grid_position.y < height:
        # 盀面グリッドの範囲内だったら true を返す
		return true
	else:
        # 盀面グリッドの範囲倖だったら false を返す
		return false

ここで、ゲヌムのプレむダヌの入力画面のタッチ操䜜を凊理するプログラムを蚘述しおいく。

###Grid.gd###

# ゲヌムのメむンルヌプで毎フレヌム呌ばれる関数
func _process(_delta):
    # もしマッチングの凊理䞭でなければ
	if not is_waiting:
        # プレむダヌの入力を凊理する
		touch_input() # このあず定矩

# プレむダヌの入力を凊理するメ゜ッド
func touch_input():
    # もし画面に指が觊れたら
	if Input.is_action_just_pressed("touch"):
        # 指の䜍眮をピクセルからグリッドに倉換
		var start_pos = get_global_mouse_position()
		var start_grid = pixel_to_grid(start_pos.x, start_pos.y)
        # 指の䜍眮が盀面グリッドの範囲内だったら
		if is_in_grid(start_grid):
            # 画面に指を觊れた䜍眮を保存
			touched_pos = start_grid
            # ステヌトを画面に指が觊れおいる状態にする
			is_touching = true
	# もし画面から指が離れたら
    if Input.is_action_just_released("touch"):
        # 指の䜍眮をピクセルからグリッドに倉換
		var end_pos = get_global_mouse_position()
		var end_grid = pixel_to_grid(end_pos.x, end_pos.y)
        # もし指の䜍眮が盀面グリッドの範囲内...
        # か぀ステヌトが画面に指が觊れおいる状態だったら
		if is_in_grid(end_grid) and is_touching:
            # 指を離した䜍眮情報ずしお保存
			released_pos = end_grid
            # 指が觊れた䜍眮ず指を離した䜍眮でピヌスの移動を凊理するメ゜ッドを呌ぶ
			touch_and_release() # このあず定矩
        # ステヌトを画面から指が離れおいる状態にする
		is_touching = false

䞊蚘コヌドで、盀面グリッド内で画面に指を觊れた䜍眮ず画面から指を離した䜍眮を取埗し、それらの情報を利甚しおピヌスの移動を凊理するtouch_and_releaseメ゜ッドを呌び出しおいる。このメ゜ッドずその䞭でさらに呌び出すswap_piecesずいうヘルパヌメ゜ッドを定矩しおいこう。ヘルパヌメ゜ッドずいうのは簡単に蚀うず、メ゜ッドの䞭で呌び出されるメ゜ッドで、芪のメ゜ッドをシンプルに保぀圹割をする。

###Grid.gd###

# 指が觊れた䜍眮ず指を離した䜍眮を利甚しおピヌスの移動を凊理するメ゜ッド
func touch_and_release():
    # 指が觊れた䜍眮ず指が離れた䜍眮ずの差を蚈算
	var difference = released_pos - touched_pos
    # x 軞方向の差の絶察倀が y 軞方向の差の絶察倀より倧きい堎合
	if abs(difference.x) > abs(difference.y):
        # x 軞方向の差が 0 より倧きい堎合
		if difference.x > 0:
            # ヘルパヌメ゜ッドを呌び、觊れた䜍眮のピヌスず右に隣接するピヌスを入れ替える
			swap_pieces(touched_pos, Vector2.RIGHT) # このあず定矩
        # x 軞方向の差が 0 より小さい堎合
		elif difference.x < 0:
            # ヘルパヌメ゜ッドを呌び、觊れた䜍眮のピヌスず巊に隣接するピヌスを入れ替える
			swap_pieces(touched_pos, Vector2.LEFT) # このあず定矩
    # x 軞方向の差の絶察倀が y 軞方向の差の絶察倀より小さい堎合
	elif abs(difference.x) < abs(difference.y):
        #  y 軞方向の差が 0 より倧きい堎合
		if difference.y > 0:
            # ヘルパヌメ゜ッドを呌び、觊れた䜍眮のピヌスず䞋に隣接するピヌスを入れ替える
			swap_pieces(touched_pos, Vector2.DOWN) # このあず定矩
        # y 軞方向の差が 0 より小さい堎合
		elif difference.y < 0:
            # ヘルパヌメ゜ッドを呌び、觊れた䜍眮のピヌスず䞊に隣接するピヌスを入れ替える
			swap_pieces(touched_pos, Vector2.UP) # このあず定矩

# ピヌスを入れ替えるヘルパヌメ゜ッド
func swap_pieces(pos, dir):
    # 指を觊れた䜍眮のピヌスを党ピヌスの二次元配列から取埗
	var touched_piece = all_pieces[pos.x][pos.y]
    # 指を離した方向に隣接するピヌスを党ピヌスの二次元配列から取埗
	var target_piece = all_pieces[pos.x + dir.x][pos.y + dir.y]
    # 党ピヌスの二次元配列䞊、どちらのピヌスも存圚しおいる堎合
	if touched_piece != null and target_piece != null:
        # 党ピヌスの二次元配列の䞭から指を觊れた䜍眮のピヌスを指を離した方に隣接するピヌスで䞊曞きする
		all_pieces[pos.x][pos.y] = target_piece
        # 党ピヌスの二次元配列䞊、指を離した方に隣接するピヌスを指を觊れた䜍眮のピヌスで䞊曞きする
		all_pieces[pos.x + dir.x][pos.y + dir.y] = touched_piece
        # 盀面䞊、指を觊れた䜍眮のピヌスむンスタンスを指を離した方にに1グリッド移動する
		touched_piece.move(grid_to_pixel(pos.x + dir.x, pos.y + dir.y))
        # 盀面䞊、指を離した方に隣接するピヌスむンスタンスを指を觊れた䜍眮ぞ移動する
		target_piece.move(grid_to_pixel(pos.x, pos.y))
        # ここからマッチしたピヌスの凊理が始たるので、自動凊理ステヌトを凊理䞭にする
		is_waiting = true

䞊蚘コヌドで、プレむダヌがドラッグしたピヌスが隣接するピヌスず入れ替わる凊理が実装できた。入力操䜜が正しく機胜するか、プロゞェクトを実行しお確認しおおこう。
run project - swap pieces


ここからはピヌスを入れ替えたあずに自動的に実行されるべき凊理を実装しおいく。倧たかには以䞋の流れだ。

  1. 自動凊理ステヌトを凊理䞭に倉曎する。
  2. マッチしたピヌスが1組でもあるかチェックする。1組でもある堎合は以䞋の凊理をルヌプする。
    1. 党おのピヌスをチェックしおマッチしたピヌスにフラグを立おる。
    2. フラグの立っおいるピヌスを削陀する。
    3. 削陀しお空になったスペヌスぞ同じ列の䞊のグリッドからピヌスを移動させお詰める。
    4. ピヌスを䞋ぞ詰めたら、最埌に空のスペヌスに新しいピヌスを生成する。
  3. マッチしたピヌスが1組もなくなったら自動凊理ステヌトを停止䞭にする。

䞊蚘の倧たかな流れをコヌディングしよう。

たず先に定矩したピヌスを入れ替えるswap_piecesメ゜ッドの最埌にis_waiting = trueの1行を远加しよう。぀たりこれは、自動凊理ステヌトを凊理䞭に倉曎しおいる。

###Grid.gd###

func swap_pieces(pos, dir):
	var touched_piece = all_pieces[pos.x][pos.y]
	var target_piece = all_pieces[pos.x + dir.x][pos.y + dir.y]
	if touched_piece != null and target_piece != null:
		all_pieces[pos.x][pos.y] = target_piece
		all_pieces[pos.x + dir.x][pos.y + dir.y] = touched_piece
		touched_piece.move(grid_to_pixel(pos.x + dir.x, pos.y + dir.y))
		target_piece.move(grid_to_pixel(pos.x, pos.y))

        # 以䞋を远加
        # ここからマッチしたピヌスの自動凊理が開始するので自動凊理ステヌトを凊理䞭にする
		is_waiting = true

touch_inputメ゜ッドに「もし自動凊理実行䞭のステヌトだったら」ずいうif構文を蚘述し、そのブロックの䞭に自動的に実行させたい凊理を远加しおいこう。远加する䜍眮はちょうど指での操䜜が終わったあずだ。凊理が完了した埌にはis_waiting = falseの1行远加しお、自動凊理ステヌトを停止䞭にしおおこう。

###Grid.gd###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# 省略
	if Input.is_action_just_released("touch"):
		# 省略

        # ここから远加
        # もし凊理䞭だったら
		if is_waiting:
            # マッチしたピヌスが1組でもあるかチェック > ある限りルヌプ
			while check_matches(): # このあず定矩
                pass
            # マッチしたピヌスが1組もなく凊理が完了したら、自動凊理ステヌトを停止䞭にする
			is_waiting = false

whileルヌプで、マッチしたピヌスが1組でもあれば必芁な凊理を繰り返すようにしおいる。


たずはwhileルヌプのルヌプ条件ずもなっおいるcheck_matches()を以䞋の通りに定矩する。

###Grid.gd###

# マッチしたピヌスが1組でもあるかチェックしお結果を返すメ゜ッド
func check_matches() -> bool:
    # 盀面の x軞方向のグリッドでルヌプ
	for i in width:
        # 盀面の y軞方向のグリッドでルヌプ
		for j in height:
            # その グリッドの座暙にピヌスが存圚する堎合
			if all_pieces[i][j] != null:
                # そのグリッド座暙のピヌスがマッチしおいれば true を返しおメ゜ッドも終了
				if match_at(i, j, all_pieces[i][j].color):
					return true
    # 党ピヌスをチェックしおマッチしおいるピヌスが䞀぀もなければ false を返す
	return false

続いお、whileルヌプ内の最初の凊理」党おのピヌスをチェックしおマッチしたピヌスにはフラグを立おる」を行うためのfind_matchesメ゜ッドを以䞋のように定矩しよう。ここでいう「フラグを立おる」ず蚀うのは、぀たり、ピヌスのむンスタンスのmatchedプロパティをtrueに倉曎する、ずいうこずだ。

###Grid.gd###

# マッチしおいるピヌスを芋぀けおフラグを立おるメ゜ッド
func find_matches():
    # 盀面の x 軞方向のグリッド数だけルヌプ
	for i in width:
        # 盀面の y 軞方向のグリッド数だけルヌプ
		for j in height:
            # そのグリッドの座暙にピヌスが存圚する堎合
			if all_pieces[i][j] != null:
                # 珟圚の色をそのグリッド座暙のピヌスの色ず定矩する
				var current_color = all_pieces[i][j].color
                # もしその x 軞座暙が x 軞方向のグリッド数 - 2 より小さければ
				if i < width - 2:
                    # そのピヌスの右隣ずさらにその右隣にピヌスが存圚する堎合
					if all_pieces[i+1][j] != null \
					and all_pieces[i+2][j] != null:
                        # それらのピヌスの色が珟圚の色ず同じ堎合
						if all_pieces[i+1][j].color == current_color \
						and all_pieces[i+2][j].color == current_color:
                            # そのピヌスにフラグが立っおなければ
							if not all_pieces[i][j].matched:
                                # そのピヌスの matched プロパティを true にしおフラグを立おる
                                # 同時にそのピヌスのテクスチャの色を半透明にする
								all_pieces[i][j].make_matched()
                            # そのピヌスの右隣のピヌスにフラグが立っおなければ
							if not all_pieces[i+1][j].matched:
                                # そのピヌスの matched プロパティを true にしおフラグを立おる
                                # 同時にそのピヌスのテクスチャの色を半透明にする
								all_pieces[i+1][j].make_matched()
                            # そのピヌスの぀右隣のピヌスにフラグが立っおなければ
							if not all_pieces[i+2][j].matched:
                                # そのピヌスの matched プロパティを true にしおフラグを立おる
                                # 同時にそのピヌスのテクスチャの色を半透明にする
								all_pieces[i+2][j].make_matched()
				# もしそのピヌスの y 座暙が y 軞方向のグリッド数 - 2 より小さければ
                if j < height - 2:
                    # そのピヌスの䞊ずさらにその䞊にピヌスが存圚する堎合
					if all_pieces[i][j+1] != null \
					and all_pieces[i][j+2] != null:
                        # それらのピヌスの色が珟圚の色ず同じ堎合
						if all_pieces[i][j+1].color == current_color \
						and all_pieces[i][j+2].color == current_color:
                            # そのピヌスにフラグが立っおなければ
							if not all_pieces[i][j].matched:
                                # そのピヌスの matched プロパティを true にしおフラグを立おる
                                # 同時にそのピヌスのテクスチャの色を半透明にする
								all_pieces[i][j].make_matched()
                            # そのピヌスの䞊のピヌスにフラグが立っおなければ
							if not all_pieces[i][j+1].matched:
                                # そのピヌスの matched プロパティを true にしおフラグを立おる
                                # 同時にそのピヌスのテクスチャの色を半透明にする
								all_pieces[i][j+1].make_matched()
                            # そのピヌスの぀䞊のピヌスにフラグが立っおなければ
							if not all_pieces[i][j+2].matched:
                                # そのピヌスの matched プロパティを true にしおフラグを立おる
                                # 同時にそのピヌスのテクスチャの色を半透明にする
								all_pieces[i][j+2].make_matched()

このfind_matchesメ゜ッドをtouch_inputメ゜ッド内のwhileルヌプの䞭で呌び出す必芁があるので、以䞋のように曎新しよう。

###Grid.gd###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# 省略
	if Input.is_action_just_released("touch"):
		# 省略

		if is_waiting:
            # マッチしたピヌスが1組でもあるかチェック > ある限りルヌプ
			while check_matches():
                # マッチしたピヌスを芋぀けおフラグを立おる
				find_matches()
                # 凊理を芖芚的にわかりやすくするため0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
			is_waiting = false

これで、マッチした各ピヌスのむンスタンスのmatchedプロパティはtrueになり、ピヌスの色も半透明になるはずだ。プロゞェクトを実行しお確認しおみよう。
run project - flag on matched pieces


続いお、whileルヌプ内の2番目の凊理「フラグの立っおいるピヌスを削陀する」を実行するdelete_matchesメ゜ッドを以䞋のように定矩しおほしい。

###Grid.gd###

# マッチしおいるピヌスを削陀するメ゜ッド
func delete_matches():
    # 盀面の x 軞方向のグリッド数だけルヌプ
	for i in width:
        # 盀面の y 軞方向のグリッド数だけルヌプ
		for j in height:
            # そのグリッド座暙にピヌスが存圚する堎合
			if all_pieces[i][j] != null:
                 # そのグリッド座暙のピヌスにフラグが立っおいたら
				if all_pieces[i][j].matched:
                    # そのグリッド座暙のピヌスを削陀する
					all_pieces[i][j].queue_free()
                    # 党ピヌスの二次元配列からそのグリッド座暙の芁玠を空にする
					all_pieces[i][j] = null

このdelete_matchesメ゜ッドをtouch_inputメ゜ッドのwhileルヌプ内に远加しよう。

###Grid.gd###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# 省略
	if Input.is_action_just_released("touch"):
		# 省略

		if is_waiting:
            # マッチしたピヌスが1組でもあるかチェック > ある限りルヌプ
			while check_matches():
                # マッチしたピヌスを芋぀けおフラグを立おる
				find_matches()
                # 0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
                # フラグを立おたピヌスを削陀する
				delete_matches()
                # 0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
            # マッチしたピヌスが1組もなく凊理が完了したら、凊理䞭状態を無効にする
			is_waiting = false

これでマッチしたピヌスが半透明になった埌、削陀されるはずだ。プロゞェクトを実行しお確認しおみよう。
run project - delete matched pieces


次は、whileルヌプ内の3番目の凊理「削陀しお空になったスペヌスぞ同じ列の䞊のグリッドからピヌスを移動させお詰める」を実行するcollapse_columnsメ゜ッドを以䞋のように曎新しおほしい。

###Grid.gd###

# 列ごずにピヌスが存圚しないスペヌスには䞊にあるピヌスを移動しお詰めるメ゜ッド
func collapse_columns():
    # 盀面の x 軞方向のグリッド数だけルヌプ
	for i in width:
        # 盀面の y 軞方向のグリッド数だけルヌプ
		for j in height:
            # そのグリッド座暙にピヌスが存圚しないnull堎合
			if all_pieces[i][j] == null:
                # そのグリッドの y 座暙より぀䞊の行から䞀番䞊の行たでのルヌプ
				for k in range(j + 1, height):
                    # 1぀䞊のグリッドにピヌスが存圚しおいる堎合
					if all_pieces[i][k] != null:
                        # ぀䞊のグリッドのピヌスを䞋の空いおいるグリッドに移動する
						all_pieces[i][k].move(grid_to_pixel(i, j))
                        # 党ピヌスの二次元配列の珟圚のグリッド座暙に぀䞊のピヌスを入れる
						all_pieces[i][j] = all_pieces[i][k]
                        # 党ピヌスの二次元配列の぀䞊のグリッド座暙を空にする
						all_pieces[i][k] = null
                        # ルヌプを抜ける
						break

このcollapse_columnsメ゜ッドをtouch_inputメ゜ッドのwhileルヌプ内で呌び出そう。

###Grid.gd###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# 省略
	if Input.is_action_just_released("touch"):
		# 省略

		if is_waiting:
            # マッチしたピヌスが1組でもあるかチェック > ある限りルヌプ
			while check_matches():
                # マッチしたピヌスを芋぀けおフラグを立おる
				find_matches()
                # 0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
                # フラグを立おたピヌスを削陀する
				delete_matches()
                # 0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
                # 空のスペヌスに同じ列の䞊にあるピヌスを移動しお詰める
				collapse_columns()
                # 0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
            # マッチしたピヌスが1組もなく凊理が完了したら、凊理䞭状態を無効にする
			is_waiting = false

これでピヌスが削陀されお空いたスペヌスに䞊にあるピヌスを詰める仕組みができた。さらにwhileルヌプにより、空いたスペヌスにピヌスを詰めた あずに マッチしたピヌスも連続的に削陀されるはずだ。プロゞェクトを実行しお確認しおみよう。
run project - collapse columns


最埌に、すでに定矩枈みのspawn_piecesメ゜ッドをピヌスを䞋に詰めたあずに呌び出すようにすれば、空いた空間に新しいピヌスが配眮され盀面が埋たるはずだ。touch_inputメ゜ッドを以䞋のように曎新しよう。

###Grid.gd###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# 省略
	if Input.is_action_just_released("touch"):
		# 省略

		if is_waiting:
            # マッチしたピヌスが1組でもあるかチェック > ある限りルヌプ
			while check_matches():
                # マッチしたピヌスを芋぀けおフラグを立おる
				find_matches()
                # 0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
                # フラグを立おたピヌスを削陀する
				delete_matches()
                # 0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
                # 空のスペヌスに同じ列の䞊にあるピヌスを移動しお詰める
				collapse_columns()
                # 0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
                # 空のスペヌスに新しいピヌスを生成しお配眮する
				spawn_pieces()
                # 0.3秒埅機
				yield(get_tree().create_timer(0.3), "timeout")
            # マッチしたピヌスが1組もなく凊理が完了したら、凊理䞭状態を無効にする
			is_waiting = false

以䞊で自動凊理郚分のコヌディングは完了だ。このチュヌトリアルの手順もここたでずなる。最埌にプロゞェクトを実行しお動䜜を確認しおおこう。
run project - whole check



サンプルゲヌム

今回のチュヌトリアルで䜜成したプロゞェクトをさらにブラッシュアップしたサンプルゲヌムを甚意した。ちなみに以䞋のGIF画像は3倍速で再生しおいるので実際はもう少し穏やかだ。

sample game


サンプルゲヌムのプロゞェクトファむルは、GitHubリポゞトリ に眮いおいるので、そこから .zip ファむルをダりンロヌドしおいただき、「Sample」フォルダ内の「project.godot」ファむルを Godot Engine でむンポヌトすれば確認しおいただけるはずだ。



おわりに

今回はマッチ3パズルゲヌムを䜜った。シンプルな操䜜で䜕回でも楜しめるモバむルゲヌムにはうっお぀けのゲヌムゞャンルだ。

今回のようなシンプルなマッチ3パズルゲヌムを䜜るずきのポむントをたずめおおこう。

  • 最䜎限必芁なシヌンは盀面ずピヌスの぀だけ。
  • 雛圢のピヌスシヌンを䜜っおから、それを継承しお各色のピヌスシヌンを䜜る。
  • 二次元配列を利甚しお盀面グリッドに配眮するピヌスを管理する。
  • ピヌスを入れ替えるずきは、画面䞊のピヌスの䜍眮の入れ替えず二次元配列の芁玠の入れ替えの䞡方が必芁。
  • スクリプトは以䞋がポむント。
    • プレむダヌがピヌスを動かす凊理
      1. 画面に指が觊れた䜍眮ず画面から指が離れた䜍眮を取埗する。
      2. ぀の䜍眮がグリッド内か有効な操䜜か刀定する。
      3. ぀の䜍眮の差からピヌスを入れ替える方向を刀定する。
    • マッチした時の自動凊理ルヌプ。
      1. マッチしおいるピヌスが1組でもあるかチェックルヌプの条件。
      2. マッチしおいるピヌスにフラグを立おる。
      3. フラグの立っおいるピヌスを削陀する。
      4. 削陀されお空いたスペヌスに䞊のピヌスを詰める。
      5. 詰めお空いたスペヌスに新しいピヌスを生成する。


参照