JavaScript - ピンポンゲームの作り方 Part 3 - ゲームループ

3回に分けて解説してきたピンポンゲームの作り方はこの記事が最終回です。ゲームの要となるゲームループ関数を作成し、ゲームを仕上げます。記事の最後にはすべての JavaScript コードを掲載しています。

JavaScript - ピンポンゲームの作り方 Part 3 - ゲームループ
Photo by Lucas Davies / Unsplash

JavaScript でゲーム作成|ゲームの流れを制御する

HTML のキャンバス <canvas> と JavaScript でピンポンゲームを作成する方法を、3回にわたってお届けしています。今回はその最終回、Part 3 です。

  1. Part 1 では、Enter キーの押下でゲームを開始するコードと、パドルを上下に動かすコードを解説しました。
  2. Part 2 では、ボールの衝突判定をして、壁で跳ね返ったりパドルで打ち返せるようにし、打ち返しに失敗したら得点が入るようにコーディングしました。
  3. Part 3 (この記事)では、ゲーム全体を制御するゲームループを作成し、ピンポンゲームを完成させます。

サンプルプロジェクト

あらためてピンポンゲームの動きを確認しておきましょう。

  • どちらかの点数が5点になるまで、ゲームは続行する
  • どちらかの点数が5点になったら、Game Over が表示される
  • キャンバスが最初の状態に戻り、再びゲームができるようになる
ピンポンゲームのサンプル
ピンポンゲームのサンプル

ピンポンゲームを構成するパドル、ボール、得点はキャンバスに描画して動かします。JavaScript でキャンバスに描画する方法は以下の記事で紹介しているので参考にしてください。

JavaScript - Canvas に図形を描く方法
HTML の canvas 要素を利用すると、JavaScript でグラフィックを描くことができます。基本的なメソッドを学んで、四角形や三角形、円やテキストなど簡単な図形を描画できるようになりましょう。

これから説明していく内容は、大きく分けて次の三つです。Part 1Part 2 で作成した関数も使って、ゲームを完成させます。

  1. キャンバスに描画するコード
  2. ゲームの流れを制御するコード
  3. ゲームを画面に表示するコード

1 キャンバスに描画するコード

Part 1Part 2 では、ピンポンゲームを構成するパーツであるパドル、ボール、得点に関連するコードを書いてきました。でもまだ、キャンバスには何も描画されていません。これまでに書いてきたコードは、パドルやボールの描画位置、得点の値を指定するだけの内容だったからです。

ここからは、指定できるようになった値を使って、実際にパドル、ボール、得点を描画できるようにするコードを書いていきましょう。

関数|オブジェクトを描画 - draw

キャンバスにオブジェクトを描画する関数 draw を定義します。

//オブジェクトを描画
function draw() {

  //------------------------------
  // キャンバス全体を消去する
  // 描画スタイルを設定する
  // オブジェクトを描画する
  //------------------------------
 
}

キャンバス全体を消去する

キャンバスに描いたオブジェクト(今回の場合はパドル、ボール、得点)に動きをつけるときは、必ずキャンバスをクリアするコードが必要です。これは、古い内容を消去して、常に新しい内容でキャンバスに描画するためです。

  • clearRect() メソッドで、キャンバス全体を消去します。
function draw() {

  //キャンバス全体を消去する
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  //------------------------------
  // 描画スタイルを設定する
  // オブジェクトを描画する
  //------------------------------

}

描画スタイルを設定する

オブジェクトを描画する前に、描画スタイルを設定しておきましょう。

  • fillStyle プロパティで、オブジェクトの色を白にします。
  • strokeStyle プロパティで、線の色を白にします。これは、ゲームボードの中央に線を引くときの色になります。
  • font プロパティで、得点の見た目を設定します。これは、ゲーム終了時の「Game Over」の文字スタイルにもなります。
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  //描画スタイルを設定する
  //塗りつぶしの色
  ctx.fillStyle = 'white';
  //線の色
  ctx.strokeStyle = 'white'
  //フォントスタイル
  ctx.font = 'bold 50px sans-serif';  

  //------------------------------
  // オブジェクトを描画する
  //------------------------------

}

描画スタイルについては、以下の記事を参考にしてください。

JavaScript - 図形に色やグラデーションを設定する方法
JavaScript で描画する図形にスタイルを設定するためのプロパティや、グラデーションカラーを作成するメソッドを紹介します。レインボーカラーや図形を立体的に見せるためのサンプルコードも掲載しているので参考にしてください。

オブジェクトを描画する

描画スタイルを設定したら、ピンポンゲームに必要なすべてのパーツを描画していきます。

  • fillRect() メソッドで、左右のパドルを描画します。
  • arc() メソッドで、ボールを描画します。
  • fillText() メソッドで、得点を描画します。描画位置は、キャンバスのサイズやフォントサイズを考慮して設定してください。
  • ゲームボードの中央に直線を描きます。
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = 'white';
  ctx.strokeStyle = 'white'
  ctx.font = 'bold 50px sans-serif';  
  
  //オブジェクトを描画する
  //左パドル
  ctx.fillRect(leftPaddleX, leftPaddleY, paddleWidth, paddleHeight);
  //右パドル
  ctx.fillRect(rightPaddleX, rightPaddleY, paddleWidth, paddleHeight);

  //ボール
  ctx.beginPath();
  ctx.arc(ballX, ballY, ballRadius, 0, Math.PI * 2);
  ctx.fill();
  ctx.closePath();

  //左パドルの得点
  ctx.fillText(score1, canvas.width/2-80, canvas.height/3);
  //右パドルの得点
  ctx.fillText(score2, canvas.width/2+50, canvas.height/3);

  //中央線
  ctx.beginPath();
  ctx.moveTo(canvas.width / 2, 0);
  ctx.lineTo(canvas.width / 2, canvas.height);
  ctx.stroke();

}

以上で、キャンバスにオブジェクトを描画する関数 draw を定義できました。

2 ゲームの流れを制御するコード

キャンバスに描いたパドルやボールを動かしたり、得点を変更するためには、「描画する内容を更新」そして「実際にキャンバスに描画する」ということを、短い間隔で繰り返してアニメーションします。そのためのコードを書いていきましょう。

変数|アニメーション用

アニメーションを停止するときに必要になる変数、animationId を用意しておきます。

//アニメーション
let animationId;

関数|ゲーム終了またはゲーム続行 - gameLoop

ゲームを終了するか、キャンバスの内容を更新してゲームを続行(アニメーション)するかを制御する関数 gameLoop を定義しましょう。gameLoop はゲームの要となる関数で、ゲーム全体の流れをコントロールします。

//ゲームループ(ゲーム終了または続行)
function gameLoop() {

  //------------------------------
  // キャンバスの内容を更新して描画する
  // ゲーム終了か続行かを判断する
  //------------------------------

}

キャンバスの内容を更新して描画する

ゲームを終了または続行するかどうかを判断する前に、キャンバスの内容をアップデートします。

function gameLoop() {

  //キャンバスの内容を更新して描画する
  updatePaddle();
  updateBall();
  collisionDetection();
  addScore();

  draw();

  //------------------------------
  // ゲーム終了か続行かを判断する
  //------------------------------

}

ゲーム終了か続行かを判断する

ゲームを続行するのは、得点が5点未満の間だけです。どちらかの得点が5点になったら、キャンバスの更新と描画(アニメーション)を停止してゲーム終了です。if...else 文で条件分岐しましょう。

  • もし、score1 または score2 の値が maxScore (5点)なら、関数 stopGame を呼び出してゲームを終了します。
  • そうでなければ、requestAnimationFrame() メソッドで関数 gameLoop を繰り返し呼び出してゲームを続行します。このあと定義する関数 stopGame 内でこの関数(キャンバスの更新・描画の繰り返し)を停止できるように、変数 animationId を使用してください。
function gameLoop() {
  updatePaddle();
  updateBall();
  collisionDetection();
  addScore();
  draw();

  //ゲーム終了か続行かを判断する
  if (score1 === maxScore || score2 === maxScore) {
    //ゲーム終了
    stopGame();
  } else {
    //ゲーム続行(キャンバスの更新・描画を繰り返す)
    animationId = requestAnimationFrame(gameLoop);
  }

}

以上で、ゲームを終了、または、ゲームを続行(キャンバスの更新と描画を繰り返してアニメーション)する関数 gameLoop を定義できました。

関数|ゲームを終了 - stopGame

次に、どちらかの得点が5点になったら呼び出す関数 stopGame を定義しましょう。ゲームを終了し、そのあとまたゲームを再開できるようにキャンバスの準備をしますよ。

//ゲーム終了
function stopGame() {

  //------------------------------
  // ゲームを終了する
  // ゲームを再開できるようにする
  //------------------------------

}

ゲームを終了する

  • cancelAnimationFrame() メソッドで、キャンバスの更新・描画の繰り返し(アニメーション)を停止します。
  • fillText() メソッドで、「Game Over」という文字を描画します。描画位置は、キャンバスのサイズやフォントサイズを考慮して設定してください。
function stopGame() {

  //ゲームを終了する
  //アニメーションを停止
  cancelAnimationFrame(animationId);
  //Game Overを描画
  ctx.fillText('Game Over', canvas.width/2-145, canvas.height/2+100);

  //------------------------------
  // ゲームを再開できるようにする
  //------------------------------

}

ゲームを再開できるようにする

キャンバスをリセットして、再びゲームを開始できるように準備しましょう。

  • ゲームを停止してから3秒後にキャンバスを最初の状態に戻すために、setTimeout() メソッドを使います。
  • キャンバスを最初の状態に戻すための関数、resetCanvas を呼び出します。
  • 関数 draw を呼び出して、キャンバスにオブジェクト(ピンポンゲームを構成するパーツ)を描画します。
  • 変数 playingfalse にしてゲーム中ではないことを示し、ゲームを再開できるようにします。
function stopGame() {
  cancelAnimationFrame(animationId);
  ctx.fillText('Game Over', canvas.width/2-145, canvas.height/2+100);

  //ゲームを再開できるようにする
  //3秒後に実行
  setTimeout(() => {
    //キャンバスをリセット
    resetCanvas();
    //オブジェクトを描画
    draw();
    //ゲーム中ではないことを示す
    playing = false;
  }, 3000);

}

以上で、ゲームを終了およびゲームの再開準備をする関数 stopGame を定義できました。

関数|キャンバスをリセット - resetCanvas

続けて、ゲームを再開できるようにキャンバスをリセットする関数 resetCanvas を定義しましょう。

  • 左右のパドルの位置は、キャンバスの中央です。
  • ボールをキャンバスの中央にセットし直すために、関数 resetBall を呼び出します。(関数 resetBall については Part 2 の記事で解説しています)
  • 両方の得点を 0 にします。
//キャンバスをリセット
function resetCanvas() {

  //パドルの位置をキャンバスの中央にする
  leftPaddleY = canvas.height / 2 - paddleHeight / 2;
  rightPaddleY = canvas.height / 2 - paddleHeight / 2;
  //ボールをセットし直す
  resetBall();
  //得点を0にする
  score1 = 0;
  score2 = 0;

}

以上で、キャンバスをリセットする関数 resetCanvas を定義できました。

3 ゲームを画面に表示するコード

お疲れ様でした。これが最後のコードです。ウェブページを読み込んだらピンポンゲームが画面に表示されるよう、関数 draw を呼び出しましょう。

//オブジェクトを描画してゲームを画面に表示する
draw();

完成コード

Part 1Part 2 で作成したコードと合わせて、ピンポンゲームのコードが完成です。

/************************/
/* ピンポンゲームのコード */
/************************/

//キャンバスに描画する準備
const canvas = document.querySelector('#game-canvas');
const ctx = canvas.getContext('2d');

/* 変数の宣言 ↓ ========================*/
//パドル
const paddleWidth = 10;
const paddleHeight = 100;
let leftPaddleY = canvas.height / 2 - paddleHeight / 2;
const leftPaddleX = 30;
let rightPaddleY = canvas.height / 2 - paddleHeight / 2;
const rightPaddleX = canvas.width - 40;
const paddleVelocity = 5;
//ボール
const ballRadius = 10;
let ballX = canvas.width / 2;
let ballY = canvas.height / 2;
let ballVelX = 6;
let ballVelY = 0;
//得点
let score1 = 0;
let score2 = 0;
//勝敗を決める点数
const maxScore = 5;
//ゲーム中かどうかどうか(trueまたはfalse)
let playing = false;
//キーが押されているかどうか(trueまたはfalse)
let wPressed = false;
let sPressed = false;
//アニメーション
let animationId;
/*====================================*/

/* 関数の定義 ↓ =======================*/
//Enterキーの押下でゲーム開始
function startGame(e) {
  if (e.key === 'Enter') {
    if (!playing) {
      gameLoop();
      playing = true;
    }
  }
}

//そのキーが押されたことを示す
function keyDownHandler(e) {
  switch (e.key) {
    case 'w':
      wPressed = true;
      break;
    case 's':
      sPressed = true;
      break;
  }
}

//そのキーが離されたことを示す
function keyUpHandler(e) {
  switch (e.key) {
    case 'w':
      wPressed = false;
      break;
    case 's':
      sPressed = false;
      break;
  }
}

//パドルの位置を更新
function updatePaddle() {
  //----- 左パドル (ユーザー操作)--------------------
  if (wPressed && leftPaddleY > 0) {
    leftPaddleY -= paddleVelocity;
  } else if (sPressed && leftPaddleY + paddleHeight < canvas.height)  {
    leftPaddleY += paddleVelocity;
  }
  //----- 右パドル(自動) -------------------------
  let computerLevel = 0.06;
  rightPaddleY += (ballY - (rightPaddleY + paddleHeight / 2)) * computerLevel;
  if (rightPaddleY < 0) {
    rightPaddleY  = 0;
  }
  if (rightPaddleY + paddleHeight > canvas.height) {
    rightPaddleY  = canvas.height - paddleHeight;
  }
}

//ボールの位置を更新
function updateBall() {
  ballX += ballVelX;
  ballY += ballVelY;
}

//ボールの向きを変更(衝突判定)
function collisionDetection() {
  //----- キャンバスの上に当たった場合 ----------
  if (ballY - ballRadius < 0) {
    ballY = ballRadius;
    ballVelY = -ballVelY;
  }
  //----- キャンバスの下に当たった場合 ----------
  if (ballY + ballRadius > canvas.height) {
    ballY = canvas.height - ballRadius;
    ballVelY = -ballVelY;
  }
  //----- 左パドルに当たった場合 ---------------
  if (ballX - ballRadius < leftPaddleX + paddleWidth) {
    if (ballY > leftPaddleY && ballY < leftPaddleY + paddleHeight) {
      ballX = leftPaddleX + paddleWidth + ballRadius;
      ballVelX = -ballVelX;
      ballVelY = Math.random() * 10 - 5;
    }
  }
  //----- 右パドルに当たった場合 ---------------
  if (ballX + ballRadius > rightPaddleX) {
    if (ballY > rightPaddleY && ballY < rightPaddleY + paddleHeight) {
      ballX = rightPaddleX - ballRadius;
      ballVelX = -ballVelX;
      ballVelY = Math.random() * 10 - 5;
    }
  }
}

//得点を加算
function addScore() {
  //----- 右パドル打ち返し失敗 ユーザーの得点 -----
  if (ballX + ballRadius > canvas.width) {
    score1++;
    if (score1 < maxScore) {
      resetBall();
    }
  }
  //----- 左パドル打ち返し失敗 コンピューターの得点 -----
  if (ballX - ballRadius < 0 ) {
    score2++;
    if (score2 < maxScore) {
      resetBall();
    }  
  }
}

//ボールをセットし直す
function resetBall() {
  ballX = canvas.width / 2;
  ballY = canvas.height / 2;
  ballVelX = -ballVelX;
  ballVelY = 0;
}

//オブジェクトを描画
function draw() {
  //----- キャンバスクリア -------------
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  //----- スタイル設定 ----------------
  ctx.fillStyle = 'white';
  ctx.strokeStyle = 'white'
  ctx.font = 'bold 50px sans-serif';  
  //----- パドル --------------------
  ctx.fillRect(leftPaddleX, leftPaddleY, paddleWidth, paddleHeight);
  ctx.fillRect(rightPaddleX, rightPaddleY, paddleWidth, paddleHeight);
  //----- ボール -------------------
  ctx.beginPath();
  ctx.arc(ballX, ballY, ballRadius, 0, Math.PI * 2);
  ctx.fill();
  ctx.closePath();
  //----- 得点 ---------------------
  ctx.fillText(score1, canvas.width/2-80, canvas.height/3);
  ctx.fillText(score2, canvas.width/2+50, canvas.height/3);
  //----- 中央線 --------------------
  ctx.beginPath();
  ctx.moveTo(canvas.width / 2, 0);
  ctx.lineTo(canvas.width / 2, canvas.height);
  ctx.stroke();
}

//ゲームループ(ゲーム終了または続行)
function gameLoop() {
  updatePaddle();
  updateBall();
  collisionDetection();
  addScore();
  draw();
  //----- ゲーム終了か続行かを判断 ----------
  if (score1 === maxScore || score2 === maxScore) {
    stopGame();
  } else {
    animationId = requestAnimationFrame(gameLoop);
  }
}

//ゲーム終了
function stopGame() {
  cancelAnimationFrame(animationId);
  ctx.fillText('Game Over', canvas.width/2-145,canvas.height/2+100);
  //----- ゲーム再開準備 ----------
  setTimeout(() => {
    resetCanvas();
    draw();
    playing = false;
  }, 3000);
}

//キャンバスをリセット
function resetCanvas() {
  leftPaddleY = canvas.height / 2 - paddleHeight / 2;
  rightPaddleY = canvas.height / 2 - paddleHeight / 2;
  resetBall();
  score1 = 0;
  score2 = 0;
}
/*==================================*/

/* イベントの追加 ↓ =====================*/
//キーが押されたときにゲーム開始
document.addEventListener('keydown', startGame);
//キーが押されたときにそのキーが押されたことを示す
document.addEventListener('keydown', keyDownHandler);
//キーが離されたときにそのキーが離されたことを示す
document.addEventListener('keyup', keyUpHandler);
/*==================================*/

//オブジェクトを描画してゲームを画面に表示する
draw();

See the Pen JavaScript - Pong Game by Pyxofy (@pyxofy) on CodePen.

Pyxofy (著)「きょうからはじめるスクラッチプログラミング入門」

Pyxofy が Scratch の電子書籍を出版しました!Kindle・Apple Books からご購入ください。

詳細はこちら

まとめ

ここまで3回にわたって、JavaScript で作るピンポンゲームについて解説してきました。コードの分量が多くて大変だったかもしれませんが、キーで操作する方法や衝突判定、ゲーム内容の「更新・描画」を繰り返すゲームループなど、JavaScript で作成するゲームの基本を学ぶことができたのではないでしょうか。

最後まで読んでいただき、ありがとうございます。この記事をシェアしてくれると嬉しいです!

SNSで Pyxofy とつながりましょう! LinkedInThreadsMastodon X (Twitter) @pyxofyFacebook

関連記事

JavaScript - 数当てゲームの作り方【基本編】
数当てゲームはランダムに選ばれた数を予想するゲームです。プログラミング初心者の方でも分かりやすいようにゲームの基本的な考え方をシンプルなコードで解説します。
CSS Art - How to Make a Space Shuttle - Rocket
Yes, you can create a space shuttle rocket with CSS. Join us in this two part step-by-step article series to find out how.
CSS Animation – Turntable – Part 1
Vinyl records are popping and crackling! Learn how to animate a record turntable with CSS step-by-step in this two-part article.
スクラッチプログラミング - たっきゅうゲームのつくりかた - Part 1
コンピューターと対戦(たいせん)する卓球(たっきゅう)ゲームをつくってみましょう。ラケットのプログラムは簡単(かんたん)です。ボールをいろいろな方向(ほうこう)にうごくようにするのが、プログラミングのポイントになります。