イラストACにてイラストも公開中。まずはここのブログでチェック!!欲しいイラストがあれば無料でダウンロード出来ますのでDownloadへGo!

物理演算落ちものパズル『ボロリス』開発の全記録 〜企画からMatter.jsでの実装、iPhoneバグとの死闘まで〜

物理演算落ちものパズル『ボロリス』開発の全記録 〜企画からMatter.jsでの実装、iPhoneバグとの死闘まで〜

はじめに:ただの落ちものパズルじゃない、”崩れる”楽しさを求めて

皆さん、こんにちは!

突然ですが、「落ちものパズル」と聞いて、どんなゲームを思い浮かべますか?
おそらく多くの人が、上から降ってくるブロックをきれいに積み上げ、ラインをそろえて消していく、あの古典的で完成されたゲーム性を想像するでしょう。僕もその一人で、あのシンプルながら奥深いゲームの虜になってきました。

しかし、ある日ふと思ったのです。シンプルなのはやっぱりやるだけでおもしろい。砂でもおもしろい。ならブロックでやってみたい!「あの整然と並んだブロックが、もし物理法則に従ってガラガラと崩れたら、もっと面白くなるんじゃないか?」と。

ブロックを消した後の、あの機械的な「再配置」ではなく、物理演算によって上のブロックが自然に落下し、予期せぬ連鎖が生まれる。そんな、予測不能でダイナミックな落ちものパズルがあったら――。

そんな衝動から生まれたのが、今回ご紹介する物理演算落ちものパズルゲーム**『ボロリス』**です。

この記事では、単なる思いつきだったアイデアが、どのようにして遊べるゲームになったのか、その企画から完成までの全記録を余すところなくお伝えします。

▼ゲームはこちらからプレイ!(PCでもiphoneでもandroidでも)▼
https://rino-illustrator.info/block_Tetris/index.html

本記事で語ること:

  • 『ボロリス』がどんなゲームなのか、その魅力の紹介

  • なぜ物理演算エンジンにMatter.jsを選んだのか

  • 物理演算とパズルゲームを融合させるための技術的な挑戦

  • 連鎖ランキング機能といったシステムの裏側

  • 開発終盤で僕を苦しめ続けた、忌まわしきiPhone特有バグとの死闘の記録

プログラミングにある程度詳しい方はもちろん、「ゲーム開発の裏側ってどうなってるの?」と興味のある方にも楽しんでいただけるよう、専門的な話と開発のドラマを織り交ぜて解説していきます。

この記事を読み終える頃には、きっとあなたも『ボロリス』をプレイしてみたくなるはず。そして、ゲーム開発の奥深さと楽しさを感じていただけたら、これほど嬉しいことはありません。

ちなみに音楽は自分で制作しました。

ゲーム紹介:物理演算落ちものパズル『ボロリス』とは?

まずは、このゲームがどんなものなのかをご紹介します。『ボロリス』は、伝統的な落ちものパズルに物理演算の要素を大胆に取り入れた、新しい感覚のパズルゲームです。

基本ルール:繋げて、消す。シンプルだけど奥深い。

ゲームの目的は非常にシンプルです。画面上部から出現する様々な形のブロックを操作し、フィールドに積み上げていきます。

普通の落ちものパズルと違うのは、ここからです。

同じ色のブロックが、フィールドの左壁から右壁まで途切れることなく繋がると、その繋がったブロックが一斉に消滅します。テトリスのように「横一列」をそろえるのではなく、「同じ色で両端を繋ぐ」のがこのゲームのキモです。

同じ色のブロックが左壁と右壁を繋ぐと… ラインが消滅!

####最大の特徴:物理演算がもたらす”崩壊”のカタルシス

『ボロリス』の真骨頂は、ブロックが消えた後にあります。

ラインが消滅すると、その上に乗っていたブロックたちは、物理法則に従ってガラガラと崩れ落ちます。 きれいに積んだはずの塔が、土台を失ってバランスを崩し、思いもよらない形で再配置されるのです。

この「崩れる」というワンクッションが、ゲームに大きな戦略性と偶発性をもたらします。そして、この崩落によって偶然にも新たなラインが繋がり、**「連鎖」**が発生した時の爽快感は、まさに格別です。

ブロックが消えた後、上のブロックは物理法則に従って自然に落下・崩壊する。

スコアシステムと連鎖ボーナス

スコアは、ブロックを素早く落とすことによるボーナスや、消したブロックの数によって加算されます。そして、このゲームのハイスコアの鍵を握るのが連鎖ボーナスです。

  • 2連鎖: スコア 1.5倍

  • 3連鎖: スコア 2.0倍

  • 4連鎖: スコア 2.5倍

連鎖が続けば続くほど、スコアは爆発的に増加していきます。物理演算による偶然の連鎖を狙うか、あるいは自らの手で連鎖の土台を組み上げるか。プレイヤーの戦略が試されます。

もちろん、オンラインランキング機能も搭載。世界中のプレイヤーとスコアを競い合うことができます。

開発技術スタック:ウェブの標準技術だけでゲームは作れる

『ボロリス』は、特別なゲームエンジンやフレームワークをほとんど使わず、現代のウェブブラウザが持つ標準的な技術だけで作られています。

  • 言語: HTML, CSS, JavaScript

  • 物理演算エンジンMatter.js

  • サウンドエンジンWeb Audio API

  • ランキング機能: PHP, JSON

特筆すべきは、物理演算の心臓部であるMatter.jsです。これはJavaScriptで書かれた2D物理演算エンジンで、比較的軽量ながらも、今回のようなゲームを作るには十分すぎるほどの機能を備えています。

それでは、ここから具体的な実装の裏側を覗いていきましょう。

技術解説①:Matter.jsで実現する「ブロックの物理挙動」

ゲームの核となる物理演算。これをどう実装したのか、Matter.jsの機能と共に解説します。

ブロックの生成:パーツを合体させて1つの物体に

まず、テトリスのようなブロック(テトロミノ)を、Matter.jsの世界でどう表現するか。Matter.jsには、複数の小さな物体(ボディ)を合体させて、1つの大きな複合ボディを作り出す機能があります。

『ボロリス』のブロックは、PARTICLE_SIZE(20px)四方の正方形のパーツから成り立っています。例えばL字ブロックなら、4つの正方形パーツをL字型に配置し、それを Body.create メソッドで1つの物体として生成します。

JavaScript

// block.js より抜粋

function createBlock(shape, color, x, y, options = {}) {
    // ... ブロックの中心座標を計算 ...
    
    // shape配列(例: [[0, 0], [0, 1], [0, 2], [1, 2]])を元に
    // 複数の四角いパーツ(Bodies.rectangle)を生成
    const parts = shape.map(pos => {
        const partX = x + (pos[0] - logicalCenterX) * PARTICLE_SIZE;
        const partY = y + (pos[1] - logicalCenterY) * PARTICLE_SIZE;
        return Bodies.rectangle(partX, partY, PARTICLE_SIZE, PARTICLE_SIZE, {
            render: { fillStyle: color }
        });
    });

    // parts配列を元に、1つの複合ボディを生成
    const block = Body.create({
        parts: parts, // ここにパーツの配列を指定する
        friction: 0,
        restitution: 0,
        ...options
    });
    
    // 回転しすぎないように慣性を無限大に設定
    Body.setInertia(block, Infinity);
    return block;
}

この仕組みのおかげで、落下中は4つのパーツが一体となって動きますが、後述する「分解」の際には、個々のパーツとして扱えるようになります。

落下と衝突、そして”崩壊”へ

プレイヤーがブロックを落下させると、それはMatter.jsの物理ワールドに追加され、重力に従って落ちていきます。地面や他のブロックに衝突すると、物理法則に基づいたリアルな挙動を見せます。

そして、このゲームの最大の見せ場である「崩壊」の瞬間です。ブロックが地面や他の静的なブロックに衝突すると、collisionActive というイベントが発生します。このイベントを監視し、衝突したブロックに対して「分解せよ」という命令を下します。

JavaScript

// game.js より抜粋

function breakBlock(block) {
    if (!block || !block.isBreakable) return;
    playSound('break-sound');
    block.isBreakable = false; // 一度だけ分解されるようにフラグ管理

    // 複合ボディから、個々の子パーツの位置や色といった情報を抽出
    const newPartsData = block.parts.slice(1).map(part => ({ 
        x: part.position.x, 
        y: part.position.y, 
        render: part.render 
    }));

    // Matter.jsのワールドから、元の複合ボディを削除
    World.remove(engine.world, block, true);

    // 抽出した情報を元に、同じ場所に新しい独立したパーティクルを再生成
    const newParts = newPartsData.map(data => Bodies.rectangle(data.x, data.y, PARTICLE_SIZE, PARTICLE_SIZE, {
        render: data.render,
        label: 'particle', // 物理挙動を持ったただの粒子
        // ... その他の物理パラメータ ...
    }));
    
    // 新しいパーティクルをワールドに追加
    World.add(engine.world, newParts);
}

この breakBlock 関数が、『ボロリス』の”崩れる”楽しさの源泉です。複合ボディを消し去り、その場所に同じ見た目の独立した粒子を再配置することで、まるでブロックがバラバラに砕けたかのような演出を実現しています。

固定化:グリッドに吸着するパーティクル

バラバラになったパーティクルは、しばらく物理法則に従って転がったりしますが、最終的にはグリッド(マス目)に沿って静止しなければなりません。さもないと、プレイヤーは次のブロックを正確に積むことができません。

これを実現するため、afterUpdate という、物理演算の1フレームが終わるたびに呼び出されるイベントを利用します。

このイベント内で、まだ固定されていない全パーティクル (isSnapped: false) をチェックし、「真下にあるマスがすでに埋まっている」または「地面に到達した」という条件を満たした場合、そのパーティクルを静的(static)な物体に変化させ、グリッドにぴったり合うように座標を強制的に補正します。

JavaScript

// game.js の Events.on(engine, 'afterUpdate', ...) 内より抜粋

// ...
const onGround = gridY >= effectiveGridHeight - 1; // 地面か?
const supported = occupiedCells.has(`${gridX},${gridY + 1}`); // 下にブロックがあるか?

if (onGround || supported) {
    // 座標をグリッドに強制補正
    const snappedX = WALL_THICKNESS + PARTICLE_SIZE / 2 + gridX * PARTICLE_SIZE;
    const snappedY = gridY * PARTICLE_SIZE + PARTICLE_SIZE / 2;
    Body.setPosition(p, { x: snappedX, y: snappedY });
    
    // 角度を0にリセット
    Body.setAngle(p, 0); 
    
    // 物理挙動をオフにし、静的な物体に変化させる
    Body.setStatic(p, true); 
    
    p.isSnapped = true; // 固定済みフラグを立てる
    somethingSnappedThisFrame = true;
}

この処理により、物理演算のダイナミックな動きと、パズルゲームとしての整然とした操作性を両立させています。

技術解説②:「連鎖」と「スコア」を司るゲームロジック

物理演算の上に、パズルゲームとしての面白さを加えるロジックを実装していきます。

ライン消去判定:幅優先探索で”繋がり”を見つけ出す

「同じ色のブロックが左右の壁に繋がっているか」を判定するのは、このゲームで最も複雑なロジックの一つです。これを実現するために、幅優先探索(BFS) というアルゴリズムを用いています。

  1. まず、固定された全パーティクルを二次元配列(グリッド)にマッピングします。

  2. グリッドを左上から右下へ順番にスキャンしていきます。

  3. まだチェックしていないパーティクルを見つけたら、そこを起点に探索を開始します。

  4. キュー(探索待ちのパーティクルを入れるリスト)を使い、同じ色で隣接しているパーティクルを次々と見つけていきます。

  5. この探索の過程で、**「左端の列(x=0)に到達したか」「右端の列に到達したか」**をフラグで記録します。

  6. キューが空になり、探索が完了した時点で両方のフラグが立っていれば、探索したパーティクルの集合は「左右の壁を繋いでいる」と判断し、消去対象リストに追加します。

この処理をすべてのパーティクルがチェック済みになるまで繰り返すことで、フィールド内のすべての消去対象ラインを一度に見つけ出すことができます。

連鎖の実装:非同期処理(async/await)で流れを制御する

連鎖は、「ライン消去 → ブロック落下 → 再びライン消去」という一連の流れです。これをプログラムで実現するために、async/await を使った非同期処理と再帰を組み合わせました。

JavaScript

// game.js より抜粋

async function checkLinesAndChain(chainCount = 1) {
    // 1. 現在の盤面で消えるラインを探す
    const particlesToRemove = findRemovableParticles(); // 探索ロジック

    // 2. 消えるラインがなければ、連鎖終了。スコア0を返す。
    if (particlesToRemove.size === 0) {
        return 0;
    }

    // 3. 消えるラインがあった場合の処理
    // スコアを計算(連鎖ボーナスを適用)
    const multiplier = 1 + (chainCount - 1) * 0.5;
    const scoreForThisChain = particlesToRemove.size * multiplier;

    // 消去エフェクトを見せるために、少し待つ
    await wait(150); 
    
    // パーティクルを削除し、上のブロックを落下・再配置
    removeAndRearrangeParticles(particlesToRemove);

    // 再配置が終わるのを少し待つ
    await wait(100);

    // 4. 自分自身を再び呼び出し、次の連鎖をチェック
    // chainCountを+1する
    const scoreFromNextChains = await checkLinesAndChain(chainCount + 1);

    // 5. 今回のスコアと、次の連鎖以降のスコアを合算して返す
    return scoreForThisChain + scoreFromNextChains;
}

この checkLinesAndChain 関数が連鎖システムの心臓部です。

  • async/await を使うことで、「待つ」という処理を直感的に書けるようになり、複雑なアニメーションの流れを制御しやすくなっています。

  • 処理の最後に自分自身を呼び出す(再帰)ことで、消えるラインがなくなるまで、半自動的に連鎖の判定が繰り返される仕組みです。

  • 連鎖がすべて終わった後、最終的な合計スコアが返され、プレイヤーのスコアに一度に加算されます。

この間、プレイヤーが新しいブロックを落とせないように isChaining というフラグで操作をロックし、連鎖が終わると解除します。これにより、派手な連鎖演出をじっくりと楽しむことができます。

開発の壁:僕を苦しめたバグとの戦いの記録

順調に見えた開発ですが、特に終盤、特定の環境でだけ発生する不可解なバグに何度も頭を悩まされました。ここでは、その中でも特に印象的だった2つのバグとの戦いをご紹介します。

事例①:ゴーストブロック衝突事件

【症状】
PCでは問題ないのに、iPhoneでプレイすると、落下中のブロックが、地面に表示されるはずのゴーストブロック(半透明の着地予測)に衝突し、空中でバラバラに分解されてしまう。

【原因】
Matter.jsには isSensor という便利なプロパティがあります。これを true に設定すると、その物体は「当たり判定はあるが、物理的な衝突はしない(すり抜ける)」という、まさにゴーストのような状態になります。

ゴーストブロック本体にはこの設定をしていたのですが、複合ボディを構成する個々の子パーツにまで、この isSensor 設定が自動で引き継がれていなかったのです。PCブラウザの処理速度では偶然問題が起きませんでしたが、タイミングがシビアなiPhoneでは、落下中のブロックの一部のパーツが、ゴーストブロックの一部のパーツ(センサーになっていない)に衝突し、分解イベントが誤爆していました。

【解決策】
ブロックを生成する createBlock 関数を修正し、親ボディがセンサーの場合は、そのすべての子パーツにも isSensor: true を明示的に設定するようにしました。たったこれだけの修正で、あの悪夢のような空中分解はピタリと収まりました。

事例②:消えたランキング事件

【症状】
これもiPhoneでのみ発生。ゲームを起動しても、スタート画面に表示されるはずのオンラインランキングが真っ白なまま。PCでは問題なく表示される。

【原因】
これもまた、iPhone (Safari) 特有の、非常に厳しいセキュリティポリシーが原因でした。

  • 音声再生の制限: Safariは、ユーザーによる明確なタップ操作がない限り、音声関連の処理(AudioContextの初期化)を開始させてくれません。

  • 非同期処理の競合: 僕のコードは、「ゲーム開始のタップ」をきっかけに、「音声の初期化」と「ランキングデータの取得(fetch)」をほぼ同時に開始していました。PCでは問題なく両立できていましたが、Safariはこの複雑な処理をうまく捌ききれず、音声初期化を優先(あるいはブロック)する過程で、ランキング取得の処理が失敗していたのです。

  • 強力なキャッシュ: さらに追い打ちをかけたのが、Safari、特に「ホーム画面に追加」したWebアプリの強力なキャッシュ機能です。一度この不具合が起きると、その状態を頑固に記憶し続け、こちらがコードを修正しても古いバグのあるファイルを読み込み続けてしまうのです。

【解決策】
まず、iPhoneのSafariのキャッシュを完全にクリアしました。そして、game.js の起動処理を見直し、「ゲーム開始のタップ」というユーザーの最初の操作をきっかけに、①音声初期化 → ②BGM再生 → ③ゲームループ開始 というように、処理が一つずつ順番に、確実に行われるようにロジックを整理しました。

このキャッシュと非同期処理の問題は、Webでリッチなアプリケーションを作る際に多くの開発者が直面する壁です。今回の経験は、僕にとって大きな学びとなりました。

今後の展望

『ボロリス』は、これで一旦完成としますが、まだまだアイデアは尽きません。

  • 新しいブロック形状や特殊ブロック: ゲーム性をさらに豊かにする新しい要素。

  • 対戦モード: 友達やオンラインの誰かと、リアルタイムで腕を競い合えたら…。

  • パフォーマンス改善: さらに多くのブロックが表示されても、常に滑らかな物理演算を維持するための最適化。

もし、このゲームを遊んでくれた皆さんから「こんな機能があったら面白い!」といったフィードバックをいただけたら、今後のアップデートの参考にさせていただきます。

おわりに:創造の喜びと、完成の達成感

たった一つの「ブロックが崩れたら面白そう」というアイデアから始まった『ボロリス』の開発。それは、Matter.jsという素晴らしいツールとの出会いであり、物理演算の奥深さに触れる旅であり、そして予期せぬバグとの長い戦いでもありました。

この記事を通じて、ゲームの裏側にある技術的な工夫や、開発者がどんな壁にぶつかり、どう乗り越えていくのか、その一端を感じていただけたなら幸いです。

何より、この記事を読んで『ボロリス』に興味を持ってくれたあなたに、心から感謝します。ぜひ一度、この”崩れる”落ちものパズルの新しい楽しさを体験してみてください。そして、もしよろしければ、ランキングにあなたの名前を刻んでいってください。

▼ゲームはこちらからプレイ!(PCでもiphoneでもandroidでも)▼
https://rino-illustrator.info/block_Tetris/index.html

最後までお読みいただき、本当にありがとうございました!


バージョン履歴

1.1 2025/11/22 回転、落下ボタンの配置とUI、ゴーストブロックの不具合を調整
1.0 2025/11/21 公開