ゲームプログラミング入門:レトロ壁打ちテニスゲーム完成

ゲーム

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

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

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

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

この記事で作るもの

なんの味付けもない壁打ちテニスですが、遊んでみてください。

先に読んでおくもの

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



全ソースコード

<html>
<body>
    <canvas id="gameCanvas" width="800" height="400"
            style="border:1px solid #000000; background-color: #000;"></canvas>
    <script>
  JavaScriptコード
  </script>
</body>
</html>

Java Scriptの部分以外は前回から変わりません。JavaScriptのコードだけを抽出します。

const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');

const State = {
    STANBY: 0,
    GAME: 1,
    GAME_OVER: 2
}

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

// ボール
const ballRadius = 10;
let ballX;
let ballY;
let ballSpeedX;
let ballSpeedY;

// ゲーム状態管理
let gameState = State.STANBY;
let score = 0;
let high_score = 0;

// 入力ハンドラー
document.addEventListener('keydown', keyDownHandler, false);
document.addEventListener('keyup', keyUpHandler, 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 (event.key === 'Left' || event.key === 'ArrowLeft') {
        padSpeed = -10;
    } 

    if (event.key === 'Right' || event.key === 'ArrowRight') {
        padSpeed = +10;
    }

    if (event.key == 's') {
        if (gameState == State.STANBY) gameState = State.GAME;
    }
    if (gameState == State.GAME_OVER) gameState = State.STANBY; 
}

function keyUpHandler(event) {
}

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

function update() {

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

    // ボールの位置が下の境界を超えたらゲームオーバー
    if (ballY + ballRadius > canvas.height) {
        gameState = State.GAME_OVER;
    }

    // ボールの位置が上の境界に触れたら跳ね返って点数追加
    if(ballY - ballRadius < 0) {
        ballSpeedY = -ballSpeedY;
        score += 1;
    }

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

    padX += padSpeed;
    if (padX < 0) padX = 0;
    if (padX + padWidth > canvas.width) padX = canvas.width - padWidth;
    if (padSpeed > 0) padSpeed -= 1;
    if (padSpeed < 0) padSpeed += 1;

    if (isHitPaddle()) {
        if (padSpeed != 0) {
            dx = padSpeed > 0 ? 1 : -1;
            if (Math.abs(ballSpeedX+dx) >= 1 && Math.abs(ballSpeedX+dx) <= 3) 
                ballSpeedX += dx; 
        }
        if (ballY >= padY && ballY <= padY+padHeight) {
            ballSpeedX = -ballSpeedX;
        }
        ballSpeedY = -ballSpeedY;
    }

}

function draw_text_center(text, font='30px Consolas') {
    context.font = font;
    context.textAlign = 'left';
    text_w = context.measureText(text).width;
    context.fillText(text, canvas.width/2-text_w/2, canvas.height/2);
}

function draw_scores() {
    context.font = '20px Consolas';
    context.textAlign = 'left';

    score_text = 'Score: ' + score;
    context.fillText(score_text, 10, canvas.height-10);

    hscore_text = 'High Score: ' + high_score;
    context.fillText(hscore_text, canvas.width-200, canvas.height-10);
}

function init() {
    // パドル位置の初期化
    padX = canvas.width / 2 - padWidth/2;
    // ボールの位置の初期化
    ballX = padX + padWidth/2; 
    ballY = padY - ballRadius*2;
    ballSpeedX = 2;
    ballSpeedY = -2;
    // スコア初期化
    if (high_score < score) high_score = score;
    score = 0;
}

function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    draw_scores();
    drawBall();
    drawPaddle();
    switch (gameState) {
    case State.STANBY:
        init();
        draw_text_center("Press 'S' Key to Start");
        break;
    case State.GAME:
        update();
        break;
    case State.GAME_OVER:
        draw_text_center("Game Over!!");
        break;
    }
}

setInterval(draw, 10);


ゲームの仕様を考える

ゲームとしての仕様は以下のとおりです。

  • 画面の上部の壁(境界)にボールを当てたら+1点
  • 画面の下部(境界)にボールが触れたらゲームオーバー

という、とてもシンプルなルールです。あとはScoreとHigh Scoreが表示されます。

ボールの動きは、基本的に大きく変化しないので非常に単調で、やろうと思えば延々と壁打ち可能です。

ただし、前回の実装から、パドルの移動部分とボールとの当たり判定部分に多少改良を加えました。ボールがパドルに当たる瞬間、パドルが左右に動いている場合は、その方向に移動量が一定の範囲内で加味されます。これで多少は単調さが解消されてはいます。

ゲーム性をあげるための仕掛けは色々考えられます。この記事の最後に発展形のアイディアを出してみたいと思います。

ゲームの状態遷移

このゲームを構成する状態は3つあります。

  • 開始待ち画面(STANDY
  • ゲーム中(GAME)
  • ゲームオーバー(GAME_OVER)

以下がその状態遷移図です。

JavaScriptの中で状態遷移はどうやって実装しているのでしょうか? draw()関数の以下の部分です。

function draw() {
   ・・・
    switch (gameState) {
    case State.STANBY:
        init();
        draw_text_center("Press 'S' Key to Start");
        break;
    case State.GAME:
        update();
        break;
    case State.GAME_OVER:
        draw_text_center("Game Over!!");
        break;
    }
}

draw()関数は10ミリ秒に一回呼ばれます。その時に、今の状態を見て処理を振り分ける、というわけです。状態別にそれぞれに応じた処理を書けばよいだけです。とてもシンプルです。

状態の変更は、gameState 変数を変更することで行います。コードの中の該当部分を追っていきましょう。

初期状態は STANDBYです。

let gameState = State.STANBY;

特定の状態にいるときに特定のキーが入力されたら遷移します。

function keyDownHandler(event) {
   ・・・
    if (event.key == 's') {
        if (gameState == State.STANBY) gameState = State.GAME;
    }
    if (gameState == State.GAME_OVER) gameState = State.STANBY; 
}

ボールが下の境界を超えたらゲームオーバの部分の処理です。

function update() {
    ・・・
    // ボールの位置が下の境界を超えたらゲームオーバー
    if (ballY + ballRadius > canvas.height) {
        gameState = State.GAME_OVER;
    }
    ・・・
}

さて、最後に State の定義を見てみます。オブジェクトのプロパティとして定義していて、中身は数字です。

const State = {
    STANBY: 0,
    GAME: 1,
    GAME_OVER: 2
}

さて、この後は

さて、これでいったんゲームは完成とします。ミニマムなゲームとはいえ、必要な構成要素は一通り入っています。

  • 画面上にオブジェクトを描画して動かす
  • ユーザの入力を受けてゲームの中のオブジェクトを反応させる
  • ゲームの状態遷移を管理し、状態によって振る舞いを変える

これらの骨組みは、ほぼすべてのアクションゲームに共通するものです。

今回作成した「壁打ちテニス」はこれ以上ないシンプルさでしたが、それでもいろんなバリエーションが考えられます。例えば、

  • 壁の位置によってボールの跳ね方を変える
  • 時間が経つにつれてボールの速度を上げる
  • パドルが連続して動かせる時間を制限する(人間の体力パラメータのようなもの)

などです。

ゲームに登場するオブジェクトを増やすのなら、アイディア次第でバリエーションはほぼ無限です。パドルを増やして二人用ゲームにしたり、障害物を画面中に置いたりなど、好きなように作り変えてみてください。


コメント

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