JavaScript - ピンポンゲームの作り方 Part 2 - ボールを動かす

JavaScript でゲーム作成|衝突判定と得点

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

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

サンプルプロジェクト

あらためてピンポンゲームの動きを確認しておきましょう。ボールの動きに注目してみてくださいね。

  • ボールはキャンバスの上下、左右のパドルに当たると跳ね返る
  • 左右の壁にボールが当たると、相手の得点になる
  • 点数が入ったあと、ボールは中央から再スタートする
ピンポンゲームのサンプル

ボールはキャンバスに描画して動かします。JavaScript でキャンバスに描画する方法は以下の記事で紹介しているので参考にしてください。

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

今回は、ピンポンゲームのボールに関連するコードを書いていきます。説明していく内容は、大きく分けて次の二つです。どちらも、ボールがどの位置にあるかを確認しながら処理を実行します。

  1. ボールを動かすコード
  2. 得点を加えて再スタートするコード

1 ボールを動かすコード

ボールを動かして、キャンバスの上下と左右のパドルで跳ね返るようにコーディングしましょう。

変数|ボールのサイズ・位置・速度

ボールを描くために必要な変数を用意します。

// 半径
const ballRadius = 10;
// 位置
let ballX = canvas.width / 2;
let ballY = canvas.height / 2;
// 移動速度
let ballVelX = 6;
let ballVelY = 0;

ballVelXballVelY は、ボールを動かすときに必要になる変数です。ここでは、縦移動の速度 ballVelY の値を 0 として、最初は真横に動くようにしています。let ballVelY = 3 のように値を指定すると、ボールの動きに角度がつきます。より難しいゲームにしたい場合は、Math.random()*4-2 などの乱数を指定して、ボールがどこに行くか分かりにくくするのも良いですね。

関数|ボールの位置を更新 - updateBall

変数を用意できたら、ボールを動かすため関数 updateBall を定義しましょう。この関数の内容はとてもシンプルです。

  • ボールの横の位置 ballX を更新して、横へ動かします。
  • ボールの縦の位置 ballY を更新して、縦へ動かします。
// ボールの位置を更新
function updateBall() {

  // 横の位置
  ballX += ballVelX;
  // 縦の位置
  ballY += ballVelY;
	
}

基本のボールの動きは、ボールの描画位置に ballVelXballVelY をプラスして右方向へ移動するように設定していますが、この次に説明する衝突判定の処理で、ボールの向きが変わるようにしていきますよ。

関数|ボールの向きを変更(衝突判定) - collisionDetection

ボールは、キャンバスの上下やパドルに当たったら跳ね返るようにします。跳ね返らせるためには、ボールの動く方向を逆にします。定義する関数は collisionDetection です。

// ボールの向きを変更(衝突判定)
function collisionDetection() {

  // キャンバスの上で跳ね返す
  // キャンバスの下で跳ね返す
  // 左パドルで跳ね返す
  // 右パドルで跳ね返す

}

キャンバスの上で跳ね返す

まずは、ボールがキャンバスの上に当たっていることを確認して跳ね返らせるコードを書きましょう。

  • ボールがキャンバスの上端 0 からはみ出ていることを if 文の条件にします。ボールの位置を確認するためには、ボールの上端を表す ballY - ballRadius を使ってください。
  • 条件を満たす場合は、ボールの中心をボールの半径の値と同じ位置にします。これは、ボールの上端がキャンバスの上端にちょうど触れる位置です。
  • 縦移動 ballVelY にマイナス記号をつけて、動く方向(上下)を逆にします。
function collisionDetection() {

  // キャンバスの上で跳ね返す
  // ボールの上端の位置の条件
  if (ballY - ballRadius < 0) {
    // ボールの位置を指定する
    ballY = ballRadius;
    // ボールの動く方向を逆にする(跳ね返す)
    ballVelY = -ballVelY;
  }

  // キャンバスの下で跳ね返す
  // 左パドルで跳ね返す
  // 右パドルで跳ね返す
  // …

}

ボールを跳ね返すとき、当たるタイミングによっては、ボールがキャンバスの壁にくっついたようになって上手く跳ね返らないことが、たまに起こります。この現象を防ぐために、次のように考えてコーディングしています。

  • if 文の条件に、<= ではなく < を使用する。
    • ボールの端がキャンバスの端にちょうど触れている場合は条件に含まない。
    • キャンバスの端よりボールの端が外側にある(はみ出ている)ことを条件にする。
  • 跳ね返す前に、ボールの位置を ballY = ballRadius(ボールの上端がキャンバスの上端にちょうど触れる位置)に指定しておくことで、上手く跳ね返らない現象を防ぐことができる。
ボールの上端がキャンバス上端に触れる位置

キャンバスの下で跳ね返す

キャンバスの下で跳ね返す場合も考え方は同じで、下のようなコードになります。

function collisionDetection() {
  if (ballY - ballRadius < 0) {
    ballY = ballRadius;
    ballVelY = -ballVelY;
  }

  // キャンバスの下で跳ね返す
  if (ballY + ballRadius > canvas.height) {
    ballY = canvas.height - ballRadius;
    ballVelY = -ballVelY;
  }

  // 左パドルで跳ね返す
  // 右パドルで跳ね返す
  // …

}

左パドルで跳ね返す

次は、パドルで跳ね返します。左パドルから見ていきましょう。

  • ボールの左端の位置がパドルの右端より小さいことを if 文の条件にします。これは、ボールの横の位置を確認するための if 文です。
  • さらに if 文で、ボールの中心がパドルの高さ内にあることを条件にし、ボールの縦の位置も確認します。
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) {
      // 跳ね返す
      // …
    }
  }

  // 右パドルで跳ね返す
  // …
 
}
左パドルとボールの位置関係
  • 条件を満たす場合は、ボールの位置をパドルの手前にします。これは、パドルにボールがくっついたようになって上手く跳ね返らない現象を防ぐための文です。
  • 横移動 ballVelX にマイナス記号をつけて、動く方向(左右)を逆にします。
  • 縦移動 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;
    }
  }

  // 右パドルで跳ね返す
  // …
 
}

右パドルで跳ね返す

右パドルで跳ね返す場合も考え方は同じで、下のようなコードになります。

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;
    }
  }

}

以上で、ボールを動かしてキャンバスの上下や左右のパドルで跳ね返す関数 collisionDetection を定義できました。

ボールを跳ね返す方法はこちらで詳しく解説しているので併せてご覧ください。

乱数(ランダム) Math.random() についてはこちらを参考にしてください。

2 得点を加えて再スタートするコード

ここからは、ボールがパドルに当たらなかった場合のコードを書いていきます。打ち返しに失敗した場合は相手の得点を増やして、ボールをキャンバスの中央から再スタートさせるようにコーディングしましょう。

変数|得点を格納する

得点を格納する変数を宣言します。score1 は左パドル(ユーザー)の、score2 は右パドル(コンピューター)の得点です。どちらも 0 点で初期化しておいてください。今回は、勝敗を決める点数 maxScore5 点で用意します。

// 得点
let score1 = 0;
let score2 = 0;
// 勝敗を決める点数
const maxScore = 5;

関数|得点を加算 - addScore

得点を加算する関数 addScore を定義していきましょう。ボールの位置を確認し、キャンバスの右端または左端に当たっていたら、得点を増やします。さらに、得点が5点未満の場合はゲームを続行できるようにしますよ。

// 得点を加算
function addScore() {

  // 右パドルの打ち返し失敗
  // 左パドルの打ち返し失敗

}

右パドルの打ち返し失敗

まずは、ユーザーの得点となる場合です。

  • ボールの右端がキャンバスの右端に当たったことを if 文の条件にします。
  • 得点をインクリメント演算子 ++ で1増やします。
  • さらに if 文を追加します。得点が maxScore 未満の場合はゲームが続行されるように、関数 resetBall を呼び出してボールをセットし直します。
function addScore() {

  // 右パドルの打ち返し失敗
  // ボールの右端がキャンバスの右端に当たったら
  if (ballX + ballRadius > canvas.width) {
    // 左パドルの得点を1増やす
    score1++;
    // 5点未満の場合はボールをセットし直す
    if (score1 < maxScore) {
      resetBall();
    }
  }

  // 左パドルの打ち返し失敗
  // …

}

左パドルの打ち返し失敗

コンピューターの得点となる場合も考え方は同じで、下のようなコードになります。

function addScore() {
  if (ballX + ballRadius > canvas.width) {
    score1++;
    if (score1 < maxScore) {
      resetBall();
    }
  }

  // 左パドルの打ち返し失敗
  if (ballX - ballRadius < 0 ) {
    score2++;
    if (score2 < maxScore) {
      resetBall();
    } 
  }

}

以上で、ボールがキャンバスの左右に当たっていることを確認して得点を加算する関数 addScore を定義できました。

関数|ボールをセットし直す - resetBall

続けて、得点が5点未満の場合に呼び出す関数 resetBall を定義しましょう。ボールをキャンバスの中央から再スタートさせますよ。

  • ボールの位置 ballXballY をキャンバスの中央にします。
  • 得点を加算した際、ボールの動く向きは、打ち返しに失敗した方に向いています。再開したら得点した方へボールが行くように、ボールの動く方向 ballVelX を逆にします。
  • ボールが真横へ動くように、ballVelY の値を 0 にします。
// ボールをセットし直す
function resetBall() {

  // ボールの位置
  ballX = canvas.width / 2;
  ballY = canvas.height / 2;
  // ボールの動く方向
  ballVelX = -ballVelX;
  // ボールの角度
  ballVelY = 0;

}

以上で、ボールをキャンバスの中央にセットし直す関数 resetBall を定義できました。

Part 2 で解説した全コード

ピンポンゲームの作り方 Part 2 はここまでとなります。解説してきた全コードを下にまとめました。

//=== 変数 ===================================
// ボール
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;

//=== 関数 ===================================
// ボールの位置を更新
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;
}

現段階では、キャンバスには何も表示されません。上のコードでは、「ボールの位置によって、ボールの動く方向を変更する。得点を加算する。」などの指示ができるようになっただけで、実際にキャンバスに「描画する」処理はまだ書いていないからです。

最後に、下の CodePen で実際に遊んでみてください。

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

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

Pyxofy から Scratch の基本をまとめた電子書籍を出版しました。
Apple Books・Kindle でご購入ください。

特設ページはこちら

まとめ

今回は、JavaScript で作るピンポンゲームのコードのうち、ボールの動きと得点について解説しました。

ボールを動かすときは、ボールがどの位置にあるのかを確認することが重要です。ボールの位置とサイズ、パドルの位置とサイズ、そしてキャンバスのサイズを考慮に入れ、それぞれに条件を指定して、壁で跳ね返ったりパドルで打ち返せるように動かしました。

Part 1 でボールを打ち返すためのパドルを、今回の Part 2 でボールと得点を準備できたので、残るはゲームの仕上げです。Part 3 ではゲームの要となるゲームループを作成し、ピンポンゲームを完成させます。

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

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

関連記事

JavaScript - ボールが跳ね返るアニメーションの作り方
ボールが動き続けるアニメーションを JavaScript で作ります。requestAnimationFrame() メソッドの使い方や、キャンバスに図形を描画してアニメーションを作る基本的な方法を学びましょう。
CSS Art - How to Make a Game Character - Super Mario
A plumber jumping in and out of pipes, throwing fireballs while rescuing a princess. Step-by-step article to create a world renowned game character.
CSS Animation – Car Driving Infinite Scroll – Part 1
Infinite, auto-scrolling animations are amazing to look at and easy to create with CSS. Learn how in this multi-part, step-by-step article.
スクラッチプログラミング - ボールをバウンドさせよう
ボールをバウンドさせるときにポイントとなるブロックは、「もしはしについたら、はねかえる」です。ボールが画面(がめん)のはしにぶつかってはねかえるプログラムを、一緒(いっしょ)につくってみましょう。