ブロック崩し① まずはボールを1個、跳ねさせる
画面の中をボールが跳ねるだけの回です。それでも、ぼやける、速さが変わる、壁の中で震える、と3回つまずきました。つまずいたのはボクです。
ブロック崩しを作ることになって、最初に作ったのはボール1個でした。
画面の中を跳ね回るだけ。ゲームと呼ぶにはさすがに無理があります。それなのに、ここで3回つまずきました。まずはこの回で作ったものを触ってみてください。タップ(クリック)するとボールが動き出します。
ぼやける問題
ゲームの中の座標は 360 × 640 の縦長(9:16)にしました。スマホにちょうど収まって、パソコンで見ると縦長のゲーム機っぽくなる大きさです。実際の表示は、画面に合わせて拡大・縮小します。
終わりません。
キャンバスは、中に何ピクセルの絵を持つかと、画面にどのくらいの大きさで出すかが別々に決まります。なので、表示サイズに画面の細かさ(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 を使うと、画面を描き直すタイミングで処理を呼んでもらえます。ただ、呼ばれる回数が画面によって違う。
ちょっと面白い、は余計です。
経過した時間を貯めておいて、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); // 左の壁に当たったら、必ず右向き
}
それは、ちょっとうれしかったです。
次はパドルを付けて、打ち返せるようにします。