6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GPT-4で、JSブロックくずしをステップバイステップで作ってみた

Last updated at Posted at 2023-03-25

前回に続いて、ブロックくずしゲームをChatGPTで作ってみました。

break_out2.png

特長はこんな感じ。

  • GPT-4を使った
  • 実装言語は、JavaScript
  • 一人用ピンポンゲームから作り始めて、少しずつ機能を追加していった
  • 途中で、横方向から縦方向に一発で変更できた
  • そこに、ブロックを追加したらブロックくずしになっていた
  • すごい!

記事中では、あたえたプロンプトを日本語で表示しています。今回はプロンプトをDeepLで英語に変換して与えています。英語のほうが高品質なアウトプットがあるというウワサを聞いたので試してみましたが、あまり差は感じられませんでした。

コードは、間違いがあってもできる限り手動で修正せず、ChatGPTに直してもらっています。

では、だれかの参考になれば幸いです。

ゲームの基本形を作る

Javascriptを使って、最小限の機能を持った一人用ポンゲームのプログラムを作ります。まず、作る手順を短く簡潔に教えて。

JavaScriptを使って、最小限の機能を持つ1人用のポンゲームのプログラムを作成するには、以下の手順が必要です。

  1. HTMLキャンバスを設定する:パドル、ボール、ゲームエリアなど、ゲーム要素を描画するためのキャンバス要素を含むHTMLファイルを作成します。
  2. JavaScriptファイルを作成する:ゲームのロジックと描画を処理するJavaScriptファイルを作成し、HTMLファイルにリンクさせます。
  3. ゲーム要素を初期化する:ボール、パドル、ゲームエリアの大きさに関する変数を宣言します。それらの初期位置、サイズ、速度を定義します。
  4. ゲームループを作成する:requestAnimationFrame()関数を使って、ゲーム状態を更新し、要素を再描画するゲームループを作成します。
  5. 要素を描画する:fillRect()メソッドを使って、キャンバスにボール、パドル、ゲームエリアを描画する関数を定義します。
  6. ゲーム状態を更新する:ボールの位置を速度に基づいて更新し、壁やパドルとの衝突を処理する関数を作成します。
  7. ユーザー入力を追加する:キーボード入力を処理するイベントリスナーを追加し、ユーザー入力に基づいてパドルの位置を更新します。
  8. スコアとゲームオーバーを実装する:ボールがパドルを過ぎたときにプレイヤーのスコアを更新し、ポイントが獲得された後にボールの位置をリセットするか、一定のスコアに達したときにゲームオーバーのメッセージを表示します。

こちらは、一人用のPONGゲームの最小限のコード例です:

index.html
htmlCopy code<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Single Player Pong</title>
  <style>
    canvas { display: block; margin: auto; background-color: #000; }
  </style>
</head>
<body>
  <canvas id="gameCanvas" width="640" height="480"></canvas>
  <script src="pong.js"></script>
</body>
</html>
pong.js
canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const paddleHeight = 100;
const paddleWidth = 10;
const ballSize = 10;
let paddleY = (canvas.height - paddleHeight) / 2;
let ballX = canvas.width / 2;
let ballY = canvas.height / 2;
let ballVelX = 5;
let ballVelY = 5;

function drawPaddle() {
  ctx.fillStyle = 'white';
  ctx.fillRect(canvas.width - paddleWidth, paddleY, paddleWidth, paddleHeight);
}

function drawBall() {
  ctx.fillStyle = 'white';
  ctx.fillRect(ballX, ballY, ballSize, ballSize);
}

function updateBall() {
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballY <= 0 || ballY + ballSize >= canvas.height) ballVelY = -ballVelY;

  if (ballX + ballSize >= canvas.width - paddleWidth && ballY >= paddleY && ballY <= paddleY + paddleHeight) {
    ballVelX = -ballVelX;
  } else if (ballX + ballSize >= canvas.width) {
    // Reset ball and update score or show game over
    ballX = canvas.width / 2;
    ballY = canvas.height / 2;
  }
}

function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  updateBall();
  drawBall();
  drawPaddle();
  requestAnimationFrame(gameLoop);
}

document.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowUp' && paddleY > 0)
        Copy codepaddleY -= 10;

  if (e.key === 'ArrowDown' && paddleY + paddleHeight < canvas.height) paddleY += 10; });

gameLoop();

このコードは、最小限の単一プレイヤーPongゲームを作成します。パドルは矢印キーを使用して制御でき、ボールは壁とパドルから跳ね返ります。この例にはスコアリングやゲームオーバー状態が含まれていないことに注意してください。これらの機能を追加して、ゲームのビジュアルやメカニクスを強化することで機能を拡張できます。

では、JavaScript部分のコードを、できるだけ少ない機能で簡潔に書いてください。

以下は、1人用ポンゲームのJavaScriptコードを、より少ない関数で簡潔にしたバージョンです。

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleHeight = 100, paddleWidth = 10, ballSize = 10;
let paddleY = (canvas.height - paddleHeight) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = 5, ballVelY = 5;

document.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowUp' && paddleY > 0) paddleY -= 10;
  if (e.key === 'ArrowDown' && paddleY + paddleHeight < canvas.height) paddleY += 10;
});

(function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // パドルとボールを描画
  ctx.fillStyle = 'white';
  ctx.fillRect(canvas.width - paddleWidth, paddleY, paddleWidth, paddleHeight);
  ctx.fillRect(ballX, ballY, ballSize, ballSize);

  // ボールの位置を更新し、衝突を処理
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballY <= 0 || ballY + ballSize >= canvas.height) ballVelY = -ballVelY;
  if (ballX + ballSize >= canvas.width - paddleWidth && ballY >= paddleY && ballY <= paddleY + paddleHeight) {
    ballVelX = -ballVelX;
  } else if (ballX + ballSize >= canvas.width) {
    ballX = canvas.width / 2;
    ballY = canvas.height / 2;
  }

  requestAnimationFrame(gameLoop);
})();

このコードバージョンでは、描画と更新の関数をメインのゲームループに統合し、関数が少なくなります。パドルとボールの描画コードが統合されており、ボールの更新と衝突処理のコードも統合されています。ゲームループは、ゲームを自動的に開始するために、即時呼び出し関数式(IIFE)になりました。

このコードを実行すると、ゲーム画面とフレームが表示されますが、パドルとボールが表示されません。正しいコードを出力してください。

混乱させてしまって申し訳ありません。以下は、修正された最小限の機能を持つ1人用ポンゲームのJavaScriptコードです。

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleHeight = 100, paddleWidth = 10, ballSize = 10;
let paddleY = (canvas.height - paddleHeight) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = -5, ballVelY = 5;

document.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowUp' && paddleY > 0) paddleY -= 10;
  if (e.key === 'ArrowDown' && paddleY + paddleHeight < canvas.height) paddleY += 10;
});

(function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // パドルとボールを描画
  ctx.fillStyle = 'white';
  ctx.fillRect(canvas.width - paddleWidth, paddleY, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // ボールの位置を更新し、衝突を処理
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballY <= 0 || ballY + ballSize / 2 >= canvas.height) ballVelY = -ballVelY;
  if (ballX <= ballSize / 2) {
    ballX = canvas.width / 2;
    ballY = canvas.height / 2;
  } else if (ballX + ballSize / 2 >= canvas.width - paddleWidth && ballY >= paddleY && ballY <= paddleY + paddleHeight) {
    ballVelX = -ballVelX;
  }

  requestAnimationFrame(gameLoop);
})();

問題は、ボールの描画座標にありました。ボールを描画するための ctx.fillRect メソッドで、x 座標と y 座標は ballSize の半分だけオフセットされるべきでした。これにより、パドルとボールが正しく表示され、機能するようになります。

pong_0.png

ボールとパドルは表示されるようになりましたが、ボールが左壁に到達すると画面の中央に戻ってしまいます。左壁で反射させる必要があります。

誤解を招いてしまい申し訳ありません。ここでコードを更新して、ボールが画面の中央にリセットされるのではなく、左壁で反射するようにしました。

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleHeight = 100, paddleWidth = 10, ballSize = 10;
let paddleY = (canvas.height - paddleHeight) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = -5, ballVelY = 5;

document.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowUp' && paddleY > 0) paddleY -= 10;
  if (e.key === 'ArrowDown' && paddleY + paddleHeight < canvas.height) paddleY += 10;
});

(function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // パドルとボールを描画
  ctx.fillStyle = 'white';
  ctx.fillRect(canvas.width - paddleWidth, paddleY, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // ボールの位置を更新し、衝突を処理
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballY <= 0 || ballY + ballSize / 2 >= canvas.height) ballVelY = -ballVelY;
  if (ballX <= ballSize / 2 || (ballX + ballSize / 2 >= canvas.width - paddleWidth && ballY >= paddleY && ballY <= paddleY + paddleHeight)) {
    ballVelX = -ballVelX;
  }

  requestAnimationFrame(gameLoop);
})();

更新されたコードでは、左壁でボールを反射させる条件とパドルでボールを反射させる条件が統合されています。これで、ボールが左壁とパドルの両方で正しく反射するようになりました。

※この現象、前回も出ていた。共通性があるのか。最小限の機能だから左の反射を省略しているのか。

操作性と見た目の改善

今度はボールが反射します。しかし、マウスでパドルを操作した方が遊びやすいでしょう。

おっしゃる通りです。マウスでパドルを操作すると、ゲームが遊びやすくなります。ここでは、マウスを使ってパドルを操作するための更新されたコードを示します。

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleHeight = 100, paddleWidth = 10, ballSize = 10;
let paddleY = (canvas.height - paddleHeight) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = -5, ballVelY = 5;

canvas.addEventListener('mousemove', (e) => {
  const relativeY = e.clientY - canvas.getBoundingClientRect().top;
  if (relativeY - paddleHeight / 2 > 0 && relativeY + paddleHeight / 2 < canvas.height) {
    paddleY = relativeY - paddleHeight / 2;
  }
});

(function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // パドルとボールを描画
  ctx.fillStyle = 'white';
  ctx.fillRect(canvas.width - paddleWidth, paddleY, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // ボールの位置を更新し、衝突を処理
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballY <= 0 || ballY + ballSize / 2 >= canvas.height) ballVelY = -ballVelY;
  if (ballX <= ballSize / 2 || (ballX + ballSize / 2 >= canvas.width - paddleWidth && ballY >= paddleY && ballY <= paddleY + paddleHeight)) {
    ballVelX = -ballVelX;
  }

  requestAnimationFrame(gameLoop);
})();

この更新されたコードでは、keydown イベントリスナーをキャンバスにアタッチされた mousemove イベントリスナーに置き換えました。これで、パドルがマウスの縦位置に従って動くようになり、操作が容易になります。

遊びやすいですね。ボールがパドルを通過したら、GameOverを表示して一時停止してください。

ボールがパドルを通過したときに「ゲームオーバー」を表示し、ゲームを一時停止するように更新されたコードを示します。

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleHeight = 100, paddleWidth = 10, ballSize = 10;
let paddleY = (canvas.height - paddleHeight) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = -5, ballVelY = 5;
let gamePaused = false;

canvas.addEventListener('mousemove', (e) => {
  const relativeY = e.clientY - canvas.getBoundingClientRect().top;
  if (relativeY - paddleHeight / 2 > 0 && relativeY + paddleHeight / 2 < canvas.height) {
    paddleY = relativeY - paddleHeight / 2;
  }
});

function displayGameOver() {
  ctx.fillStyle = 'white';
  ctx.font = '48px sans-serif';
  ctx.fillText('Game Over', canvas.width / 2 - 100, canvas.height / 2);
}

(function gameLoop() {
  if (gamePaused) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // パドルとボールを描画
  ctx.fillStyle = 'white';
  ctx.fillRect(canvas.width - paddleWidth, paddleY, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // ボールの位置を更新し、衝突を処理
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballY <= 0 || ballY + ballSize / 2 >= canvas.height) ballVelY = -ballVelY;
  if (ballX <= ballSize / 2) {
    ballVelX = -ballVelX;
  } else if (ballX + ballSize / 2 >= canvas.width - paddleWidth && ballY >= paddleY && ballY <= paddleY + paddleHeight) {
    ballVelX = -ballVelX;
  } else if (ballX + ballSize / 2 >= canvas.width) {
    displayGameOver();
    gamePaused = true;
    return;
  }

  requestAnimationFrame(gameLoop);
})();

この更新されたコードでは、ゲームループの実行を制御する新しい変数 gamePaused を追加しました。ボールがパドルを通過すると、displayGameOver() 関数が呼び出され、「ゲームオーバー」のメッセージが表示され、gamePausedtrue に設定することでゲームが一時停止されます。

これで、ボールがパドルを通過したときに「ゲームオーバー」を表示し、一時停止するようになります。

pong_1.png

いいですね。画面をクリックすることでゲームを開始し、リセットしてください。

画面をクリックすることでゲームを開始し、リセット、再開できるように更新されたコードを示します:

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleHeight = 100, paddleWidth = 10, ballSize = 10;
let paddleY = (canvas.height - paddleHeight) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = -5, ballVelY = 5;
let gamePaused = true;

canvas.addEventListener('mousemove', (e) => {
  const relativeY = e.clientY - canvas.getBoundingClientRect().top;
  if (relativeY - paddleHeight / 2 > 0 && relativeY + paddleHeight / 2 < canvas.height) {
    paddleY = relativeY - paddleHeight / 2;
  }
});

canvas.addEventListener('click', () => {
  if (gamePaused) {
    resetGame();
    gamePaused = false;
    gameLoop();
  }
});

function displayGameOver() {
  ctx.fillStyle = 'white';
  ctx.font = '48px sans-serif';
  ctx.fillText('Game Over', canvas.width / 2 - 100, canvas.height / 2);
}

function resetGame() {
  ballX = canvas.width / 2;
  ballY = canvas.height / 2;
  ballVelX = -5;
  ballVelY = 5;
}

function gameLoop() {
  if (gamePaused) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // パドルとボールを描画
  ctx.fillStyle = 'white';
  ctx.fillRect(canvas.width - paddleWidth, paddleY, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // ボールの位置を更新し、衝突を処理
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballY <= 0 || ballY + ballSize / 2 >= canvas.height) ballVelY = -ballVelY;
  if (ballX <= ballSize / 2) {
    ballVelX = -ballVelX;
  } else if (ballX + ballSize / 2 >= canvas.width - paddleWidth && ballY >= paddleY && ballY <= paddleY + paddleHeight) {
    ballVelX = -ballVelX;
  } else if (ballX + ballSize / 2 >= canvas.width) {
    displayGameOver();
    gamePaused = true;
    return;
  }

  requestAnimationFrame(gameLoop);
}

この更新されたコードでは、ボールの位置と速度をリセットするための新しい関数 resetGame() を追加しました。キャンバスがクリックされ、ゲームが一時停止している場合、resetGame() 関数がゲームの状態をリセットするために呼び出され、その後ゲームループが開始されます。

これで、画面をクリックするとゲームが開始され、リセットされ、再開されます。

素晴らしい! 次のメッセージを最初に表示して。タイトル:Pong Game、説明:Click to start!

タイトルと説明をHTMLファイルに追加することができます。タイトルと説明が含まれる更新されたHTMLコードは次のとおりです。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pong Game</title>
  <style>
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-color: black;
      margin: 0;
    }
    canvas {
      border: 1px solid white;
    }
  </style>
</head>
<body>
  <div>
    <h1 style="color: white; text-align: center;">Pong Game</h1>
    <p style="color: white; text-align: center;">Click to start!</p>
  </div>
  <div>
    <canvas id="gameCanvas" width="640" height="480"></canvas>
  </div>
  <script src="pong.js"></script>
</body>
</html>

この更新されたHTMLコードには、タイトル「Pong Game」のための<h1>要素と説明「Click to start!」のための<p>要素が含まれています。両方の要素の色は白に設定され、text-alignプロパティを使用して中央に配置されています。ゲームのキャンバスは説明の下に配置されています。

pong_2.png

タイトルとメッセージが表示されましたが、改行していません。

混乱させてしまい申し訳ありません。 改行を追加するには、HTML コードで
タグを使用できます。 改行を含む更新された HTML コードは次のとおりです。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pong Game</title>
  <style>
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-color: black;
      margin: 0;
    }
    canvas {
      border: 1px solid white;
    }
  </style>
</head>
<body>
  <div>
    <h1 style="color: white; text-align: center;">Pong Game</h1>
    <p style="color: white; text-align: center;">Click to start!</p>
    <br>
    <canvas id="gameCanvas" width="640" height="480"></canvas>
  </div>
  <script src="pong.js"></script>
</body>
</html>

説明文とゲームキャンバスの間に
タグを追加して、改行を追加しています。これで、タイトル、説明文、ゲームキャンバスが別々の行で表示されるはずです。

タイトルとメッセージの下にキャンバスを表示して。

今回更新したHTMLコードでは、タイトル、メッセージ、キャンバスの周りに、クラス「container」を持つ新しいコンテナ

要素を追加しています。また、コンテナには新しいCSSクラスを追加し、列方向のFlexboxを適用しています(flex-direction: column)。これにより、タイトル、メッセージ、キャンバスが縦列に表示され、キャンバスはタイトルとメッセージの下に表示されます。
index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>一人用PONGゲーム</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <div class="container">
        <h1 style="color: white;">Pong Game</h1>
        <p style="color: white;">Click to start!</p>
        <canvas id="gameCanvas" width="640" height="480""></canvas>
    </div>
    <script src=" pong.js"></script>
</body>

</html>
style.css
body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: black;
    margin: 0;
  }
  canvas {
    border: 1px solid white;
  }
  .container {
    display: flex;
    flex-direction: column;
    align-items: center;
  }

この更新されたHTMLコードでは、タイトル、メッセージ、キャンバスの周りに、クラス「container」を持つ新しいコンテナ

要素を追加しています。また、コンテナには新しいCSSクラスを追加し、列方向のFlexboxを適用しています(flex-direction: column)。これにより、タイトル、メッセージ、キャンバスが縦列に表示され、キャンバスはタイトルとメッセージの下に表示されます。

pong_3.png

※ ここで、htmlとcssファイルを手動で分離した。

デバッグ

すばらしい。このゲームを試しましたが、ボールがパドルにかすかに接触すると、ボールの動きがおかしくなります。

問題が発生し、申し訳ありません。ボールがかすかに接触したときにパドルに「引っかかる」ようです。この問題を解決するために、衝突判定に小さなバッファを追加して、ボールが引っかかるのを防ぎます。以下が更新されたJavaScriptコードです:

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleHeight = 100, paddleWidth = 10, ballSize = 10;
let paddleY = (canvas.height - paddleHeight) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = -5, ballVelY = 5;
let gamePaused = true;

canvas.addEventListener('mousemove', (e) => {
  const relativeY = e.clientY - canvas.getBoundingClientRect().top;
  if (relativeY - paddleHeight / 2 > 0 && relativeY + paddleHeight / 2 < canvas.height) {
    paddleY = relativeY - paddleHeight / 2;
  }
});

canvas.addEventListener('click', () => {
  if (gamePaused) {
    resetGame();
    gamePaused = false;
    gameLoop();
  }
});

function displayGameOver() {
  ctx.fillStyle = 'white';
  ctx.font = '48px sans-serif';
  ctx.fillText('Game Over', canvas.width / 2 - 100, canvas.height / 2);
}

function resetGame() {
  ballX = canvas.width / 2;
  ballY = canvas.height / 2;
  ballVelX = -5;
  ballVelY = 5;
}

function gameLoop() {
  if (gamePaused) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw paddle and ball
  ctx.fillStyle = 'white';
  ctx.fillRect(canvas.width - paddleWidth, paddleY, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // Update ball position and handle collisions
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballY <= 0 || ballY + ballSize / 2 >= canvas.height) ballVelY = -ballVelY;
  if (ballX <= ballSize / 2) {
    ballVelX = -ballVelX;
  } else if (ballX + ballSize / 2 >= canvas.width - paddleWidth - 1 && ballY >= paddleY - ballSize / 2 && ballY <= paddleY + paddleHeight + ballSize / 2) {
    ballVelX = -ballVelX;
  } else if (ballX + ballSize / 2 >= canvas.width) {
    displayGameOver();
    gamePaused = true;
    return;
  }

  requestAnimationFrame(gameLoop);
}

更新されたコードでは、ボールとパドルの衝突をチェックする際に、小さなバッファ「-1」を追加しました。これにより、ボールがかすかに接触したときにパドルに引っかかるのを防ぎます。これで、ゲームプレイ中にボールが期待通りに動くはずです。

ボールとパドルの接触チェックと、ボールとリーチの右端のチェックを逆にするべきだと思います。

混乱させてしまい申し訳ありません。ボールとパドルとの接触チェックと、ボールとキャンバスの右端のチェックの順序を逆にするようコードを更新しました:

pong.js
function gameLoop() {
  if (gamePaused) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw paddle and ball
  ctx.fillStyle = 'white';
  ctx.fillRect(canvas.width - paddleWidth, paddleY, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // Update ball position and handle collisions
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballY <= 0 || ballY + ballSize / 2 >= canvas.height) ballVelY = -ballVelY;
  if (ballX <= ballSize / 2) {
    ballVelX = -ballVelX;
  } else if (ballX + ballSize / 2 >= canvas.width) {
    displayGameOver();
    gamePaused = true;
    return;
  } else if (ballX + ballSize / 2 >= canvas.width - paddleWidth - 1 && ballY >= paddleY - ballSize / 2 && ballY <= paddleY + paddleHeight + ballSize / 2) {
    ballVelX = -ballVelX;
  }

  requestAnimationFrame(gameLoop);
}

これで、コードはまずボールがキャンバスの右端に達したかどうかをチェックします。そうであれば、ゲームオーバーメッセージを表示し、ゲームを一時停止します。その後、ボールとパドルの衝突をチェックします。これにより、ゲームプレイ中のチェックの順序が正しくなります。

ゲーム方向を変更

このゲームでは、パドルが右側にあります。ボールはX方向に移動し、左の壁から反射し、右端に来るとゲームオーバーになります。これを変更して、パドルが下にあり、ボールがX方向に移動し、上の壁から反射し、下の端に達するとゲームオーバーになるようにしてください。

キャンバスの下部にパドルを配置し、ボールを水平方向に移動させ、上の壁から反射させ、下の端に達したときにゲームオーバーになるようにコードを変更しました:

※ プロンプトが間違っていて、変換後にボールがY方向に移動するところ、X方向となっているが、正しいプログラムが出力された。

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleWidth = 100, paddleHeight = 10, ballSize = 10;
let paddleX = (canvas.width - paddleWidth) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = 5, ballVelY = -5;
let gamePaused = true;

canvas.addEventListener('mousemove', (e) => {
  const relativeX = e.clientX - canvas.getBoundingClientRect().left;
  if (relativeX - paddleWidth / 2 > 0 && relativeX + paddleWidth / 2 < canvas.width) {
    paddleX = relativeX - paddleWidth / 2;
  }
});

canvas.addEventListener('click', () => {
  if (gamePaused) {
    resetGame();
    gamePaused = false;
    gameLoop();
  }
});

function displayGameOver() {
  ctx.fillStyle = 'white';
  ctx.font = '48px sans-serif';
  ctx.fillText('Game Over', canvas.width / 2 - 100, canvas.height / 2);
}

function resetGame() {
  ballX = canvas.width / 2;
  ballY = canvas.height / 2;
  ballVelX = 5;
  ballVelY = -5;
}

function gameLoop() {
  if (gamePaused) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw paddle and ball
  ctx.fillStyle = 'white';
  ctx.fillRect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // Update ball position and handle collisions
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballX <= 0 || ballX + ballSize / 2 >= canvas.width) ballVelX = -ballVelX;
  if (ballY <= ballSize / 2) {
    ballVelY = -ballVelY;
  } else if (ballY + ballSize / 2 >= canvas.height - paddleHeight - 1 && ballX >= paddleX - ballSize / 2 && ballX <= paddleX + paddleWidth + ballSize / 2) {
    ballVelY = -ballVelY;
  } else if (ballY + ballSize / 2 >= canvas.height) {
    displayGameOver();
    gamePaused = true;
    return;
  }

  requestAnimationFrame(gameLoop);
}

この更新されたコードは、キャンバスの下部にパドルを配置し、新たな方向に対する衝突判定を更新しています。これで、ボールは水平方向に移動し、上の壁から反射し、キャンバスの下端に達したときにゲームオーバーがトリガーされます。パドルは、マウスを使用して水平方向に制御されます。

pong_horizontal.png

すごいね。パドルの位置を80ピクセル上に移動して、GameOverのメッセージをちょうど左右中央にして。

コードを変更して、パドルの位置を80ピクセル上に移動し、ゲームオーバーメッセージを水平および垂直に中央に配置しました。

※マウス操作なので、マウスポイントを動かす領域を確保した。

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleWidth = 100, paddleHeight = 10, ballSize = 10;
let paddleX = (canvas.width - paddleWidth) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = 5, ballVelY = -5;
let gamePaused = true;

canvas.addEventListener('mousemove', (e) => {
  const relativeX = e.clientX - canvas.getBoundingClientRect().left;
  if (relativeX - paddleWidth / 2 > 0 && relativeX + paddleWidth / 2 < canvas.width) {
    paddleX = relativeX - paddleWidth / 2;
  }
});

canvas.addEventListener('click', () => {
  if (gamePaused) {
    resetGame();
    gamePaused = false;
    gameLoop();
  }
});

function displayGameOver() {
  ctx.fillStyle = 'white';
  ctx.font = '48px sans-serif';
  ctx.fillText('Game Over', (canvas.width - ctx.measureText('Game Over').width) / 2, canvas.height / 2);
}

function resetGame() {
  ballX = canvas.width / 2;
  ballY = canvas.height / 2;
  ballVelX = 5;
  ballVelY = -5;
}

function gameLoop() {
  if (gamePaused) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw paddle and ball
  ctx.fillStyle = 'white';
  ctx.fillRect(paddleX, canvas.height - paddleHeight - 80, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // Update ball position and handle collisions
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballX <= 0 || ballX + ballSize / 2 >= canvas.width) ballVelX = -ballVelX;
  if (ballY <= ballSize / 2) {
    ballVelY = -ballVelY;
  } else if (ballY + ballSize / 2 >= canvas.height - paddleHeight - 80 - 1 && ballX >= paddleX - ballSize / 2 && ballX <= paddleX + paddleWidth + ballSize / 2) {
    ballVelY = -ballVelY;
  } else if (ballY + ballSize / 2 >= canvas.height) {
    displayGameOver();
    gamePaused = true;
    return;
  }

  requestAnimationFrame(gameLoop);
}

この更新されたコードでは、パドルを描画するときにY座標から80を減算することで、パドルの位置を80ピクセル上に移動しました(canvas.height - paddleHeight - 80)。さらに、「ゲームオーバー」メッセージをX位置を((canvas.width - ctx.measureText('Game Over').width) / 2)、Y位置をcanvas.height / 2として水平および垂直に中央に配置しました。

pong_horizontal_2.png

ボールがパドルの位置より下にいった時、パドルとボールのX座標が重なると、ボールが反射してしまう。ボールの下位置とパドル上面に接触した時だけ反射するように変更して。

ボールの下端とパドルの上端の衝突を確認するために、コードを更新しました。これで、ボールはパドルの上面に接触したときにのみ反射されます。

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleWidth = 100, paddleHeight = 10, ballSize = 10;
let paddleX = (canvas.width - paddleWidth) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = 5, ballVelY = -5;
let gamePaused = true;

canvas.addEventListener('mousemove', (e) => {
  const relativeX = e.clientX - canvas.getBoundingClientRect().left;
  if (relativeX - paddleWidth / 2 > 0 && relativeX + paddleWidth / 2 < canvas.width) {
    paddleX = relativeX - paddleWidth / 2;
  }
});

canvas.addEventListener('click', () => {
  if (gamePaused) {
    resetGame();
    gamePaused = false;
    gameLoop();
  }
});

function displayGameOver() {
  ctx.fillStyle = 'white';
  ctx.font = '48px sans-serif';
  ctx.fillText('Game Over', (canvas.width - ctx.measureText('Game Over').width) / 2, canvas.height / 2);
}

function resetGame() {
  ballX = canvas.width / 2;
  ballY = canvas.height / 2;
  ballVelX = 5;
  ballVelY = -5;
}

function gameLoop() {
  if (gamePaused) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw paddle and ball
  ctx.fillStyle = 'white';
  ctx.fillRect(paddleX, canvas.height - paddleHeight - 80, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);

  // Update ball position and handle collisions
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballX <= 0 || ballX + ballSize / 2 >= canvas.width) ballVelX = -ballVelX;
  if (ballY <= ballSize / 2) {
    ballVelY = -ballVelY;
  } else if (
    ballY + ballSize / 2 >= canvas.height - paddleHeight - 80 - 1 &&
    ballY + ballSize / 2 <= canvas.height - paddleHeight - 80 + 1 &&
    ballX >= paddleX - ballSize / 2 &&
    ballX <= paddleX + paddleWidth + ballSize / 2
  ) {
    ballVelY = -ballVelY;
  } else if (ballY + ballSize / 2 >= canvas.height) {
    displayGameOver();
    gamePaused = true;
    return;
  }

  requestAnimationFrame(gameLoop);
}

この更新されたコードでは、ボールとパドルの衝突チェックに条件を追加して、ボールの下端がパドルの上端の1ピクセル上または下にあることを確認します(ballY + ballSize / 2 >= canvas.height - paddleHeight - 80 - 1 && ballY + ballSize / 2 <= canvas.height - paddleHeight - 80 + 1)。これにより、この更新されたコードでは、ボールとパドルの衝突チェックをより正確に行っています。

ブロックくずし

それでは、ゲームをもっと面白くしよう。画面の上半分の位置に8個 x 3列のブロックを追加して。

8個3列のブロックを画面の上半分に追加するよう、コードをアップデートしました。

pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleWidth = 100, paddleHeight = 10, ballSize = 10, blockWidth = 70, blockHeight = 20, blockRows = 3, blockCols = 8;
let paddleX = (canvas.width - paddleWidth) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = 5, ballVelY = -5;
let gamePaused = true;

const blocks = [];
for (let i = 0; i < blockRows; i++) {
  blocks[i] = [];
  for (let j = 0; j < blockCols; j++) {
    blocks[i][j] = { x: j * (blockWidth + 10) + 25, y: i * (blockHeight + 10) + 25, active: true };
  }
}

canvas.addEventListener('mousemove', (e) => {
  const relativeX = e.clientX - canvas.getBoundingClientRect().left;
  if (relativeX - paddleWidth / 2 > 0 && relativeX + paddleWidth / 2 < canvas.width) {
    paddleX = relativeX - paddleWidth / 2;
  }
});

canvas.addEventListener('click', () => {
  if (gamePaused) {
    resetGame();
    gamePaused = false;
    gameLoop();
  }
});

function displayGameOver() {
  ctx.fillStyle = 'white';
  ctx.font = '48px sans-serif';
  ctx.fillText('Game Over', (canvas.width - ctx.measureText('Game Over').width) / 2, canvas.height / 2);
}

function resetGame() {
  ballX = canvas.width / 2;
  ballY = canvas.height / 2;
  ballVelX = 5;
  ballVelY = -5;
  blocks.forEach(row => {
    row.forEach(block => {
      block.active = true;
    });
  });
}

function drawBlocks() {
  ctx.fillStyle = 'white';
  for (let i = 0; i < blockRows; i++) {
    for (let j = 0; j < blockCols; j++) {
      if (blocks[i][j].active) {
        ctx.fillRect(blocks[i][j].x, blocks[i][j].y, blockWidth, blockHeight);
      }
    }
  }
}

function checkBlockCollision() {
  for (let i = 0; i < blockRows; i++) {
    for (let j = 0; j < blockCols; j++) {
      const block = blocks[i][j];
      if (block.active && ballX + ballSize / 2 >= block.x && ballX - ballSize / 2 <= block.x + blockWidth && ballY + ballSize / 2 >= block.y && ballY - ballSize / 2 <= block.y + blockHeight) {
        block.active = false;
        ballVelY = -ballVelY;
        return;
      }
    }
  }
}

function gameLoop() {
  if (gamePaused) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw paddle, ball, and blocks
  ctx.fillStyle = 'white';
  ctx.fillRect(paddleX, canvas.height - paddleHeight - 80, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);
  drawBlocks();


  // Update ball position and handle collisions 
  ballX += ballVelX; ballY += ballVelY;

  if (ballX <= 0 || ballX + ballSize / 2 >= canvas.width) ballVelX = -ballVelX;
  if (ballY <= ballSize / 2) {
        ballVelY = -ballVelY; 
  } else if ( ballY + ballSize / 2 >= canvas.height - paddleHeight - 80 - 1 
    && ballY + ballSize / 2 <= canvas.height - paddleHeight - 80 + 1 
    && ballX >= paddleX - ballSize / 2 
    && ballX <= paddleX + paddleWidth + ballSize / 2 ) {
        ballVelY = -ballVelY; 
  } else if (ballY + ballSize / 2 >= canvas.height) {
        displayGameOver(); gamePaused = true; return;
  }

checkBlockCollision();

requestAnimationFrame(gameLoop); }

この更新されたコードでは、画面の上半分に8x3のブロックの行が追加されています。ブロックは、x、y座標と active プロパティを持つブロックオブジェクトで初期化された blocks という2D配列で表現されています。 drawBlocks 関数は、キャンバスにブロックを描画するためのものであり、checkBlockCollision 関数は、ボールがアクティブなブロックと衝突したかどうかをチェックします。衝突が発生した場合、ブロックの active プロパティが false に設定され、ボールの Y 速度が反転します。

break_out.png

素晴らしい。全体を調整したい。まずキャンバスのサイズを640 x 640にする。それから、ブロックを少し下げて、左右均等に並べて。

キャンバスのサイズを640 x 640に調整し、ブロックをわずかに下方にシフトして均等に再配置するようにコードを更新しました。

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>一人用PONGゲーム</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <div class="container">
        <h1 style="color: white;">Pong Game</h1>
        <p style="color: white;">Click to start!</p>
        <canvas id="gameCanvas" width="640" height="640""></canvas>
    </div>
    <script src=" pong.js"></script>
</body>

</html>
pong.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const paddleWidth = 100, paddleHeight = 10, ballSize = 10, blockWidth = 70, blockHeight = 20, blockRows = 3, blockCols = 8;
let paddleX = (canvas.width - paddleWidth) / 2, ballX = canvas.width / 2, ballY = canvas.height / 2, ballVelX = 5, ballVelY = -5;
let gamePaused = true;

const blocks = [];
const padding = (canvas.width - (blockCols * (blockWidth + 10))) / 2;
for (let i = 0; i < blockRows; i++) {
    blocks[i] = [];
    for (let j = 0; j < blockCols; j++) {
    blocks[i][j] = { x: j * (blockWidth + 10) + padding, y: i * (blockHeight + 10) + 60, active: true };
    }
}

canvas.addEventListener('mousemove', (e) => {
    const relativeX = e.clientX - canvas.getBoundingClientRect().left;
    if (relativeX - paddleWidth / 2 > 0 && relativeX + paddleWidth / 2 < canvas.width) {
    paddleX = relativeX - paddleWidth / 2;
    }
});

canvas.addEventListener('click', () => {
    if (gamePaused) {
    resetGame();
    gamePaused = false;
    gameLoop();
    }
});

function displayGameOver() {
    ctx.fillStyle = 'white';
    ctx.font = '48px sans-serif';
    ctx.fillText('Game Over', (canvas.width - ctx.measureText('Game Over').width) / 2, canvas.height / 2);
}

function resetGame() {
    ballX = canvas.width / 2;
    ballY = canvas.height / 2;
    ballVelX = 5;
    ballVelY = -5;
    blocks.forEach(row => {
    row.forEach(block => {
        block.active = true;
    });
    });
}

function drawBlocks() {
    ctx.fillStyle = 'white';
    for (let i = 0; i < blockRows; i++) {
    for (let j = 0; j < blockCols; j++) {
        if (blocks[i][j].active) {
        ctx.fillRect(blocks[i][j].x, blocks[i][j].y, blockWidth, blockHeight);
        }
    }
    }
}

function checkBlockCollision() {
    for (let i = 0; i < blockRows; i++) {
    for (let j = 0; j < blockCols; j++) {
        const block = blocks[i][j];
        if (block.active && ballX + ballSize / 2 >= block.x && ballX - ballSize / 2 <= block.x + blockWidth && ballY + ballSize / 2 >= block.y && ballY - ballSize / 2 <= block.y + blockHeight) {
        block.active = false;
        ballVelY = -ballVelY;
        return; 
        }
    }
    }
}

function gameLoop() {
  if (gamePaused) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw paddle, ball, and blocks
  ctx.fillStyle = 'white';
  ctx.fillRect(paddleX, canvas.height - paddleHeight - 80, paddleWidth, paddleHeight);
  ctx.fillRect(ballX - ballSize / 2, ballY - ballSize / 2, ballSize, ballSize);
  drawBlocks();

  // Update ball position and handle collisions
  ballX += ballVelX;
  ballY += ballVelY;

  if (ballX <= 0 || ballX + ballSize / 2 >= canvas.width) ballVelX = -ballVelX;
  if (ballY <= ballSize / 2) {
    ballVelY = -ballVelY;
  } else if (
    ballY + ballSize / 2 >= canvas.height - paddleHeight - 80 - 1 &&
    ballY + ballSize / 2 <= canvas.height - paddleHeight - 80 + 1 &&
    ballX >= paddleX - ballSize / 2 &&
    ballX <= paddleX + paddleWidth + ballSize / 2
  ) {
    ballVelY = -ballVelY;
  } else if (ballY + ballSize / 2 >= canvas.height) {
    displayGameOver();
    gamePaused = true;
    return;
  }

  checkBlockCollision();

  requestAnimationFrame(gameLoop);
}

この更新されたコードでは、キャンバスのサイズを640 x 640に変更し、ブロックの位置を水平方向に中央揃えするために必要なパディングを計算して調整しました。また、ブロックを60ピクセル下げてキャンバスの上端との間により多くのスペースを提供しました。

break_out2.png

感想

  • 今回も、指示だけでコードがどんどん出来ていくのはスゴイ!

  • GPT-4のほうが、アウトプットの精度が高い感じを受けた。

  • 特に、ゲーム方向の変更とブロック追加が一発でできるのは衝撃

  • ブロックを追加しろと言うだけで、ボールの反射も実装してくれた。

  • 日にちをまたいだ作業だと、作業経緯を覚えてくれなさそう

  • まだまだ不足している機能がある

    • スコア
    • 効果音
    • ゲームクリア時の処理
    • ボールがパドルに当たる位置で方向を変更
  • pong.jsを分割しようとしたが、そこでGPT-4の回数制限に引っかかった。

  • 早い段階からファイルの分割を指示した方が、出力する量が減って良さそう。

  • GitHub Copilotとか、もっとゴリゴリ使えばいいのか

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?