Skip to content

パドルとボールとの衝突判定

前章で、ボールとの衝突によりブロックを破壊することができるようになりました。 しかし現状だとボールの方向が常に一定です。 ゲームとしての戦略性や面白さを持たせるため、ボールの角度を変更できるようにしてみましょう。

ボールの射出速度をベクトルに変更する

本題へと移る前に、ボールの速度を設定している箇所を見てみます。

javascript
        let vx = 8;
        let vy = 8;
        scene.onUpdate.add(() => {
            ball.x += vx;
            ball.y -= vy;
            ball.modified();

            ...
        });
        let vx = 8;
        let vy = 8;
        scene.onUpdate.add(() => {
            ball.x += vx;
            ball.y -= vy;
            ball.modified();

            ...
        });

今のコードはボールの速度を vxvy それぞれ独立した変数で管理しています。 これでは一般的に再利用性と拡張性が低くなります。

そこでボールの速度をベクトルを使って表現してみましょう。 ベクトルは、大きさと向きを持つ量を表現するための数学的な概念です。 通常ベクトルは矢印で表され、長さが大きさ、矢印の方向が向きを表します。 上記コードの場合、速度のベクトルは (vx, vy) すなわち (8, 8) と表すことができます。

ゲーム開発においては、オブジェクトの位置、速度、衝突検知などをベクトルで表現することはごく一般的です。 以降ではボールの速度をベクトルを使って表現します。

それではボールの速度 vxvy を定義している部分をベクトルで書き換えてみましょう。

javascript
        let vx = 8; 
        let vy = 8; 
        let direction = [1, 1];
        let speed = 8;
        let vx = speed * direction[0];
        let vy = speed * direction[1];

        scene.onUpdate.add(() => {
            ...
        });
        let vx = 8; 
        let vy = 8; 
        let direction = [1, 1];
        let speed = 8;
        let vx = speed * direction[0];
        let vy = speed * direction[1];

        scene.onUpdate.add(() => {
            ...
        });

direction は2つの要素を持つベクトル配列で、1つ目の要素が x 方向、2つ目の要素が y 方向の大きさを示しています。

これは以前の処理と同じ結果となりますが、大きな違いがあります。 大きさと向きという独立した値から vxvy を算出している点です。

ただしまだ問題があります。 ボールを射出する方向をもう少し上にしたい、と考え以下のようにコードを書き換えます。

javascript
        let direction = [1, 1]; 
        let direction = [1, 2];
        let speed = 8;
        let vx = speed * direction[0];
        let vy = speed * direction[1];

        scene.onUpdate.add(() => {
            ...
        });
        let direction = [1, 1]; 
        let direction = [1, 2];
        let speed = 8;
        let vx = speed * direction[0];
        let vy = speed * direction[1];

        scene.onUpdate.add(() => {
            ...
        });

意図通り、以前よりも上の方に射出されるかと思います。 ですがボールの移動が若干速くなったように感じるかもしれません。

その原因はベクトルの大きさが増加したためです。

(1, 2) のベクトルの大きさは (1, 1) のベクトルの大きさの約 1.58 倍になります。 したがって、ボールの速さも同様に約 1.58 倍に増加することになります。

ボールの速さを一定にしたまま方向を定めるにはどのようにすればよいでしょうか。 それは 単位ベクトル によって解決します。

単位ベクトルとは大きさが 1 のベクトルのことをいいます。 任意の方向ベクトルを単位ベクトルに変換することで、常に速さが一定のボールの動きを実現できます。

単位ベクトルは任意の方向ベクトルをその大きさ (長さ) で割ることで得られます。これをベクトルの正規化といいます。 (x, y) のベクトルの大きさは Math.sqrt(x * x + y * y) で求めることができます。 Math.sqrt() は平方根を返す JavaScript の組み込みメソッドです。

それでは任意の [x, y] のベクトルを正規化する関数 normalize()scene.onUpdate の直後に追加しましょう。

javascript
        scene.onUpdate.add(() => {
            ...
        });

        function normalize(vec) {
            const magnitude = Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]);
            if (magnitude === 0) return vec; // 大きさが 0 の場合は正規化不可のため元のベクトルを返す
            return [vec[0] / magnitude, vec[1] / magnitude];
        }
        scene.onUpdate.add(() => {
            ...
        });

        function normalize(vec) {
            const magnitude = Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]);
            if (magnitude === 0) return vec; // 大きさが 0 の場合は正規化不可のため元のベクトルを返す
            return [vec[0] / magnitude, vec[1] / magnitude];
        }

ボールの速度を定義している部分を以下のように書き換えます。

javascript
        let direction = [1, 2];
        let direction = normalize([1, 2]);
        let speed = 8;
        let speed = 12;
        let vx = speed * direction[0];
        let vy = speed * direction[1];

        scene.onUpdate.add(() => {
            ...
        });
        let direction = [1, 2];
        let direction = normalize([1, 2]);
        let speed = 8;
        let speed = 12;
        let vx = speed * direction[0];
        let vy = speed * direction[1];

        scene.onUpdate.add(() => {
            ...
        });

ベクトルの正規化に伴い速さが若干小さくなるため、 speed の値を大きくしています。 normalize([1, 1])normalize([2, 1]) に変更してもボールの速さが変化しなくなることがわかります。

TIP

ここでは物理学の用語として速度と速さを使っており、この二つは異なる概念であることに注意してください。 速度はベクトルであり、大きさ(速さ)と向き(方向)を持ちます。 速さは移動のスカラー量を表します。速さは大きさだけを示し、方向を持ちません。

ボールの反射角度を変更する

本題に入ります。

今のままだとボールの角度が常に一定となっておりゲーム性がありません。 そこで、パドルとボールとが衝突したときにボールの角度を変更するようにしてみます。

今回はパドルとボールとが衝突した位置によって反射角度を変えてみましょう。 パドルの中央付近では上向き、右端では右向きの方向に反射させてみます。 ちょうどパドルが半円の形をしているようなイメージです。

どのように再現すればよいでしょう。 実は先ほどのベクトルを使えば簡単に解決できます。

まずパドルとボールとが衝突したときの座標を求めます。 これはそのまま

javascript
(ball.x, ball.y)
(ball.x, ball.y)

となります。

次に方向を決める基準点となる座標を以下のように設定します。

javascript
(paddle.x, paddle.y + h)
(paddle.x, paddle.y + h)

h は変数として自由に値を変更できるようにしておきます。 これらの関係を図に示すと以下のようになります。

図からも分かる通り (パドルとボールとが衝突した座標) - (基準点の座標) がそのままボールの反射速度の方向ベクトルとなることがわかります。

javascript
(ball.x - paddle.x, ball.y - (paddle.y + h))
(ball.x - paddle.x, ball.y - (paddle.y + h))

これをコードにすると以下のようになります。 scene.onUpdate 内に追加してください。

javascript
        scene.onUpdate.add(() => {
            ...

            // パドルとボールとが衝突したとき
            if (intersect(ball, paddle)) {
                vy = -vy;
                const h = 30;
                const direction = normalize([ball.x - paddle.x, -(ball.y - (paddle.y + h))]);
                vx = speed * direction[0];
                vy = speed * direction[1];
            }

            ball.modified();
        });
        scene.onUpdate.add(() => {
            ...

            // パドルとボールとが衝突したとき
            if (intersect(ball, paddle)) {
                vy = -vy;
                const h = 30;
                const direction = normalize([ball.x - paddle.x, -(ball.y - (paddle.y + h))]);
                vx = speed * direction[0];
                vy = speed * direction[1];
            }

            ball.modified();
        });

y 軸は下向きが正のため全体をマイナス (-) でくくっている点に注意してください。

上記コードでは h の値を 30 にしています。 以下の図からも分かるように h の値が大きくなるほど左右への角度が緩やかになります。 この値を色々変更してみて変化を確認してみてください。

パドルの動きを制限する

パドルがブロック内や画面外に飛び出してしまうのを防ぐため、動かせる範囲を制限しましょう。

ブロックの横幅は 128、パドルの中心から端までの距離は paddle.width / 2 のため、ゲーム画面からこれらの値の合計値 128 + paddle.width / 2 に移動を制限してみます。 パドルを動かしている部分に以下のコードを追加します。

javascript
        // スワイプでパドルが左右に動くようにする
        const paddleMargin = 128 + paddle.width / 2; // ブロックの横幅+パドルの中心から端までの距離
        scene.onPointMoveCapture.add((event) => {
            paddle.x += event.prevDelta.x;

            // パドルの移動範囲を制限
            if (paddle.x <= paddleMargin) {
                paddle.x = paddleMargin;
            } else if (paddle.x >= game.width - paddleMargin) {
                paddle.x = game.width - paddleMargin;
            }

            paddle.modified();
        });
        // スワイプでパドルが左右に動くようにする
        const paddleMargin = 128 + paddle.width / 2; // ブロックの横幅+パドルの中心から端までの距離
        scene.onPointMoveCapture.add((event) => {
            paddle.x += event.prevDelta.x;

            // パドルの移動範囲を制限
            if (paddle.x <= paddleMargin) {
                paddle.x = paddleMargin;
            } else if (paddle.x >= game.width - paddleMargin) {
                paddle.x = game.width - paddleMargin;
            }

            paddle.modified();
        });

ボールの制御

現状のコードは簡略化のためボールが自動的に動くようになっています。 これを、プレイヤーの操作によって開始するように修正します。

javascript
        let direction = normalize([1, 2]);
        let speed = 12;
        let vx = speed * direction[0];
        let vy = speed * direction[1];

        let isStarted = false;
        scene.onPointUpCapture.add(() => {
            if (!isStarted) {
                isStarted = true;
            }
        });
        let direction = normalize([1, 2]);
        let speed = 12;
        let vx = speed * direction[0];
        let vy = speed * direction[1];

        let isStarted = false;
        scene.onPointUpCapture.add(() => {
            if (!isStarted) {
                isStarted = true;
            }
        });

isStarted はゲームが開始しているかを保持する真理値です。 この値が true の場合にボールを自動的に動くようにします。

scene.onPointUpCapture は画面内でスワイプ操作を終了したときに通知されるトリガです。 if (!isStarted) の中では、スワイプ操作を終了したタイミングで isStartedtrue に変更しています。 これにより、パドルをスワイプで操作し終えたタイミングでボールを発射し、ゲームを開始しています。

続いて scene.onUpdate 内のボールの移動処理を isStarted を使って制御します。

javascript
        scene.onUpdate.add(() => {
            ball.x += vx;
            ball.y -= vy;
            if (isStarted) {
                ball.x += vx;
                ball.y -= vy;
            } else {
                ball.x = paddle.x;
                ball.y = paddle.y - paddle.height / 2 - ball.height / 2 - 5;
            }

            // ボールが画面の左右に到達したとき
            if ((ball.x > game.width - ball.width / 2) || (ball.x < ball.width / 2)) {
                vx = -vx;
            }
            // ボールが画面の上端に到達したとき
            if (ball.y < ball.height / 2) {
                vy = -vy;
            }
            // ボールが画面の下端に到達したとき
            if (ball.y > game.height + ball.height / 2) {
                isStarted = false;
                vx = speed * direction[0];
                vy = speed * direction[1];
            }
        scene.onUpdate.add(() => {
            ball.x += vx;
            ball.y -= vy;
            if (isStarted) {
                ball.x += vx;
                ball.y -= vy;
            } else {
                ball.x = paddle.x;
                ball.y = paddle.y - paddle.height / 2 - ball.height / 2 - 5;
            }

            // ボールが画面の左右に到達したとき
            if ((ball.x > game.width - ball.width / 2) || (ball.x < ball.width / 2)) {
                vx = -vx;
            }
            // ボールが画面の上端に到達したとき
            if (ball.y < ball.height / 2) {
                vy = -vy;
            }
            // ボールが画面の下端に到達したとき
            if (ball.y > game.height + ball.height / 2) {
                isStarted = false;
                vx = speed * direction[0];
                vy = speed * direction[1];
            }

isStartedfalse のときはボールをパドルの上部に配置しておきます。 これによりパドルのスワイプ中にボールも同時に移動させています。

またボールが画面の下端に達したときに isStartedfalse に設定することで、ゲームを再開できるようにします。 ボールの速度 vx vy も忘れずにリセットしておきます。

実行例とソースコード

Playground で実行