JavaScript - ピンポンゲームの作り方 Part 3 - ゲームループ
3回に分けて解説してきたピンポンゲームの作り方はこの記事が最終回です。ゲームの要となるゲームループ関数を作成し、ゲームを仕上げます。記事の最後にはすべての JavaScript コードを掲載しています。
JavaScript でゲーム作成|ゲームの流れを制御する
HTML のキャンバス <canvas>
と JavaScript でピンポンゲームを作成する方法を、3回にわたってお届けしています。今回はその最終回、Part 3 です。
- Part 1 では、Enter キーの押下でゲームを開始するコードと、パドルを上下に動かすコードを解説しました。
- Part 2 では、ボールの衝突判定をして、壁で跳ね返ったりパドルで打ち返せるようにし、打ち返しに失敗したら得点が入るようにコーディングしました。
- Part 3 (この記事)では、ゲーム全体を制御するゲームループを作成し、ピンポンゲームを完成させます。
サンプルプロジェクト
あらためてピンポンゲームの動きを確認しておきましょう。
- どちらかの点数が5点になるまで、ゲームは続行する
- どちらかの点数が5点になったら、Game Over が表示される
- キャンバスが最初の状態に戻り、再びゲームができるようになる
ピンポンゲームを構成するパドル、ボール、得点はキャンバスに描画して動かします。JavaScript でキャンバスに描画する方法は以下の記事で紹介しているので参考にしてください。
これから説明していく内容は、大きく分けて次の三つです。Part 1、Part 2 で作成した関数も使って、ゲームを完成させます。
- キャンバスに描画するコード
- ゲームの流れを制御するコード
- ゲームを画面に表示するコード
1 キャンバスに描画するコード
Part 1、Part 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';
//------------------------------
// オブジェクトを描画する
//------------------------------
}
描画スタイルについては、以下の記事を参考にしてください。
オブジェクトを描画する
描画スタイルを設定したら、ピンポンゲームに必要なすべてのパーツを描画していきます。
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
を呼び出して、キャンバスにオブジェクト(ピンポンゲームを構成するパーツ)を描画します。 - 変数
playing
をfalse
にしてゲーム中ではないことを示し、ゲームを再開できるようにします。
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 1、Part 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 とつながりましょう! LinkedIn・ Threads・ Mastodon・ X (Twitter) @pyxofy・ Facebook