【ChatGPT】実際に遊べる!ブロック崩しをプログラミング無しで生成AIに作らせてみた【プロンプト・ソースコード全公開】

どもっ!ROBO艦長です。

最近 ChatGPT であっという間にゲームができましたとかSNSで見かけますよね。

私もプロのプログラマーの端くれ、ブロック崩しぐらい自力でも作れなくもなくもないですが、さすがに「あっという間」というわけにはいきません。
まる1日あればそれっぽいのがなんとかかんとか?ぐらい?ですかね。。。

ChatGPTなら一瞬で作ってくれるらしいのですが、本当にそんなに簡単にできるのか実際に試してみました。

この記事は「多少WEBプログラムができるけどそこまで詳しくない」「ChatGPTはちょっと使ってみた程度」ぐらいの人を対象にしています。

まったくプログラム知識がない人は何を言っているのか意味が分からないかもしれません。
でも、わからないなりに書いてあることをそのまままねしても同じようなものが作れるかもしれません。
作れなくてもAIプログラミングの勉強になると思います。ぜひ、最後まで読んで試してみてください。

長くなりますが、あえてChatGPTへの指示内容(プロンプト)やChatGPTの回答、ソースコードなどを省略することなくすべて記載します。

また、記事内のリンク先から作ったブロック崩しを実際に遊べます(PC向けマウス操作)

なぜなら、いつも他の人のブログを読んでいて「省略されている部分がわからん(汗)」と思うからです。

ブロック崩しを作ってもらうための指示(プロンプト) 1回目

今回はChatGPTでGPT-4を使用します。
早速、試してみましょう。

※OpenAIのChatGPTだとGPT-4は有料ですが、MicrosoftのCopilotなら無料でGPT-4が使えますのでそちらを使ってもいいかもしれません。

まずは下記の指示をChatGPTに伝えます。
ChatGPTはできるだけ詳しく条件を伝えるほど回答の精度が上がります。

ブラウザで動くブロック崩しゲームを作りたいと思いますので協力してください。
・プロのゲームプログラマーとして考えてください。
・WEBブラウザ上で動作するようにしてください。
・使用する言語、フレームワーク、プラグインなどは無料のものだけにしてください。
・ソースコードのフォルダ構成・ファイル構成を提示してください。
・各ソースファイルごとにファイル名が分かるようにしてコードブロックで囲んで記載してください。
・マウスで操作できるようにしてください。
・タイトル画面、ゲーム画面、ゲームオーバー画面を用意してください。
・マウスの左クリックでゲームスタートしてください。
・ブロックを崩したら得点が入るようにして、画面左上にスコアを表示してください。
・3ステージ用意してください。
・3回失敗したらゲームオーバーにしてください。残機数を画面右上に表示してください。
・ゲームオーバー画面で左クリックしたら最初のステージからスタートしなおしてください。

本当にあっという間、わずか1~2分程度で回答とソースコードが生成されました。

OpenAI ChatGPT CPT-4 の回答 1回目

このゲームの開発には、以下のようなフォルダ構成をお勧めします。

フォルダ構成

breakout-game/
│
├── index.html
├── style.css
└── script.js

各ファイルの内容

index.html

HTMLファイルでは、ゲームの基本的なマークアップを設定します。
ゲームのキャンバス、スコア表示、ライフ表示などが含まれます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ブロック崩しゲーム</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="gameContainer">
        <canvas id="gameCanvas"></canvas>
        <div id="scoreContainer">スコア: <span id="score">0</span></div>
        <div id="livesContainer">残機: <span id="lives">3</span></div>
    </div>
    <script src="script.js"></script>
</body>
</html>

style.css

CSSファイルでは、ゲームのビジュアルスタイルを設定します。
キャンバスのサイズやスコア表示の位置などを定義します。

body, html {
    margin: 0;
    padding: 0;
    overflow: hidden;
    height: 100%;
}

#gameContainer {
    position: relative;
    width: 800px;
    height: 600px;
    margin: auto;
}

#gameCanvas {
    background: #000;
    display: block;
    width: 100%;
    height: 100%;
}

#scoreContainer, #livesContainer {
    position: absolute;
    color: #fff;
    font-family: Arial, sans-serif;
}

#scoreContainer {
    top: 10px;
    left: 10px;
}

#livesContainer {
    top: 10px;
    right: 10px;
}

script.js 1回目

以下は、ブラウザで動作するブロック崩しゲームのためのscript.jsのソースコード全体です。
このコードは、ゲームの主な機能を実装しています:
ゲームの初期化、マウスでのパドル制御、ブロックの描画と衝突検知、スコアとライフの管理、ゲームオーバーと勝利の条件です。

document.addEventListener('DOMContentLoaded', () => {
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    let score = 0;
    let lives = 3;

    let ballRadius = 10;
    let x = canvas.width / 2;
    let y = canvas.height - 30;
    let dx = 2;
    let dy = -2;

    let paddleHeight = 10;
    let paddleWidth = 75;
    let paddleX = (canvas.width - paddleWidth) / 2;

    let rightPressed = false;
    let leftPressed = false;

    let brickRowCount = 5;
    let brickColumnCount = 3;
    let brickWidth = 75;
    let brickHeight = 20;
    let brickPadding = 10;
    let brickOffsetTop = 30;
    let brickOffsetLeft = 30;
    let bricks = [];

    for(let c = 0; c < brickColumnCount; c++) {
        bricks[c] = [];
        for(let r = 0; r < brickRowCount; r++) {
            bricks[c][r] = { x: 0, y: 0, status: 1 };
        }
    }

    document.addEventListener("mousemove", mouseMoveHandler, false);

    function mouseMoveHandler(e) {
        let relativeX = e.clientX - canvas.offsetLeft;
        if(relativeX > 0 && relativeX < canvas.width) {
            paddleX = relativeX - paddleWidth / 2;
        }
    }

    function collisionDetection() {
        for(let c = 0; c < brickColumnCount; c++) {
            for(let r = 0; r < brickRowCount; r++) {
                let b = bricks[c][r];
                if(b.status == 1) {
                    if(x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
                        dy = -dy;
                        b.status = 0;
                        score++;
                        if(score == brickRowCount * brickColumnCount) {
                            alert("You Win, Congratulations!");
                            document.location.reload();
                        }
                    }
                }
            }
        }
    }

    function drawBall() {
        ctx.beginPath();
        ctx.arc(x, y, ballRadius, 0, Math.PI*2);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
    }

    function drawPaddle() {
        ctx.beginPath();
        ctx.rect(paddleX, canvas.height-paddleHeight, paddleWidth, paddleHeight);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
    }

    function drawBricks() {
        for(let c = 0; c < brickColumnCount; c++) {
            for(let r = 0; r < brickRowCount; r++) {
                if(bricks[c][r].status == 1) {
                    let brickX = (c*(brickWidth+brickPadding))+brickOffsetLeft;
                    let brickY = (r*(brickHeight+brickPadding))+brickOffsetTop;
                    bricks[c][r].x = brickX;
                    bricks[c][r].y = brickY;
                    ctx.beginPath();
                    ctx.rect(brickX, brickY, brickWidth, brickHeight);
                    ctx.fillStyle = "#0095DD";
                    ctx.fill();
                    ctx.closePath();
                }
            }
        }
    }

    function drawScore() {
        ctx.font = "16px Arial";
        ctx.fillStyle = "#0095DD";
        ctx.fillText("Score: " + score, 8, 20);
    }

    function drawLives() {
        ctx.font = "16px Arial";
        ctx.fillStyle = "#0095DD";
        ctx.fillText("Lives: " + lives, canvas.width - 65, 20);
    }

    function draw() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawBricks();
        drawBall();
        drawPaddle();
        collisionDetection();
        drawScore();
        drawLives();

        if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
            dx = -dx;
        }
        if(y + dy < ballRadius) {
            dy = -dy;
        } else if(y + dy > canvas.height - ballRadius) {
            if(x > paddleX && x < paddleX + paddleWidth) {
                dy = -dy;
            } else {
                lives--;
                if(!lives) {
                    alert("GAME OVER");
                    document.location.reload();
                } else {
                    x = canvas.width / 2;
                    y = canvas.height - 30;
                    dx = 3;
                    dy = -3;
                    paddleX = (canvas.width - paddleWidth) / 2;
                }
            }
        }

        if(rightPressed && paddleX < canvas.width - paddleWidth) {
            paddleX += 7;
        } else if(leftPressed && paddleX > 0) {
            paddleX -= 7;
        }

        requestAnimationFrame(draw);
    }

    draw();
});

このスクリプトは、HTMLとCSSと合わせて、シンプルなブロック崩しゲームの全体的な構造を完成させます。
ゲームのさらなる発展には、追加の機能や改善、例えばレベルの追加、パワーアップ、改善されたグラフィックスや音響効果などを含めることができます。

1回目の指示で作成した「ブロック崩し」ゲームはこちら

下の画像クリックでゲーム画面が新しいタブで開きます。

おぉ!?んん??

正直なところ、ちょっとそれっぽい画面が出ただけでも驚きですが、残念ながらブロック崩しになっていないようです。
ブロックがでかすぎて画面を埋め尽くしていますし、ボールが動きません。
マウスカーソルを画面全体の左側に移動するとたまにパドルが動きますが、いまいちおかしい感じです。

プログラマーならここまで作ってくれればあとは自分で仕上げてもいいかもしれません。
ゼロから作るよりは早いかも。

でも、今回はこれをChatGPTに直してもらいたいと思います。

ブロック崩しを作ってもらうための指示(プロンプト) 2回目

次の修正指示をChatGPTに伝えます。

問題点を挙げるので修正してください。
・ブロックが大きすぎます。ブロックのサイズを0.2倍にしてください。
・ブロックは画面の上部30%に収まるようにしてください。
・ブロックとブロックの隙間が大きすぎます。間隔を0にしてください。
・ボールが動きません。左クリックを押したらゲームがスタートしてボールが動くようにしてください。
・スコアと残機の表示がテキストとキャンバスの2重になっています。テキストだけにしてください。

OpenAI ChatGPT CPT-4 の回答 2回目

問題点に対する修正を行います。以下は修正点を適用したscript.jsのコードです。

  • ブロックのサイズを0.2倍にする
  • ブロックは画面の上部30%に収まるように配置する
  • ブロックとブロックの隙間を0にする
  • 左クリックでゲームスタートし、ボールが動き出すようにする
  • スコアと残機の表示をキャンバス上ではなく、テキストでのみ表示する

script.js 2回目①

document.addEventListener('DOMContentLoaded', () => {
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    let score = 0;
    let lives = 3;
    let gameStarted = false;

    let ballRadius = 10;
    let x = canvas.width / 2;
    let y = canvas.height - 30;
    let dx = 2;
    let dy = -2;

    let paddleHeight = 10;
    let paddleWidth = 75;
    let paddleX = (canvas.width - paddleWidth) / 2;

    // ブロックの設定を調整
    let brickRowCount = 5;
    let brickColumnCount = Math.floor(canvas.width / (75 * 0.2)); // 画面幅に合わせて列数を調整
    let brickWidth = 75 * 0.2; // 0.2倍
    let brickHeight = 20 * 0.2; // 0.2倍
    let brickPadding = 0; // 隙間を0に
    let brickOffsetTop = 30;
    let brickOffsetLeft = 0; // 左端からスタート

    // 画面の上部30%に収まるように調整
    let brickSectionHeight = canvas.height * 0.3;
    let maxBrickRows = Math.floor(brickSectionHeight / (brickHeight + brickPadding));
    brickRowCount = Math.min(brickRowCount, maxBrickRows);

    let bricks = [];
    for(let c = 0; c < brickColumnCount; c++) {
        bricks[c] = [];
        for(let r = 0; r < brickRowCount; r++) {
            bricks[c][r] = { x: 0, y: 0, status: 1 };
        }
    }

    document.addEventListener("mousemove", mouseMoveHandler, false);
    canvas.addEventListener("click", function() {
        if (!gameStarted) {
            gameStarted = true;
            draw();
        }
    });

    function mouseMoveHandler(e) {
        let relativeX = e.clientX - canvas.offsetLeft;
        if(relativeX > 0 && relativeX < canvas.width) {
            paddleX = relativeX - paddleWidth / 2;
        }
    }

    // 衝突検知、ボール、パドル、ブロックの描画、スコアとライフの管理などの関数は変更なし

    function draw() {
        if (!gameStarted) {
            return; // ゲームがスタートしていなければ何も描画しない
        }
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawBricks();
        drawBall();
        drawPaddle();
        collisionDetection();
        updateScoreAndLives(); // スコアとライフの更新をテキストで表示する関数

        // ボールの動きとゲームオーバーのロジックは以前と同じ

        requestAnimationFrame(draw);
    }

    function updateScoreAndLives() {
        // スコアとライフをテキストで更新するためのDOM操作
        document.getElementById('score').textContent = `Score: ${score}`;
        document.getElementById('lives').textContent = `Lives: ${lives}`;
    }

    // drawBricks(), drawBall(), drawPaddle(), collisionDetection() などの関数は基本的に以前と同じ
});

このコードは、問題点に基づいて以下の修正を行っています:

  • ブロックのサイズが元の75×20から15×4(0.2倍)に調整されました。
  • ブロックの配置が画面の上部30%内に

回答が出ましたが、どうやら一部が省略されているようです。
改めて指示をします。

修正指示(プロンプト)

変更のない部分も含めjsのソースコード全体をコードブロックでもう一度書いてください

修正指示へのChatGPTの回答

以下は、指摘された問題点に対処し、ブロック崩しゲームの機能を全て含むscript.jsのソースコード全体です。

script.js 2回目②

document.addEventListener('DOMContentLoaded', () => {
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    let score = 0;
    let lives = 3;
    let gameStarted = false;

    let ballRadius = 10;
    let x = canvas.width / 2;
    let y = canvas.height - 30;
    let dx = 2;
    let dy = -2;

    let paddleHeight = 10;
    let paddleWidth = 75;
    let paddleX = (canvas.width - paddleWidth) / 2;

    let brickRowCount = 5;
    let brickColumnCount = Math.floor(canvas.width / (75 * 0.2));
    let brickWidth = 75 * 0.2; // 0.2倍
    let brickHeight = 20 * 0.2; // 0.2倍
    let brickPadding = 0; // 隙間を0にする
    let brickOffsetTop = 30;
    let brickOffsetLeft = 0;

    let bricks = [];
    for(let c = 0; c < brickColumnCount; c++) {
        bricks[c] = [];
        for(let r = 0; r < brickRowCount; r++) {
            bricks[c][r] = { x: 0, y: 0, status: 1 };
        }
    }

    function mouseMoveHandler(e) {
        let relativeX = e.clientX - canvas.offsetLeft;
        if(relativeX > 0 && relativeX < canvas.width) {
            paddleX = relativeX - paddleWidth / 2;
        }
    }

    function collisionDetection() {
        for(let c = 0; c < brickColumnCount; c++) {
            for(let r = 0; r < brickRowCount; r++) {
                let b = bricks[c][r];
                if(b.status == 1) {
                    if(x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
                        dy = -dy;
                        b.status = 0;
                        score++;
                        if(score == brickRowCount * brickColumnCount) {
                            alert("You Win, Congratulations!");
                            document.location.reload();
                        }
                    }
                }
            }
        }
    }

    function drawBall() {
        ctx.beginPath();
        ctx.arc(x, y, ballRadius, 0, Math.PI*2);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
    }

    function drawPaddle() {
        ctx.beginPath();
        ctx.rect(paddleX, canvas.height-paddleHeight, paddleWidth, paddleHeight);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
    }

    function drawBricks() {
        for(let c = 0; c < brickColumnCount; c++) {
            for(let r = 0; r < brickRowCount; r++) {
                if(bricks[c][r].status == 1) {
                    let brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
                    let brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
                    bricks[c][r].x = brickX;
                    bricks[c][r].y = brickY;
                    ctx.beginPath();
                    ctx.rect(brickX, brickY, brickWidth, brickHeight);
                    ctx.fillStyle = "#0095DD";
                    ctx.fill();
                    ctx.closePath();
                }
            }
        }
    }

    function draw() {
        if (!gameStarted) return;

        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawBricks();
        drawBall();
        drawPaddle();
        collisionDetection();

        if(x + dx > canvas.width-ballRadius || x + dx < ballRadius) {
            dx = -dx;
        }
        if(y + dy > canvas.height-ballRadius || y + dy < ballRadius) {
            dy = -dy;
        }

        if(y + dy < ballRadius) {
            dy = -dy;
        } else if(y + dy > canvas.height - ballRadius) {
            if(x > paddleX && x < paddleX + paddleWidth) {
                dy = -dy * 1.1; // Increase speed after hit
            } else {
                lives--;
                if(!lives) {
                    alert("GAME OVER");
                    document.location.reload();
                } else {
                    x = canvas.width / 2;
                    y = canvas.height - 30;
                    dx = 3;
                    dy = -3
                    paddleX = (canvas.width - paddleWidth) / 2;
                }
            }
        }

        x += dx;
        y += dy;
        requestAnimationFrame(draw);
    }

    function keyDownHandler(e) {
        if(e.key == "Right" || e.key == "ArrowRight") {
            rightPressed = true;
        } else if(e.key == "Left" || e.key == "ArrowLeft") {
            leftPressed = true;
        }
    }

    function keyUpHandler(e) {
        if(e.key == "Right" || e.key == "ArrowRight") {
            rightPressed = false;
        } else if(e.key == "Left" || e.key == "ArrowLeft") {
            leftPressed = false;
        }
    }

    function mouseMoveHandler(e) {
        let relativeX = e.clientX - canvas.offsetLeft;
        if(relativeX > 0 && relativeX < canvas.width) {
            paddleX = relativeX - paddleWidth / 2;
        }
    }

    document.addEventListener("keydown", keyDownHandler, false);
    document.addEventListener("keyup", keyUpHandler, false);
    document.addEventListener("mousemove", mouseMoveHandler, false);

    canvas.addEventListener("click", function() {
        if (!gameStarted) {
            gameStarted = true;
            x = canvas.width / 2;
            y = canvas.height - 30;
            dx = 2;
            dy = -2;
            paddleX = (canvas.width - paddleWidth) / 2;
            score = 0;
            lives = 3;
            bricks = [];
            for(let c = 0; c < brickColumnCount; c++) {
                bricks[c] = [];
                for(let r = 0; r < brickRowCount; r++) {
                    bricks[c][r] = { x: 0, y: 0, status: 1 };
                }
            }
            draw();
        }
    });

    // Initialize game without automatically starting the animation loop
    drawBricks(); // Draw bricks for the first view
});

この修正版は以下の機能と変更を含みます:

  • ブロックサイズと隙間: ブロックのサイズを元のサイズの0.2倍に調整し、隙間を0に設定しています。これによりブロックは画面の上部30%に収まります。
  • ゲーム開始のトリガー: 左クリックでゲームがスタートし、ボールが動き出すようになりました。gameStarted変数を用いて、クリックによるゲーム開始の管理を行います。
  • スコアと残機の表示: キャンバス上ではなく、HTMLのテキスト要素を用いてスコアと残機を表示します。これにより表示の二重化を解消します。
  • キーボード操作の追加: パドルの左右の移動に対応するため、キーボードのイベントリスナーを追加しています。ただし、問題の指示には含まれていなかったため、必要に応じて削除してください。
  • このコードはゲームの基本的なフレームワークを提供し、指摘された問題点を解決します。更なるゲームプレイの改善や機能の追加が可能です。

2回目の指示で作成した「ブロック崩し」ゲームはこちら

下の画像クリックでゲーム画面が新しいタブで開きます。

操作方法
  • 黒い枠内をクリックするとスタートします。
  • マウスカーソルをゲーム枠より左側に持っていくとパドルが左右に動きます。

おぉ!かなり、ブロック崩しっぽい感じになってきましたね!

ちゃんとボールが動いて、ブロックを崩しています。
しかし、パドル(操作する板)が画面のかなり左の方でマウスを動かさないと動きませんし、パドルに当たらなくてもアウトになりません。
あと、ボールがなんかでかすぎます。

そこで、さらに改良を指示します。

ブロック崩しを作ってもらうための指示(プロンプト) 3回目

次の修正指示をChatGPTに伝えます。

今後、ブロック崩しのプレイヤーが操作する板を「パドル」と呼びます。
以下の通りにプログラムを修正してください。
- ゲーム画面の描画範囲内でマウスカーソル操作したときにマウスカーソルのX軸とパドルのX軸の中央が一致するようにしてください。
- ボールのサイズを0.3倍にして楕円ではなく円にしてください。
- ブロックが壊れたらスコアを+1してください。
- すべてのブロックを壊したら次のステージに進んでください。
- ボールをパドルで反射できるようにしてください。
- ボールがパドルのない画面下部にぶつかったら、残機を1減らして再スタートしてください。
- 残機が0になったらゲームオーバーにしてください。
- ゲームオーバーの後、左クリックをしたらスコア0、残機3で再スタートしてください。

OpenAI ChatGPT CPT-4 の回答 3回目

以下のプログラムは、指定された要件を満たすようにscript.jsを修正したものです。
これにはパドルの動きの調整、ボールのサイズ変更と形状の変更(円に)、スコアの加算、ステージ進行のロジック、パドルでのボールの反射、残機の管理とゲームオーバーの処理が含まれます。

script.js 3回目

document.addEventListener('DOMContentLoaded', () => {
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    let score = 0;
    let lives = 3;
    let gameStarted = false;

    let ballRadius = 10 * 0.3; // ボールのサイズを0.3倍にする
    let x = canvas.width / 2;
    let y = canvas.height - 30;
    let dx = 2;
    let dy = -2;

    const paddleHeight = 10;
    const paddleWidth = 75;
    let paddleX = (canvas.width - paddleWidth) / 2;

    let brickRowCount = 5;
    let brickColumnCount = 3;
    let brickWidth = 75 * 0.2; // ブロックのサイズ調整
    let brickHeight = 20 * 0.2;
    let brickPadding = 0;
    let brickOffsetTop = 30;
    let brickOffsetLeft = 30;
    let bricks = [];

    for (let c = 0; c < brickColumnCount; c++) {
        bricks[c] = [];
        for (let r = 0; r < brickRowCount; r++) {
            bricks[c][r] = { x: 0, y: 0, status: 1 };
        }
    }

    function collisionDetection() {
        for (let c = 0; c < brickColumnCount; c++) {
            for (let r = 0; r < brickRowCount; r++) {
                let b = bricks[c][r];
                if (b.status == 1) {
                    if (x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
                        dy = -dy;
                        b.status = 0;
                        score++;
                        if (score == brickRowCount * brickColumnCount) {
                            alert("YOU WIN, CONGRATULATIONS!");
                            document.location.reload();
                        }
                    }
                }
            }
        }
    }

    function drawBricks() {
        for (let c = 0; c < brickColumnCount; c++) {
            for (let r = 0; r < brickRowCount; r++) {
                if (bricks[c][r].status == 1) {
                    let brickX = (c * (brickWidth + brickPadding)) + brickOffsetLeft;
                    let brickY = (r * (brickHeight + brickPadding)) + brickOffsetTop;
                    bricks[c][r].x = brickX;
                    bricks[c][r].y = brickY;
                    ctx.beginPath();
                    ctx.rect(brickX, brickY, brickWidth, brickHeight);
                    ctx.fillStyle = "#0095DD";
                    ctx.fill();
                    ctx.closePath();
                }
            }
        }
    }

    function drawBall() {
        ctx.beginPath();
        ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
    }

    function drawPaddle() {
        ctx.beginPath();
        ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
    }

    function mouseMoveHandler(e) {
        let relativeX = e.clientX - canvas.offsetLeft;
        if (relativeX > 0 && relativeX < canvas.width) {
            paddleX = relativeX - paddleWidth / 2;
        }
    }

    function draw() {
        if (!gameStarted) return;

        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawBricks();
        drawBall();
        drawPaddle();
        collisionDetection();

        if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
            dx = -dx;
        }
        if (y + dy < ballRadius) {
            dy = -dy;
        } else if (y + dy > canvas.height - ballRadius
        ) {
            if (x > paddleX && x < paddleX + paddleWidth) {
                dy = -dy;
            } else {
                lives--;
                if (!lives) {
                    alert("GAME OVER");
                    document.location.reload();
                } else {
                    gameStarted = false;
                    x = canvas.width / 2;
                    y = canvas.height - 30;
                    dx = 2;
                    dy = -2;
                    paddleX = (canvas.width - paddleWidth) / 2;
                    setTimeout(() => { gameStarted = true; draw(); }, 1000); // 1秒後にゲーム再開
                }
            }
        }

        x += dx;
        y += dy;
        if (gameStarted) {
            requestAnimationFrame(draw);
        }
    }

    document.addEventListener("mousemove", mouseMoveHandler, false);
    canvas.addEventListener("click", function() {
        if (!gameStarted && lives > 0) {
            gameStarted = true;
            draw();
        } else if (lives === 0) { // ゲームオーバーからの再スタート
            score = 0;
            lives = 3;
            for (let c = 0; c < brickColumnCount; c++) {
                for (let r = 0; r < brickRowCount; r++) {
                    bricks[c][r].status = 1;
                }
            }
            x = canvas.width / 2;
            y = canvas.height - 30;
            dx = 2;
            dy = -2;
            paddleX = (canvas.width - paddleWidth) / 2;
            gameStarted = true;
            draw();
        }
    });

    // ゲームの初期化
    draw();
});

このコードは、ゲームの再開始時にマウスクリックを検出して動作を開始し、残機が0になった場合のリスタートロジックを含んでいます。
また、マウスカーソルの位置に基づいてパドルを動かし、ボールとブロックの衝突検知、スコアと残機の管理、そしてボールの動きを制御しています。

ゲームがゲームオーバーになった後、ユーザーが左クリックすることでスコアを0にリセットし、残機を3に設定してゲームを再開します。
また、すべてのブロックを初期状態に戻して新しいゲームセッションを開始します。

3回目の指示で作成した「ブロック崩し」ゲームはこちら

下の画像クリックでゲーム画面が新しいタブで開きます。

操作方法
  • 黒い枠内をクリックするとスタートします。
  • マウスカーソルをゲーム枠より左側に持っていくとパドルが左右に動きます。

あるぇぇ!?なんだか見た目にはかえっておかしくなりました。。。
パドルも動きも変わりません。
ブロックが左上の方にしかありませんね。ただ、ボールはいい感じの小ささです。

さらにもう一度、修正を指示します。

ブロック崩しを作ってもらうための指示(プロンプト) 4回目

次の修正指示をChatGPTに伝えます。

問題点を挙げるので修正してください。
- パドルが動きません。マウス操作で動くようにしてください。
- ブロックを画面横幅いっぱいに並べてください。
- スコアの描画が0のまま更新されません。ブロックを1つ壊すごとにスコア+1で再描画してください。
- 残機の描画が3のまま更新されません。残機が減るたびに残機の表示を再描画してください。
- 変更のない部分も含めjsのソースコード全体をコードブロックでもう一度書いてください

OpenAI ChatGPT CPT-4 の回答 4回目

指摘された問題点を修正した完全なscript.jsのソースコードを以下に示します。
このコードではパドルのマウス操作、ブロックの画面幅いっぱいへの配置、スコアと残機の更新・描画の問題を解決しています。

script.js 4回目

document.addEventListener('DOMContentLoaded', () => {
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    let score = 0;
    let lives = 3;
    let gameStarted = false;

    const ballRadius = 10 * 0.3;
    let x = canvas.width / 2;
    let y = canvas.height - 30;
    let dx = 2;
    let dy = -2;

    const paddleHeight = 10;
    const paddleWidth = 75;
    let paddleX = (canvas.width - paddleWidth) / 2;

    const brickRowCount = 5;
    let brickColumnCount = Math.floor(canvas.width / (75 * 0.2)); // 画面横幅いっぱいに配置
    const brickWidth = canvas.width / brickColumnCount;
    const brickHeight = 20 * 0.2;
    const brickPadding = 0;
    const brickOffsetTop = 30;
    const brickOffsetLeft = 0;
    let bricks = [];

    for (let c = 0; c < brickColumnCount; c++) {
        bricks[c] = [];
        for (let r = 0; r < brickRowCount; r++) {
            const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
            const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
            bricks[c][r] = { x: brickX, y: brickY, status: 1 };
        }
    }

    function drawBricks() {
        for (let c = 0; c < brickColumnCount; c++) {
            for (let r = 0; r < brickRowCount; r++) {
                if (bricks[c][r].status == 1) {
                    ctx.beginPath();
                    ctx.rect(bricks[c][r].x, bricks[c][r].y, brickWidth, brickHeight);
                    ctx.fillStyle = "#0095DD";
                    ctx.fill();
                    ctx.closePath();
                }
            }
        }
    }

    function collisionDetection() {
        for (let c = 0; c < brickColumnCount; c++) {
            for (let r = 0; r < brickRowCount; r++) {
                let b = bricks[c][r];
                if (b.status == 1) {
                    if (x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
                        dy = -dy;
                        b.status = 0;
                        score++;
                        drawScore();
                    }
                }
            }
        }
    }

    function drawBall() {
        ctx.beginPath();
        ctx.arc(x, y, ballRadius, 0, Math.PI*2);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
    }

    function drawPaddle() {
        ctx.beginPath();
        ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
    }

    function drawScore() {
        ctx.font = '16px Arial';
        ctx.fillStyle = '#0095DD';
        ctx.fillText('Score: ' + score, 8, 20);
    }

    function drawLives() {
        ctx.font = '16px Arial';
        ctx.fillStyle = '#0095DD';
        ctx.fillText('Lives: ' + lives, canvas.width - 65, 20);
    }

    function mouseMoveHandler(e) {
        let relativeX = e.clientX - canvas.offsetLeft;
        if(relativeX > 0 && relativeX < canvas.width) {
            paddleX = relativeX - paddleWidth / 2;
        }
    }
    document.addEventListener('mousemove', mouseMoveHandler);

    function draw() {
        if (!gameStarted) return;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawBricks();
        drawBall();
        drawPaddle();
        drawScore();
        drawLives();
        collisionDetection();

        if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
            dx = -dx;
        }
        if(y + dy < ballRadius) {
            dy = -dy;
        } else if(y + dy > canvas.height - ballRadius) {
            if(x > paddleX
            && x < paddleX + paddleWidth) {
                dy = -dy;
            } else {
                lives--;
                drawLives(); // 残機を再描画
                if (!lives) {
                    alert("GAME OVER");
                    document.location.reload(); // ページをリロードしてゲームをリセット
                } else {
                    gameStarted = false;
                    x = canvas.width / 2;
                    y = canvas.height - 30;
                    dx = 2;
                    dy = -2;
                    paddleX = (canvas.width - paddleWidth) / 2;
                    setTimeout(() => { gameStarted = true; draw(); }, 1000); // 1秒後にゲームを再開
                }
            }
        }

        x += dx;
        y += dy;
        if (gameStarted) {
            requestAnimationFrame(draw);
        }
    }

    canvas.addEventListener("click", function() {
        if (!gameStarted) {
            gameStarted = true;
            draw();
        }
    });

    draw(); // ゲームの初期状態を描画
});

この修正では、以下の点を対応しました:

  • パドルの動き: マウス操作によってパドルが動くように、mouseMoveHandler関数を追加し、mousemoveイベントリスナーを設定しています。
  • ブロックの配置: ブロックを画面横幅いっぱいに配置するように、ブロックの列数を動的に計算し、brickWidthを画面幅に応じて調整しています。
  • スコアの再描画: ブロックを壊すたびにスコアを更新し、drawScore関数でスコアを再描画しています。
  • 残機の再描画: 残機が減るたびにdrawLives関数を呼び出して残機を再描画しています。
  • ゲームオーバーと再スタートの処理: 残機が0になったときにゲームオーバーを表示し、ページをリロードすることでゲームをリセットします。また、画面をクリックすることでゲームが再スタートするようにしています。
  • このコードは、指摘された問題点を修正し、ブロック崩しゲームの基本的な機能を提供します。

4回目の指示で作成した「ブロック崩し」ゲームはこちら

下の画像クリックでゲーム画面が新しいタブで開きます。

操作方法
  • 黒い枠内をクリックするとスタートします。
  • マウスカーソルをゲーム枠より左側に持っていくとパドルが左右に動きます。

どうにも、スコア表示と残機がテキストと青いキャンバスの表示2重表示の上、テキストが更新されず、キャンバスの文字は再描画が微妙、マウスもゲーム画面より左の欄外で操作しないといけませんが。。。

  • 一応、パドルが動く
  • ブロックがちゃんと並んでいる
  • ボールが当たるとブロックが崩れる
  • パドルでボールを拾えなかったらアウトになる
  • 3回アウトでゲームオーバー

という最低限「ブロック崩し」といえる動きはしています。
忘れてましたが「タイトル画面」とか「3ステージ」とか最初に書いた指示はガン無視ですねw

この後、何度か修正を試みましたが、残念ながらこれ以上良くはなりませんでした。

小技:ChatGPTに指示をする場合のコツ

長い回答になる場合、途中で止まってしまうことがあります。その場合は

続きを書いて

と言えば、続きを書いてくれます。

なぜか、ChatGPTの回答の途中でコードブロックからソースコードがはみ出たり、中途半端に省略されたコードを答えることがあるのでその時は

script.jsの完成版のソースコード全体をコードブロックに囲って書いて

と言えば書き直してくれます。

まとめ

今回はここまでです。

残念ながら、「あっという間に完璧なゲーム完成」とはいきませんでした。
しかし、わずか数分でここまでのプログラムを生成できるというのは驚きです。

しかも、私自身はプログラムを1文字も書いてません。

プログラム知識が全くない人には少しつらいですが、プログラマーなら「ざっと動くものを作って細部は修正する」という使い方でも十分役に立ちそうです。

また、ChatGPTは一度おかしな回答をし始めるとどう指示をしても修正できなくなり、何度も同じ指摘をすると最後には「もうこれ以上話したくない」みたいな回答になることがあります。

その場合は、そのチャットにはおなくな・・・消えてもらって、新しいチャットで生まれ変わったChatGPTに対して、最初からやり取りする方が早かったりします。

その場合でも、前のチャットで指示を繰り返したことで指示内容(プロンプト)が洗練されているので、前回よりは答えに早くたどり着くことが多いと思います。

次回また、今回よりも洗練された「プロンプト」で挑戦してみたいと思います。

最後までお読みいただきありがとうございました。
次回もお楽しみに!ROBO艦長でした。ではっ!

コメント

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