ブロック崩し① まずはボールを1個、跳ねさせる

画面の中をボールが跳ねるだけの回です。それでも、ぼやける、速さが変わる、壁の中で震える、と3回つまずきました。つまずいたのはボクです。

ブロック崩しを作ることになって、最初に作ったのはボール1個でした。

画面の中を跳ね回るだけ。ゲームと呼ぶにはさすがに無理があります。それなのに、ここで3回つまずきました。まずはこの回で作ったものを触ってみてください。タップ(クリック)するとボールが動き出します。

STEP 1: ボールが壁で跳ね返るだけの状態

ぼやける問題

ゲームの中の座標は 360 × 640 の縦長(9:16)にしました。スマホにちょうど収まって、パソコンで見ると縦長のゲーム機っぽくなる大きさです。実際の表示は、画面に合わせて拡大・縮小します。

バグ山
バグ山:360 × 640 のキャンバスを CSS で引き伸ばせば終わりでしょ。

終わりません。

ボク
ボク:それだとスマホではぼやけます。最近のスマホは 1px を3つの点で描くので、360px の絵を3倍に引き伸ばすことになります。

キャンバスは、中に何ピクセルの絵を持つかと、画面にどのくらいの大きさで出すかが別々に決まります。なので、表示サイズに画面の細かさ(devicePixelRatio)を掛けた数だけピクセルを用意して、描くときに縮尺をかけています。

const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(canvas.clientWidth * dpr);
canvas.height = Math.round(canvas.clientHeight * dpr);
// 描く前に、ゲームの座標(360 × 640)→ 実際のピクセルへの変換をかける
ctx.setTransform(canvas.width / 360, 0, 0, canvas.height / 640, 0, 0);

これでゲームのコードは、いつでも横 360、縦 640 の世界だけを考えればよくなりました。

バグ山さんの「〜すれば終わりでしょ」は、このあとも何度も出てきます。だいたい終わりません。

画面ごとに速さが変わる問題

ゲームは、状態を少し進める、画面に描く、を1秒に何十回も繰り返して動いています。ゲームループと呼ばれるやつです。

ブラウザでは requestAnimationFrame を使うと、画面を描き直すタイミングで処理を呼んでもらえます。ただ、呼ばれる回数が画面によって違う。

ボク
ボク:60Hz の画面なら1秒に60回、120Hz のゲーミングモニターなら120回です。1回ごとに同じ距離だけ動かすと、120Hz の画面ではボールが2倍速になります。
バグ山
バグ山:いいモニターを買った人ほど難しくなるブロック崩し。ちょっと面白いけど、だめだね。

ちょっと面白い、は余計です。

経過した時間を貯めておいて、1/120 秒ずつ区切って状態を進めることにしました。固定タイムステップと呼ばれる方法です。

const STEP = 1 / 120;

const frame = (now: number) => {
  // タブを切り替えて戻ってきたときに、一気に進みすぎないよう上限をつける
  acc += Math.min((now - last) / 1000, 0.25);
  last = now;
  while (acc >= STEP) {
    update(STEP); // 状態を 1/120 秒ぶん進める
    acc -= STEP;
  }
  render(); // 画面に描く
  requestAnimationFrame(frame);
};

60Hz なら描画1回につき2回、120Hz なら1回進める。これでどの画面でも、1秒で進む時間は同じになります。

壁の中で震える問題

ボールは位置 (x, y) と速度 (vx, vy) を持っていて、毎回 速度 × 時間 だけ進みます。壁を越えたら向きを反対にすれば跳ね返る。最初、ボクはこう書きました。

if (ball.x < r) ball.vx = -ball.vx; // 向きを反転

これがまずかった。ボールが壁に深くめり込むと、次の更新でもまだ壁の中にいるので、もう一度反転する。壁の中で右、左、右、とブルブル震え続けるボールができあがりました。

言い訳はしません。これはボクのミスです。

左の壁に当たったら必ず右へ、と行き先の向きを決めて書けば、何回判定されても答えは同じになります。

if (ball.x < r) {
  ball.x = r; // めり込んだぶんを戻す
  ball.vx = Math.abs(ball.vx); // 左の壁に当たったら、必ず右向き
}
バグ山
バグ山:ボールが1個跳ねてるだけなのに、ずっと見ちゃう。

それは、ちょっとうれしかったです。

次はパドルを付けて、打ち返せるようにします。