この記事では落ち物パズルゲームの作り方を解説します。
目次
落ちものパズル
落ち物パズルとは、「テトリス」「ぷよぷよ」「ズーキーパー」「ツムツム」のようなゲームを表すジャンルです。落ちものパズルは、おおよそ以下のルールとなっています。
- 1. 消去可能なブロックが上から落下してくる
- 2. 一定ルール (同じ色をつなげるなど) でブロックの消去ができる
- 3. 1〜2を繰り返して出現位置がブロックでふさがったり、一定時間内に消せないとゲームオーバー
通常、ブロックは下に遮るものがないと重力的な落下を行います。それによりフィールド上に配置されたブロックを瞬時に消すための思考力・判断力を求められるアクション性の高いパズルゲームとなっています。
(※ゲームによっては、非リアルタイムであったり、消去できないブロックが存在することもあります)
ズーキーパーの作り方
それでは、日本での Match-three game (マッチ3ゲーム) の代表とも言える、ズーキーパーの作り方を紹介します。
消去判定の作り方
落ち物パズルで重要なのが、ブロックをどのようにすると消すことができるのか、というルールの実装です。
このルールを実装するには、ゲームのフィールドに配置されているブロックの情報を2次元の配列で格納すると都合が良いです。
(後述しますが、この方法はツムツムを除きます)
例えば、ズーキーパーであれば、8×8のフィールドなので、8×8の二次元配列を用意します。
int blockList[8][8] = {
{1,2,4,6,5,2,3,7},
{1,3,3,1,5,1,3,4},
{4,2,5,1,1,6,7,1},
{2,4,2,3,1,2,2,1},
{7,6,5,4,2,1,1,5},
{3,5,2,1,7,7,2,5},
{7,5,3,1,7,2,5,2},
{3,1,3,7,1,3,4,6},
};
上記のデータは、ブロックが以下のように並んでいるイメージです。

なぜ、2次元配列でブロックの管理をするのかというと、ゲーム画面での表示は、ブロックのサイズの間隔で配置されるからです。例えば、ブロックの画面上のサイズが 32×32 ピクセルとすると、左上から、(0, 0), (32, 0), (64, 0), … という座標になります。この座標系では隣り合っているブロックを取得するには、少し面倒です。
そこで、2次元配列の座標系でブロックデータを扱うと、(0, 0), (1, 0), (2, 0), … というように、データへアクセスすることができてわかりやすくなって、プログラム上で処理の都合が良くなります。

ある程度プログラムをやるとわかることですが、「見た目の表示(=ユーザーインターフェース)」と「実際のデータ」との区別をしっかり行うと、役割分担が明確となり、プログラムの見通しが良くなります。
- 見た目の表示:画面上でのブロック表示
- 実際のデータ:2次元配列で管理しているブロックのデータ
では、この2次元配列を使って消去判定を行います。(x, y) = (4, 3) にあるブロックを (3, 3) に移動させると消せそうなので、この移動を行うとします。

すると以下のような配置になります。

このデータをもとに消去判定を行います。判定のアルゴリズムは、基準となる位置から「上下左右」に探索をするものとなります。
まず左から調べるとします。その場合、ブロックは違う色なのでこちらの方向には消すことができるブロックはない、という結果となります。

次に、上を調べます。すると同じ色なので消すことができる可能性があります。

なので、さらに上を調べます。

さらに上を調べる
すると3つ同じ色が揃ったので消すことができます。
消すことができるブロックの情報は直接ブロック情報を書き換えるのではなく、同じサイズの2次元配列を別に用意して、そこに格納します。
// 消去判定用の2次元配列データ
int blockEraseList[8][8] = {
{0,0,0,0,0,0,0,0},
{0,0,0,1,0,0,0,0},
{0,0,0,1,0,0,0,0},
{0,0,0,1,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
};
消去ブロックが複数ある場合は、このように情報を保持しておくと、余計な情報がないので消去処理が簡単にできます。このような感じで、上下左右を判定した後、消去対処が存在していれば消去処理を行います。
blockEraseList の要素が “1” であるものを消します。(3, 1), (3, 2), (3, 3) が消去対象なので、それぞれの実データ(ゲーム画面に表示している側のデータ)を “0” に置き換えました。

落下判定
消去ができたので、次は落下処理です。ブロックを落下させるために必要なのは、以下の情報です。
- どのブロックを落下させるのか
- ブロックをどこからどこへ落下させるのか
先ほどの図のような状態であれば、(3, 0) にある “6” を (3, 3) まで落下させる、ということを求める計算が必要になります。
方法としては、一番下の段にあるブロックから “0” のブロックを探します。そしてその段を調べ終わったら、1段ずつ上を調べていくようにします。

そして、 “0” を見つけたら、上方向を順番に調べます。先ほどの消去アルゴリズムと同様に、1マスずつ調べて、”0” でないブロックを見つけます。

上方向にブロックを見つけたので、以下の情報をどこかに格納しておきます。
- 対象: “6” のブロック
- 開始地点: (3, 0)
- 落下地点: (3, 3)
落下情報をすべて作成し終わったら、この情報を使って落下アニメーションを行います。なお、落下アニメーションなしで、とりあえず動かしたいのであれば、1フレームで落下を完了させても問題ありません。
さきほどの、ブロックの並びでは問題ないのですが、状況によっては、落下ブロックのさらに上にブロックが存在することもあります。(というか、かなりの確率でそうなることが多いですね)

「6」の上に「4」を配置しました
少し図を変更しました。”6” の上に “4” が存在しています。”4″も一緒に落下させる計算をするためには、落下計算用の2次元配列を別途用意します。そして、落下ブロックが存在した場合は、そちらに落下済みのブロックを移動させます。

これにより、その上にあるブロックの落下位置を正しく計算できるようになります。
処理のまとめ
ここまでの処理ステップを図にまとめてみました。

▼処理ステップ
1. ブロック操作を行い、ブロックを移動する
2. 消去判定を行う。
a. 消去可能なブロックがあれば、3へ進む
b. 消去ブロックが存在しない場合は、1へ戻る
3. 消去処理を実行してブロックをフィールドから消す
4. 落下可能なブロックがあれば落下処理を行い、落下完了したら 2に戻る
以上が、落ち物パズルの基本的な作り方となります。マッチ3パズルであれば、おおよそ、この流れで実装できます。テトリスは消去判定よりもブロックの回転処理が少し面倒ですが、基本は同じです。
テトリスの作り方については以下のページにまとめました。

Godot Engine でマッチ3パズルのサンプルを作ってみました。サンプルコードを公開していて、そのコードを解説する記事となります。

あと、ぷよぷよは、直線だけでなくジグザグにもつながるので、上下左右のつながりを連続で判定することになります。
ツムツムの作り方
ツムツムは他の落ちものパズルと比べてやや特殊な作りです。
ブロックが格子状に並んでおらず、箱にボールを入れたような状態になるため、やや複雑な衝突処理を実装する必要があります。
ただ、「物理エンジン」があれば、ブロック落下時の衝突処理が簡単にできるため、ツムツムの実装が容易となります。特にUnityでは簡単に物理エンジンを扱うことができるので、Unity初心者向けの題材としても良いです。
もちろん、衝突処理を自作するのもとても勉強になるので、プログラムに自信のある方にはそこから作ってみてもよいでしょう。
ひとまず、ここでは落下処理を物理エンジンで実装するとして、消去ロジックの実装方法を紹介します。
消去判定

まず、真ん中にある水色のブロックを選んだとします。
そうしたら、そのブロックを中心に円(距離)で一定の範囲内に同じ色のブロックがあるかどうかを判定して、距離内であれば接続可能、そうでなければ接続不可、という判定をします。

上と下にあるブロックは接続可能ですが、左上にあるブロックには届かないので接続できない、という判定になります。
ここでは、下にあるブロックに接続したとします。

そうしたら、接続したブロックから、さらに別のブロックに接続できるかをチェックします。この時、接続済のブロックは除外して判定を行います。そうして、スクリーンから手を離した時に、接続済のブロックを消去する、という判定で実装が可能です。
なお、消去可能な範囲 (距離) は適切な値に調整する必要があります。広くしすぎるとあまりにも消してやすくなってしまい退屈なゲームになってしまうかもしれません。逆に狭くしすぎると、なかなか消せなくてストレスのたまるゲームになってしまいます。
また、ブロック落下中にスワイプ操作でブロック同士を接続した場合、ブロックが落下して接続済のブロックとの距離が離れ過ぎてしまう問題があります。この場合は、その時点で強制的に消去処理に進むなど、不自然な接続にならないよう、特別な処理を入れても良いかもしれません。
落ちものパズルのシステムをどう展開していくか
落ちものパズルをより長く楽しませるには、どういう構成が良いかを考えてみます。
1つの案としては、すでにフィールドにブロックが配置されていて、それらを全て消すとステージクリアにするというものです。
それに対して、私がよく作るパターンとしては、ブロックをたくさん消すほど多くのダメージを敵に与えて、一定ダメージを与えると敵を倒せる、というルールを採用しています。
要はパズドラ方式ですね。
以下は昔 iPhone 向けに作った落ち物パズル「かずおち」です。

「たくさん消すと敵に大ダメージ」というシステムを採用すると、たくさん消すことがスコアという抽象的なものではく、敵へのダメージというわかりやすいものに反映されるため、手軽に爽快感が得られます。
また、敵はブロックを降らしたり、一定ターン数で攻撃するといった単純なAIで実装できて楽です。敵を強くしたければ、HPを増やしたり、厄介なブロックを登場させたりするだけで良いので、これまた難易度調整が容易にできてしまうのですよね。
ということで、特に面白くするアイデアが思いつかない場合は、このルールを採用すると、それなりに面白くなるのでオススメです。
もし、もっと長い開発期間をかけられるのであれば、ステージをクリアするごとにお金が手に入り、それで部屋の飾り付けなどができるコレクション要素を入れても良いかもしれません。