【AIゲーム開発】ChatGPTで本格シューティングゲームを作ってみた(1日目)

ChatGPTゲームプログラミング 弾幕シューティング 作ってみた(1日目)

ども。ROBO船長です。

今回はChatGPTでシューティングゲームを作ってみたいと思います。

それも、ちょっとシューティングゲームっぽいもの作ってみた、というものではなく、本気で遊んでも楽しめる本格シューティングゲームを作ってみたいと思います。

今回は有料のChatGPT4を使用していますが、無料のChatGPT3.5でもほぼ同じことができます。
ただし、生成されるソースコードの品質や指示に対する気の利き方はChatGPT4の方が当然上になります。

  1. ゲーム内容について
  2. 動作環境、操作性の前提条件
  3. ChatGPTにゲーム作成の最初の指示をする(プロンプト)
  4. ChatGPTの回答(ソースコード)
    1. 1. プロジェクトの構成
    2. 2. HTMLの基本構造 (index.html)
    3. 3. CSSで基本的なスタイルを定義 (style.css)
    4. 4. JavaScriptでゲームのロジックを実装 (game.js)
      1. 4.1. キャンバスの設定と背景のスクロール
      2. 4.2. 自機の表示と操作
  5. ChatGPTに対する修正を指示する
    1. ChatGPTの回答(ソースコード game.js全体)
    2. 画面サイズやキャラクターサイズ、ビーム発射について追加の指示をする(プロンプト)
    3. ChatGPTの回答(ソースコード game.js 更新版)
  6. ChatGPTにエラーを伝える(プロンプト)
    1. ChatGPTの回答(ソースコード game.js エラー対応)
  7. 画面設定、敵の追加(プロンプト)
    1. ChatGPTの回答(ソースコード 画面調整、敵の追加)
      1. 1. ゲーム画面の中央表示とブラウザ操作の無効化
        1. CSSを追加 (style.css)
      2. 2. 敵キャラクターの追加と爆発パーティクル
        1. JavaScript (game.jsの更新部分)
      3. 3. 爆発パーティクルの実装
  8. 敵が表示されないことを修正指示(プロンプト)
    1. ChatGPTの回答(ソースコード 敵の表示に対する修正)
    2. 敵が表示されない原因と修正方法
    3. 敵が表示されない問題の修正例
  9. ChatGPTに対する追加指示まとめ(プロンプト)
  10. ChatGPTのソースコード確認と、自分のソースコードをChatGPTに渡す方法(プロンプト)
  11. 敵の動きのパターンをChatGPTに考えてもらう(プロンプト)
    1. ChatGPTの回答(敵の動きのパターンのアイデア)
      1. Small Enemies Movement Types
      2. Medium Enemies Movement Types
    2. 敵の動きの定義、アイテムの追加、ゲームオーバー表示などについての追加指示(プロンプト)
  12. この時点のソースコード(game07)
      1. index.html(ブラウザでアクセスするページ)
      2. style.css(画面サイズなどのCSS定義)
      3. game.js (ゲームのメインプログラム)
      4. enemySpawnPatterns.js(敵の出現定義の定数宣言)
  13. 1日目 まとめ この時点の実際のシューティングゲーム(実際に遊べる)

ゲーム内容について

次のようなシューティングゲームを作りたいと思います。

  • やや難易度高めの縦スクロール弾幕シューティングゲーム
  • 懐かしのスター〇ルジャーのようなイメージ
  • 様々な動きをする小型の敵
  • たまに登場する中型の強い敵
  • 複数の大型ボス
  • 超大型のラスボス
  • 一部の敵はパワーアップアイテムを落とす
  • パワーアップは複数の種類がある
  • 簡単にはクリアできなけれど、何度もやればぎりぎりクリアできるぐらいの難易度に調整

動作環境、操作性の前提条件

動作環境や操作性は次のようなものにしたいと思います。

  • PCとスマホ両方のブラウザで動くように、JavaScript、HTML、CSSだけで実装する
  • 特別なフレームワークやゲームエンジンは使用しない
  • PCはマウスで操作、スマホはタッチで操作、弾の発射は自動
  • 操作性の快適さを追求

ChatGPTにゲーム作成の最初の指示をする(プロンプト)

最初からいきなり、複雑なゲームの条件をすべて一度に伝えても、なかなか思ったようなゲームはできません。

段階的に指示をするプロンプトを出していきます。
プロンプトは長くなるので、先にテキストエディタなどに整理して書いたものをChatGPTに貼り付けます。

ブラウザで動くシューティングゲームを作りたいと思います。

前提条件
・プロのゲームプログラマーとして考えてください。
・PCとスマートフォンのWEBブラウザ上で動作するようにしてください。
・ソースコードはコードブロックで囲って書いてください。
・ソースコードのフォルダ構成・ファイル構成を提示してください。
・各ソースファイルごとにファイル名が分かるようにしてください。
・HTML、JavaScript、CSSで作成してください。

まずは、自機の表示・操作と画面のスクロールを実現します
・縦スクロール
・キャラクターはPNG画像で指定
・自機はマウス操作、タッチ操作で操作する
 ・自機が指で見えなくならないようにスマホはタッチ地点よりやや上に自機を表示する

ChatGPTの回答(ソースコード)

ブラウザで動く縦スクロールシューティングゲームを作るためには、HTML、CSS、JavaScriptを使って実装するのが一般的です。PCとスマホ両方で遊べるように、レスポンシブデザインとタッチイベントの処理に注意が必要です。以下に基本的な実装方法を示します。

1. プロジェクトの構成

  • index.html:ゲームのフロントエンドを構築します。
  • style.css:ゲームのスタイルを定義します。
  • game.js:ゲームのロジック(自機の操作、背景のスクロール、キャラクターの動き)を実装します。
  • images/:自機や背景などのPNG画像を格納するディレクトリ。

2. HTMLの基本構造 (index.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>シューティングゲーム</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="game-container">
        <!-- ゲームのキャンバスやUIをここに配置 -->
    </div>
    <script src="game.js"></script>
</body>
</html>

3. CSSで基本的なスタイルを定義 (style.css)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>シューティングゲーム</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="game-container">
        <!-- ゲームのキャンバスやUIをここに配置 -->
    </div>
    <script src="game.js"></script>
</body>
</html>

4. JavaScriptでゲームのロジックを実装 (game.js)

  • 自機の表示と操作、背景のスクロール、タッチイベントの処理について基本的なコードを示します。

4.1. キャンバスの設定と背景のスクロール

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
document.getElementById('game-container').appendChild(canvas);

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

let backgroundY = 0; // 背景のY座標
const backgroundImage = new Image();
backgroundImage.src = 'images/background.png'; // 背景画像を設定

function updateBackground() {
    backgroundY += 2; // 背景を下にスクロール
    if (backgroundY >= canvas.height) backgroundY = 0;
    ctx.drawImage(backgroundImage, 0, backgroundY, canvas.width, canvas.height);
    ctx.drawImage(backgroundImage, 0, backgroundY - canvas.height, canvas.width, canvas.height); // 画面外に背景を描画
}

// ゲームループ
function gameLoop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    updateBackground();
    // 他のゲームロジックをここに追加
    requestAnimationFrame(gameLoop);
}

gameLoop();

4.2. 自機の表示と操作

const playerImage = new Image();
playerImage.src = 'images/player.png'; // 自機画像を設定
let playerX = canvas.width / 2;
let playerY = canvas.height / 2;

function updatePlayer(mouseX, mouseY) {
    playerX = mouseX;
    playerY = mouseY - 50; // タッチ地点よりやや上
    ctx.drawImage(playerImage, playerX - playerImage.width / 2, playerY - playerImage.height / 2);
}

canvas.addEventListener('mousemove', (event) => {
    updatePlayer(event.clientX, event.clientY);
});

canvas.addEventListener('touchmove', (event) => {
    const touch = event.touches[0];
    updatePlayer(touch.clientX, touch.clientY);
    event.preventDefault(); // スクロールやズームを防ぐ
},
シューティングゲーム自機

ChatGPTに対する修正を指示する

game.jsを部分的に回答してきましたが、これでは実際のgame.jsの全体像がよくわかりません。
そのため、全体を書いてもらいます。

game.jsを省略せずに全体を書いて

このように言うことで、省略した部分をちゃんと書いてくれるようになります。
それでも、長いソースコードの場合はやはり省略されてしまうことがあるため、そのときは部分ごとに詳細を聞きなおします。

例えば、次のように関数ごとに詳細を聞いたりします。

function gameLoop()を省略せずに書いて

このようなやり取りをして、次のようなソースコードが出力されました。

ChatGPTの回答(ソースコード game.js全体)

ゲームの基本的なフレームワークを完成させるために、game.jsの全体のコードを提供します。このコードは、自機の表示と操作、背景のスクロール、マウスとタッチイベントの処理を含みます。ゲーム開発の基本をカバーしており、これを基に拡張していくことができます。

// キャンバスの設定
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
document.getElementById('game-container').appendChild(canvas);

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// 背景の設定
let backgroundY = 0; // 背景のY座標
const backgroundSpeed = 2; // 背景のスクロールスピード
const backgrounds = ['images/background1.png', 'images/background2.png']; // 背景画像リスト
let currentBackgroundIndex = 0;
const backgroundImage = new Image();

function switchBackground() {
    backgroundImage.src = backgrounds[currentBackgroundIndex++ % backgrounds.length];
}

switchBackground(); // 初期背景をロード

function updateBackground() {
    backgroundY += backgroundSpeed;
    if (backgroundY >= canvas.height) {
        backgroundY = 0;
        switchBackground(); // 背景画像を切り替える
    }
    ctx.drawImage(backgroundImage, 0, backgroundY, canvas.width, canvas.height);
    ctx.drawImage(backgroundImage, 0, backgroundY - canvas.height, canvas.width, canvas.height); // 画面外に背景を描画
}

// 自機の設定
const playerImage = new Image();
playerImage.src = 'images/player.png'; // 自機画像を設定
let playerX = canvas.width / 2;
let playerY = canvas.height - 100; // 初期位置を画面下部に設定

function updatePlayer() {
    ctx.drawImage(playerImage, playerX - playerImage.width / 2, playerY - playerImage.height / 2);
}

// 自機の操作
function handleMouseMove(event) {
    playerX = event.clientX;
    playerY = event.clientY - 50; // タッチ地点よりやや上に自機を表示
}

function handleTouchMove(event) {
    const touch = event.touches[0];
    playerX = touch.clientX;
    playerY = touch.clientY - 50; // タッチ地点よりやや上に自機を表示
    event.preventDefault(); // スクロールやズームを防ぐ
}

canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });

// ゲームループ
function gameLoop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    updateBackground();
    updatePlayer();
    requestAnimationFrame(gameLoop);
}

gameLoop(); // ゲームループを開始

このコードは、縦スクロールする背景と自機の基本的な表示・操作を実装しています。背景画像はリストから連続的に切り替えられるようになっており、自機はマウスやタッチ操作で動かすことができます。タッチ操作時に自機が指に隠れないよう、タッチ地点よりやや上に自機を表示するようにしています。

このスターターキットをベースに、敵キャラクターの追加、攻撃システム、スコアシステムなど、さらに多くの機能を追加していくことが可能です。ゲーム開発は試行錯誤の過程なので、さまざまなアイデアを試しながら楽しんでください。

画面サイズやキャラクターサイズ、ビーム発射について追加の指示をする(プロンプト)

次のように変更して
画面サイズ 幅320、縦420
キャラクターサイズ 32
自動的にビームが連続で発射されるようにして

ChatGPTの回答(ソースコード game.js 更新版)

画面サイズを幅320ピクセル、高さ420ピクセルに設定し、キャラクターサイズを32ピクセルの正方形に変更することに加えて、自機から自動的にビームが連続で発射される機能を追加します。以下のgame.jsのコードを更新して、これらの要件を満たします。

// キャンバスの設定
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
document.getElementById('game-container').appendChild(canvas);

canvas.width = 320; // 画面幅を320に設定
canvas.height = 420; // 画面高さを420に設定

// 背景の設定
let backgroundY = 0;
const backgroundSpeed = 2;
const backgrounds = ['images/background1.png', 'images/background2.png'];
let currentBackgroundIndex = 0;
const backgroundImage = new Image();

function switchBackground() {
    backgroundImage.src = backgrounds[currentBackgroundIndex++ % backgrounds.length];
}

switchBackground();

function updateBackground() {
    backgroundY += backgroundSpeed;
    if (backgroundY >= canvas.height) {
        backgroundY = 0;
        switchBackground();
    }
    ctx.drawImage(backgroundImage, 0, backgroundY, canvas.width, canvas.height);
    ctx.drawImage(backgroundImage, 0, backgroundY - canvas.height, canvas.width, canvas.height);
}

// 自機の設定
const playerImage = new Image();
playerImage.src = 'images/player.png';
let playerX = canvas.width / 2;
let playerY = canvas.height - 50; // 画面の下部に配置

function updatePlayer() {
    ctx.drawImage(playerImage, playerX - 16, playerY - 16, 32, 32); // キャラクターサイズを32に設定
}

// ビームの設定
const beams = [];
const beamImage = new Image();
beamImage.src = 'images/beam.png'; // ビームの画像

function fireBeam() {
    beams.push({ x: playerX, y: playerY - 20 }); // ビームを自機の位置から発射
}

function updateBeams() {
    for (let i = beams.length - 1; i >= 0; i--) {
        beams[i].y -= 10; // ビームを上に動かす
        ctx.drawImage(beamImage, beams[i].x - 16, beams[i].y, 32, 32); // ビームのサイズを調整
        if (beams[i].y < 0) {
            beams.splice(i, 1); // 画面外に出たビームを削除
        }
    }
}

// 自機の操作
canvas.addEventListener('mousemove', (event) => {
    playerX = event.clientX;
    playerY = event.clientY - 50;
});

canvas.addEventListener('touchmove', (event) => {
    const touch = event.touches[0];
    playerX = touch.clientX;
    playerY = touch.clientY - 50;
    event.preventDefault();
}, { passive: false });

// ゲームループ
function gameLoop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    updateBackground();
    updatePlayer();
    updateBeams();
    requestAnimationFrame(gameLoop);
}

setInterval(fireBeam, 200); // 200ミリ秒ごとにビームを発射

gameLoop();

このコードでは、以下の変更を加えました:

  1. 画面サイズ:幅を320ピクセル、高さを420ピクセルに固定。
  2. キャラクターサイズ:自機の表示サイズを32×32ピクセルに設定。
  3. ビームの自動発射:200ミリ秒ごとに自機からビームを発射する機能を追加。ビームは上方向に動き、画面外に出たら自動的に削除されます。

ビームの画像はimages/beam.pngと仮定していますが、実際の画像パスに応じて

ChatGPTにエラーを伝える(プロンプト)

エラーが発生した場合、「このエラーが何か教えて」や「エラーが出たので修正して」などの指示とともにエラーメッセージをコピペすれば対応してくれますが、指示は単に「エラー」と書くだけや、指示なしでエラーメッセージだけ貼り付けてもほとんどの場合は対応してくれます。

ただし、一度エラーメッセージをそのまま貼り付けてうまくいかない場合や、エラーメッセージであることが分かりにくい文章のエラーメッセージの場合は、明確に指示を書いてエラーを貼り付けた方がうまく対応してくれます。

エラー
Failed to load resource: net::ERR_FILE_NOT_FOUND
game.js:54 Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.
    at updateBeams (file:///C:/Users/kazu/Documents/MyWebSite/ai/20240211/GPT4_01/game01/game.js:54:13)
    at gameLoop (file:///C:/Users/kazu/Documents/MyWebSite/ai/20240211/GPT4_01/game01/game.js:79:5)

ChatGPTの回答(ソースコード game.js エラー対応)


エラーメッセージに基づいて、updateBeams関数内でdrawImageを呼び出す際にHTMLImageElementが「壊れた」状態、つまりまだ画像が完全にロードされていない状態であることが原因でエラーが発生しています。JavaScriptで画像を動的に読み込むとき、画像が完全に読み込まれる前にそれを使用しようとするとこのようなエラーが発生します。

この問題を解決するためには、HTMLImageElementonloadイベントを利用して、画像が完全にロードされた後にゲームループを開始するようにする必要があります。同様に、自機やビームの画像が複数ある場合は、すべての画像がロードされたことを確認してからゲームループを開始するようにします。

以下は、画像が正しくロードされた後にゲームループを開始するようにgame.jsを修正した例です。

// キャンバスの設定
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
document.getElementById('game-container').appendChild(canvas);

canvas.width = 320;
canvas.height = 420;

// 背景と自機の画像をロード
const backgroundImage = new Image();
const playerImage = new Image();
const beamImage = new Image();

let imagesLoaded = 0; // ロードされた画像のカウンター
const totalImages = 3; // 全画像の総数

// 画像ロード完了のチェック
function imageLoaded() {
    imagesLoaded++;
    if (imagesLoaded === totalImages) {
        // すべての画像がロードされたらゲームループを開始
        gameLoop();
        setInterval(fireBeam, 200); // 200ミリ秒ごとにビームを発射
    }
}

// 画像のソースを設定し、ロード完了時にカウンターをインクリメント
backgroundImage.src = 'images/background1.png';
backgroundImage.onload = imageLoaded;
playerImage.src = 'images/player.png';
playerImage.onload = imageLoaded;
beamImage.src = 'images/beam.png';
beamImage.onload = imageLoaded;

// 以降のゲームロジック(updateBackground, updatePlayer, handleMouseMove, handleTouchMove, updateBeams, gameLoop)はここに続きます

この修正では、すべての画像(背景、自機、ビーム)に対してonloadイベントハンドラーを設定して、画像が完全にロードされたかを追跡します。すべての画像がロードされたら(imagesLoadedtotalImagesと等しくなったら)、ゲームループを開始し、ビームを定期的に発射するように設定します。

この方法で画像のロード問題を解決し、Uncaught DOMExceptionエラーを防ぐことができます。

画面設定、敵の追加(プロンプト)

ゲーム画面の位置調整、スマホでの拡大縮小の禁止、敵の出現についてさらに指示を出します。
パーティクルとは粒子状のエフェクトを指す用語です。

今回は爆発などのエフェクト表現は画像を使わずにプログラムでパーティクルを描画するようにします。古典的なゲーム開発手法ですがゲームエンジンなどを使わなくても三角関数などを用いたのプログラムの計算式でパーティクルは表現できます。

ゲーム画面をウィンドウの中央に表示してください。
スマホでブラウザのスクロールや拡大縮小ができないようにしてください。
敵1が出てるようにしてください。
敵が破壊されたら爆発のパーティクルを表示

敵1
 出現条件:ランダム
 出現位置:画面上部から現れるが、X軸はゲーム画面幅からはみ出さない範囲内でランダム
 動作:上から下にゆっくり移動
 消滅条件:画面欄外に出る、または爆発
 

当たり判定
 敵にビームが当たったら敵の体力-1
 敵が自機に衝突したら時期が爆発する

ChatGPTの回答(ソースコード 画面調整、敵の追加)

ゲーム画面をウィンドウの中央に表示し、スマホでのブラウザ操作(スクロールや拡大縮小)を無効にする方法、そして敵キャラクターの追加と爆発のパーティクルを表示する機能をgame01コードに組み込む方法について説明します。

1. ゲーム画面の中央表示とブラウザ操作の無効化

CSSを追加 (style.css)
body, html {
    margin: 0;
    padding: 0;
    overflow: hidden; /* スクロールを無効に */
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    touch-action: none; /* スマホでのブラウザ操作を無効に */
}

#game-container {
    width: 320px; /* ゲーム画面の幅 */
    height: 420px; /* ゲーム画面の高さ */
    position: relative;
    overflow: hidden;
}

2. 敵キャラクターの追加と爆発パーティクル

JavaScript (game.jsの更新部分)
// 敵の設定
const enemies = [];
const enemyImage = new Image();
enemyImage.src = 'images/enemy.png'; // 敵キャラクターの画像パスを設定

function spawnEnemy() {
    if (Math.random() < 0.01) { // 1%の確率で敵を出現させる
        const enemy = {
            x: Math.random() * (canvas.width - 32), // 32は敵の幅
            y: -32, // 画面上から出現
            width: 32,
            height: 32,
            health: 1, // 敵の体力
        };
        enemies.push(enemy);
    }
}

function updateEnemies() {
    for (let i = enemies.length - 1; i >= 0; i--) {
        const enemy = enemies[i];
        enemy.y += 1; // 敵を下に移動させる
        
        // 敵とビームの当たり判定
        for (let j = beams.length - 1; j >= 0; j--) {
            const beam = beams[j];
            if (beam.x < enemy.x + enemy.width &&
                beam.x + 32 > enemy.x &&
                beam.y < enemy.y + enemy.height &&
                beam.y + 32 > enemy.y) {
                // 当たり判定
                enemies.splice(i, 1); // 敵を削除
                beams.splice(j, 1); // ビームを削除
                // ここで爆発パーティクルを追加する処理を書く
                break;
            }
        }

        if (enemy.y > canvas.height) {
            enemies.splice(i, 1); // 画面外に出たら削除
        } else {
            ctx.drawImage(enemyImage, enemy.x, enemy.y);
        }
    }
}

// ゲームループ内で敵の更新を呼び出す
function gameLoop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    updateBackground();
    updatePlayer();
    spawnEnemy();
    updateEnemies();
    updateBeams();
    requestAnimationFrame(gameLoop);
}

3. 爆発パーティクルの実装

爆発パーティクルの実装は少し複雑になります。基本的には、敵が破壊されたときにパーティクルオブジェクトの配列を生成し、それらを小さい円としてキャンバス上に描画し、時間と共に消えていくようにアニメーションします。この機能の実装は、上記のコードの基本的な構造に続けて行われるべきです。

爆発パーティクルの実装やその他の機能についての詳細なガイドが必要であれば、さらに具体的な指示を提供できますので、お知らせください。

敵が表示されないことを修正指示(プロンプト)

提示されたソースコードを動かしましたが、敵が表示されません。
単純にそれを伝えます。

敵が表示されません

ChatGPTの回答(ソースコード 敵の表示に対する修正)

敵が表示されない問題を解決するために、game.jsにおいて敵の出現と表示に関するコードを再確認し、必要な修正を加えます。以下のステップを踏んで、敵が正しく画面上に表示されるようにしましょう。

敵が表示されない原因と修正方法

  1. 敵の画像が正しくロードされているか確認する: enemyImage.srcに指定したパスが正しいか、また画像がプロジェクトフォルダ内の正しい位置にあるかを再確認してください。
  2. spawnEnemy関数がゲームループ内で呼び出されているか確認する: gameLoop関数内でspawnEnemyを呼び出して、定期的に敵が生成されるようにしているか確認してください。
  3. 敵の出現確率を調整する: 現在の確率Math.random() < 0.01はかなり低いため、敵が出現する頻度が非常に少ない可能性があります。開発段階ではこの確率を上げて、敵が確実に出現するかテストしてみてください。
  4. コンソールでエラーを確認する: ブラウザの開発者ツールを開き、コンソールにエラーメッセージが表示されていないか確認してください。JavaScriptのエラーは、問題の原因を特定する手がかりになります。
  5. 敵の描画コードを確認する: ctx.drawImage(enemyImage, enemy.x, enemy.y); が正しく実行されているか、またenemy.xenemy.yの値がキャンバスの表示範囲内にあるかを確認してください。

敵が表示されない問題の修正例

以下のコード例では、敵の出現確率を一時的に高めて、問題の解決を試みます。

function spawnEnemy() {
    if (Math.random() < 0.5) { // 出現確率を高める(テスト用)
        const enemy = {
            x: Math.random() * (canvas.width - 32), // 敵がはみ出さないように調整
            y: -32, // 画面上部から出現
            width: 32,
            height: 32,
            health: 1,
        };
        enemies.push(enemy);
    }
}

// ゲームループ内でこの関数を呼び出す
function gameLoop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    updateBackground();
    updatePlayer();
    spawnEnemy(); // 敵を生成する関数を呼び出す
    updateEnemies(); // 敵を更新・表示する関数を呼び出す
    updateBeams();
    requestAnimationFrame(gameLoop);
}

この変更を加えた後、敵が画面に表示されるかどうかを確認してください。敵の画像が正しくロードされているか、またはその他のコードに誤りがないかを再度チェックしてください。敵が正しく表示されない場合は、更に詳細なデバッグが必要になるかもしれません。

ChatGPTに対する追加指示まとめ(プロンプト)

この後、次のような指示を出しています。
どのようなプロンプトを流しているのかの参考までに記載しますが、全部読む必要はありません。
※実際は一度に流しているのではなく、行間ごとにChatGPTの回答がありますがそれは省略しています。

敵のサイズを32、自機のサイズを64にしてください

ビームのサイズも64に変更し、自機が64になったことに合わせてビームの出現位置を調整

ビームは幅32、高さ64にして
自機に重ならないようにもう少し上の位置から発射して

ゲーム画面の位置をウィンドウの中心にしたことにより、
ゲームキャラクターの表示位置にずれが生じていますので修正してください

ビームが敵を素通りするようになりました。ちゃんと当たるようにしてください。
また、自機と敵が当たったら自機の耐久値を-1してください。
自機のスタート時の耐久値は5です。
耐久値が0になったら自機を爆発させてください。
自機の爆発アニメーションが終わったらゲームオーバーです。
画面中央に「GAME OVER」と表示してください。

自機と敵の当たり判定の位置が見た目とずれています。
見た目よりも右側の方で当たり判定が発生しているようですので修正してください。
また、実際に自機がダメージを受ける当たり判定の範囲は自機の見た目の中央から半径16の範囲にしてください

自機の当たり判定の位置を確認するため、当たり判定と同じ大きさの円を描画してください

当たり判定が自機の右下に大きくズレていました。自機の見た目の中央になるように調整してください。

円の中心位置と自機の中心は一致しましたが、当たり判定は右下にあります。
円の描画位置に当たり判定の位置を合わせてください。

自機の当たり判定の位置が正しくなりましたので、これが正しいと覚えておいてください。
また、爆発のパーティクルを完成させてください。
敵の爆発は赤っぽい色、自機の爆発は青っぽい色にしてください。

パーティクルの修正
敵の爆発パーティクルの範囲は敵の大きさと同じぐらいにして、色を水色っぱい色に変更し、透明度を少し上げて

自機の爆発パーティクルはオレンジや赤っぽい色で自機と同じぐらいの大きさにして。

自機が爆発したときはパーティクルの描画が終わってからゲームオーバーの表示にして

playerHealthが0になった時点でビームの発射を停止、自機の移動も停止して、自機を非表示にしてください。
また、自機の爆発パーティクルは一度だけ表示してください。

自機の操作ができなくなったので前のように操作できるようにして
スマホで表示する際は、画面の幅に合わせて拡大して

スマホでゲーム画面が小さいため、画面幅に合わせて拡大してください。
CSSやHTMLで対応できるならそれを優先してください。

スマートフォンで画面サイズが小さいまま変わりません。
また、画面の縦横中央ではなく、画面上部の左右中央に表示されるようになりました。
画面縦横中央にゲーム画面が表示され、画面幅に合わせて拡大するようにしてください。

複数の種類の敵を扱いたい。
具体的には小型敵8種類(大きさ32)、中型敵2種類(大きさ64)、大型敵2種類(大きさ128)、
超大型敵1種類(大きさ256)をそれぞれ複数扱いたい。
種類ごとに違う動きをさせたい。
また、敵の出現パターンをランダムではなく、規則的にしたい。
どの敵がいつどこから現れるか定義できるようにしたい。
どのようにすればよいか?

const enemyTypesの定義を完成させて

ChatGPTのソースコード確認と、自分のソースコードをChatGPTに渡す方法(プロンプト)

色々と修正指示をだしていると部分的なソースコードをやり取りしているうちに全体像がよくわからなくなる場合があります。

その場合は、ChatGPTが内部でどのようなソースコードを持っている状態なのか確認するため、次のように指示します。

ここまでのgame.jsを省略せずにすべて書いて

逆に、自分でソースコードを直した場合、その内容をChatGPTに伝えるため、下記のようにソースコードを渡すこともできます。

その際に「game02」などソースコードの世代に名前を付けることで、後から「game02の状態に戻して」などといった指示も可能です。

下記の game.js をgame02 としてください

(ここにgame.jsのソースコードを貼り付ける)

敵の動きのパターンをChatGPTに考えてもらう(プロンプト)

ソースコードを修正してもらうだけではなく、ゲームのアイデア自体を相談することも当然できますので、敵の動きのパターンのアイデアを出してもらうことにします。

smallとmediumのmovementの種類を8種類考えて

ChatGPTの回答(敵の動きのパターンのアイデア)

小型(small)敵と中型(medium)敵の動き(movement)の種類を8種類ずつ考えると、ゲーム内で敵キャラクターが多様な行動パターンを示すことができます。ここでは、それぞれのカテゴリーについて異なる動きのパターンを提案します。

Small Enemies Movement Types

  1. Linear: 直線的に進む。
  2. Zigzag: ジグザグに進む。
  3. SineWave: 正弦波のような動きで進む。
  4. Dive: 一定距離進んだ後、急降下する。
  5. Looping: ループや円を描くように動く。
  6. Random: ランダムな方向に小刻みに動く。
  7. Chase: プレイヤーの位置を追跡する。
  8. Retreat: プレイヤーに近づいた後、逆方向に退避する。
小型敵1
小型敵2
小型敵3
小型敵4
小型敵5
小型敵6
小型敵7
小型敵8

Medium Enemies Movement Types

  1. Circular: 円運動をする。
  2. Spiral: スパイラル状に進む。
  3. BackAndForth: 往復する動きをする。
  4. HorizontalSweep: 水平に大きく移動しながら進む。
  5. VerticalSweep: 垂直に大きく移動しながら進む。
  6. Bounce: 壁や画面端で跳ね返るように動く。
  7. Teleport: 短い間隔でランダムな位置にテレポートする。
  8. Hover: 一定の位置でホバリングし、突然速く動き出す。
中型敵1
中型敵2

これらの動きは、敵キャラクターの挙動を多様化し、ゲームプレイに深みを加えるためのものです。各動きは敵のタイプやゲームの難易度、プレイヤーの進行状況に応じて選択されることが想定されています。実装時には、これらの動きを具体的なアルゴリズムや関数としてゲームのコードに組み込む必要があります。

※中型は2種類にしたため、2と6のみ採用
※画像はChatGPTの回答ではなく、ブログ記事上のイメージです。

※実際のキャラクター画像は Bing Copilot で作成しています。

敵の動きの定義、アイテムの追加、ゲームオーバー表示などについての追加指示(プロンプト)

さらにこの後、次のような指示を出しています。
どのようなプロンプトを流しているのかの参考までに記載しますが、全部読む必要はありません。
※実際は一度に流しているのではなく、行間ごとにChatGPTの回答がありますがそれは省略しています。

largeはボスのため、それぞれ独自の動きとします。
また、extraLargeはラスボスのため、さらに複雑な動きとします。それらを定義できるようにしてください。

const enemyTypesの定義を小型8種、中型2種、大型ボス2種、超大型ラスボス1種で完成させてください

movementのパターンを加味して敵の生成と動きの実装を完成させてください

function updateEnemiesをmovementの全パターン完成させてください

circularとspiralも定義してください

敵の出現位置は画面の横幅を5等分して左から1,2,3,4,5で指定できるようにして
敵のサイズは S,M,L,LL de
また、敵の出現パターンを次の配列で定義できるようにして

敵のタイプは s,m,l,x で指定できるようにして
敵の種類はenemyTypesのインデックスで指定できるようにして
敵の出現位置は画面の横幅を5分割してx軸の40,100,160,220,280を1,2,3,4,5で指定できるようにして
敵の出現パターンを次の配列で定義できるようにして
 出現時間(ゲーム開始からの秒数),敵のタイプ,敵の種類のインデックス,出現位置
 例(ゲーム開始から5秒後に小型種類3の敵が画面左から2番目の出現位置x:100より出現)
 5,s,3,2

敵は表示されるようになりましたが、ビームが当たらずすり抜けます。自機と敵も衝突しないようです。

パーティクルの処理が抜けていませんか?

敵の種類ごとに耐久値を設定して。
ビーム1発で爆発する敵もいれば、10発当てないと爆発しない丈夫な敵などを設定できるように。

ゲームオーバーの時にタップ(クリック)で最初からゲームをスタートして。
その際にすべての値の初期化とタイマーの初期化を確実に。

ゲームオーバーから再スタートすると敵が出なくなることがあります

自機の当たり判定が想定よりも大きい気がします。
デバッグ用に当たり判定の円を描画して、円の範囲で当たり判定が発生するようにしてください

switch (enemy.movement)に前に提案してくれた敵の動きの下記パターン の動きを実装した case を書いて
	Circular: 円運動をする。
	Spiral: スパイラル状に進む。
	BackAndForth: 往復する動きをする。
	HorizontalSweep: 水平に大きく移動しながら進む。
	VerticalSweep: 垂直に大きく移動しながら進む。
	Bounce: 壁や画面端で跳ね返るように動く。
	Teleport: 短い間隔でランダムな位置にテレポートする。
	Hover: 一定の位置でホバリングし、突然速く動き出す。

この動きは退避するはずなのに退避せずにその場にとどまってしまいます
 一定距離まで直線的に移動したら退避するように修正してください
            case 'retreat': // 8
                if (enemy.y < 100) {
                    enemy.y += 3; // 一定距離まで直線的に移動
                } else {
                    enemy.y -= 3; // 退避
                }
                break;

以下の3つの動きがよく似ているのでもっとそれぞれに特徴を出して

            case 'zigzag':  // 2 ジグザグに進む。
                enemy.x += Math.sin(enemy.y / 20) * 5; // ジグザグに移動
                enemy.y += 5;
                break;
                
            case 'sineWave':    // 3 正弦波のような動きで進む。
                enemy.x += Math.sin(enemy.y / 20) * 10; // 正弦波のような動き
                enemy.y += 3;
                break;
                
            case 'looping': // 5 ループや円を描くように動く。
                enemy.x += Math.sin(enemy.y / 10) * 15; // ループを描く
                enemy.y += 1.5;
                break;

敵の出現定義enemySpawnPatternsを別ファイルに分けて

自機の当たり判定の半径を半分にしてください
当たり判定の円を描画(デバッグ用)も当たり判定に合わせて修正してください

ビームは画像を使うのをやめてパーティクルのように演算でアニメーションするようにしてください。

既存のupdateBeamsとgameLoopを考慮してください
また、ビームの発射についてはこれまで通り一定間隔でビームを発射します。

エラーです
game.js:217 Uncaught SyntaxError: Identifier 'fireBeam' has already been declared

先ほどのエラーは消えましたが別のエラーです
game.js:288 Uncaught SyntaxError: Identifier 'updateBeams' has already been declared (at game.js:288:1)

updateBeamsは古い関数が残っていたので消したことでエラーは解消されました。
背景も画像を使わずに表現できるようにしたいと思います。
大小さまざまな星が多重スクロールで上から下に流れているように見える表現を演算でできますか?
基本的には黒い宇宙空間に白っぽい星が流れるイメージですが、近くの星と遠くの星で流れる速さが違ったり、点よりも線にみえたり、遠い方が薄く見えるといった感じにしてください。

逆にこれまでのコードで背景画像を使っている部分で不要になり削除する必要がある箇所を教えて

星は点ではなく流れ星のような感じにして、全体的に色を暗くしてください

流れ星は前の点のままのロジックにして、色と数を目立ちすぎないように調整しました。

enemySpawnPatternsでパワーアップアイテムを落とすかどうかを設定できるようにしたいと思います。


const enemySpawnPatterns = [
    { time: 1, type: 'small', variant: 1, position: 1, item: 1 }, // 例
    // 以下、必要に応じてさらに敵の出現パターンを追加
];

パワーアップアイテムには画像を設定します。
パワーアップアイテムは2種類ありますので、アイテムを出す敵は1か2を指定します。出さない敵は0を指定します。
パワーアップアイテムを落とす敵を倒した場合、敵がいた場所にアイテムが表示され、ゆっくり下降します。
画面外に出たら消えます。
自機でアイテムに触れるとパワーアップアイテムを取得できます。
パワーアップアイテムを取るとどちらのアイテムでもplayerHealthが1回復します。

パワーアップの効果については後で定義しますので、まずはアイテムの定義、出現、取得ができるようにしてください。

アイテムが表示されません。
enemySpawnPatterns の定義は下記のようにしています。

// 敵の出現パターンの定義
const enemySpawnPatterns = [
    { time: 1, type: 'small', variant: 1, position: 1, item: 1 },
    { time: 2, type: 'small', variant: 1, position: 2, item: 2 },
    { time: 3, type: 'small', variant: 1, position: 3, item: 1 },
    { time: 4, type: 'small', variant: 1, position: 4, item: 2 },
    { time: 5, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 8, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 9, type: 'small', variant: 1, position: 4, item: 0 },
    // 以下、必要に応じてさらに敵の出現パターンを追加
];

現在のupdateEnemiesを共有します。これをもとに修正してください
(略 updateEnemies関数のソースコード)

エラーは解消されたがアイテムが出ません

現在のgame.jsを共有しますのでこれをもとに修正してください
(略 game.js全体のソースコード)

指摘箇所を修正しましたが、やはりアイテムが表示されません。コンソールにエラーはありません。

Updatingログは表示されますがずっと Updating 0 power-up items です
Dropping item of type は一度も表示されません

ここに問題がありそうですが修正できますか

        // 敵が画面外に出たか、耐久値が0以下になった場合
        if (!enemy.alive || enemy.y > canvas.height + enemy.size || enemy.health <= 0) {
            if (enemy.health <= 0) {
                // アイテムをドロップする処理
                dropItem(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, enemy.itemType);
            }
            // 敵を配列から削除
            enemies.splice(index, 1);
        }

敵を倒しても if (enemy.item) { // enemy.item が設定されているかチェック 
に設定したブレークポイントに進みません

itemプロパティは正しく設定されているようですが、敵を倒しても if (enemy.health <= 0) { の条件が成立していないようです。

おそらく updateBeams で下記のロジックがあるため、先に敵が消えていると思われます
                if (enemy.health <= 0) {
                    enemies.splice(j, 1); // 敵を削除
                    createParticles(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, "0, 255, 255", 2, 3); // 敵破壊時のパーティクル
                }

両方のfunctionでif (enemy.health <= 0)を判定して敵を削除しているため、
おそらくupdateEnemiesの方は働いていないと思われます。整理してください。

updateBeamsでenemy.itemがundefinedになっています

アイテムの当たり判定を画像のサイズに合わせてください

パワーアップアイテムを取得したら白いパーティクルを表示してください。
また、敵が自機にぶつかったとき、2秒間敵との当たり判定をOFF(無敵)にしてください。無敵のあいだは自機を点滅させてください。

無敵点滅の点滅速度を速くして

パワーアップアイテムを取ってもplayerHealthの最大は5までとしてください。

自機に敵が当たっても敵が消えないようにしてください。
また、自機との衝突で敵にダメージも入りません。

playerHealthが0になったら、自機を消し、ビームの発射を止めてください。
それから2秒経過後に GAME OVER と表示して全体を停止してください。

GAMEOVERの時にちゃんと自機を消して自機の衝突パーティクルも表示したいのですが、自機が消える前にゲームが止まってしまいます。
きちんと自機が消え、自機爆発パーティクルが表示されてからゲームを停止するにはどうすればよいか?

今は下記のようになっているがsetupGameOverListenerは使用しないでよいのか?

    if (gameOver) {
        showGameOver();
        setupGameOverListener(); // ゲームオーバーリスナーを設定
        return;
    }


今のアドバイスとは異なる修正をしてGAMEOVER時にちゃんと自機が爆発するアニメーションが表示されるようにしました。
現時点のgame.jsのソースコードを共有します。これをgame07とします。

この時点のソースコード(game07)

この時点でのソースコードは次の状態でした。


尚、敵の種類や出現タイミングを定義した定数の宣言(enemySpawnPatterns)が長くなったため、別ファイルに分けました。

index.html(ブラウザでアクセスするページ)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>シンプルシューティングゲーム</title>
    <link rel="stylesheet" href="style.css">
    <link rel="icon" type="image/png" href="images/player.png">
</head>
<body>
    <div id="game-container"></div>
    <script type="module" src="game.js"></script>
</body>
</html>

style.css(画面サイズなどのCSS定義)

html, body {
    height: 100%;
    margin: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: #666; /* 背景色を設定、必要に応じて変更してください */
}

#game-container {
    width: 100%; /* コンテナの幅を画面幅に合わせる */
    max-width: 320px; /* キャンバスの最大幅を指定 */
    height: auto; /* 高さを自動調整 */
    aspect-ratio: 320 / 420; /* キャンバスのアスペクト比を維持 */
}

#game-container canvas {
    width: 100%; /* キャンバスの幅をコンテナの幅に合わせる */
    height: auto; /* 高さを自動調整してアスペクト比を維持 */
    image-rendering: pixelated; /* ピクセルの境界がはっきりするように */
}

game.js (ゲームのメインプログラム)

// キャンバスの設定
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
document.getElementById('game-container').appendChild(canvas);

canvas.width = 320;
canvas.height = 420;

let currentTime = 0; // ゲーム開始からの経過時間
let invincible = false; // 無敵状態のフラグ
let invincibleTime = 0; // 無敵時間のカウンタ

let gameOverWaitTime = 0.5; // 自機の耐久が0になってからゲームオーバー表示までの待機時間
let playerDeadTime = 0; // 自機の耐久が0になった時間(ゲーム開始からの経過時間を格納)

/* // 背景の設定
let backgroundY = 0;
const backgroundSpeed = 2;
const backgrounds = ['images/background1.png', 'images/background2.png'];
let currentBackgroundIndex = 0;
const backgroundImage = new Image(); */

// 星の宣言
const stars = [];
const starCount = 30; // 生成する星の数

// 星のデータを生成する
function createStars() {
    for (let i = 0; i < starCount; i++) {
        stars.push({
            x: Math.random() * canvas.width, // 星の初期X座標
            y: Math.random() * canvas.height, // 星の初期Y座標
            size: Math.random() * 3, // 星の大きさ(1pxから3px)
            speed: Math.random() * 5 + 3, // 星の速度(3から5の範囲でランダム)
            opacity: Math.random() * 0.1 + 0.5, // 星の透明度(0.1から0.6の範囲でランダム)
        });
    }
}

// 星を描画し、位置を更新する
function updateStars() {
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height); // 背景を黒で塗りつぶす

    stars.forEach(star => {
        ctx.beginPath();
        ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
        ctx.closePath();

        // 星の透明度と大きさで遠近感を表現
        ctx.fillStyle = `rgba(128, 128, 128, ${star.opacity})`;
        ctx.fill();

        // 星を下に移動させる
        star.y += star.speed;

        // 星が画面下端を超えたら上に戻す
        if (star.y > canvas.height) {
            star.y = 0;
            star.x = Math.random() * canvas.width;
        }
    });
}

// パワーアップアイテムの画像パス
const itemTypes = {
    1: 'images/powerup_1.png',
    2: 'images/powerup_2.png',
};

console.log(itemTypes); // itemTypes オブジェクトの内容を確認

// パワーアップアイテムのインスタンスを格納する配列
const powerUpItems = [];

// パワーアップアイテムの出現
function dropItem(x, y, itemType) {
//    console.log(`dropItem called with itemType: ${itemType}`); // itemType の値を確認
    if (itemType !== 0 && itemTypes[itemType]) { // アイテムをドロップする条件をチェック
        const itemSize = 32; // アイテムのサイズ
        powerUpItems.push({
            x: x - itemSize / 2, // アイテムの中心軸を調整
            y: y - itemSize / 2, // アイテムの中心軸を調整
            type: itemType,
            image: loadedImages[itemTypes[itemType]], // 事前にロードした画像
            speed: 2, // アイテムの下降速度
        });
    }
}

// パワーアップアイテムの描画と更新
function updatePowerUpItems() {
    console.log(`Updating ${powerUpItems.length} power-up items`); // アイテム更新時のログ
    for (let i = powerUpItems.length - 1; i >= 0; i--) {
        const item = powerUpItems[i];
        item.y += item.speed; // アイテムを下に移動

        // アイテムを描画(サイズを32x32に設定)
        ctx.drawImage(item.image, item.x, item.y, 32, 32);

        // 画面外に出たら配列から削除
        if (item.y > canvas.height) {
            powerUpItems.splice(i, 1);
        }
    }
}

// パワーアップアイテムの取得
function checkItemCollection() {
    for (let i = powerUpItems.length - 1; i >= 0; i--) {
        const item = powerUpItems[i];
        if (playerX < item.x + 32 &&
            playerX + playerImage.width > item.x &&
            playerY < item.y + 32 &&
            playerY + playerImage.height > item.y) {
            // アイテムを取得
            if (playerHealth < 5) {
                playerHealth += 1; // 健康回復
            }
            createParticles(item.x + 16, item.y + 16, '255, 255, 255', 2, 2); // 白いパーティクルを生成
            powerUpItems.splice(i, 1); // アイテムを削除
        }
    }
}



// 自機の設定
const playerImage = new Image();
playerImage.src = 'images/player.png';
let playerX = canvas.width / 2;
let playerY = canvas.height - 50;

// 自機の耐久値を設定
let playerHealth = 5;
let gameOver = false; // ゲームオーバーフラグ

// ビームの設定
const beams = [];

// ビーム発射のフラグ
let firing = true;

// ビームを発射する関数
function fireBeam() {
    // ビームが発射されている状態であれば、ビームを追加
    if (firing) {
        beams.push({
            x: playerX, // 自機の位置から発射
            y: playerY - 20, // 自機の上部から発射
            width: 4, // ビームの幅
            height: 20, // ビームの高さ
            speed: 10, // ビームの速度
        });
    }
}

// 自機の表示フラグ
let playerVisible = true;

// パーティクル配列
const particles = [];

// 画像のプリロードを管理するためのオブジェクト
const loadedImages = {};

const spawnPositions = [40, 100, 160, 220, 280]; // 画面の横幅を5分割したX座標


// 敵の出現パターンの定義をインポート
import { enemySpawnPatterns } from './enemySpawnPatterns.js';

// 敵キャラの定義
const enemyTypes = {
    small: [
        { size: 32, imageSrc: 'images/enemy_small_1.png', movement: 'linear', health: 1 },
        { size: 32, imageSrc: 'images/enemy_small_2.png', movement: 'zigzag', health: 2 },
        { size: 32, imageSrc: 'images/enemy_small_3.png', movement: 'verticalSweep', health: 3 },
        { size: 32, imageSrc: 'images/enemy_small_4.png', movement: 'dive', health: 3 },
        { size: 32, imageSrc: 'images/enemy_small_5.png', movement: 'verticalSweep', health: 5 },
        { size: 32, imageSrc: 'images/enemy_small_6.png', movement: 'backAndForth', health: 8 },
        { size: 32, imageSrc: 'images/enemy_small_7.png', movement: 'chase', health: 5 },
        { size: 32, imageSrc: 'images/enemy_small_8.png', movement: 'retreat', health: 5 }
    ],
    medium: [
        { size: 64, imageSrc: 'images/enemy_medium_1.png', movement: 'bounce', health: 20 },
        { size: 64, imageSrc: 'images/enemy_medium_2.png', movement: 'spiral', health: 15 }
    ],
    large: [
        {
            size: 128,
            imageSrc: 'images/enemy_large_boss_1.png',
            movement: 'bossPattern1',
            behavior: function() {
                // 大型ボス1の挙動
            }
        },
        {
            size: 128,
            imageSrc: 'images/enemy_large_boss_2.png',
            movement: 'bossPattern2',
            behavior: function() {
                // 大型ボス2の挙動
            }
        }
    ],
    extraLarge: [
        {
            size: 256,
            imageSrc: 'images/enemy_extra_large_boss.png',
            movement: 'finalBossPattern',
            behavior: function() {
                // 超大型ラスボスの挙動
            }
        }
    ]
};

let enemies = []; // 敵のインスタンスを保持する配列

// ゲームの初期化
function initializeGame() {
    currentTime = 0;
    playerHealth = 5;
    gameOver = false;
    firing = true;
    playerVisible = true;
    playerX = canvas.width / 2;
    playerY = canvas.height - 50;

    // constで宣言された配列を空にする
    beams.length = 0;
    enemies.length = 0;
    particles.length = 0;
}

// ゲームオーバー時のイベントリスナー追加
function setupGameOverListener() {
    canvas.addEventListener('click', function restartGame() {
        if (gameOver) {
            initializeGame();
            // ゲームループを再開する
            //requestAnimationFrame(gameLoop);
            // このリスナーが再度実行されることを防ぐためにリスナーを削除
            canvas.removeEventListener('click', restartGame);
        }
    });
}

// パーティクルの生成関数(色、透明度、範囲をパラメータ化)
function createParticles(x, y, color, velocityRange, size) {
    const particleCount = 30; // 生成するパーティクルの数
    for (let i = 0; i < particleCount; i++) {
        particles.push({
            x: x,
            y: y,
            velocityX: (Math.random() - 0.5) * velocityRange, // X方向の速度
            velocityY: (Math.random() - 0.5) * velocityRange, // Y方向の速度
            color: color,
            size: size,
            life: Math.random() * 0.5 + 0.5, // パーティクルの生存期間(0.5から1の間でランダム)
        });
    }
}

// パーティクルの更新
function updateParticles() {
    for (let i = particles.length - 1; i >= 0; i--) {
        const p = particles[i];
        p.x += p.velocityX;
        p.y += p.velocityY;
        p.life -= 0.02; // パーティクルの生存期間を減少させる

        if (p.life <= 0) {
            particles.splice(i, 1); // パーティクルの寿命が尽きたら削除
        }
    }
}

// パーティクルの描画
function drawParticles() {
    particles.forEach(p => {
        ctx.fillStyle = `rgba(${p.color},${p.life})`;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
        ctx.fill();
    });
}

function updatePlayer() {
    // 自機の画像を描画
    // 自機のサイズが64x64ピクセルであると仮定して、playerX, playerYは自機の中心座標を表すようにします。

    if (playerVisible) {
        if (!invincible || Math.floor(Date.now() / 50) % 2) { // 無敵状態で点滅
            ctx.drawImage(playerImage, playerX - 32, playerY - 32, 64, 64);
        }
    }

    // 当たり判定の円を描画(デバッグ用)
    // 自機の見た目の中央に円が来るように、playerX, playerYを直接使用します。
/*     ctx.beginPath();
    ctx.arc(playerX, playerY, 8, 0, Math.PI * 2);
    ctx.strokeStyle = 'red';
    ctx.stroke(); */
}

// ビームと敵の当たり判定
function updateBeams() {
    for (let i = beams.length - 1; i >= 0; i--) {
        const beam = beams[i];
        beam.y -= beam.speed; // ビームを上に移動させる

        // ビームを描画
        ctx.fillStyle = '#ff0'; // ビームの色
        ctx.fillRect(beam.x - beam.width / 2, beam.y, beam.width, beam.height); // ビームの描画

        for (let j = enemies.length - 1; j >= 0; j--) {
            const enemy = enemies[j];
            // 当たり判定
            if (beam.x < enemy.x + enemy.size && beam.x + beam.width > enemy.x &&
                beam.y < enemy.y + enemy.size && beam.y + beam.height > enemy.y) {
                enemy.health -= 1; // 敵の耐久値を減少
                if (enemy.health <= 0) {
                    // itemTypeがundefinedではない、かつ0より大きい場合にアイテムをドロップする
                    if (enemy.itemType && enemy.itemType > 0) {
                        dropItem(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, enemy.itemType);
                    }
                    enemies.splice(j, 1); // 敵を削除
                    createParticles(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, "0, 255, 255", 2, 3); // 敵破壊時のパーティクル
                }
                beams.splice(i, 1); // ビームを削除
                break;
            }
        }

        // 画面外に出たビームを配列から削除
        if (beam.y < 0) {
            beams.splice(i, 1);
        }
    }
}

// 自機と敵の円形当たり判定
function checkPlayerCollision() {
    for (let i = enemies.length - 1; i >= 0; i--) {
        const enemy = enemies[i];
        const dx = enemy.x + enemy.size / 2 - playerX; // 敵の中心から自機の中心までのX距離
        const dy = enemy.y + enemy.size / 2 - playerY; // 敵の中心から自機の中心までのY距離
        const distance = Math.sqrt(dx * dx + dy * dy); // 中心点間の距離

        // 当たり判定: 自機の当たり判定の円(半径8ピクセル)と敵の中心との距離を比較
        if (distance < 8 + enemy.size / 2 && !invincible) { // 無敵状態ではないときのみ判定
            invincible = true; // 無敵状態にする
            // 当たり判定が成立した場合、処理を実行
            playerHealth -= 1;

            if(playerHealth > 0){
                // プレイヤーが生きているときの衝突用
                createParticles(playerX, playerY, "255,165,0", 3, 3); // 自機の衝突パーティクル(オレンジ)を生成
            }
            // 自機と衝突しても敵は消えない
//            enemies.splice(i, 1); // 敵を削除

            if (playerHealth <= 0) {
                if(playerDeadTime == 0){
                    playerDeadTime = currentTime;   // プレイヤーがやられた時間を保存
                    firing = false;
                    playerVisible = false;
                    createParticles(playerX, playerY, "255,0,0", 5, 8); // 自機の爆発パーティクル(赤)を生成
                }
                if(playerDeadTime + gameOverWaitTime < currentTime){
                    gameOver = true;
                }
            }
            break; // 重複判定を避けるためにループから抜ける
        }
    }
}



function spawnEnemies() {
    enemySpawnPatterns.forEach((pattern, index) => {
        if (currentTime >= pattern.time) {
            // 敵のタイプとバリアントから適切な敵を選択
            const typeArray = enemyTypes[pattern.type];
            if (typeArray && typeArray.length > pattern.variant - 1) {
                const enemyDef = typeArray[pattern.variant - 1];
                const newEnemy = {
                    ...enemyDef,
                    x: spawnPositions[pattern.position - 1] - enemyDef.size / 2,
                    y: -enemyDef.size,
                    alive: true,
                    health: enemyDef.health, // 耐久値を設定
                    itemType: pattern.item // ここで itemType を設定
                };
                enemies.push(newEnemy);
            }
            // 出現した敵はスケジュールから削除
            enemySpawnPatterns.splice(index, 1);
        }
    });
}


function updateEnemies(deltaTime) {
    enemies.forEach((enemy, index) => {
        switch (enemy.movement) {
            case 'linear':  // 1 直線的に進む。
                enemy.y += 4; // 直線的に下に移動
                break;

            case 'zigzag':  // ジグザグに進む。
                enemy.x += Math.cos(enemy.y / 15) * (5 + Math.sin(enemy.y / 50) * 5); // 振幅変動を加えたジグザグ
                enemy.y += 1; // やや遅い下降を維持
                break;

            case 'sineWave':    // 正弦波のような動きで進む。
                enemy.x += Math.sin(enemy.y / 10) * 5; // 振幅を大きくして、波の形を強調
                enemy.y += 6; // やや速い下降を維持
                break;
            

            case 'dive':    // 4 一定距離進んだ後、急降下する。
                if (enemy.y < 100) {
                    enemy.y += 2; // 一定距離まで直線的に移動
                } else {
                    enemy.y += 8; // 急降下
                }
                break;

            case 'looping': // ループや円を描くように動く。
                if (!enemy.loopCounter) enemy.loopCounter = 0; // ループカウンタの初期化
                enemy.loopCounter += 0.1; // カウンタをインクリメント
                enemy.x += Math.cos(enemy.loopCounter) * 5; // x軸方向の動きを円形に近づける
                enemy.y += Math.sin(enemy.loopCounter) * 5 + 2; // y軸はゆるやかに下降
                break;
            

            case 'random':  // 6 ランダムな方向に小刻みに動く。
                enemy.x += (Math.random() - 0.3) * 10; // ランダムに左右に動く
                enemy.y += 2;
                break;
            case 'chase':   // 7 プレイヤーの位置を追跡する。
                // ここではplayerXを追跡する簡単な例を示します。
                // 実際にはプレイヤーの位置をより正確に追跡するためのロジックが必要です。
                enemy.x += (playerX - enemy.x) * 0.02;
                enemy.y += 2;
                break;

            case 'retreat': // 8 一定距離まで直線的に下降移動した後、逆方向に退避する。
                // 敵の動きの状態を管理するフラグが未設定の場合、初期化する
                if (enemy.retreatState === undefined) {
                    enemy.retreatState = 'advancing'; // 'advancing' または 'retreating'
                    enemy.initialY = enemy.y; // 初期位置を設定
                }

                // 進行中の動き
                if (enemy.retreatState === 'advancing') {
                    enemy.y += 3; // 下に移動
                    // 一定距離(ここでは400ピクセル)に達したかチェック
                    if (enemy.y >= enemy.initialY + 400) {
                        enemy.retreatState = 'retreating'; // 状態を退避中に変更
                    }
                } 
                // 退避中の動き
                else if (enemy.retreatState === 'retreating') {
                    enemy.y -= 3; // 上に移動
                    // 初期位置よりも上に移動したかチェック
                    if (enemy.y <= enemy.initialY) {
                        enemy.retreatState = 'advancing'; // 再び進行中の状態に戻る
                        enemy.initialY = enemy.y; // 新しい初期位置を設定
                    }
                }
                break;
            case 'circular':    // 9 円運動をする。
                // 円運動の中心点を定義(例: 一定位置まで下りてきたらそこを中心にする)
                if (!enemy.center) {
                    enemy.center = { x: enemy.x, y: enemy.y };
                    enemy.angle = 0; // 初期角度
                }
                enemy.angle += 0.05; // 角度を増加させる(動きの速さを調整)
                enemy.x = enemy.center.x + Math.cos(enemy.angle) * 50; // 半径50の円運動
                enemy.y = enemy.center.y + Math.sin(enemy.angle) * 50;
                break;
            case 'spiral':  // 10 スパイラル状に進む。
                if (!enemy.center) {
                    enemy.center = { x: enemy.x, y: enemy.y };
                    enemy.angle = 0; // 初期角度
                    enemy.radius = 0; // 初期半径
                }
                enemy.angle += 0.05; // 角度を増加させる(動きの速さを調整)
                enemy.radius += 0.5; // 半径を徐々に増加させる(螺旋の広がりを調整)
                enemy.x = enemy.center.x + Math.cos(enemy.angle) * enemy.radius;
                enemy.y = enemy.center.y + Math.sin(enemy.angle) * enemy.radius;
                break;

                case 'backAndForth':    // 11 往復する動きをする。
                // 敵の動きの状態を管理するフラグが未設定の場合、初期化する
                if (enemy.retreatState === undefined) {
                    enemy.retreatState = 'advancing'; // 'advancing' または 'retreating'
                    enemy.initialY = enemy.y; // 初期位置を設定
                }

                // 進行中の動き
                if (enemy.retreatState === 'advancing') {
                    enemy.y += 3; // 下に移動
                    // 一定距離(ここでは200ピクセル)に達したかチェック
                    if (enemy.y >= enemy.initialY + 200) {
                        enemy.retreatState = 'retreating'; // 状態を進行後に変更
                    }
                } 

                // 進行後の動き
                else if (enemy.retreatState === 'retreating') {
                    if (!enemy.direction) enemy.direction = 1;
                    if (enemy.x > canvas.width - enemy.size || enemy.x < 0) {
                        enemy.direction *= -1; // 方向転換
                    }
                    enemy.x += 2 * enemy.direction; // 横方向に固定量で移動
                }
                break;

            case 'horizontalSweep':     // 12 水平に大きく移動しながら進む。
                enemy.x += Math.sin(enemy.y / 50) * 5; // 横方向に大きく移動
                enemy.y += 1; // 垂直方向に固定量で進む
                break;

            case 'verticalSweep':   // 13 垂直に大きく移動しながら進む。
                enemy.y += Math.sin(enemy.x / 50) * 5; // 垂直方向に大きく移動
                enemy.x += 1; // 横方向に固定量で進む
                break;

            case 'bounce':  // 14 壁や画面端で跳ね返るように動く。
                // 敵の動きの状態を管理するフラグが未設定の場合、初期化する
                if (enemy.retreatState === undefined) {
                    enemy.retreatState = 'advancing'; // 'advancing' または 'retreating'
                    enemy.initialY = enemy.y; // 初期位置を設定
                }

                // 進行中の動き
                if (enemy.retreatState === 'advancing') {
                    enemy.y += 3; // 下に移動
                    // 一定距離(ここでは100ピクセル)に達したかチェック
                    if (enemy.y >= enemy.initialY + 100) {
                        enemy.retreatState = 'retreating'; // 状態を進行後に変更
                    }
                } 

                // 進行後の動き
                else if (enemy.retreatState === 'retreating') {
                    // 壁や画面端で跳ね返るように動く。
                    if (!enemy.velocityX) enemy.velocityX = 2;
                    if (!enemy.velocityY) enemy.velocityY = 2;
                    if (enemy.x > canvas.width - enemy.size || enemy.x < 0) enemy.velocityX *= -1;
                    if (enemy.y > canvas.height - enemy.size || enemy.y < 0) enemy.velocityY *= -1;
                    enemy.x += enemy.velocityX;
                    enemy.y += enemy.velocityY;
                }
                break;

            case 'teleport':    // 15 短い間隔でランダムな位置にテレポートする。
                if (!enemy.teleportTimer) enemy.teleportTimer = 0;
                enemy.teleportTimer++;
                if (enemy.teleportTimer > 180) { // 180フレームごとにテレポート
                    enemy.x = Math.random() * (canvas.width - enemy.size);
                    enemy.y = Math.random() * (canvas.height - enemy.size);
                    enemy.teleportTimer = 0;
                }
                break;

            case 'hover':   // 16 一定の位置でホバリングし、突然速く動き出す。
                if (!enemy.hoverTime) enemy.hoverTime = 0;
                enemy.hoverTime++;
                if (enemy.hoverTime < 120) {
                    // 最初の120フレームはほとんど動かない
                    enemy.x += Math.random() * 2 - 1;
                    enemy.y += Math.random() * 2 - 1;
                } else {
                    // それ以降は速く動き出す
                    enemy.x += Math.random() * 20 - 10;
                    enemy.y += Math.random() * 20 - 10;
                }
                break;

            case 'bossPattern1':
            case 'bossPattern2':
            case 'finalBossPattern':
                // ボス専用の動きや行動をここに実装
                if (enemy.behavior) {
                    enemy.behavior();
                }
                break;
            default:
                // 未定義の動きがあった場合の処理
                console.warn("Undefined movement pattern:", enemy.movement);
                break;
        }

        // 画面外に出た敵を削除
        if (enemy.y > canvas.height + enemy.size) {
            enemies.splice(index, 1);
        }
    });
}

function drawEnemies() {
    enemies.forEach(enemy => {
        const img = loadedImages[enemy.imageSrc]; // プリロードされた画像を取得
        if (img) {
            ctx.drawImage(img, enemy.x, enemy.y, enemy.size, enemy.size);
        }
    });
}

// イベントリスナーで座標の変換
function getCanvasRelativePosition(event) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: event.clientX - rect.left,
        y: event.clientY - rect.top,
    };
}

// ゲームオーバー時のテキスト表示
function showGameOver() {
    ctx.fillStyle = 'red';
    ctx.font = '48px Arial';
    ctx.textAlign = 'center';
    ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2);
}

function gameLoop() {
    const deltaTime = 1 / 60;
    currentTime += deltaTime;
    
    if (invincible) {
        invincibleTime += deltaTime;
        if (invincibleTime >= 2) { // 2秒後に無敵状態を解除
            invincible = false;
            invincibleTime = 0;
        }
    }

    if (gameOver) {
        showGameOver();
        setupGameOverListener(); // ゲームオーバーリスナーを設定
        return;
    }

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    updateStars();  // 星の更新
//    updateBackground();
    if (!gameOver) {
        updatePlayer();
        updateBeams();
    }
    spawnEnemies(); // 敵を生成
    drawEnemies(); // 追加: 敵を描画
    updateEnemies(deltaTime);
    checkPlayerCollision();
    updateParticles();
    drawParticles();
    updatePowerUpItems(); // パワーアップアイテムの更新と描画
    checkItemCollection(); // パワーアップアイテムの取得チェック

    requestAnimationFrame(gameLoop);
}


canvas.addEventListener('mousemove', event => {
    const pos = getCanvasRelativePosition(event);
    playerX = pos.x;
    playerY = pos.y - 50;
});

canvas.addEventListener('touchmove', event => {
    const touch = event.touches[0];
    const pos = getCanvasRelativePosition(touch);
    playerX = pos.x;
    playerY = pos.y - 50;
    event.preventDefault();
}, { passive: false });


// 画像プリロード関数
function preloadImages(callback) {
    const imageSources = ["images/player.png"]; // 自機の画像もプリロード対象に含める
    let imagesLoaded = 0;

    // enemyTypesから画像パスを抽出してimageSourcesに追加
    Object.values(enemyTypes).forEach(group => {
        group.forEach(enemy => {
            if (!imageSources.includes(enemy.imageSrc)) {
                imageSources.push(enemy.imageSrc);
            }
        });
    });

    // パワーアップアイテムの画像パスもプリロード処理に追加
    Object.values(itemTypes).forEach(src => {
        if (!imageSources.includes(src)) {
            imageSources.push(src);
        }
    });
    
    // 各画像のプリロード
    imageSources.forEach(src => {
        const img = new Image();
        img.onload = () => {
            imagesLoaded++;
            loadedImages[src] = img; // ロード完了した画像をオブジェクトに格納
            if (imagesLoaded === imageSources.length) {
                callback(); // すべての画像のロードが完了したらコールバックを実行
            }
        };
        img.src = src;
    });
}

// 一定間隔でビームを発射
setInterval(fireBeam, 200);

// 星の初期化
createStars();

// ゲームの初期化と開始
preloadImages(() => {
    initializeGame(); // ゲームの状態を初期化
    // 画像のプリロードが完了した後にゲームループを開始
    gameLoop();
});

enemySpawnPatterns.js(敵の出現定義の定数宣言)

この定数宣言を書き換えることで、出てくる敵の種類やタイミングを変更することができるようにしました。

// 敵の出現パターンの定義
const enemySpawnPatterns = [
    { time: 1, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 2, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 3, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 4, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 5, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 7, type: 'small', variant: 4, position: 2, item: 1 },
    { time: 7, type: 'small', variant: 4, position: 4, item: 2 },
    { time: 8, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 9, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 10, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 11, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 12, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 14, type: 'small', variant: 4, position: 2, item: 2 },
    { time: 14, type: 'small', variant: 4, position: 4, item: 1 },
    { time: 15, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 15, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 16, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 16, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 17, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 18, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 18, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 19, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 19, type: 'small', variant: 2, position: 1, item: 0 },
    { time: 22, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 22, type: 'small', variant: 2, position: 2, item: 0 },
    { time: 22, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 22, type: 'small', variant: 2, position: 4, item: 0 },
    { time: 22, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 23, type: 'small', variant: 2, position: 1, item: 0 },
    { time: 24, type: 'small', variant: 2, position: 5, item: 0 },
    { time: 25, type: 'small', variant: 3, position: 3, item: 0 },
    { time: 26, type: 'small', variant: 2, position: 2, item: 0 },
    { time: 27, type: 'small', variant: 2, position: 4, item: 0 },
    { time: 28, type: 'small', variant: 3, position: 3, item: 0 },
    { time: 30, type: 'small', variant: 6, position: 1, item: 1 },
    { time: 30, type: 'small', variant: 6, position: 5, item: 2 },
    { time: 32, type: 'small', variant: 2, position: 1, item: 0 },
    { time: 32, type: 'small', variant: 2, position: 5, item: 0 },
    { time: 34, type: 'small', variant: 3, position: 3, item: 0 },
    { time: 36, type: 'small', variant: 2, position: 2, item: 0 },
    { time: 36, type: 'small', variant: 2, position: 4, item: 0 },
    { time: 38, type: 'small', variant: 3, position: 3, item: 0 },
    { time: 40, type: 'small', variant: 2, position: 1, item: 0 },
    { time: 40, type: 'small', variant: 2, position: 5, item: 0 },
    { time: 42, type: 'small', variant: 3, position: 3, item: 0 },
    { time: 44, type: 'small', variant: 2, position: 2, item: 0 },
    { time: 44, type: 'small', variant: 2, position: 4, item: 0 },
    { time: 46, type: 'small', variant: 3, position: 3, item: 0 },
    { time: 48, type: 'small', variant: 4, position: 5, item: 1 },
    { time: 50, type: 'small', variant: 4, position: 1, item: 2 },
    { time: 52, type: 'small', variant: 4, position: 4, item: 1 },
    { time: 54, type: 'small', variant: 4, position: 2, item: 2 },
    { time: 56, type: 'small', variant: 4, position: 3, item: 1 },
    { time: 58, type: 'small', variant: 4, position: 1, item: 2 },
    { time: 58, type: 'small', variant: 4, position: 3, item: 1 },
    { time: 58, type: 'small', variant: 4, position: 5, item: 2 },
    { time: 59, type: 'medium', variant: 1, position: 3, item: 1 },
    { time: 59, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 60, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 61, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 62, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 63, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 64, type: 'small', variant: 5, position: 2, item: 1 },
    { time: 64, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 65, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 66, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 67, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 68, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 69, type: 'small', variant: 5, position: 4, item: 2 },
    { time: 69, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 69, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 70, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 70, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 71, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 72, type: 'medium', variant: 2, position: 3, item: 1 },
    { time: 72, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 72, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 73, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 73, type: 'small', variant: 2, position: 1, item: 0 },
    { time: 75, type: 'small', variant: 5, position: 3, item: 2 },
    { time: 75, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 75, type: 'small', variant: 2, position: 2, item: 0 },
    { time: 75, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 75, type: 'small', variant: 2, position: 4, item: 0 },
    { time: 75, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 78, type: 'small', variant: 7, position: 1, item: 0 },
    { time: 78, type: 'small', variant: 7, position: 3, item: 0 },
    { time: 78, type: 'small', variant: 7, position: 5, item: 0 },
    { time: 79, type: 'small', variant: 4, position: 5, item: 1 },
    { time: 80, type: 'small', variant: 7, position: 2, item: 0 },
    { time: 80, type: 'small', variant: 7, position: 4, item: 0 },
    { time: 81, type: 'small', variant: 4, position: 1, item: 1 },
    { time: 82, type: 'small', variant: 7, position: 1, item: 0 },
    { time: 82, type: 'small', variant: 7, position: 3, item: 0 },
    { time: 82, type: 'small', variant: 7, position: 5, item: 0 },
    { time: 83, type: 'small', variant: 4, position: 4, item: 2 },
    { time: 84, type: 'small', variant: 7, position: 2, item: 0 },
    { time: 84, type: 'small', variant: 7, position: 4, item: 0 },
    { time: 85, type: 'small', variant: 4, position: 2, item: 2 },
    { time: 86, type: 'small', variant: 7, position: 1, item: 0 },
    { time: 86, type: 'small', variant: 7, position: 3, item: 0 },
    { time: 86, type: 'small', variant: 7, position: 5, item: 0 },
    { time: 86, type: 'small', variant: 8, position: 1, item: 0 },
    { time: 87, type: 'small', variant: 4, position: 3, item: 1 },
    { time: 88, type: 'small', variant: 7, position: 1, item: 0 },
    { time: 88, type: 'small', variant: 7, position: 3, item: 0 },
    { time: 88, type: 'small', variant: 7, position: 5, item: 0 },
    { time: 88, type: 'small', variant: 8, position: 3, item: 0 },
    { time: 89, type: 'small', variant: 4, position: 1, item: 0 },
    { time: 90, type: 'small', variant: 7, position: 1, item: 0 },
    { time: 90, type: 'small', variant: 7, position: 3, item: 0 },
    { time: 90, type: 'small', variant: 7, position: 5, item: 0 },
    { time: 90, type: 'small', variant: 8, position: 5, item: 0 },
    { time: 91, type: 'small', variant: 4, position: 3, item: 1 },
    { time: 92, type: 'small', variant: 7, position: 3, item: 0 },
    { time: 93, type: 'small', variant: 4, position: 5, item: 2 },
    { time: 95, type: 'small', variant: 6, position: 1, item: 2 },
    { time: 95, type: 'small', variant: 2, position: 2, item: 0 },
    { time: 95, type: 'small', variant: 6, position: 3, item: 1 },
    { time: 95, type: 'small', variant: 2, position: 4, item: 0 },
    { time: 95, type: 'small', variant: 6, position: 5, item: 2 },
    { time: 100, type: 'small', variant: 6, position: 1, item: 1 },
    { time: 100, type: 'small', variant: 2, position: 2, item: 0 },
    { time: 100, type: 'small', variant: 6, position: 3, item: 2 },
    { time: 100, type: 'small', variant: 2, position: 4, item: 0 },
    { time: 100, type: 'small', variant: 6, position: 5, item: 1 },
    { time: 100, type: 'medium', variant: 1, position: 3, item: 2 },
    { time: 103, type: 'medium', variant: 2, position: 3, item: 2 },
    { time: 110, type: 'small', variant: 4, position: 1, item: 1 },
    { time: 111, type: 'small', variant: 2, position: 2, item: 0 },
    { time: 112, type: 'small', variant: 4, position: 3, item: 1 },
    { time: 113, type: 'small', variant: 2, position: 4, item: 0 },
    { time: 114, type: 'small', variant: 4, position: 5, item: 1 },
    { time: 115, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 116, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 117, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 118, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 119, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 120, type: 'small', variant: 5, position: 2, item: 1 },
    { time: 121, type: 'small', variant: 1, position: 5, item: 0 },
    { time: 122, type: 'small', variant: 1, position: 4, item: 0 },
    { time: 123, type: 'small', variant: 1, position: 3, item: 0 },
    { time: 124, type: 'small', variant: 1, position: 2, item: 0 },
    { time: 125, type: 'small', variant: 1, position: 1, item: 0 },
    { time: 126, type: 'small', variant: 5, position: 4, item: 1 },
    { time: 128, type: 'medium', variant: 1, position: 1, item: 2 },
    { time: 128, type: 'medium', variant: 1, position: 5, item: 1 },
    { time: 122, type: 'medium', variant: 2, position: 3, item: 1 },
    { time: 122, type: 'small', variant: 8, position: 1, item: 0 },
    { time: 123, type: 'small', variant: 8, position: 5, item: 0 },
    { time: 124, type: 'small', variant: 8, position: 2, item: 0 },
    { time: 125, type: 'small', variant: 8, position: 4, item: 0 },
    { time: 126, type: 'small', variant: 8, position: 3, item: 0 },
    // 以下、必要に応じてさらに敵の出現パターンを追加
];

export { enemySpawnPatterns };

1日目 まとめ この時点の実際のシューティングゲーム(実際に遊べる)

この時点の実際のシューティングゲームはこちら

※まだ敵の弾を実装していないので弾幕にはなっていません。アイテムの効果も今後実装します。

ChatGPTで作った弾幕シューティングゲーム(1日目)

PC、スマホ(Android、iPhone)で動きます。

タイトル画面がないのでリンク先でいきなりゲームが始まります。
また、再スタートを実装していないのでゲームオーバー時の再スタートはブラウザのリロードでお願いします。

実装されているのは下記までです。

  • 自機の操作
  • 小型、中型の敵の出現と動作
  • 敵と自機の当たり判定
  • 敵と自機が発射したビームの当たり判定
  • 敵を倒したときやダメージを受けた時の爆発パーティクル
  • アイテムの出現(ただし、取ってもパワーアップは未実装)
  • GAME OVER表示

一応、シューティングゲームとして成り立ってはいますが、音もボスもないので少々味気ない状態です。

しかし、ほぼChatGPT任せのプログラミングで、ここまで数時間でできたことに驚きです。

今後、さらに下記などを実装する予定です。

  • タイトル画面
  • アイテムを取ってパワーアップ
  • 敵が弾を発射
  • ボスの追加
  • 画面サイズの拡大や調整
  • サウンド、BGM
  • ゲームバランス調整

以上、ROBO船長でした。では、また次回!

コメント

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