GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

javascript ブロック崩し 反射角で変わる9パターン

javascript ブロック崩し は9パターンとも同じコートで動き、パターンごとに読む値だけが違います。パドルの中央ではなく端で打て — 当たった位置の値が反射角を決める。2番はその値で跳ね返る角度を曲げ、6番は値が大きいほど残像を長く伸ばし、8番は壁に当たる瞬間同じ角度値を反転させる。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

javascript ブロック崩し 9パターン — 反射角はどこで分かれるか

以下の9パターンはすべて480×300のキャンバスに同じコート・パドル・ボールを使い、シード固定の擬似乱数配置でリロードしても同じ見た目になります。パターンごとに変わるのは、次に何が起きるかを決めるためにコードがどの一つの値を読んでいるかです——1番はドラッグの差、2番は当たった位置、5番は落下速度を読みます。冒頭で触れた3つが軸に選ばれているのには理由があり、2番はこの先すべてが依存する反射ルールそのもの、6番はその同じルールの出力を残像の長さとして見せ、8番は2番の角度計算をやり直す代わりに符号を反転させるだけの境界ケースです。

こういうブロック崩しデモが実際に置かれる場所

動くブロック崩しは単体のブラウザゲームとして公開されることは今はほとんどなく、ビルド待ちの画面で遊べる待機演出や、リンク切れの404ページの隠し要素、本物のキャンバス計算が書けることを見せるポートフォリオページの主役として使われます。ブロック崩し 作り方 簡単な例だけで満足せず数式まで踏み込みたい人ほど、こういう使い方をします。小さなSaaSダッシュボードの順番待ち画面や、カフェのキオスク端末のアイドルタイマー、展示ブースの余った画面にも、タイトル画面やライフの管理を持つ一つの完成したゲームループより、こうした9つの小さな物理ルールのほうが向いています。

01ドラッグ位置を追いかけるパドル

パドルはポインターの位置にぴたりと吸い付くのではなく、ドラッグした目標値との差を毎フレーム少しずつ縮めながら追いかけます。指数減衰の係数がPADDLE_LERP_FACTORで、大きいほど吸い付きは強く、小さいほど重たく遅れます。矢印キーでも同じ目標値を動かせます。

ドラッグ位置を追いかけるパドル
新しいタブで開く

01 · ドラッグ位置を追いかけるパドル — 計測 累積面積 30.092% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PADDLE_LERP_FACTORdragGapPxrebuildLevel
  function paddleStep(dt) {
    if (keys.left) paddle.target -= 160 * dt;
    if (keys.right) paddle.target += 160 * dt;
    paddle.target = clamp(paddle.target, WALL + paddle.w / 2, LW - WALL - paddle.w / 2);
    var goal = paddle.target - paddle.w / 2;
    var k = 1 - Math.pow(1 - PADDLE_LERP, dt * 60);
    paddle.x += (goal - paddle.x) * k;
    paddle.x = clamp(paddle.x, WALL, LW - WALL - paddle.w);
  }

ここで実際に計算しているのはフレームレートに依存しない減衰係数k = 1 - Math.pow(1 - PADDLE_LERP, dt * 60)です。60fpsでも30fpsでも同じ追従感になり、paddle.x += (goal - paddle.x) * PADDLE_LERP_FACTORのような単純な線形補間より安定します。

計測値: 累積面積30.1%、強度平均80.4、動いたフレーム23/23。

02当たった位置で反射角が分かれるボール

ボールが跳ね返る角度は固定ではありません。パドルの幅のどこに当たったかという位置の値を読み取り、それをそのまま反射角に変換します。中央に当たればほぼ真上へ、端に当たれば大きく斜めへ跳ねます。

当たった位置で反射角が分かれるボール
新しいタブで開く

02 · 当たった位置で反射角が分かれるボール — 計測 累積面積 35.241% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

MAX_REFLECT_ANGLElastHitPosguideTimer
  function onPaddleHit() {
    var hitPos = clamp((ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2), -1, 1);
    var angle = hitPos * MAX_REFLECT_ANGLE;
    var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
    ball.vx = Math.sin(angle) * speed;
    ball.vy = -Math.abs(Math.cos(angle)) * speed;
    ball.y = PADDLE_Y - BALL_R - 0.5;
    spawnFlash(ball.x, PADDLE_Y);
    lastHitPos = hitPos; lastAngle = angle; guideTimer = 0.4;
  }

hitPosを-1から1にクランプしMAX_REFLECT_ANGLE(62度)を掛けてからMath.sin/Math.cosに渡す、その一回の掛け算がこの javascript ゲーム ブロック崩し 全体が土台にしている反射ルールです。

計測値: 累積面積35.2%、強度平均103.6、動いたフレーム23/23。

03衝突判定のたびに得点が上がるブロック列

ボールとブロックはどちらも矩形として扱われ、軸並行境界ボックス(AABB)判定で重なった瞬間を検出します。当たったブロックだけが消え、得点はそれまでに壊した数に応じて増える累積型です——一撃ごとの加点ではありません。

衝突判定のたびに得点が上がるブロック列
新しいタブで開く

03 · 衝突判定のたびに得点が上がるブロック列 — 計測 累積面積 30.576% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SCORE_PER_BRICKscoreaabb
  function onLevelClear() { spawnFlash(LW / 2, LH / 2); rebuildLevel(); }
  function onBrickHit(b) {
    spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
    b.alive = false; cleared += 1; score = cleared * SCORE_PER_BRICK;
    if (cleared >= bricks.length) { onLevelClear(); }
  }

この9パターンの半分近くが頼っているaabb()は4つの比較だけで重なりを判定し、score = cleared * SCORE_PER_BRICKが壊した数をそのまま得点の値に変換します。

計測値: 累積面積30.6%、強度平均86.5、動いたフレーム23/23。

04複数回当てないと崩れないひび割れブロック

ブロックは一撃では消えません。当たった回数の値hitsが増えるたびにひびが一本ずつ追加され、値がBRICK_HIT_LIMITに達した瞬間だけブロックが崩れ、レベルクリア判定に加わります。

複数回当てないと崩れないひび割れブロック
新しいタブで開く

04 · 複数回当てないと崩れないひび割れブロック — 計測 累積面積 30.507% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BRICK_HIT_LIMITcrackLinesdrawBrick
  function onLevelClear() { spawnFlash(LW / 2, LH / 2); rebuildLevel(); }
  function onBrickHit(b) {
    spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
    b.hits += 1;
    if (b.hits >= BRICK_HIT_LIMIT) {
      b.alive = false; cleared += 1;
      if (cleared >= bricks.length) { onLevelClear(); }
    }
  }

塗りの不透明度も同じファイルの別の場所で同じ比率から決まります。damage = b.hits / BRICK_HIT_LIMITがヒット数に応じてブロックを透明に近づけるため、ひびの本数と色の薄さは常に一致します。

計測値: 累積面積30.5%、強度平均82.2、動いたフレーム23/23。

05重力値どおりに落ちるアイテムを受けるパドル

壊れたブロックのうちおよそ半分の位置からアイテムが現れ、落下速度は一定ではありません。重力加速の値が毎フレーム速度に加算され、落ち始めはゆっくり、時間が経つほど速く落ちます。

重力値どおりに落ちるアイテムを受けるパドル
新しいタブで開く

05 · 重力値どおりに落ちるアイテムを受けるパドル — 計測 累積面積 28.728% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GRAVITY_ACCELpowerupscatchFlash
  function onBrickHit(b) {
    spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
    b.alive = false; cleared += 1;
    dropSeq += 1;
    if (dropSeq % 2 === 0) {
      powerups.push({ x: b.x + b.w / 2, y: b.y + b.h / 2, vy: 0 });
    }
    if (cleared >= bricks.length) { onLevelClear(); }
  }

dropSeq % 2 === 0が実際のドロップ条件で、壊した2個に1個の割合でアイテムが出ます。加速の値自体は数行下で速度に先に加算されてから位置が動くため、実際に落下する物体が加速していく順序と同じです。

計測値: 累積面積28.7%、強度平均82.1、動いたフレーム23/23。

06速度値の分だけ伸びる球の残像

ボールの後ろには過去の位置の残像が残り、見える残像の数は今のボールの速度によって変わります。遅いときはほぼ残像がなく、パドルに何度も当たって加速すると尾のように長く伸びます。

速度値の分だけ伸びる球の残像
新しいタブで開く

06 · 速度値の分だけ伸びる球の残像 — 計測 累積面積 31.421% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPEED_STEPtrailpushTrail
  function pushTrail() { trail.push({ x: ball.x, y: ball.y }); }
  function stepExtra(dt) {
    pushTrail();
    var speedNow = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
    var count = Math.round(clamp((speedNow - BASE_SPEED) / (SPEED_MAX - BASE_SPEED), 0, 1) * (TRAIL_MAX - 3)) + 3;
    while (trail.length > count) trail.shift();
  }

countは現在の速度がBASE_SPEEDとSPEED_MAXのどこにあるかから毎フレーム計算し直され、残像の配列はその長さまで切り詰められます。固定サイズの尾が薄く消えるのではなく、長さの値そのものが毎フレーム変わります。

計測値: 累積面積31.4%、強度平均82.5、動いたフレーム23/23。

07連続ヒットごとに倍率が上がるコンボ値

ブロックを連続で当て続けるたびにコンボの値が1ずつ増え、画面の倍率表示はその値に増分を掛けた数です。一度でも外すとコンボは0に戻り、倍率も即座に元に戻ります。

連続ヒットごとに倍率が上がるコンボ値
新しいタブで開く

07 · 連続ヒットごとに倍率が上がるコンボ値 — 計測 累積面積 30.928% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

COMBO_MULT_STEPcomboCountresetCombo
  function onLevelClear() { spawnFlash(LW / 2, LH / 2); rebuildLevel(); }
  function onBrickHit(b) {
    spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
    b.alive = false; cleared += 1; comboCount += 1;
    if (cleared >= bricks.length) { onLevelClear(); }
  }
  function onMiss() {
    spawnFlash(ball.x, LH - 4);
    resetCombo();
    resetBall();
  }

倍率は1 + comboCount * COMBO_MULT_STEPという一行だけで毎回計算し直されるため、外した瞬間に数値がアニメーションせず即座に戻ります。

計測値: 累積面積30.9%、強度平均87.1、動いたフレーム23/23。

08壁に当たると角度が反転する跳ね返り

ボールが左右の壁に触れても、横方向の速度が単純に反転するわけではありません。符号だけが反転し、縦方向の値はそのまま残るため、入ってきた角度と同じ角度で壁を離れます。

壁に当たると角度が反転する跳ね返り
新しいタブで開く

08 · 壁に当たると角度が反転する跳ね返り — 計測 累積面積 30.296% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

mirrorWallBounceWALL_MARGINmirrorFlashTimer
  function ballStep(dt) {
    ball.x += ball.vx * dt;
    ball.y += ball.vy * dt;
    if (ball.x - BALL_R <= WALL) { ball.x = WALL + BALL_R; mirrorWallBounce('left'); }
    else if (ball.x + BALL_R >= LW - WALL) { ball.x = LW - WALL - BALL_R; mirrorWallBounce('right'); }
    if (ball.y - BALL_R <= WALL) { ball.y = WALL + BALL_R; ball.vy = Math.abs(ball.vy); }
    if (ball.vy > 0 && aabb(ball.x - BALL_R, ball.y - BALL_R, BALL_R * 2, BALL_R * 2, paddle.x, PADDLE_Y, paddle.w, PADDLE_H)) {
      onPaddleHit();
    }
    for (var i = 0; i < bricks.length; i += 1) {
      var b = bricks[i];
      if (b.alive && aabb(ball.x - BALL_R, ball.y - BALL_R, BALL_R * 2, BALL_R * 2, b.x, b.y, b.w, b.h)) {
        ball.vy = -ball.vy;
        onBrickHit(b);
        break;
      }
    }
    if (ball.y - BALL_R > LH) onMiss();
  }

mirrorWallBounce()の中身は三項演算子一つだけです。角度を計算し直すのではなく符号だけを反転させることで、入射角と反射角が完全に一致します——だから2番の角度計算を同じ跳ね返りで二度走らせる必要がありません。

計測値: 累積面積30.3%、強度平均93.6、動いたフレーム23/23。

09ブロックが全て消えると弾ける粒子

最後のブロックのaliveフラグがfalseになった瞬間、コートの中央から粒子が一斉に放たれ、全方向へ均等に広がります。これが次のレベルへ進む合図です。

ブロックが全て消えると弾ける粒子
新しいタブで開く

09 · ブロックが全て消えると弾ける粒子 — 計測 累積面積 29.451% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PARTICLE_COUNTspawnBurstparticles
  function spawnBurst() {
    for (var i = 0; i < PARTICLE_COUNT; i += 1) {
      var ang = (i / PARTICLE_COUNT) * Math.PI * 2 + rand() * 0.3;
      var sp = 60 + rand() * 90;
      particles.push({ x: LW / 2, y: LH / 2 - 20, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp, life: 1 });
    }
  }

粒子の発射角度はi / PARTICLE_COUNT * Math.PI * 2で円周上に均等に割り振られ、そこに小さな乱数のずれが足されます。どの粒子も同じx: LW / 2, y: LH / 2 - 20から始まる——コートそのものの中央であって、たまたま最後に壊れたブロックの位置ではありません。

計測値: 累積面積29.5%、強度平均90.4、動いたフレーム23/23。

値が反応を変える場所 — 位置・重力・連続回数

9つの中でも値と反応のつながりがいちばん見えやすいのはこの3つです。1番のドラッグの差の値が変えるのはパドルのx位置だけで、しかも平滑化されています。5番の重力の値が変えるのはアイテムの落下速度だけで、毎フレーム積み上がります。7番の連続回数が変えるのは表示される倍率の文字だけで、comboCount+1、そして一度外せば即座に0へ戻ります。3つとも互いの状態には触れないため、定数を一つ動かして一つだけ動く様子を見たいときの出発点として向いています。

どこで壊れるか — reduced-motion設定・低スペック端末・タッチ端末

9つとも最初のフレームより前にprefers-reduced-motionを確認し、設定されていればroot.classList.remove('is-demo')が自動再生を止め、実際のポインターや矢印キーの入力を待つようになります。低スペック端末での影響は少し違う形で出ます——WebGLも大量のパーティクルも使っていませんが、480×300のキャンバスをバックグラウンドタブで毎フレーム塗り直すコストはそれでも発生し、9番の36個のパーティクルが一瞬だけ描画をいちばん重くする瞬間です。タッチ端末はマウスと同じpointerdown/pointermoveのリスナーでカバーされているため、スマートフォンでもパドルのドラッグはデスクトップと同じように動きます。zipを開く前に知っておくべき本当の落とし穴は、9つとも固定シード(seedRand)で動いていてこのページのレイアウトはリロードしても変わらないという点で、実際の定数と9つのソースファイルを開けるnnb29y4jだけが、眺めているデモを実際に調整できるゲームに変えます。

9パターンが実際にフレームごとに動かした量

累積面積は480×300のキャンバスのうち計測中にピクセル単位で変化した割合です。ボールが常に動き続けるこの構成では、測定した23フレームすべてで動きが検出されるのが期待どおりの結果です。

項目 累積面積 強度平均 動いたフレーム
01 パドルのドラッグ追従 30.1% 80.4 23/23
02 ボールの反射角分岐 35.2% 103.6 23/23
03 衝突と得点 30.6% 86.5 23/23
04 複数回ヒットで崩壊 30.5% 82.2 23/23
05 重力落下アイテム 28.7% 82.1 23/23
06 速度連動の残像 31.4% 82.5 23/23
07 連続ヒットのコンボ倍率 30.9% 87.1 23/23
08 壁反射のミラー角度 30.3% 93.6 23/23
09 クリア時の粒子爆発 29.5% 90.4 23/23

この編で最初に触ってみたい3つの定数

以下の変形はどれも定数を一つだけ変え、周りのロジックには手を加えていません。感触の違いはその一つの数値までたどれます。

定数 変更 何が変わるか
PADDLE_LERP_FACTOR 0.15 → 0.4 パドルが毎フレーム縮めるドラッグの差が大きくなり、遅れが減って吸い付きが強くなる
BRICK_HIT_LIMIT 3 → 5 ブロックが崩れるまでにあと2回多く耐え、04番のひび割れが長く積み重なる
COMBO_MULT_STEP 0.5 → 1 07番の倍率が連続ヒットごとに2倍の速さで上がり、外した瞬間にリセットされる

javascript ブロック崩し は ブロック崩し 作り方 と何が違うか?

そのまま「ブロック崩し」だけで検索すると、ゲームエンジンの書き出しやスマホアプリなど完成済みのゲームがほとんど出てきます。javascript ゲーム ブロック崩し のこの編はわざと未完成です——9つの関数がそれぞれ独立して読めるようになっていて、ライフや画面遷移、勝敗判定を持たず、既存のページにそのまま置けるコードや学習用の例として使えます。衝突判定の一般的な考え方はMDNの2D衝突判定ガイドが1つのデモページより詳しく扱っています。同じく一つの値が場面全体を動かす考え方でまとめた編としてはCSS ゲーム 作成 — CSSだけで動くミニゲーム9選がこの9枚の属するより広い系列で、web ルーレット — 弾いて回し摩擦で止まる回転盤キャンバス9種はここの2番と同じ値から角度への計算を回転盤に当てはめていて、ボタン 押し込み UI 9選 — 押し込みの値が戻る計算の9通りは1番のドラッグ追従と同じ目標値の平滑化をボタンの押し込みに使っている点で見比べる価値があります。

javascript ブロック崩し を組み込む前によくある質問

この9パターンは本当に遊べるゲームの仕組みですか、それともキャンバスの演出だけですか?

どれも実際の速度・衝突・時間の計算で動いています。パドル・ボール・ブロックは同じaabb()の重なり判定とdtベースの物理を使っていて、出荷されるゲームと同じ仕組みです。デモのために固定しているのはシード(seedRand)と定数であって、仕組み自体ではありません。

zipにはこの9つが1つのゲームとしてまとまっていますか、それとも9つ別々ですか?

index.htmlとstyle.scssのペアが9つ、パターンごとに別々に入っています。反射ルール一つや重力の値一つだけを既存のプロジェクトに取り込みたいときに、残り8つを持ち込まずに済みます。

05番のアイテムを外せば、この javascript ゲーム ブロック崩し はもっと簡単に組み込めますか?

はい。ドロップを決めているのはdropSeq % 2 === 0の一行だけなので、その判定、またはpowerups配列そのものを外しても、01〜04番と06〜09番は測定した数値のまま変わらずに動きます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。