仕組み解説 — Pixel Balanceの内部実装

このページでは、Pixel Balanceがブラウザの中でどのように動いているのかを、実際のソースコードに即して解説します。使っている物理エンジンの設定値、同色ブロックが消える判定の仕組み、ゲームオーバーの条件など、すべて現在公開中のバージョンの実装そのままです。ゲームの挙動に「なぜ?」と思ったことがある方や、ブラウザゲームの作り方に興味がある方に向けた技術ドキュメントです。

最終更新: 2026年8月11日

この記事の要点

1. 物理エンジンMatter.jsと重力設定

Pixel BalanceはJavaScript製の2D物理エンジン「Matter.js」を使用しています。剛体シミュレーション、衝突検知、拘束処理を内蔵したライブラリで、ブラウザ上でそのまま動作するため、プラグインもネイティブコードも必要ありません。

重力は次の1行で設定しています。

engine = Engine.create();
engine.world.gravity.y = 1.2;

Matter.jsのデフォルト重力は1.0ですが、本作では1.2とやや強めにしています。理由は待ち時間の短縮です。ブロックを落としてから着地するまでの時間が長いと、1回のプレイのテンポが間延びします。かといって強くしすぎると着地の衝撃で塔が崩れやすくなりすぎるため、両者の折り合いとしてこの値にしています。重力は「上から下」の一方向のみで、ゲーム中に変化することはありません。プレイヤーは常に同じ物理ルールの上で戦略を組み立てられます。

2. ブロックの生成 — 実は「1種類」しかない

画面に落ちてくるブロックは、すべて横長の長方形1種類です。ただし幅にランダムな揺らぎを持たせているため、プレイ中は大小さまざまなブロックが降ってくるように感じられます。生成コードは次の通りです。

const w = (baseDimension * 0.08) + (Math.random() * baseDimension * 0.05);
const h = baseDimension * 0.045;  // 高さは固定

currentBlock = Bodies.rectangle(canvas.width / 2, spawnY, w, h, {
    label: 'pixel',
    isStatic: true,
    render: { fillStyle: color },
    friction: 0.8,
    restitution: 0.05
});

baseDimensionmin(画面幅, 画面高さ×1.5)で求める基準寸法で、端末の画面サイズが違っても相対的なゲームバランスが変わらないようにするための仕組みです。幅は基準寸法の8%〜13%の間でランダム、高さは4.5%固定。つまりアスペクト比がおよそ1.8:1〜2.9:1の間で変化します。「幅の広いブロックが来たら土台に、細いブロックが来たら隙間埋めに」という判断が生まれるのは、この揺らぎのおかげです。

色は次の7色のパステルカラーからランダムに選ばれ、画面上部に「次のブロックの色」がプレビュー表示されます。

const COLORS = ['#F48FB1', '#CE93D8', '#90CAF9',
                '#A5D6A7', '#FFF59D', '#FFCC80', '#EF9A9A'];

3. 摩擦係数と反発係数

ブロックには摩擦係数0.8・反発係数0.05、土台(地面)には摩擦係数1.0を設定しています。

4. 土台とストッパー

塔を載せる土台は、画面中央に置かれた静的な長方形です。幅は基準寸法の60%で、画面の端までは届いていません。つまり土台からはみ出した位置に積んだブロックは、支えを失って落下します。

土台の左右の端には、小さなストッパー(幅は基準寸法の1.5%、高さ3%の突起)が付いています。これは最下段のブロックが着地の勢いでそのまま土台の外へ滑り出てしまう事故を防ぐための「縁」です。ストッパーがあるおかげで、土台の端ぎりぎりを攻める配置にも挑戦できます。

5. 操作の実装 — 振り子とドロップ

画面上部で左右に揺れているブロックは、物理演算で動いているのではなく、isStatic: trueのまま毎フレーム座標を直接書き換えています。

const time = Date.now() * 0.003;
const range = (baseDimension / 2) - (baseDimension * 0.1);
const x = (canvas.width / 2) + Math.sin(time) * range;
Body.setPosition(currentBlock, { x: x, y: spawnY });

サインカーブによる単純な往復運動なので、端に近づくほど遅く、中央付近で最も速く動きます。「端を狙うのは簡単、中央付近のピンポイントは難しい」という操作の緩急は、この式から自然に生まれています。

タップ(クリック)するとBody.setStatic(currentBlock, false)が呼ばれ、その瞬間からブロックは物理エンジンの管理下に入って落下します。700ミリ秒後にスコアが1加算され、次のブロックが出現します。

6. スコアの仕組み — すべて「+1」

本作のスコアはきわめてシンプルで、加点はすべて1点ずつです。

高さボーナス、連鎖倍率、コンボといった仕組みはありません。画面に表示されるフローティングテキストが常に「+1」なのはこのためです。倍率システムを入れなかったのは意図的な設計判断で、スコアを「何個積み、何個消したか」の合計そのものにすることで、ランキングの数字がそのままプレイ内容を表すようにしています。100点のプレイヤーは、100回の投下と消去を成功させた人です。

7. マッチ判定アルゴリズム

「同じ色のブロックが3個以上くっつくと消える」判定は、次の流れで実装されています。

  1. トリガー:Matter.jsのcollisionStartイベント(新しい接触の発生)でフラグを立て、物理更新後のafterUpdateで1回だけ判定関数を実行します。毎フレーム無条件に判定するのではなく「接触に変化があったときだけ」動かすことで、計算負荷を抑えています。
  2. クラスタ探索:現在の全接触ペアを調べ、同じ色のブロック同士の接触をたどって幅優先探索(BFS)で「同色のつながり(クラスタ)」を構築します。
  3. 閾値判定:クラスタのサイズが3以上なら、そのクラスタ全体を消去対象にします。4個・5個つながっていれば、その全部が一度に消えます(消えた個数ぶん加点)。
  4. 消去演出:対象ブロックは300ミリ秒かけて縮小するアニメーションを再生し、その後World.removeで物理世界から削除されます。

削除の直後には、もうひとつ大事な処理があります。

all.forEach(b => { if (!b.isStatic) Matter.Sleeping.set(b, false); });

物理エンジン上で静止状態と見なされていたブロックを全部「起こす」処理です。消えたブロックの上に載っていたブロックは、支えを失ったのだから落下し直す必要があります。この1行がないと、宙に浮いたまま止まっているように見えるブロックが発生することがあります。なお、Matter.jsのSleeping機能(静止した物体を計算から除外する最適化)自体は本作では有効化しておらず、この呼び出しは消去後の再沈降を確実にするためだけに使っています。

8. ゲームオーバー判定と復活

ゲームオーバーの条件は「ブロックが画面の下端より100ピクセル下まで落ちたこと」です。描画ループの中で毎フレーム、全ブロックの座標をチェックしています。

if (pos.y > canvas.height + 100 && !body.isStatic && !body.isRemoving) {
    gameOver(body);
}

猶予フレームや復帰判定はなく、条件を満たした瞬間に即ゲームオーバーです。画面外100ピクセルというマージンがあるため、塔から弾かれたブロックが画面の外に見えなくなってから一拍おいて終了する、という体感になります。

ゲームオーバー時には、1プレイに1回だけ使える復活が提示されます。復活すると、ゲームオーバーの引き金になったブロックが取り除かれ、isInvincible フラグが約2秒間立ちます。この無敵時間の間に画面外へ落ちたブロックはゲームオーバー判定を通らず World.remove() で削除されるだけになるため、崩れかけた塔が落ち着くまでの猶予として機能します。

9. 「同時のせ」と「高さ」の計測

プレイ記録として計測している「最大同時のせ個数」と「最大高さ」は、30フレームに1回の頻度で次のように計算しています。

  1. 地面・ストッパーなどの静的ボディを起点に、全ボディ間の接触(Bounds.overlapsで粗く絞り込み、SAT.collidesで正確に判定)から接触グラフを構築します。
  2. グラフを幅優先探索でたどり、「地面まで接触がつながっているブロック」だけを支持されているブロックとして数えます。空中に浮いている(落下中の)ブロックは含まれません。
  3. 支持されているブロックの個数の最大値が「同時のせ」記録に、最上端の座標から地面表面までの距離をブロック1段分の高さで割った値が「高さ」記録になります。

単に「存在するブロック数」を数えるのではなく「塔として成立しているブロック数」を数えているのがポイントで、ステッカーの解除条件(同時20個のせ、高さ5ブロックなど)はこの値を参照しています。

10. 描画とランキング

描画にはMatter.js付属のレンダラー(Matter.Render)を使わず、requestAnimationFrameで回す自前のCanvas 2D描画を採用しています。物理エンジンからは各ボディの位置と角度だけを受け取り、塗り・枠線・上面のハイライト・消去時の縮小アニメーションなどをすべて自分で描いています。見た目の表現(テーマカラーやステッカー背景など)を物理演算から切り離して自由に差し替えるための構成です。

ランキングはFirebase Firestoreで実装しています。Googleアカウントでログインすると、スコアが日付キー・月キーとともに保存され、「日次」「月間」「全期間」の3種類のランキングに反映されます。ランキングはゲーム画面からいつでも閲覧できます。

← トップに戻る