ゲームプログラミング入門:レトロ壁打ちテニスの基本部分を作る

ゲーム

ゲームエンジンやライブラリを使わずに、HTMLとJavaScriptだけでゲームを作ります。

以下のような人のためにあります。

ゲームを作ってみたいけど、何をどうしていいか分からない。
単純なゲームを作りたいだけなのに、UnityとかUnreal Engineは面倒くさい。

ある程度プログラミングの知識がある人を対象にしています。

この記事の内容

この記事では、壁打ちテニスのゲームの基本となる動きの部分を作ります。
黒い画面内で、キーボードの←、→を押してみてください。

環境の準備

この記事は以下の記事の続きです。読んでいない方は、先に下記をお読みください。

全ソースコード

<html>
<body>
    <canvas id="gameCanvas" width="800" height="400"
            style="border:1px solid #000000; background-color: #000;"></canvas>
    <script>
        const canvas = document.getElementById('gameCanvas');
        const context = canvas.getContext('2d');

        // ボール
        const ballRadius = 10;
        let ballX = canvas.width / 2;
        let ballY = canvas.height / 2;
        let ballSpeedX = 2;
        let ballSpeedY = 2;

        // パドル
        let padX = canvas.width / 2;
        const padY = canvas.height - 50;
        const padWidth = 100;
        const padHeight = 20;
        const padSpeed = 20;

        // 入力ハンドラー
        document.addEventListener('keydown', keyDownHandler, false);

        function drawBall() {
            context.fillStyle = '#fff';
            context.fillRect(ballX-ballRadius/2, ballY-ballRadius/2, ballRadius*2, ballRadius*2);
        }

        function drawPaddle() {
            context.fillStyle = '#fff';
            context.fillRect(padX, padY, padWidth, padHeight);
        }

        function keyDownHandler(event) {
            if (padX > 0) {
                if (event.key === 'Left' || event.key === 'ArrowLeft') {
                    padX -= padSpeed;
                } 
            }

            if (padX + padWidth < canvas.width) {
                if (event.key === 'Right' || event.key === 'ArrowRight') {
                    padX += padSpeed;
                }
            }
        }

        function isHitPaddle() {
            return ! (ballX < padX || ballX > padX+padWidth || ballY < padY || ballY+padHeight > padY+padHeight)
        }

        function update() {
            // ボールの位置を更新
            ballX += ballSpeedX;
            ballY += ballSpeedY;

            // ボールの位置が上下の境界を超えたときの処理
            if (ballY + ballRadius > canvas.height || ballY - ballRadius < 0) {
                ballSpeedY = -ballSpeedY;
            }

            // ボールの位置が左右の境界を超えたときの処理
            if (ballX - ballRadius < 0 || ballX + ballRadius > canvas.width) {
                ballSpeedX = -ballSpeedX;
            }

            if (isHitPaddle()) {
                ballSpeedY = -ballSpeedY;
            }

        }

        function draw() {
            context.clearRect(0, 0, canvas.width, canvas.height);
            drawBall();
            drawPaddle();
            update();
        }

        setInterval(draw, 10);
    </script>
</body>
</html>


全体の構成

このデモの基本的な構成物は、下記に示す通りです。

ゲーム領域の大きさは800×400ピクセル。このサイズの設定は、<canvas … width=”800″ heigth=”400″ …> タグの属性で行っています。また、JavaScriptのソース中のcanvas.width, canvas.height のように、canvasオブジェクトのプロパティとして参照しています。

これらのプロパティはJavaScriptの中でどう使われているのでしょうか?例えばボールが境界に当たるときの判定です。

        // ボールの位置が上下の境界を超えたときの処理
        if (ballY + ballRadius > canvas.height || ballY - ballRadius < 0) {
            ballSpeedY = -ballSpeedY;
        }

この部分で、800とか400とか直接数字を書かないのは、後でキャンバスの大きさを変えたい場合に、<canvas … > タグの属性の一箇所だけを変更すれば良くなるからです。例えば、600×350 とかにしたときに、JavaScriptの中で800とか400数字を書いていたりすると、いちいち全部を直さないといけなくなります。

ボールの移動と当たり判定

ボールの座標は、ballX, ballY 変数で管理しています。ボールの動きはX座標、Y座標のそれぞれ向きの速度ballSpeedX, ballSpeedYによって決まります。以下はそれぞれの初期値です。

    let ballX = canvas.width / 2;
    let ballY = canvas.height / 2;
    let ballSpeedX = 2;
    let ballSpeedY = 2;

update() 関数は、10ミリ秒に一回呼ばれます。ボールは 10ミリ秒に一回、(ballSpeedX, ballSpeedY)分だけ進むと言うことです。

        // ボールの位置を更新
        ballX += ballSpeedX;
        ballY += ballSpeedY;

ボールが画面の境界に当たった場合は、跳ね返る処理をします。左右にぶつかった場合は、ballSpeedXを逆方向に、上下にぶつかった場合は、ballSpeedYを逆方向にします。以下がその処理部分です。

        // ボールの位置が上下の境界を超えたときの処理
        if (ballY + ballRadius > canvas.height || ballY - ballRadius < 0) {
            ballSpeedY = -ballSpeedY;
        }

        // ボールの位置が左右の境界を超えたときの処理
        if (ballX - ballRadius < 0 || ballX + ballRadius > canvas.width) {
            ballSpeedX = -ballSpeedX;
        }

ボールは(ballX, ballY)の座標を中心として、上下左右にballRadius分広げた正方形として描画されていますので、当たり判定のときは、そこを考慮する必要があります。

パドルの移動とボールとの当たり判定

パドルは単なる長方形です。以下が描画部分です。ボールの描画と全く一緒ですね。context.fillRect() 関数に指定する、x, y 座標とwidth, heigthの参照変数が異なるだけです。

    function drawPaddle() {
        context.fillStyle = '#fff';
        context.fillRect(padX, padY, padWidth, padHeight);
    }

パドルは左右にしか動きませんが、左右の境界に触れた時にどうするかは、後で述べます。

さて、ボールがパドルに当たったかどうかは、どうやって判定しているのでしょうか?

考え方としては、ボールの座標(x, y) がパドルの矩形の内側にあるか外側にあるか、を見ます。

y座標を例に考え方を下記の図に示します。

これは、ソースコードでいうと下記の赤字の部分です。

    function isHitPaddle() {
return ! (ballX < padX || ballX > padX+padWidth || ballY < padY || ballY+padHeight > padY+padHeight)
}

x座標についても同じ考え方です。

上記のコードの書き方は、論理演算子を使って、ボールの座標がパドルの矩形の外側にある全てのケースを判断して、最後に否定演算子 ! で打ち消しています。これにより、ボールがパドルに当たっていると判断しています。

この実装方法はプログラミングに慣れている人では常套手段ですが、そうでない人には分かりにくいかもしれません。別の書き方の一例を以下に示します。

   function isHitPaddle() {
        if (ballX < padX) {
            return false;
        }
        if (ballX > padX+padWidth) {
            return false;
        }
        if (ballY < padY) {
            return false;
        }
        if (ballY > padY+padHeight) {
            return false;
        }
        return true;
    }

このif文を使った書き方は冗長ですが、分かりやすいかと思います。書き方のどちらが良いかは、個人の好みです。特に個人の趣味のプログラミングではなおさらです。

書き方はいいとして、「ボールの x, y がパドルの外にあることを確認する」のではなくて、「ボールの x, y がパドルの内側にあることを確認する」ロジックでもいいんじゃないのか?と思った人もいるかもしれません。

もちろん、それでもゲームとしては動作します。が、処理の効率が異なります。

ボールはほとんどの時間、パドルに当たっていない状態です。

パドルの内側にあることを確認するためには、毎回必ず下記の4つのケースを確認する必要があります。

  • ballX >= padX
  • ballX <= padX+padWidth
  • ballY >= padY
  • ballY <= padY+padWidth

一方で、パドルの外側にあること確認する場合は、上記の逆の条件のどれかひとつだけ満たされていればよいです。プログラムの処理の順番で、ballX < padX が一番最初にチェックされますが、もしボールがこの状態にあれば、この時点でチェックは終わり isHitPaddle() は false を返します。

さて、ボールの当たり判定の考え方を紹介しましたが、ここまで読んで「おかしい」と思った人もいるでしょう。

そうです。ボールとパドルの当たり判定において、この実装ではボールの大きさを考慮していません。ボールの座標 (x, y) だけを使っているので、未完成です。(上下左右の境界線との当たり判定では考慮しているのに、ですね。)

興味ある方は、ボールの大きさも考慮した実装をしてみてください。デモの動きとして、見た目上どう変化するのか見るのも面白いかと思います。



キーボードの入力を受け取る方法

パドルの左右の移動は、ユーザのキーボードの入力イベントを受けて行います。処理しているのは下記の関数です。

        function keyDownHandler(event) {
            if (padX > 0) {
                if (event.key === 'Left' || event.key === 'ArrowLeft') {
                    padX -= padSpeed;
                } 
            }

            if (padX + padWidth < canvas.width) {
                if (event.key === 'Right' || event.key === 'ArrowRight') {
                    padX += padSpeed;
                }
            }
        }

パドルが左右の境界を超えないように、padX と padX+padWIdth の位置とcanvasの領域を比較して、領域内に収まっている時のみ、入力イベントを処理するようにしています。

なお、この keyDownHandler 関数でイベントを受けるには、documentオブジェクトに登録する必要があります。

document.addEventListener('keydown', keyDownHandler, false);

次回の記事では

さて、以上で「壁打ちテニス・ゲーム」の基本的な部分の実装を見てきました。これは、いわばアイディアのコアの部分のプロトタイピングみたいなものです。

ここからさらにゲームっぽくするには、ゲームの開始・終了のステータス管理、スコアの表示などを作っていく必要があります。

何をもって得点とするのか?または、何をもってゲームオーバーとするのか?など、ゲームをどういう仕様にするかは、結構自由度があります。

そこら辺の仕様はシンプルなものにして、次回は、ゲームのステータス管理をメインに作って、小粒でも完成したゲームを目指したいと思います。


コメント

タイトルとURLをコピーしました