ども。ROBO船長です。
前回2日目のつづきです。ChatGPTでシューティングゲームを作っていきたいと思います。
目指すのは本気で遊んでも楽しめる本格弾幕シューティングゲームです。
3日目時点のシューティングゲーム(実際に遊べる)
3日目時点の実際のシューティングゲームはこちら
PC、スマホ(Android、iPhone)で動きます。
※ブラウザが動くなら大体何でも。FireTVでも一応動きました。
画面サイズ固定なので画面が広すぎるスマホやタブレット、PCだと小さく表示されます。
3日目時点で実装されているのは下記となります。
- タイトル画面
- 小型敵キャラ8種類
- 中型敵キャラ2種類
- 大型ボス2種類
- 効果音、BGM
- アイテムを取ってパワーアップ(オレンジ:連射弾、青:貫通反射弾)
- アイテムを取って自機の体力回復
- 自機の体力表示(右下)
- 5分間のカウントダウンタイマー表示、5分攻撃を耐えたらクリア
- GAME OVER表示、再スタート
以下、2日目の開発内容です。
ほぼ、自分用の記録というかメモですが、ChatGPTでどうやってゲーム作っているの?と気になる方はプロンプトやソースコードも公開しているのでどうぞご覧ください。
前回、大型ボスを追加しましたがまだ1種類のため、2種類目を追加したいと思います。
この記事では有料のChatGPT4を使用していますが、無料のChatGPT3.5でもほぼ同じことができます。
※生成されるソースコードの品質や指示に対する気の利き方はChatGPT4の方が当然上になります。
大型ボス2を追加する(プロンプト)
新たに大型ボス2を追加します。
実は、こういうボスを追加しようと考えるより先に、画像生成AIでいろいろなボスっぽい画像を適当に何個も生成してみて、その中からいい感じの画像を選び、画像のイメージから動きを考えたりしています。
今回は単純な戦艦などではなくエイ〇アンと戦艦が混ざったようなキャラができていたので、これを採用します。
不気味なイメージなので、幽霊のように突然現れたり、ワープしたりする動きにしてみます。
また、弾幕シューティングらしく、大量の弾を出そうと思いますが、単純に大量にするとよけきれなさ過ぎて面白くないので、シャボン玉のようにゆっくりと漂う弾にしてみます。
このシャボン玉のようなものを吐き出すのも、機械というより生物っぽくてあっているかも、なんて思っていますw
大型ボス2(large )の動きを実装する
enemyTypesのlargeに大型ボス1と同様に各種プロパティを設定し、behavior: function(enemy)に動作を記述する。
大型ボス1と仕様を共通化できる部分はソースコードを共通化する。
大型ボス2は次の動きとします。
・通常モード(初期モード)
・透明状態(当たり判定無し)で画面中央まで下りてくる
・画面中央に到達したら透明化をゆっくり(2秒かけて)解除しつつ、攻撃と移動を開始。
・透明度が100未満(半透明)の間は当たり判定無し
・透明度100(完全に不透明)になったら当たり判定開始
・画面上のランダムな座標1点を決め、そこに向かって2秒で直進移動する。
・ボス自身の大きさも考慮して画面外にはみ出さないように。
・16方向に時計回りで1周発射を定期的に発射 shootInterval = 1
・追撃モード(ボスの体力が30%未満)
・透明化する前に16方向に時計回りで1周発射を1回発射
・じわじわと透明化(2秒かけて)
・透明、半透明の間は当たり判定無し、弾の発射停止
・画面上のランダムな座標1点を決め、そこにじわじわと出現(2秒かけて)
・ボス自身の大きさも考慮して画面外にはみ出さないように。
・透明度100(完全に不透明)になったら当たり判定開始
・不透明になったら、16方向に時計回りで1周発射を1回発射
・3秒間、自機に向かって撃つ弾丸を発射 shootInterval = 0.3
・この追撃モードの動きを最初から繰り返す
ChatGPT回答(ソースコード 大型ボス2追加)
大型ボス2の動きを実装するために、enemyTypes
のlarge
セクションに新しいボスの定義を追加します。また、大型ボス1と共通化できる部分は関数として切り出して再利用します。
まず、大型ボスの共通動作を定義する関数を作成します。この関数は、ボスが透明状態から不透明状態へ移行する際の挙動や、16方向に弾を発射する処理などを担います。
// 大型ボスの共通動作を定義する関数
function commonBossBehavior(enemy, mode) {
// モードに応じた処理を実行
switch (mode) {
case 'normal':
// 通常モードの処理
break;
case 'pursuit':
// 追撃モードの処理
break;
}
}
// 大型ボス2の定義
enemyTypes.large.push({
size: 128,
imageSrc: 'images/enemy_large_boss_2.png',
movement: 'bossPattern2',
health: 2000, // ボスの体力を設定
maxHealth: 2000, // 最大体力も設定(体力ゲージの表示に使用)
boss: true, // ボスフラグ
behavior: function(enemy) {
// 通常モードと追撃モードの挙動を定義
if (enemy.health >= enemy.maxHealth * 0.3) {
// 通常モード
commonBossBehavior(enemy, 'normal');
} else {
// 追撃モード
commonBossBehavior(enemy, 'pursuit');
}
}
});
ここで、commonBossBehavior
関数内のswitch
文で、モードに応じた挙動を実装します。大型ボス1と大型ボス2で異なる動作がある場合は、この関数内で条件分岐を行い、それぞれのボス固有の処理を追加します。
また、実際のゲームにおいては、ボスが透明状態から不透明状態に移行する際のアニメーションや、弾の発射間隔などの細かいパラメータも調整する必要があります。これらは、ゲームのバランスや演出に応じて適宜変更してください。
大型ボス2実装の追加指示(プロンプト)
先に実装した大型ボス1と同じような形式で、大型ボス2の情報を定数定義に実装させるため、大型ボス1の周りのソースコードを渡した上で指示を出します。
ここに大型ボス2の通常モード、追撃モードの処理を実装してください
// 敵キャラの定義
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: 3 },
{ size: 32, imageSrc: 'images/enemy_small_3.png', movement: 'looping', health: 8 },
{ size: 32, imageSrc: 'images/enemy_small_4.png', movement: 'dive', health: 6 },
{ size: 32, imageSrc: 'images/enemy_small_5.png', movement: 'verticalSweep', health: 10 },
{ size: 32, imageSrc: 'images/enemy_small_6.png', movement: 'backAndForth', health: 20 },
{ size: 32, imageSrc: 'images/enemy_small_7.png', movement: 'chase', health: 60 },
{ size: 32, imageSrc: 'images/enemy_small_8.png', movement: 'retreat', health: 40 }
],
medium: [
{ size: 64, imageSrc: 'images/enemy_medium_1.png', movement: 'bounce', health: 200 },
{ size: 64, imageSrc: 'images/enemy_medium_2.png', movement: 'spiral', health: 100 }
],
large: [
{
size: 128,
imageSrc: 'images/enemy_large_boss_1.png',
movement: 'bossPattern1',
health: 2000, // ボスの体力を設定
maxHealth: 2000, // 最大体力も設定(体力ゲージの表示に使用)
boss: true, // ボスフラグ
behavior: function(enemy) {
// health プロパティの存在チェック
if (typeof enemy.health === 'undefined') {
console.error('Enemy object is missing the health property');
return; // この敵の処理をスキップ
}
// 通常モード
if (enemy.health >= enemy.maxHealth * 0.3) {
if (enemy.y >= 100) {
// 左右に揺れながら移動する
if (enemy.moveRight === undefined) {
enemy.moveRight = true; // 初期方向を右に設定
}
// 左または右端に到達したら方向転換する
if (enemy.x <= 0 || enemy.x + enemy.size >= canvas.width) {
enemy.moveRight = !enemy.moveRight;
}
// 移動方向に応じてX座標を更新する
enemy.x += enemy.moveRight ? 0.5 : -0.5;
// 画面端で反転するための境界値のチェックと修正
enemy.x = Math.max(0, Math.min(canvas.width - enemy.size, enemy.x));
// 上下に揺れる動き(サイン波を使う)
enemy.y += Math.sin(Date.now() / 100) * 0.5;
} else {
// 初期の下降動作
enemy.y += Math.min(2, (128 - enemy.y) * 0.01); // 128までゆっくり下降
}
// 弾の発射設定
enemy.shootInterval = 1.5; // 発射間隔
enemy.shoots = 1; // 自機に向かって撃つ弾丸
}
// 追撃モード
else {
enemy.x += playerX > enemy.x ? 0.3 : playerX < enemy.x ? -0.3 : 0;
enemy.y += playerY > enemy.y ? 0.3 : playerY < enemy.y ? -0.3 : 0;
// 弾の発射設定
enemy.shootInterval = 3; // 発射間隔
enemy.shoots = 2; // 16方向に時計回りで1周発射
}
}
},
{
size: 128,
imageSrc: 'images/enemy_large_boss_2.png',
movement: 'bossPattern2',
health: 2000, // ボスの体力を設定
maxHealth: 2000, // 最大体力も設定(体力ゲージの表示に使用)
boss: true, // ボスフラグ
behavior: function(enemy) {
// health プロパティの存在チェック
if (typeof enemy.health === 'undefined') {
console.error('Enemy object is missing the health property');
return; // この敵の処理をスキップ
}
// 通常モード
if (enemy.health >= enemy.maxHealth * 0.3) {
// 大型ボス2の通常モードの動作を定義
}
// 追撃モード
else {
// 大型ボス2の追撃モードの動作を定義
}
}
},
],
extraLarge: [
{
size: 256,
imageSrc: 'images/enemy_extra_large_boss.png',
movement: 'finalBossPattern',
behavior: function() {
// 超大型ラスボスの挙動
}
}
]
};
ChatGPT回答(ソースコード 大型ボス2の処理)
大型ボス2の通常モードと追撃モードの処理を実装します。大型ボス2の挙動は、透明化と移動のアニメーションを組み合わせたものになります。以下はその実装例です。
{
size: 128,
imageSrc: 'images/enemy_large_boss_2.png',
movement: 'bossPattern2',
health: 2000,
maxHealth: 2000,
boss: true,
behavior: function(enemy) {
// 通常モード
if (enemy.health >= enemy.maxHealth * 0.3) {
if (enemy.opacity === undefined) {
enemy.opacity = 0; // 透明度を初期化
enemy.targetX = Math.random() * (canvas.width - enemy.size); // 移動目標地点X
enemy.targetY = Math.random() * (canvas.height / 2 - enemy.size); // 移動目標地点Y
}
if (enemy.y < canvas.height / 2) {
enemy.y += 2; // 画面中央まで下降
} else {
enemy.opacity = Math.min(enemy.opacity + 0.01, 1); // 透明度を徐々に上げる
enemy.x += (enemy.targetX - enemy.x) / 100; // 目標地点に向かって移動
enemy.y += (enemy.targetY - enemy.y) / 100;
if (enemy.opacity === 1) {
enemy.shootInterval = 1; // 発射間隔を設定
enemy.shoots = 2; // 16方向に発射
}
}
}
// 追撃モード
else {
if (enemy.opacity === 1) {
enemyShoots(enemy); // 透明化する前に一度だけ発射
enemy.opacity = 0.99; // 透明化開始
} else if (enemy.opacity > 0) {
enemy.opacity -= 0.01; // 透明化
} else {
enemy.targetX = Math.random() * (canvas.width - enemy.size); // 新しい目標地点X
enemy.targetY = Math.random() * (canvas.height / 2 - enemy.size); // 新しい目標地点Y
enemy.x += (enemy.targetX - enemy.x) / 100; // 目標地点に向かって移動
enemy.y += (enemy.targetY - enemy.y) / 100;
enemy.opacity = Math.min(enemy.opacity + 0.01, 1); // 不透明化
if (enemy.opacity === 1) {
enemy.shootInterval = 0.3; // 発射間隔を設定
enemy.shoots = 1; // 自機に向かって発射
}
}
}
// 透明度に応じて描画スタイルを変更
ctx.globalAlpha = enemy.opacity;
const img = loadedImages[enemy.imageSrc];
if (img) {
ctx.drawImage(img, enemy.x, enemy.y, enemy.size, enemy.size);
}
ctx.globalAlpha = 1; // 透明度を元に戻す
}
}
この実装では、透明度を表すopacity
プロパティを使用して、大型ボス2の透明化と不透明化を制御しています。また、targetX
とtargetY
を使用して、移動の目標地点を設定しています。通常モードでは、透明度が1(不透明)になるまで徐々に増加させ、追撃モードでは、透明度が0(完全に透明)になるまで徐々に減少させています。
さらに、透明度に応じて発射間隔や発射する弾の種類を変更しています。透明度が1のときのみ、弾を発射するように設定しています。
大型ボス2実装指示のまとめ1(プロンプト)
ざっと実装してもらいましたが、思ったように透明化されない、動きがおかしいなどの問題点が多く、都度修正指示を出していきます。
※実際の実装までのどのようなやり取りが必要かの参考までに記載しますが全部読む必要はありません。
※下記のプロンプトは実際は一度に流しているのではなく、行間ごとにChatGPTの回答がありますがそれは省略しています。
透明化されません。
また、移動先座標はあまり近すぎないようにしてください。具体的には、ボスのサイズの2倍以上は離れた座標にしてください。
大型ボス2(large )の動きが想定と異なるため再定義します
・前提
・透明化解除(透明→不透明)について
・透明状態(不透明度0%)から不透明状態(不透明度100%)は2秒かけて徐々に不透明度を上げる
・透明化(不透明→透明状態)について
・不透明状態(不透明度100%)から透明状態(不透明度0%)は2秒かけて徐々に不透明度を下げる
・透明・半透明(不透明度100%未満)の時は大型ボス2の当たり判定無し
・当たり判定無しの時は、自機と衝突せず、自機のビームも当たらない
・大型ボス2の動き
・通常モード(初期モード)
1. 登場時、透明化状態
・画面上端から200まで透明化状態で下りてくる
2. 透明化解除
3. 移動
・画面上のランダムな座標1点を移動先として決める
・ボス自身の大きさの2倍以上離れた場所
・ボス自身の大きさも考慮して画面外にはみ出さないように
・移動先に2秒で直進移動する。
4. 攻撃
・16方向に時計回りで1周発射1回発射
上記、通常モード3、4を繰り返す
・追撃モード(ボスの体力が30%未満)
1. 16方向に時計回りで1周発射を1回発射
2. 透明化
3. 移動
・通常モードと同じ
4. 透明化解除
5. 16方向に時計回りで1周発射を1回発射
6. 3秒間、自機に向かって撃つ弾丸を発射 shootInterval = 0.3
上記、追撃モード1から6を繰り返し
現在のソースコードは下記の状態ですので修正してください。
(略 game.jsのソースコード)
大型ボス2(large )の動きが想定と異なるため再定義します
・前提
・透明化解除(透明→不透明)について
・透明状態(不透明度0%)から不透明状態(不透明度100%)は2秒かけて徐々に不透明度を上げる
・透明化(不透明→透明状態)について
・不透明状態(不透明度100%)から透明状態(不透明度0%)は2秒かけて徐々に不透明度を下げる
・透明・半透明(不透明度100%未満)の時は大型ボス2の当たり判定無し
・当たり判定無しの時は、自機と衝突せず、自機のビームも当たらない
・大型ボス2の動き
・通常モード(初期モード)
・初期表示処理
・透明化状態
・xはposition指定座標、yは画面上端から200の位置に大型ボス2を配置
・透明化解除
・繰り返し処理
1. 移動
・画面上のランダムな座標1点を移動先として決める
enemy.targetX = Math.random() * (canvas.width - 2 * enemy.size) + enemy.size;
enemy.targetY = Math.random() * (canvas.height- 2 * enemy.size) + enemy.size;
・ボス自身の大きさの2倍以上離れた場所
・ボス自身の大きさも考慮して画面外にはみ出さないように
・移動先に2秒で直進移動する。
2. 攻撃
・16方向に時計回りで1周発射1回発射
上記、通常モード繰り返し処理1、2を繰り返す
・追撃モード(ボスの体力が30%未満)
1. 16方向に時計回りで1周発射を1回発射
2. 透明化
3. 移動
・通常モードと同じ(ただし透明化状態で移動)
4. 透明化解除
5. 16方向に時計回りで1周発射を1回発射
6. 3秒間、自機に向かって撃つ弾丸を発射 shootInterval = 0.3
上記、追撃モード1から6を繰り返し
下記条件は満たされていますか?
・初期表示処理
・透明化状態
・xはposition指定座標、yは画面上端から200の位置に大型ボス2を配置
・透明化解除
大型ボス2が登場した後、一瞬表示されますがそのあとすぐに消え、その後表示されません
大型ボス2が表示されなくなるのは enemy.x と enemy.y が NaN になっているからと思われます
エラーを画像でChatGPTに参照させる
エラーメッセージを文字としてコピペするだけではなく、エラーが出ている画面のスクリーンショットを取って、その画像を見てもらうこともできます。
これは有料のGPT4ではできますが、無料のGPT3.5ではできません。
単に画像の中の文字を読み取るだけでなく、驚くほど的確に画面の情報から状況を理解してくれます。
ウォッチ式のポップアップに現れる値をうまくコピペできなかったり、部分的なコピペでは問題の全体像が伝えにくい場合などに画面全体を画像で共有することでChatGPTが状況を理解して解決につながることが多々あります。
enemy.targetX と enemy.targetY は設定されています
このプロンプトと一緒にChatGPTに下記の画像を渡します。
ちなみに使っているエディタはChatGPT内臓の Visual Studio Code 互換のエディタ Cursor です。
ChatGPTの回答(エラー画面のスクリーンショットを解析してアドバイス)
画面のスクリーンショットから、enemy.targetX
は正常に設定されているようですが、enemy.y
が NaN
となっています。これは、enemy.targetY
の計算で何らかの問題がある可能性を示唆しています。特に、enemy.targetY
を設定する際の計算で enemy.size
を使用している部分を注視する必要があります。
まず、確認すべき点は enemy.size
が正しく数値として設定されているかです。もし enemy.size
が未定義、または数値でなければ NaN
が発生する可能性があります。さらに、enemy.targetY
を設定する際の乱数の計算に問題がないかも再確認しましょう。
また、enemy.x += (enemy.targetX - enemy.x) / 100;
の計算で enemy.x
が NaN
になってしまう場合、enemy.targetX
または enemy.x
のいずれか(または両方)が数値でない場合が考えられます。これは初期設定で enemy.x
が正しく設定されていないことが原因かもしれません。
修正案としては、次のようなステップを踏むことをお勧めします:
enemy.size
が適切に設定されていることを確認してください。enemy.targetX
とenemy.targetY
の設定部分のロジックを見直し、適切な値が設定されるようにしてください。enemy.x
とenemy.y
の初期設定が適切に行われていることを確認してください。- 上記の修正を行った後、再度コードを実行し、
enemy.x
とenemy.y
が数値として正しく計算されるかを確認してください。
エラーが解消されない場合は、更なるデバッグが必要となります。コードの他の部分に原因がある可能性も考慮に入れ、計算を行っている他の関数やその呼び出し方も検討する必要があります。
大型ボス2実装指示のまとめ2(プロンプト)
画像でエラー状況をほぼ完ぺきに把握してくれています。凄いですね!
とはいえ、なかなか大型ボス2が想定通りの動きにならないのでさらにやり取りを続けます。
追撃モードが想定通りになっていないので再定義
・追撃モード(ボスの体力が30%未満)
1. 16方向に時計回りで1周発射を1回発射
2. 透明化
3. 移動(透明状態)
・画面上のランダムな座標1点を移動先として決める
・ボス自身の大きさの2倍以上離れた場所
・ボス自身の大きさも考慮して画面外にはみ出さないように
・移動先にワープする
4. 透明化解除
5. 6秒間その場にとどまる
5. 16方向に時計回りで1周発射を1回発射
6. 自機に向かって撃つ弾丸を発射 shootInterval = 0.5
上記、追撃モード1から6を繰り返し
一度追撃モードになったら通常モードには戻りません
通常モードは下記で問題ありません。通常モードに影響しないように追撃モードだけ修正してください。
(略 大型ボス2動作定義のソースコードを共有)
大型ボス2が透明化中(enemy.opacity < 1)の場合でも当たり判定があるようです
透明化中は当たり判定をなくしてください。
updateBeamsとcheckPlayerCollisionに影響すると思います。
自機の当たり判定ずれを調整(プロンプト)
敵の弾と自機の当たり判定がずれて左上の方だけ弾が当たるような感じがします。
ゲームエンジンやフレームワークなどを使わずに当たり判定の処理をする場合、全てのキャラやオブジェクトの座標系を統一していないとずれが生じます。
通常、画像は左上を「x=0, y=0」としますが、画像の中心を起点とするため「x = x + 画像幅 / 2, y = y + 画像高さ/2」とする場合があります。
片方が左上を起点、もう片方は中心を起点としていたり、四角で当たり判定をする場合と円で当たり判定をする場合など、ずれが起きる要因は様々です。
ChatGPTはかなりうまく実装してくれますが、このあたりの基準を最初に決めておかないと、指示するごとに違う基準で実装してしまい、ずれが生じたりしますのでご注意ください。
そのうち、そういったことも含めて生成AI自体が補正できるようになりそうですが、現時点では人間が気にしてあげた方がうまくいくので、ある程度のゲームプログラミングの仕組みやノウハウがあるとAIプログラミングでも有利です。
敵の弾と自機の当たり判定がずれています
自機の左上の方にだけ弾が当たります
ChatGPTの回答(ソースコード 当たり判定のずれ調整)
自機と敵の弾の当たり判定がずれている場合、自機の当たり判定の範囲や位置を確認する必要があります。checkCollisionWithPlayer
関数内で自機のヒットボックスを定義している部分を見直してみましょう。
function checkCollisionWithPlayer(bulletX, bulletY, bulletWidth, bulletHeight) {
const playerHitBox = { x: playerX - 20, y: playerY - 20, width: 40, height: 40 }; // 自機のヒットボックスを仮定
if (bulletX < playerHitBox.x + playerHitBox.width &&
bulletX + bulletWidth > playerHitBox.x &&
bulletY < playerHitBox.y + playerHitBox.height &&
bulletY + bulletHeight > playerHitBox.y) {
handlePlayerHit(); // 自機がダメージを受けたときの処理を実行
updateFireBeamInterval(); // ビーム発射間隔を更新する
return true; // 当たり判定があったことを示す
}
return false; // 当たり判定がなかったことを示す
}
このコードでは、自機のヒットボックスを自機の中心座標から左上に20ピクセルずらした位置に設定しています。自機の画像サイズや実際の表示位置に応じて、このヒットボックスの位置やサイズを調整する必要があります。
自機の画像が64×64ピクセルで中心に表示されている場合は、ヒットボックスの設定を以下のように変更してみてください。
const playerHitBox = { x: playerX - 32, y: playerY - 32, width: 64, height: 64 };
この変更によって、自機の中心から32ピクセルずつ上下左右に広がる正方形のヒットボックスが設定されます。必要に応じて、ヒットボックスのサイズを自機の画像サイズや実際の当たり判定に合わせて調整してください。
当たり判定調整の追加指示(プロンプト)
敵の弾との当たり判定を下記デバッグ用の円に合わせてください
// 自機の当たり判定の範囲を緑の線で描画(デバッグ用)
ctx.strokeStyle = 'green';
ctx.beginPath();
ctx.arc(playerX, playerY, 8, 0, Math.PI * 2);
ctx.stroke();
ChatGPT回答(ソースコード 円形の当たり判定)
敵の弾との当たり判定をデバッグ用の円に合わせるために、checkCollisionWithPlayer
関数を以下のように修正してください。
function checkCollisionWithPlayer(bulletX, bulletY, bulletRadius) {
const dx = bulletX - playerX; // 敵の弾の中心から自機の中心までのX距離
const dy = bulletY - playerY; // 敵の弾の中心から自機の中心までのY距離
const distance = Math.sqrt(dx * dx + dy * dy); // 中心点間の距離
// 当たり判定: 自機の当たり判定の円(半径8ピクセル)と敵の弾の半径の和と中心点間の距離を比較
if (distance < 8 + bulletRadius) {
handlePlayerHit(); // 自機がダメージを受けたときの処理を実行
updateFireBeamInterval(); // ビーム発射間隔を更新する
return true; // 当たり判定があったことを示す
}
return false; // 当たり判定がなかったことを示す
}
この修正により、敵の弾と自機の当たり判定がデバッグ用の円(半径8ピクセル)に基づいて行われるようになります。敵の弾の半径も考慮して当たり判定を行うため、bulletRadius
パラメータを追加しています。
この変更を反映するためには、updateAndDrawBullets
関数内でcheckCollisionWithPlayer
関数を呼び出す際に、敵の弾の半径を引数として渡す必要があります。たとえば、敵の弾の半径が5ピクセルの場合、以下のように呼び出します。
if (checkCollisionWithPlayer(bullet.x, bullet.y, 5)) {
bullets.splice(i, 1); // 弾を配列から削除
}
これにより、敵の弾と自機の当たり判定がデバッグ用の円に合わせて行われるようになります。
カウントダウン表示とクリア表示追加(プロンプト)
画面の左上に5分間のカウントダウンを表示します。
将来的には、このカウントダウンが切れたところでラスボスが登場するようにしたいと思っていますが、まずは仮に、5分間攻撃を耐え抜いたらクリアしたということにします。
左上に5分間のカウントダウンを表示したい。
形式は m : ss : dd
ChatGPTの回答(ソースコード カウントダウン表示)
カウントダウンタイマーを表示するために、まずはタイマーの初期値を設定し、その後、毎フレームごとにタイマーをデクリメントし、描画処理を行います。以下はその実装例です:
// タイマーの初期値(5分)
let timer = 5 * 60 * 1000; // ミリ秒単位
// タイマーの更新と描画
function updateAndDrawTimer() {
// 残り時間を計算
timer -= 1000 / 60; // 1フレームあたりの時間を減算(60FPSを想定)
const minutes = Math.floor(timer / 60000);
const seconds = Math.floor((timer % 60000) / 1000);
const deciseconds = Math.floor((timer % 1000) / 10);
// タイマーの文字列をフォーマット
const timerString = `${minutes} : ${String(seconds).padStart(2, '0')} : ${String(deciseconds).padStart(2, '0')}`;
// タイマーを画面の左上に描画
ctx.font = '20px Arial';
ctx.fillStyle = 'white';
ctx.fillText(timerString, 10, 30);
}
// ゲームループ内でタイマーの更新と描画を呼び出す
function gameLoop() {
// 他のゲームの更新処理...
updateAndDrawTimer();
// 他のゲームの描画処理...
requestAnimationFrame(gameLoop);
}
このコードは、タイマーをミリ秒単位で保持し、毎フレームごとに減少させ、m : ss : dd
の形式で画面の左上に表示します。タイマーが0になった場合の処理は、必要に応じて追加してください。
クリア表示の追加(プロンプト)
すでに実装済みの GAME OVER 表示と同じような形式でクリア表示する指示を出します。
タイマーが0になったときは今は仮に GAME OVER と同じ仕様で、文字だけ Congratulation! と白文字で表示。タップで再スタートも GAME OVER と同じ
ChatGPT回答(クリア表示)
タイマーが0になったときに「Congratulations!」と表示し、タップで再スタートするようにするためには、以下のようにコードを修正します:
// タイマーが終了したかどうかを追跡するフラグ
let timerEnded = false;
// タイマーの更新と描画
function updateAndDrawTimer() {
if (!timerEnded) {
// 残り時間を計算
timer -= 1000 / 60; // 1フレームあたりの時間を減算(60FPSを想定)
if (timer <= 0) {
timer = 0;
timerEnded = true;
}
}
if (timerEnded) {
// タイマーが終了したときのメッセージを表示
ctx.font = '48px Arial';
ctx.fillStyle = 'white';
ctx.fillText('Congratulations!', canvas.width / 2, canvas.height / 2);
} else {
// タイマーを表示
const minutes = Math.floor(timer / 60000);
const seconds = Math.floor((timer % 60000) / 1000);
const deciseconds = Math.floor((timer % 1000) / 10);
const timerString = `${minutes} : ${String(seconds).padStart(2, '0')} : ${String(deciseconds).padStart(2, '0')}`;
ctx.font = '20px Arial';
ctx.fillStyle = 'white';
ctx.fillText(timerString, 10, 30);
}
}
// タップで再スタート
canvas.addEventListener('click', () => {
if (timerEnded) {
// タイマーとゲームをリセット
timer = 5 * 60 * 1000;
timerEnded = false;
initializeGame(); // ゲームの初期化関数を呼び出す
}
});
// ゲームループ内でタイマーの更新と描画を呼び出す
function gameLoop() {
// 他のゲームの更新処理...
updateAndDrawTimer();
// 他のゲームの描画処理...
requestAnimationFrame(gameLoop);
}
このコードでは、timerEnded
フラグを使用してタイマーが終了したかどうかを追跡し、終了した場合には「Congratulations!」と表示します。また、タップイベントリスナー内でタイマーとゲームをリセットすることで、タップで再スタートが可能になります。ゲームの初期化処理は initializeGame
関数内で行う必要があります。
カウントダウンタイマー表示、クリア表示の修正指示
修正指示(プロンプト)
タイマーが画面左端からはみ出ています
ChatGPT回答(ソースコード)
画面左端からはみ出しているタイマーを調整するには、タイマーの描画位置を変更する必要があります。以下のようにupdateAndDrawTimer
関数内のctx.fillText
のX座標を調整してください:
function updateAndDrawTimer() {
const minutes = Math.floor(timer / 60000);
const seconds = Math.floor((timer % 60000) / 1000);
const milliseconds = Math.floor((timer % 1000) / 10);
// タイマーを表示するテキストのフォーマット
const timerText = `${minutes}:${seconds.toString().padStart(2, '0')}:${milliseconds.toString().padStart(2, '0')}`;
// タイマーを描画
ctx.font = '20px Arial';
ctx.fillStyle = 'white';
ctx.textAlign = 'left'; // 左揃えに変更
ctx.fillText(timerText, 10, 30); // X座標を10に設定して左端に移動
}
この変更により、タイマーが画面の左端に描画され、はみ出すことがなくなります。ctx.textAlign = 'left';
を設定することでテキストの左端が指定したX座標に揃えられます。
修正指示(プロンプト)
Congratulationsがひだりにズレています
ChatGPT回答(ソースコード)
Congratulations
が左にズレている場合は、そのテキストの描画位置を調整する必要があります。showGameOver
関数内でCongratulations
を描画している部分を見つけ、ctx.textAlign
を'center'
に設定して、テキストがキャンバスの中央に揃うようにします。以下のように修正してください:
function showGameOver() {
if (gameOver) {
ctx.fillStyle = 'red';
ctx.font = '48px Arial';
ctx.textAlign = 'center'; // 中央揃えに設定
ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2);
} else if (timer === 0) {
ctx.fillStyle = 'white';
ctx.font = '48px Arial';
ctx.textAlign = 'center'; // 中央揃えに設定
ctx.fillText('Congratulations!', canvas.width / 2, canvas.height / 2);
}
}
この変更により、Congratulations
のテキストがキャンバスの中央に揃うようになります。
この時点のソースコード(game09_02)
この時点でのソースコードは次の状態でした。
index.html(ブラウザでアクセスするページ)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shooting Fighter</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: 342px; /* キャンバスの最大幅を指定 */
height: auto; /* 高さを自動調整 */
aspect-ratio: 342 / 608; /* キャンバスのアスペクト比を維持 */
}
#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 = 342;
canvas.height = 608;
// タイトル画面の背景画像を追加
const titleBackgroundImage = new Image();
titleBackgroundImage.src = 'images/title_background.png'; // タイトル画面の背景画像のパスを指定
// タイトル画面の表示フラグ
let showTitle = true;
// タイトル画面の点滅効果のための変数
let titleOpacity = 1;
let titleFadeDirection = -0.02;
// ブロックワイプのブロックサイズとブロックの配列を定義
const blockSize = 20;
const blocksWide = Math.ceil(canvas.width / blockSize);
const blocksHigh = Math.ceil(canvas.height / blockSize);
let blocks = [];
// BGMの設定
const bgm = new Audio('bgm/Digital_Transformation.mp3');
bgm.loop = true; // ループ再生を有効にする
// ヘルスアイコンの画像を読み込む
let healthIcon = new Image();
healthIcon.src = 'images/health_icon.png'; // ヘルスアイコンのパスを指定
// 敵の出現パターンの定義をインポート
import { originalEnemySpawnPatterns } from './enemySpawnPatterns.js';
let enemySpawnPatterns = []; // ゲーム中に使用するパターンのコピー
let currentTime = 0; // ゲーム開始からの経過時間
let invincible = false; // 無敵状態のフラグ
let invincibleTime = 0; // 無敵時間のカウンタ
let gameOverWaitTime = 0.5; // 自機の耐久が0になってからゲームオーバー表示までの待機時間
let playerDeadTime = 0; // 自機の耐久が0になった時間(ゲーム開始からの経過時間を格納)
// 自機の表示フラグ
let playerVisible = true;
// 自機の設定
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; // ゲームオーバーフラグ
// 自機の初期化部分に追加
let powerUpLevel = 0; // パワーアップのレベル(0-6)
let powerUpType = null; // パワーアップのタイプ(null, 'rapidFire', 'piercing')
// ビームの設定
const beams = [];
// パワーアップアイテムのインスタンスを格納する配列
const powerUpItems = [];
// 弾丸を格納する配列
let bullets = [];
// 星の宣言
const stars = [];
const starCount = 30; // 生成する星の数
// パーティクル配列
const particles = [];
// 画像のプリロードを管理するためのオブジェクト
const loadedImages = {};
const spawnPositions = [57, 114, 171, 228, 285]; // 画面の横幅を5分割したX座標
// 音声ファイルを格納するオブジェクトを定義
const loadedAudioFiles = {};
// AudioContextを定義
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 音声ソースを格納する配列を定義
const sources = [];
// ビーム発射のフラグ
let firing = false;
// ビーム発射の間隔を管理する変数
let fireBeamInterval = 200; // デフォルトは200ms
let beamIntervalId;
let beamHitSoundPlaying = false; // ビームが当たった音の再生フラグ
// パワーアップアイテムの画像パス
const itemTypes = {
1: 'images/powerup_1.png',
2: 'images/powerup_2.png',
};
// 効果音ファイルのパスをオブジェクトで管理する
const soundPaths = {
beam_hit: 'sounds/beam_hit.mp3',
game_over: 'sounds/game_over.mp3',
hit_by_bullet: 'sounds/hit_by_bullet.mp3',
hit_by_enemy: 'sounds/hit_by_enemy.mp3',
large_explosion: 'sounds/large_explosion.mp3',
medium_explosion: 'sounds/medium_explosion.mp3',
power_up: 'sounds/power_up.mp3',
ship_explosion: 'sounds/ship_explosion.mp3',
small_explosion: 'sounds/small_explosion.mp3',
};
// 敵のインスタンスを保持する配列
let enemies = [];
// 敵キャラの定義
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: 3 },
{ size: 32, imageSrc: 'images/enemy_small_3.png', movement: 'looping', health: 8 },
{ size: 32, imageSrc: 'images/enemy_small_4.png', movement: 'dive', health: 6 },
{ size: 32, imageSrc: 'images/enemy_small_5.png', movement: 'verticalSweep', health: 10 },
{ size: 32, imageSrc: 'images/enemy_small_6.png', movement: 'backAndForth', health: 40 },
{ size: 32, imageSrc: 'images/enemy_small_7.png', movement: 'chase', health: 80 },
{ size: 32, imageSrc: 'images/enemy_small_8.png', movement: 'retreat', health: 60 }
],
medium: [
{ size: 64, imageSrc: 'images/enemy_medium_1.png', movement: 'bounce', health: 400 },
{ size: 64, imageSrc: 'images/enemy_medium_2.png', movement: 'spiral', health: 200 }
],
large: [
{
size: 128,
imageSrc: 'images/enemy_large_boss_1.png',
movement: 'bossPattern1',
health: 3000, // ボスの体力を設定
maxHealth: 3000, // 最大体力も設定(体力ゲージの表示に使用)
boss: true, // ボスフラグ
behavior: function(enemy) {
// health プロパティの存在チェック
if (typeof enemy.health === 'undefined') {
console.error('Enemy object is missing the health property');
return; // この敵の処理をスキップ
}
// 通常モード
if (enemy.health >= enemy.maxHealth * 0.3) {
if (enemy.y >= 100) {
// 左右に揺れながら移動する
if (enemy.moveRight === undefined) {
enemy.moveRight = true; // 初期方向を右に設定
}
// 左または右端に到達したら方向転換する
if (enemy.x <= 0 || enemy.x + enemy.size >= canvas.width) {
enemy.moveRight = !enemy.moveRight;
}
// 移動方向に応じてX座標を更新する
enemy.x += enemy.moveRight ? 0.5 : -0.5;
// 画面端で反転するための境界値のチェックと修正
enemy.x = Math.max(0, Math.min(canvas.width - enemy.size, enemy.x));
// 上下に揺れる動き(サイン波を使う)
enemy.y += Math.sin(Date.now() / 100) * 0.5;
} else {
// 初期の下降動作
enemy.y += Math.min(2, (128 - enemy.y) * 0.01); // 128までゆっくり下降
}
// 弾の発射設定
enemy.shootInterval = 1.5; // 発射間隔
enemy.shoots = 1; // 自機に向かって撃つ弾丸
}
// 追撃モード
else {
enemy.x += playerX > enemy.x ? 0.25 : playerX < enemy.x ? -0.25 : 0;
enemy.y += playerY > enemy.y ? 0.3 : playerY < enemy.y ? -0.3 : 0;
// 弾の発射設定
enemy.shootInterval = 3.5; // 発射間隔
enemy.shoots = 2; // 16方向に時計回りで1周発射
}
}
},
{
size: 160,
imageSrc: 'images/enemy_large_boss_2.png',
movement: 'bossPattern2',
health: 4000,
maxHealth: 4000,
boss: true,
behavior: function(enemy) {
// 初期化処理
if (enemy.opacity === undefined) {
enemy.opacity = 0; // 透明度を初期化
// boss2Opacity = 0; // 大型ボス2用 透明度を初期化
enemy.mode = 'appearing'; // モードを登場中に設定
enemy.moveTimer = 0; // 移動タイマーを初期化
enemy.shootTimer = 0; // 発射タイマーを初期化
enemy.y = 200; // y座標を画面上端から200の位置に設定
// 移動目標地点の初期設定
enemy.targetX = enemy.x;
enemy.targetY = enemy.y;
}
// 登場中モード
if (enemy.mode === 'appearing') {
if (enemy.opacity < 1) {
enemy.opacity += 0.01; // 透明化解除
if (enemy.opacity > 1) {
enemy.opacity = 1;
}
} else {
enemy.mode = 'normal'; // モードを通常モードに切り替え
}
// boss2Opacity = enemy.opacity; // 大型ボス2の透明度を退避
}
// 通常モード
else if (enemy.mode === 'normal') {
if (enemy.health >= enemy.maxHealth * 0.3) {
enemy.moveTimer++;
if (enemy.moveTimer >= 60 * 2) { // 2秒ごとに移動
enemy.targetX = Math.random() * (canvas.width - 2 * enemy.size) + enemy.size;
enemy.targetY = Math.random() * (canvas.height / 3 * 2 - 2 * enemy.size) + enemy.size;
enemy.moveTimer = 0;
}
enemy.x += (enemy.targetX - enemy.x) / 100; // 移動
enemy.y += (enemy.targetY - enemy.y) / 100;
enemy.shootTimer++;
if (enemy.shootTimer >= 60 * 2) { // 2秒ごとに攻撃
enemy.shoots = 4; // 12方向に発射
enemyShoots(enemy);
enemy.shootTimer = 0;
}
}
// 追撃モード
else {
enemy.mode = 'pursuit';
enemy.opacity = 0.99; // 透明化開始
}
}
// 追撃モード
else if (enemy.mode === 'pursuit') {
// 初期化処理
if (enemy.shootTimer === undefined || enemy.stayTimer === undefined) {
enemy.shootTimer = 0;
enemy.stayTimer = 0;
// enemy.opacity = 1; // 不透明状態から開始
}
// 透明化フェーズ
if (enemy.opacity > 0 && enemy.stayTimer === 0) {
console.log(`Fading out: ${enemy.opacity}`); // 透明化処理のログ(デバッグ用)
enemy.opacity -= 1 / (60 * 2); // 60 * 秒かけて透明化
enemy.opacity = Math.max(enemy.opacity, 0); // 透明度が0未満にならないようにする
// boss2Opacity = enemy.opacity; // 大型ボス2の透明度を退避
if (enemy.opacity <= 0) {
enemy.opacity = 0;
enemy.shootTimer = 0; // 発射タイマーをリセット
// ワープ先の座標を決定
// enemy.targetX = Math.random() * (canvas.width - enemy.size * 4) + enemy.size * 2;
// enemy.targetY = Math.random() * (canvas.height / 2 - enemy.size * 4) + enemy.size * 2;
enemy.targetX = Math.random() * (canvas.width - 2 * enemy.size) + enemy.size;
enemy.targetY = Math.random() * (canvas.height - 2 * enemy.size) + enemy.size;
// ワープ実行
enemy.x = enemy.targetX;
enemy.y = enemy.targetY;
// 透明化解除フェーズに移行
enemy.stayTimer = 1; // stayTimerを1に設定して透明化解除フェーズを開始
}
}
// 透明化解除フェーズ
else if (enemy.stayTimer > 0) {
console.log(`Fading in: ${enemy.opacity}`); // 不透明化処理のログ(デバッグ用)
if (enemy.opacity < 1) {
enemy.opacity += 1 / (60 * 1); // 60 * 秒 で透明化解除
enemy.opacity = Math.min(enemy.opacity, 1); // 透明度が1を超えないようにする
// boss2Opacity = enemy.opacity; // 大型ボス2の透明度を退避
if (enemy.opacity >= 1) {
enemy.opacity = 1;
// boss2Opacity = enemy.opacity; // 大型ボス2の透明度を退避
// 透明化が完了したら弾丸発射
enemy.shoots = 4; // 12方向に発射
enemyShoots(enemy);
}
}
// 不透明の状態で3秒間その場に留まり弾を発射
if (enemy.opacity === 1) {
if (enemy.shootTimer >= 60 * 0.05) { // 0.05秒ごとに発射
// 不透明の間弾丸を発射
enemy.shoots = 3; // 24方向にぐるぐる発射
enemyShoots(enemy);
enemy.shootTimer = 0; // タイマーをリセット
}
enemy.shootTimer++;
// 2秒経過で次のフェーズへ
enemy.stayTimer++;
if (enemy.stayTimer >= 60 * 3) {
enemy.stayTimer = 0; // stayTimerをリセット
}
}
}
}
}
}
],
extraLarge: [
{
size: 256,
imageSrc: 'images/enemy_extra_large_boss.png',
movement: 'finalBossPattern',
behavior: function() {
// 超大型ラスボスの挙動
}
}
]
};
// 音声ファイルの事前読み込み関数
function preloadAudioFiles() {
Object.values(soundPaths).forEach(soundPath => {
fetch(soundPath)
.then(response => response.arrayBuffer())
.then(buffer => audioContext.decodeAudioData(buffer))
.then(audioBuffer => {
// 音声ファイルをデコードしてバッファに保存
loadedAudioFiles[soundPath] = audioBuffer;
})
.catch(error => {
console.error('音声ファイルの読み込みエラー:', error);
});
});
}
// AudioContextを使用して音声を再生する関数
function playSound(soundName, volume = 1) {
const soundPath = soundPaths[soundName]; // 指定された効果音のパスを取得
if (soundPath) {
const audioBuffer = loadedAudioFiles[soundPath];
if (audioBuffer) {
if (soundName === 'beam_hit' && beamHitSoundPlaying) {
return; // 'beam_hit' 効果音が既に再生中の場合は何もしない
}
const source = audioContext.createBufferSource();
const gainNode = audioContext.createGain(); // 音量を制御するためのGainNodeを作成
source.buffer = audioBuffer;
source.connect(gainNode); // GainNodeに接続
gainNode.connect(audioContext.destination);
gainNode.gain.value = volume; // 音量を設定
source.start(0); // 再生を即座に開始
if (soundName === 'beam_hit') {
beamHitSoundPlaying = true; // 'beam_hit' 効果音の再生状態を更新
source.onended = () => {
beamHitSoundPlaying = false; // 再生が終了したらフラグを更新
};
}
} else {
console.error('音声ファイルが読み込まれていません');
}
} else {
console.error('指定された効果音が見つかりません');
}
}
// ゲームオーバー時に効果音のバッファやリソースを解放する関数
function releaseSoundResources() {
sources.forEach(source => {
source.stop(); // 音声を停止
});
sources.length = 0; // 音声ソース配列を空にする
stopBgm(); // BGM停止
}
// BGMの再生を開始する関数
function startBgm() {
bgm.play();
}
// BGMの再生を停止する関数
function stopBgm() {
bgm.pause();
bgm.currentTime = 0; // 再生位置を最初に戻す
}
// ボス戦開始時にBGMを切り替える例
function startBossBattle() {
stopBgm(); // 現在のBGMを停止
bgm.src = 'path/to/your/boss_battle_bgm.mp3'; // ボス戦用のBGMに切り替え
startBgm(); // 新しいBGMを再生
}
// BGMの音量を調整する関数
function setBgmVolume(volume) {
bgm.volume = volume;
}
// BGMのバックグラウンド停止のためページの可視性が変更されたときに呼び出される関数
function handleVisibilityChange() {
if (document.hidden) {
bgm.pause(); // ページがバックグラウンドになったら音楽を停止
} else {
bgm.play(); // ページがフォアグラウンドに戻ったら音楽を再開
}
}
// BGMのバックグラウンド停止のためvisibilitychangeイベントリスナーを追加
document.addEventListener('visibilitychange', handleVisibilityChange);
// デバッグ用
// console.log(itemTypes); // itemTypes オブジェクトの内容を確認
// ビームを発射する関数
function fireBeam() {
//powerUpLevel = 6; // デバッグ用
// powerUpType = 'piercing'; // デバッグ用
if (firing) {
// パワーアップタイプに応じたビームの挙動
switch (powerUpType) {
case "rapidFire":
// 連射弾強化: レベルに応じて横に広がるビームを発射
fireRapidFireBeam();
if (powerUpLevel === 6) {
// 貫通反射 piercing ビームも同時に発射
firePiercingBeam();
}
break;
case "piercing":
// 貫通反射弾強化: レベルに応じた方向のビームを発射
firePiercingBeam();
if (powerUpLevel === 6) {
// 連射弾 rapidFire ビームも同時に発射
fireRapidFireBeam();
}
break;
default:
// パワーアップがない場合は通常のビームを発射
fireNormalBeam();
break;
}
}
}
// 連射弾 rapidFire ビームを発射する関数
function fireRapidFireBeam() {
var numberOfBeams = 2 * powerUpLevel + 1; // レベルに応じたビームの数
numberOfBeams = numberOfBeams > 11 ? 11 : numberOfBeams; // ビームが11以上なら11に固定
for (let i = 0; i < numberOfBeams; i++) {
const offset = (i - Math.floor(numberOfBeams / 2)) * 8; // 中心からのオフセット
const yOffset = Math.abs(offset) * 0.5; // 外側ほど低い位置から発射するためのオフセット
beams.push({
x: playerX + offset, // オフセットを加えた位置から発射
y: playerY + yOffset, // Y軸の開始位置を調整
width: 4, // ビームの幅
height: 20, // ビームの高さ
speed: 10, // ビームの速度
type: "rapidFire"
});
}
}
// 貫通反射弾 piercing ビームを発射する関数
function firePiercingBeam() {
// レベル6の場合のためにビームの種類を判定して、連射弾 rapidFire の場合は発射しない
if (powerUpType === "piercing") {
beams.push(createBeam(playerX, playerY - 20, "up")); // 上向きのビームを発射
if (powerUpLevel > 2) {
// 追加の方向(右上、左上)のビーム
beams.push(createBeam(playerX + 15, playerY - 15, "rightUp"));
beams.push(createBeam(playerX - 15, playerY - 15, "leftUp"));
}
if (powerUpLevel > 3) {
// 追加の方向(右下、左下)のビーム
beams.push(createBeam(playerX + 15, playerY + 20, "rightDown"));
beams.push(createBeam(playerX - 15, playerY + 20, "leftDown"));
}
}
// レベル6の場合、連射弾 rapidFire でも発射する
if (powerUpLevel > 1) {
// 追加の方向(下)のビーム
beams.push(createBeam(playerX, playerY + 20, "down"));
}
}
// 通常のビームを発射する関数
function fireNormalBeam() {
beams.push({
x: playerX,
y: playerY - 20,
width: 4,
height: 20,
speed: 10
});
}
// ビーム発射間隔を更新する関数
function updateFireBeamInterval() {
if (powerUpType === "piercing") {
fireBeamInterval = 500; // 貫通反射弾の場合、間隔を500msに設定
} else {
fireBeamInterval = 200; // それ以外の場合、間隔を200msに設定
}
scheduleBeamFiring(); // ビーム発射間隔を更新
}
// ビーム発射をスケジュールする関数(既存の関数を利用または修正が必要な場合)
function scheduleBeamFiring() {
clearInterval(beamIntervalId); // 既存の間隔で設定されたビーム発射をクリア
beamIntervalId = setInterval(fireBeam, fireBeamInterval); // 新しい間隔でビーム発射をスケジュール
}
// ビームオブジェクトを生成する関数
function createBeam(x, y, direction) {
let velocityX = 0;
let velocityY = -10; // 通常は上向きに発射
// 方向に応じて速度を調整
switch (direction) {
case "up":
velocityX = 0;
velocityY = -10;
break;
case "rightUp":
velocityX = 5;
velocityY = -5;
break;
case "leftUp":
velocityX = -5;
velocityY = -5;
break;
case "down":
velocityX = 0;
velocityY = 10;
break;
case "rightDown":
velocityX = 5;
velocityY = 5;
break;
case "leftDown":
velocityX = -5;
velocityY = 5;
break;
}
// 生成されるビームオブジェクト
return {
x: x,
y: y,
velocityX: velocityX,
velocityY: velocityY,
type: "piercing", // ここでは貫通タイプ固定
radius: 8, // ビームの半径
};
}
// 弾丸を生成する関数
function createBullet(x, y, velocityX, velocityY, radius = 6, color = 'rgba(255, 165, 0, 0.8)') {
bullets.push({ x, y, velocityX, velocityY, radius, color });
}
// 敵が弾を撃つ処理
function enemyShoots(enemy) {
const bulletX = enemy.x + enemy.size / 2; // 敵の中心X座標
const bulletY = enemy.y + enemy.size / 2; // 敵の中心Y座標
let angleIncrement; // 角度の増分
let speed = 5; // 弾丸の速度
switch (enemy.shoots) {
case 1: // 自機に向かって撃つ弾丸
const dx = playerX - bulletX;
const dy = playerY - bulletY;
const distance = Math.sqrt(dx * dx + dy * dy);
speed = 1.5; // 弾丸の速度
const velocityX = (dx / distance) * speed;
const velocityY = (dy / distance) * speed;
createBullet(bulletX, bulletY, velocityX, velocityY);
break;
case 2: // 16方向に時計回りで1周発射
speed = 1; // 弾丸の速度
angleIncrement = Math.PI * 2 / 16; // 16方向に分割するための角度の増分
for (let i = 0; i < 16; i++) {
const angle = i * angleIncrement;
const velocityX = Math.cos(angle) * speed;
const velocityY = Math.sin(angle) * speed;
createBullet(bulletX, bulletY, velocityX, velocityY, 9, 'rgba(255, 64, 0, 0.8)'); // 濃いオレンジ色
}
break;
case 3: // 24方向に時計回りで1方向ずつ撃つ
if (enemy.shootDirection === undefined) {
enemy.shootDirection = 0; // 発射方向を初期化
enemy.shootTimer = 0; // タイマーを初期化
}
enemy.shootTimer++;
if (enemy.shootTimer >= 60 * 0.01) { // 0.1秒ごとに
const angle = Math.PI * 2 * (enemy.shootDirection / 24); // 方向のうちの1つを計算
const velocityX = Math.cos(angle) * speed;
const velocityY = Math.sin(angle) * speed;
createBullet(bulletX, bulletY, velocityX, velocityY, 6, 'rgba(64, 128, 255, 0.8)'); // 明るい青色
enemy.shootDirection = (enemy.shootDirection + 1) % 24; // 次の方向へ
enemy.shootTimer = 0; // タイマーをリセット
}
break;
case 4: // 12方向に時計回りで1周発射
speed = 0.3; // 弾丸の速度
angleIncrement = Math.PI * 2 / 12; // 12方向に分割するための角度の増分
for (let i = 0; i < 12; i++) {
const angle = i * angleIncrement;
const velocityX = Math.cos(angle) * speed;
const velocityY = Math.sin(angle) * speed;
createBullet(bulletX, bulletY, velocityX, velocityY, 9, 'rgba(168, 192, 255, 0.5)'); // 白っぽい青色
}
break;
// 他の弾丸やビームの種類に応じてケースを追加
}
}
// 弾丸の更新と描画処理
function updateAndDrawBullets(ctx) {
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i];
bullet.x += bullet.velocityX;
bullet.y += bullet.velocityY;
// 弾丸が画面外に出たか、自機に当たったかをチェック
/* if (bullet.x < 0 || bullet.x > canvas.width || bullet.y < 0 || bullet.y > canvas.height ||
(checkCollisionWithPlayer(bullet.x, bullet.y, bullet.radius * 2, bullet.radius * 2) && !invincible)) {
bullets.splice(i, 1); // 弾丸を配列から削除 */
// 弾丸が画面外に出た場合、配列から削除
if (bullet.x < 0 || bullet.x > canvas.width || bullet.y < 0 || bullet.y > canvas.height) {
bullets.splice(i, 1);
}
if (checkCollisionWithPlayer(bullet.x, bullet.y, bullet.radius)) {
bullets.splice(i, 1); // 弾を配列から削除
} else {
// 弾丸を円として描画
// ctx.fillStyle = 'rgba(255, 165, 0, 0.8)';
ctx.fillStyle = bullet.color;
ctx.beginPath();
ctx.arc(bullet.x, bullet.y, bullet.radius, 0, Math.PI * 2, false);
ctx.fill();
// 当たり判定の範囲を白の線で描画(縁取り)
ctx.strokeStyle = 'white';
ctx.beginPath();
ctx.arc(bullet.x, bullet.y, bullet.radius, 0, Math.PI * 2, false);
ctx.stroke();
}
}
}
// 自機がダメージを受けたときの処理
function handlePlayerHit() {
if (!invincible) {
invincible = true; // 無敵状態にする
playerHealth -= 1;
playSound('small_explosion', 3); // 被弾爆発音(小)
// パワーアップレベルを調整する処理
if (powerUpLevel > 0) powerUpLevel--; // パワーアップレベルを下げる
if (powerUpLevel <= 0) powerUpType = null; // レベル0になったとき、パワーアップタイプをリセット
// 自機がダメージを受けた際にビーム発射間隔を更新する
updateFireBeamInterval();
// コンソールに状態を出力
// console.log(`Player Health: ${playerHealth}, PowerUp Level: ${powerUpLevel}, PowerUp Type: ${powerUpType}`);
if(playerHealth > 0){
// プレイヤーが生きているときの衝突用
createParticles(playerX, playerY, "255,165,0", 3, 3); // 自機の衝突パーティクル(オレンジ)を生成
}
if (playerHealth <= 0) {
if(playerDeadTime == 0){
playerDeadTime = currentTime; // プレイヤーがやられた時間を保存
firing = false;
playerVisible = false;
createParticles(playerX, playerY, "255,0,0", 5, 8); // 自機の爆発パーティクル(赤)を生成
// 効果音のリソースを解放
releaseSoundResources();
playSound('ship_explosion', 1); // 自機爆発音
}
}
}
}
// 自機との当たり判定
/* function checkCollisionWithPlayer(bulletX, bulletY, bulletWidth, bulletHeight) {
// ここでは自機の位置とサイズ、弾丸の位置を使って簡易的な当たり判定を行う
// 自機のサイズや位置に応じて調整が必要
const playerHitBox = { x: playerX - 32, y: playerY - 32, width: 64, height: 64 }; // 自機のヒットボックスを仮定
if (bulletX < playerHitBox.x + playerHitBox.width &&
bulletX + bulletWidth > playerHitBox.x &&
bulletY < playerHitBox.y + playerHitBox.height &&
bulletY + bulletHeight > playerHitBox.y) {
handlePlayerHit(); // 自機がダメージを受けたときの処理を実行
updateFireBeamInterval(); // ビーム発射間隔を更新する
return true; // 当たり判定があったことを示す
}
return false; // 当たり判定がなかったことを示す
} */
function checkCollisionWithPlayer(bulletX, bulletY, bulletRadius) {
const dx = bulletX - playerX; // 敵の弾の中心から自機の中心までのX距離
const dy = bulletY - playerY; // 敵の弾の中心から自機の中心までのY距離
const distance = Math.sqrt(dx * dx + dy * dy); // 中心点間の距離
// 当たり判定: 自機の当たり判定の円(半径8ピクセル)と敵の弾の半径の和と中心点間の距離を比較
if (distance < 8 + bulletRadius) {
handlePlayerHit(); // 自機がダメージを受けたときの処理を実行
updateFireBeamInterval(); // ビーム発射間隔を更新する
return true; // 当たり判定があったことを示す
}
return false; // 当たり判定がなかったことを示す
}
// 自機の描写を更新
function updatePlayer() {
// 自機の画像を描画
// 自機のサイズが64x64ピクセルであると仮定して、playerX, playerYは自機の中心座標を表すようにします。
if (playerVisible) {
if (!invincible || Math.floor(Date.now() / 50) % 2) { // 無敵状態で点滅
ctx.drawImage(playerImage, playerX - 32, playerY - 32, 64, 64);
}
}
// 自機の当たり判定の範囲を緑の線で描画(デバッグ用)
/* ctx.strokeStyle = 'green';
ctx.beginPath();
ctx.arc(playerX, playerY, 8, 0, Math.PI * 2);
ctx.stroke(); */
/* // 敵との当たり判定の円を描画(デバッグ用)
// 自機の見た目の中央に円が来るように、playerX, playerYを直接使用します。
ctx.beginPath();
ctx.arc(playerX, playerY, 8, 0, Math.PI * 2);
ctx.strokeStyle = 'red';
ctx.stroke();
// パワーアップアイテムとの当たり範囲を示す緑の線で囲む(デバッグ用)
ctx.strokeStyle = 'green'; // 線の色を緑に設定
ctx.strokeRect(playerX - 32, playerY - 32, 64, 64); // 自機のサイズに合わせて線を描画 */
}
// ビームと敵の当たり判定
function updateBeams() {
for (let i = beams.length - 1; i >= 0; i--) {
const beam = beams[i];
// ビームの位置更新
beam.x += beam.velocityX || 0;
beam.y += beam.velocityY || -beam.speed; // 貫通反射弾の場合はvelocityYを使用
// ビームのタイプに応じた描画
if (beam.type === "piercing") {
// 貫通反射弾の描画
if(powerUpLevel < 5){
ctx.fillStyle = '#00BFFF'; // 青白い色
} else {
if (powerUpType === "piercing") {
ctx.fillStyle = '#AA00FF'; // 青紫色(piercing 最大レベル)
} else {
ctx.fillStyle = '#FF00AA'; // 赤紫色(rapidFire 最大レベル)
}
}
ctx.beginPath();
ctx.arc(beam.x, beam.y, beam.radius, 0, Math.PI * 2);
ctx.fill();
// 彗星の尾のようなパーティクルを生成
createParticles(beam.x, beam.y, '135,206,250', 0.5, 5, 1, 0.3, 0.4);
// 壁での反射処理
if (beam.x <= 0 || beam.x >= canvas.width) {
beam.velocityX = -beam.velocityX;
}
// レベル5以上で下端で反射
if (beam.y + beam.radius >= canvas.height && beam.velocityY > 0 && powerUpLevel >= 5) {
beam.velocityY = -beam.velocityY;
}
} else {
// 連射弾・通常のビームの描画
if(powerUpLevel < 5){
ctx.fillStyle = '#ff0'; // 黄色
} else {
if (powerUpType === "rapidFire") {
ctx.fillStyle = '#fa0'; // オレンジ色(rapidFire 最大レベル)
} else {
ctx.fillStyle = '#0af'; // 明るい青色(piercing 最大レベル)
}
}
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 (enemy.opacity < 1) {
continue;
}
// 貫通反射弾の場合、円形の当たり判定を使用
if (beam.type === "piercing") {
const dx = enemy.x + enemy.size / 2 - beam.x;
const dy = enemy.y + enemy.size / 2 - beam.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 当たり判定:ビームの半径と敵の半径の和と中心点間の距離を比較
if (distance < beam.radius + enemy.size / 2) {
enemy.health -= 1; // 敵のHPを減少させる
playSound('beam_hit', 0.06); // 敵にビームヒット音
if (enemy.health <= 0) {
if (enemy.itemType && enemy.itemType > 0) {
dropItem(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, enemy.itemType);
// ボスの場合
if (enemy.boss) {
// ボス報酬パワーアップアイテム
dropBossItems(enemy);
}
}
enemies.splice(j, 1); // 敵を配列から削除
createParticles(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, "0, 255, 255", 2, 3); // 敵の爆発パーティクル
playSound('medium_explosion', 3); // 敵の爆発音(中)
}
// 貫通反射弾は敵に当たっても削除しない
}
} else {
// 通常弾と連射弾の矩形の当たり判定
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; // 敵のHPを減少させる
playSound('beam_hit', 0.06); // 敵にビームヒット音
if (enemy.health <= 0) {
if (enemy.itemType && enemy.itemType > 0) {
dropItem(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, enemy.itemType);
// ボスの場合
if (enemy.boss) {
// ボス報酬パワーアップアイテム
dropBossItems(enemy);
}
}
enemies.splice(j, 1);
createParticles(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, "0, 255, 255", 2, 3);
playSound('medium_explosion', 3); // 敵の爆発音(中)
}
beams.splice(i, 1); // 通常弾と連射弾は敵に当たったら削除
break; // 一つの敵に対して一度だけ効果を適用
}
}
}
// 画面外に出たビームを配列から削除(貫通反射弾は上下限のみ考慮)
if (beam.y < 0 || beam.y > canvas.height || (beam.type !== "piercing" && (beam.x < 0 || beam.x > canvas.width))) {
beams.splice(i, 1);
}
}
}
// 自機と敵の円形当たり判定
function checkPlayerCollision() {
for (let i = enemies.length - 1; i >= 0; i--) {
const enemy = enemies[i];
// 敵が透明化中であれば当たり判定をスキップ
if (enemy.opacity < 1) {
continue;
}
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;
playSound('small_explosion', 3); // 被弾爆発音(小)
// パワーアップレベルを調整する処理
if (powerUpLevel > 0) powerUpLevel--; // パワーアップレベルを下げる
if (powerUpLevel <= 0) powerUpType = null; // レベル0になったとき、パワーアップタイプをリセット
// 自機がダメージを受けた際にビーム発射間隔を更新する
updateFireBeamInterval();
// コンソールに状態を出力
// console.log(`Player Health: ${playerHealth}, PowerUp Level: ${powerUpLevel}, PowerUp Type: ${powerUpType}`);
if(playerHealth > 0){
// プレイヤーが生きているときの衝突用
createParticles(playerX, playerY, "255,165,0", 3, 3); // 自機の衝突パーティクル(オレンジ)を生成
}
if (playerHealth <= 0) {
if(playerDeadTime == 0){
playerDeadTime = currentTime; // プレイヤーがやられた時間を保存
firing = false;
playerVisible = false;
createParticles(playerX, playerY, "255,0,0", 5, 8); // 自機の爆発パーティクル(赤)を生成
// 効果音のリソースを解放
releaseSoundResources();
playSound('ship_explosion', 1); // 自機爆発音
}
}
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 を設定
shoots: pattern.shoots, // ここで shoots プロパティを設定
shootInterval: pattern.shootInterval, // ここで shootInterval を設定
shootTimer: pattern.shootInterval, // ここで shootTimer を設定
};
enemies.push(newEnemy);
}
// 出現した敵はスケジュールから削除
enemySpawnPatterns.splice(index, 1);
}
});
}
// 敵の動作の更新処理
function updateEnemies(deltaTime) {
enemies.forEach((enemy, index) => {
switch (enemy.movement) {
case 'linear': // 1 直線的に進む。
enemy.y += 1.5; // 直線的に下に移動
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 += 2; // やや速い下降を維持
break;
case 'dive': // 4 一定距離進んだ後、急降下する。
if (enemy.y < 150) {
enemy.y += 2; // 一定距離まで直線的に移動
} else {
enemy.y += 3.5; // 急降下
}
break;
case 'looping': // ループや円を描くように動く。
if (!enemy.loopCounter) enemy.loopCounter = 0; // ループカウンタの初期化
enemy.loopCounter += 0.1; // カウンタをインクリメント
enemy.x += Math.cos(enemy.loopCounter) * 4; // x軸方向の動きを円形に近づける
enemy.y += Math.sin(enemy.loopCounter) * 4 + 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 += 1;
break;
case 'retreat': // 8 一定距離まで直線的に下降移動した後、逆方向に退避する。
// 敵の動きの状態を管理するフラグが未設定の場合、初期化する
if (enemy.retreatState === undefined) {
enemy.retreatState = 'advancing'; // 'advancing' または 'retreating'
enemy.initialY = enemy.y; // 初期位置を設定
}
// 進行中の動き
if (enemy.retreatState === 'advancing') {
enemy.y += 1.5; // 下に移動
// 一定距離(ここでは550ピクセル)に達したかチェック
if (enemy.y >= enemy.initialY + 550) {
enemy.retreatState = 'retreating'; // 状態を退避中に変更
}
}
// 退避中の動き
else if (enemy.retreatState === 'retreating') {
enemy.y -= 2.5; // 上に移動
// 初期位置よりも上に移動したかチェック
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.03; // 角度を増加させる(動きの速さを調整)
enemy.radius += 0.3; // 半径を徐々に増加させる(螺旋の広がりを調整)
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 += 2; // 下に移動
// 一定距離(ここでは300ピクセル)に達したかチェック
if (enemy.y >= enemy.initialY + 300) {
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 += 1.5 * enemy.direction; // 横方向に固定量で移動
}
break;
case 'horizontalSweep': // 12 水平に大きく移動しながら進む。
enemy.x += Math.sin(enemy.y / 50) * 5; // 横方向に大きく移動
enemy.y += 0.5; // 垂直方向に固定量で進む
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 += 2; // 下に移動
// 一定距離(ここでは100ピクセル)に達したかチェック
if (enemy.y >= enemy.initialY + 100) {
enemy.retreatState = 'retreating'; // 状態を進行後に変更
}
}
// 進行後の動き
else if (enemy.retreatState === 'retreating') {
// 壁や画面端で跳ね返るように動く。
if (!enemy.velocityX) enemy.velocityX = 1;
if (!enemy.velocityY) enemy.velocityY = 1;
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':
if (enemy.behavior) {
enemy.behavior(enemy);
}
break;
case 'bossPattern2':
if (enemy.behavior) {
enemy.behavior(enemy);
}
break;
case 'finalBossPattern':
// ボス専用の動きや行動をここに実装
if (enemy.behavior) {
enemy.behavior();
}
break;
default:
// 未定義の動きがあった場合の処理
console.warn("Undefined movement pattern:", enemy.movement);
break;
}
// ボスの挙動の更新
if (enemy.movement.startsWith('bossPattern')) {
if (enemy.behavior) {
enemy.behavior(enemy);
}
}
// 敵が弾を撃つ設定の場合(大型ボス2以外)
if (enemy.movement !== "bossPattern2") {
if (enemy.shoots) {
// 敵にshootTimerが未設定の場合、初期化する
if (enemy.shootTimer === undefined) {
enemy.shootTimer = enemy.shootInterval;
}
// タイマーを更新
enemy.shootTimer -= deltaTime;
// タイマーが0以下になったら弾を発射し、タイマーをリセット
if (enemy.shootTimer <= 0) {
enemyShoots(enemy);
enemy.shootTimer = enemy.shootInterval;
}
}
}
// 画面外に出た敵を削除
if (enemy.y > canvas.height + enemy.size) {
enemies.splice(index, 1);
}
});
}
// 敵の描画
function drawEnemies() {
enemies.forEach(enemy => {
const img = loadedImages[enemy.imageSrc]; // プリロードされた画像を取得
if (img) {
if (enemy.opacity) {
// 透明度がある敵の描画
ctx.save();
console.log(`Drawing enemy at opacity: ${enemy.opacity}`); // 描画時の透明度のログ
// 透明度が0未満、1超えにならないように調整
/* if (boss2Opacity < 0) {
boss2Opacity = 0;
} else if (boss2Opacity > 1) {
boss2Opacity = 1;
} */
ctx.globalAlpha = enemy.opacity;
// ctx.globalAlpha = boss2Opacity;
ctx.drawImage(img, enemy.x, enemy.y, enemy.size, enemy.size);
ctx.restore();
} else {
// 通常の敵を描画
ctx.save();
ctx.drawImage(img, enemy.x, enemy.y, enemy.size, enemy.size);
ctx.restore();
}
// 透明度に応じて描画スタイルを変更
}
});
}
// イベントリスナーで座標の変換
function getCanvasRelativePosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
}
// パワーアップアイテムの出現
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: 1.5, // アイテムの下降速度
});
}
}
// パワーアップアイテムの描画と更新
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);
// 当たり範囲を示す緑の線で囲む(デバッグ用)
/* ctx.strokeStyle = 'green'; // 線の色を緑に設定
ctx.strokeRect(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];
// アイテムの中心座標を計算
const itemCenterX = item.x + 16; // アイテムの幅の半分
const itemCenterY = item.y + 16; // アイテムの高さの半分
// 自機の中心座標を計算(自機の画像が64x64pxと仮定)
// 修正: 自機の中心座標の計算方法を修正。playerX, playerYはすでに自機の中心座標を指している
const playerCenterX = playerX; // playerXは自機の中心座標
const playerCenterY = playerY; // playerYは自機の中心座標
// 中心点間の距離を計算
const dx = itemCenterX - playerCenterX;
const dy = itemCenterY - playerCenterY;
const distance = Math.sqrt(dx * dx + dy * dy);
// 当たり判定の範囲を決定(自機とアイテムのサイズに基づいて調整)
if (distance < (32 + 16)) { // 自機の半径とアイテムの半径の和
// アイテムを取得した場合の処理
switch (item.type) {
case 1: // 連射弾強化のアイテム
powerUpType = 'rapidFire';
if (powerUpLevel < 6) powerUpLevel++;
break;
case 2: // 貫通反射弾強化のアイテム
powerUpType = 'piercing';
if (powerUpLevel < 6) powerUpLevel++;
break;
// 他のアイテムタイプに応じた処理
}
if (playerHealth < 5) {
playerHealth += 1; // 自機回復
}
// パワーアップアイテムの取得時にビーム発射間隔を更新する
updateFireBeamInterval();
// コンソールに状態を出力
// console.log(`Player Health: ${playerHealth}, PowerUp Level: ${powerUpLevel}, PowerUp Type: ${powerUpType}`);
playSound('power_up', 0.5); // パワーアップ音
// アイテムを削除
powerUpItems.splice(i, 1);
}
}
}
// ゲームの初期化
function initializeGame() {
currentTime = 0;
playerHealth = 5;
gameOver = false;
firing = true;
playerVisible = true;
playerX = canvas.width / 2;
playerY = canvas.height - 50;
powerUpLevel = 0;
powerUpType = null;
invincible = false; // 無敵状態を解除
invincibleTime = 0; // 無敵時間をリセット
currentTime = 0;
playerDeadTime = 0;
// タイマーとゲームをリセット
timer = 5 * 60 * 1000;
timerEnded = false;
// 配列やリソースをクリアする
beams.length = 0;
enemies.length = 0;
particles.length = 0;
powerUpItems.length = 0;
bullets.length = 0; // 敵の弾をクリア
// ビーム発射のスケジュールをリセット
clearInterval(beamIntervalId);
beamIntervalId = setInterval(fireBeam, 200);
// 敵の出現パターンをコピーしてリセット
enemySpawnPatterns = originalEnemySpawnPatterns.map(pattern => ({ ...pattern }));
// ゲーム開始時にBGMを再生
setBgmVolume(0.03); // BGMボリューム設定(0-1)
startBgm(); // BGM再生
}
// ゲームオーバー時のテキスト表示
function showGameOver() {
ctx.fillStyle = 'red';
ctx.font = '48px Arial';
ctx.textAlign = 'center'; // 中央揃えに設定
ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2);
}
// ゲームオーバー時のイベントリスナー追加
function setupGameOverListener() {
canvas.addEventListener('click', function restartGame() {
if (gameOver) {
initializeGame();
// ゲームループを再開する
requestAnimationFrame(gameLoop);
// このリスナーが再度実行されることを防ぐためにリスナーを削除
canvas.removeEventListener('click', restartGame);
}
});
}
// 自機の移動処理を行う部分(例: マウス移動イベント内)での調整
canvas.addEventListener('mousemove', event => {
const pos = getCanvasRelativePosition(event);
// 自機の中心がキャンバスの端を超えないように調整
playerX = Math.min(Math.max(pos.x, 32), canvas.width - 32);
playerY = Math.min(Math.max(pos.y - 50, 32), canvas.height - 32);
});
canvas.addEventListener('touchmove', event => {
const touch = event.touches[0];
const pos = getCanvasRelativePosition(touch);
// タッチ操作に対しても同様の制約を適用
playerX = Math.min(Math.max(pos.x, 32), canvas.width - 32);
playerY = Math.min(Math.max(pos.y - 50, 32), canvas.height - 32);
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;
});
}
// 星のデータを生成する
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;
}
});
}
// ヘルスアイコンを描画する関数
function drawHealthIcons(health, context, canvas) {
const iconSize = 12; // アイコンのサイズ
const padding = 4; // アイコン間のパディング
const startX = canvas.width - iconSize - padding; // 描画開始位置(X座標)
const startY = canvas.height - (iconSize + padding) * health; // 描画開始位置(Y座標)
for (let i = 0; i < health; i++) {
// アイコンを縦に並べて描画
context.drawImage(healthIcon, startX, startY + (iconSize + padding) * i, iconSize, iconSize);
}
}
// パーティクルの作成
function createParticles(x, y, color, velocityRange, size, count = 30, lifeMin = 0.5, lifeMax = 1.5) {
for (let i = 0; i < count; i++) {
const life = Math.random() * (lifeMax - lifeMin) + lifeMin; // lifeMin から lifeMax の範囲でランダムに設定
particles.push({
x: x,
y: y,
velocityX: (Math.random() - 0.5) * velocityRange,
velocityY: (Math.random() - 0.5) * velocityRange,
color: color,
size: size,
life: life,
});
}
}
// パーティクルの更新
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 showTitleScreen() {
ctx.drawImage(titleBackgroundImage, 0, 0, canvas.width, canvas.height); // 背景画像を表示
ctx.fillStyle = `rgba(255, 255, 255, ${titleOpacity})`;
ctx.font = '48px Arial';
ctx.textAlign = 'center';
ctx.fillText('START', canvas.width / 2, canvas.height * 0.8);
// 点滅効果
titleOpacity += titleFadeDirection;
if (titleOpacity <= 0.2 || titleOpacity >= 1) {
titleFadeDirection *= -1;
}
}
// タイトル画面でのタップイベントを処理
canvas.addEventListener('click', () => {
if (showTitle) {
showTitle = false;
// ブロックワイプアニメーションのためのブロック配列をリセット
for (let y = 0; y < blocksHigh; y++) {
for (let x = 0; x < blocksWide; x++) {
blocks.push({ x: x * blockSize, y: y * blockSize, visible: false });
}
}
}
});
// ブロックワイプアニメーションを実行する関数
function blockWipeTransition() {
// 一度に表示するブロックの数を増やす
let blocksToShow = Math.min(10, blocks.filter(block => !block.visible).length);
for (let i = 0; i < blocksToShow; i++) {
let hiddenBlocks = blocks.filter(block => !block.visible);
let blockToShow = hiddenBlocks[Math.floor(Math.random() * hiddenBlocks.length)];
blockToShow.visible = true;
}
// すべてのブロックが表示されたらブロックをリセット
if (blocks.every(block => block.visible)) {
initializeGame(); // ゲーム初期化
blocks = [];
}
}
// ゲームの更新処理を行う関数
function updateGame() {
const deltaTime = 1 / 60;
currentTime += deltaTime;
if (invincible) {
invincibleTime += deltaTime;
if (invincibleTime >= 2) {
invincible = false;
invincibleTime = 0;
}
}
if (playerDeadTime !== 0 && playerDeadTime + gameOverWaitTime < currentTime) {
gameOver = true;
}
if (!gameOver) {
updateStars();
if (!gameOver) {
updatePlayer();
updateBeams();
}
updateAndDrawBullets(ctx);
spawnEnemies();
updateEnemies(deltaTime);
checkPlayerCollision();
drawEnemies();
updateParticles();
drawParticles();
updatePowerUpItems();
checkItemCollection();
drawHealthIcons(playerHealth, ctx, canvas);
drawBossHealthBar(); // ボスの体力ゲージを描画
updateAndDrawTimer(); // タイマーの更新と描画
}
}
// ゲームループを更新する関数
function gameLoop() {
if (showTitle) {
showTitleScreen();
} else {
if (blocks.length > 0) {
blockWipeTransition();
blocks.forEach(block => {
if (block.visible) {
ctx.fillStyle = 'black';
ctx.fillRect(block.x, block.y, blockSize, blockSize);
}
});
} else {
updateGame();
}
}
if (gameOver) {
showGameOver();
setupGameOverListener();
return;
}
requestAnimationFrame(gameLoop);
}
// ゲームループを開始する関数
function startGameLoop() {
gameLoop();
}
// ゲームの初期化と開始
preloadImages(() => {
preloadAudioFiles();
startGameLoop();
});
// 一定間隔でビームを発射
beamIntervalId = setInterval(fireBeam, 200);
// 星の初期化
createStars();
/////////////////////////////////////////////////////////////////////
// ボスの体力ゲージの描画
function drawBossHealthBar() {
const boss = enemies.find(enemy => enemy.movement.startsWith('bossPattern'));
if (boss) {
const barWidth = 200;
const barHeight = 6;
const x = (canvas.width - barWidth) / 2;
const y = 20;
// 背景(赤いバー)
ctx.fillStyle = 'red';
ctx.fillRect(x, y, barWidth, barHeight);
// 体力(黄色いバー)
ctx.fillStyle = 'yellow';
const healthWidth = (boss.health / boss.maxHealth) * barWidth;
ctx.fillRect(x, y, healthWidth, barHeight);
}
}
// ボス撃破時のアイテムドロップ処理
function dropBossItems(boss) {
for (let i = 0; i < 5; i++) {
const itemType = Math.floor(Math.random() * 2) + 1;
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * 128;
const x = boss.x + boss.size / 2 + Math.cos(angle) * distance;
const y = boss.y + boss.size / 2 + Math.sin(angle) * distance;
dropItem(x, y, itemType);
updatePowerUpItems();
}
}
////////////////////////////////////////////////////////////////////////////
// タイマーの初期値(5分)
let timer = 5 * 60 * 1000; // ミリ秒単位
// タイマーが終了したかどうかを追跡するフラグ
let timerEnded = false;
// タイマーの更新と描画
function updateAndDrawTimer() {
if (!timerEnded) {
// 残り時間を計算
timer -= 1000 / 60; // 1フレームあたりの時間を減算(60FPSを想定)
if (timer <= 0) {
timer = 0;
timerEnded = true;
}
}
if (timerEnded) {
// タイマーが終了したときのメッセージを表示
ctx.font = '42px Arial';
ctx.fillStyle = 'white';
ctx.textAlign = 'center'; // 中央揃えに設定
ctx.fillText('Congratulations!', canvas.width / 2, canvas.height / 2);
} else {
// タイマーを表示
const minutes = Math.floor(timer / 60000);
const seconds = Math.floor((timer % 60000) / 1000);
const deciseconds = Math.floor((timer % 1000) / 10);
const timerString = `${minutes} : ${String(seconds).padStart(2, '0')} : ${String(deciseconds).padStart(2, '0')}`;
ctx.font = '20px Arial';
ctx.fillStyle = '#999';
ctx.textAlign = 'left'; // 左揃え
ctx.fillText(timerString, 10, 30);
}
}
// タップで再スタート
canvas.addEventListener('click', () => {
if (timerEnded) {
initializeGame(); // ゲームの初期化関数を呼び出す
}
});
enemySpawnPatterns.js(敵の出現定義の定数宣言)
この定数宣言を書き換えることで、出てくる敵の種類やタイミングを変更することができます
// 敵の出現パターンの定義
export const originalEnemySpawnPatterns = [
{ time: 1, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 2, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 3, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 4, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 5, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 7, type: 'small', variant: 4, position: 2, item: 1, shoots: 0, shootInterval: 3 },
{ time: 7, type: 'small', variant: 4, position: 4, item: 2, shoots: 0, shootInterval: 3 },
{ time: 8, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 9, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 10, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 11, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 12, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 14, type: 'small', variant: 4, position: 2, item: 2, shoots: 0, shootInterval: 3 },
{ time: 14, type: 'small', variant: 4, position: 4, item: 1, shoots: 0, shootInterval: 3 },
{ time: 15, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 15, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 16, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 16, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 17, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 18, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 18, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 19, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 19, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 22, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 22, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 22, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 22, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 23, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 24, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 25, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 26, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 27, type: 'small', variant: 3, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 27, type: 'small', variant: 3, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 28, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 30, type: 'small', variant: 6, position: 1, item: 1, shoots: 1, shootInterval: 3 },
{ time: 30, type: 'small', variant: 6, position: 5, item: 2, shoots: 1, shootInterval: 3 },
{ time: 31, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 32, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 33, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 34, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 34, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 34, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 34, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 34, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 35, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 36, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 37, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 38, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 42, type: 'small', variant: 4, position: 5, item: 1, shoots: 0, shootInterval: 3 },
{ time: 42, type: 'small', variant: 4, position: 1, item: 2, shoots: 0, shootInterval: 3 },
{ time: 44, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 45, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 46, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 49, type: 'small', variant: 5, position: 2, item: 2, shoots: 1, shootInterval: 3 },
{ time: 51, type: 'small', variant: 3, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 54, type: 'small', variant: 5, position: 4, item: 2, shoots: 1, shootInterval: 3 },
{ time: 59, type: 'medium', variant: 1, position: 3, item: 1, shoots: 1, shootInterval: 3 },
{ time: 59, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 60, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 61, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 62, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 63, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 64, type: 'small', variant: 5, position: 2, item: 1, shoots: 1, shootInterval: 3 },
{ time: 64, type: 'small', variant: 4, position: 1, item: 1, shoots: 0, shootInterval: 3 },
{ time: 64, type: 'small', variant: 4, position: 5, item: 2, shoots: 0, shootInterval: 3 },
{ time: 64, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 65, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 66, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 67, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 68, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 69, type: 'small', variant: 5, position: 4, item: 2, shoots: 1, shootInterval: 3 },
{ time: 69, type: 'small', variant: 4, position: 1, item: 2, shoots: 0, shootInterval: 3 },
{ time: 69, type: 'small', variant: 4, position: 5, item: 1, shoots: 0, shootInterval: 3 },
{ time: 69, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 69, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 70, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 70, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 71, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 82, type: 'large', variant: 1, position: 3, item: 2, shoots: 1, shootInterval: 1 },
{ time: 83, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 83, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 83, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 83, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 90, type: 'small', variant: 3, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 90, type: 'small', variant: 3, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 93, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 95, type: 'small', variant: 6, position: 1, item: 1, shoots: 1, shootInterval: 3 },
{ time: 98, type: 'small', variant: 6, position: 5, item: 2, shoots: 1, shootInterval: 3 },
{ time: 99, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 100, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 101, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 102, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 103, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 104, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 105, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 105, type: 'small', variant: 6, position: 3, item: 1, shoots: 1, shootInterval: 3 },
{ time: 120, type: 'small', variant: 5, position: 2, item: 2, shoots: 1, shootInterval: 3 },
{ time: 125, type: 'small', variant: 4, position: 1, item: 1, shoots: 0, shootInterval: 3 },
{ time: 130, type: 'small', variant: 4, position: 5, item: 2, shoots: 0, shootInterval: 3 },
{ time: 135, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 135, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 135, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 135, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 135, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 138, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 138, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 138, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 139, type: 'small', variant: 4, position: 5, item: 1, shoots: 0, shootInterval: 3 },
{ time: 140, type: 'small', variant: 7, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 140, type: 'small', variant: 7, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 141, type: 'small', variant: 4, position: 1, item: 2, shoots: 0, shootInterval: 3 },
{ time: 142, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 142, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 142, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 143, type: 'small', variant: 2, position: 4, item: 0, shoots: 1, shootInterval: 3 },
{ time: 144, type: 'small', variant: 7, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 144, type: 'small', variant: 7, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 145, type: 'small', variant: 2, position: 2, item: 0, shoots: 1, shootInterval: 3 },
{ time: 146, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 146, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 146, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 146, type: 'small', variant: 8, position: 1, item: 0, shoots: 1, shootInterval: 10 },
{ time: 147, type: 'small', variant: 4, position: 3, item: 1, shoots: 0, shootInterval: 3 },
{ time: 147, type: 'small', variant: 2, position: 1, item: 0, shoots: 1, shootInterval: 3 },
{ time: 147, type: 'small', variant: 2, position: 3, item: 0, shoots: 1, shootInterval: 3 },
{ time: 147, type: 'small', variant: 2, position: 5, item: 0, shoots: 1, shootInterval: 3 },
{ time: 148, type: 'small', variant: 8, position: 3, item: 0, shoots: 1, shootInterval: 10 },
{ time: 149, type: 'small', variant: 4, position: 1, item: 2, shoots: 0, shootInterval: 3 },
{ time: 149, type: 'small', variant: 2, position: 2, item: 0, shoots: 1, shootInterval: 3 },
{ time: 149, type: 'small', variant: 2, position: 3, item: 0, shoots: 1, shootInterval: 3 },
{ time: 149, type: 'small', variant: 2, position: 4, item: 0, shoots: 1, shootInterval: 3 },
{ time: 150, type: 'small', variant: 8, position: 5, item: 0, shoots: 1, shootInterval: 10 },
{ time: 151, type: 'small', variant: 3, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 151, type: 'small', variant: 2, position: 3, item: 0, shoots: 1, shootInterval: 3 },
{ time: 153, type: 'small', variant: 3, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 155, type: 'small', variant: 6, position: 1, item: 2, shoots: 1, shootInterval: 3 },
{ time: 155, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 155, type: 'small', variant: 6, position: 3, item: 1, shoots: 1, shootInterval: 3 },
{ time: 155, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 155, type: 'small', variant: 6, position: 5, item: 2, shoots: 1, shootInterval: 3 },
{ time: 160, type: 'small', variant: 6, position: 1, item: 1, shoots: 1, shootInterval: 3 },
{ time: 160, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 160, type: 'small', variant: 6, position: 3, item: 2, shoots: 1, shootInterval: 3 },
{ time: 160, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 163, type: 'medium', variant: 1, position: 1, item: 2, shoots: 1, shootInterval: 3 },
{ time: 163, type: 'medium', variant: 1, position: 5, item: 1, shoots: 1, shootInterval: 3 },
{ time: 163, type: 'medium', variant: 2, position: 3, item: 2, shoots: 2, shootInterval: 3 },
{ time: 163, type: 'small', variant: 3, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 163, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 165, type: 'small', variant: 4, position: 3, item: 1, shoots: 0, shootInterval: 3 },
{ time: 165, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 165, type: 'small', variant: 3, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 165, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 165, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 165, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 167, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 167, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 168, type: 'small', variant: 5, position: 2, item: 2, shoots: 1, shootInterval: 3 },
{ time: 168, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 168, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 168, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 168, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 168, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 168, type: 'small', variant: 5, position: 4, item: 1, shoots: 1, shootInterval: 3 },
{ time: 175, type: 'large', variant: 2, position: 3, item: 1, shoots: 2, shootInterval: 1 },
{ time: 195, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 195, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 195, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 195, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 196, type: 'small', variant: 4, position: 2, item: 1, shoots: 0, shootInterval: 3 },
{ time: 196, type: 'small', variant: 4, position: 4, item: 2, shoots: 0, shootInterval: 3 },
{ time: 197, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 197, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 197, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 198, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 198, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 198, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 199, type: 'small', variant: 4, position: 2, item: 1, shoots: 0, shootInterval: 3 },
{ time: 199, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 200, type: 'small', variant: 5, position: 2, item: 1, shoots: 1, shootInterval: 3 },
{ time: 201, type: 'small', variant: 5, position: 4, item: 2, shoots: 1, shootInterval: 3 },
{ time: 202, type: 'small', variant: 5, position: 2, item: 1, shoots: 1, shootInterval: 3 },
{ time: 203, type: 'small', variant: 5, position: 4, item: 2, shoots: 1, shootInterval: 3 },
{ time: 204, type: 'small', variant: 5, position: 3, item: 1, shoots: 1, shootInterval: 3 },
{ time: 205, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 206, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 207, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 208, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 209, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 212, type: 'small', variant: 4, position: 1, item: 1, shoots: 0, shootInterval: 3 },
{ time: 212, type: 'small', variant: 4, position: 5, item: 1, shoots: 0, shootInterval: 3 },
{ time: 215, type: 'small', variant: 8, position: 2, item: 0, shoots: 1, shootInterval: 10 },
{ time: 215, type: 'small', variant: 8, position: 4, item: 0, shoots: 1, shootInterval: 10 },
{ time: 218, type: 'small', variant: 4, position: 3, item: 2, shoots: 0, shootInterval: 3 },
{ time: 220, type: 'small', variant: 6, position: 1, item: 2, shoots: 1, shootInterval: 3 },
{ time: 220, type: 'small', variant: 6, position: 5, item: 1, shoots: 1, shootInterval: 3 },
{ time: 221, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 222, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 223, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 224, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 225, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 228, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 228, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 228, type: 'small', variant: 7, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 228, type: 'small', variant: 7, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 228, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 230, type: 'small', variant: 8, position: 1, item: 0, shoots: 1, shootInterval: 10 },
{ time: 230, type: 'small', variant: 8, position: 5, item: 0, shoots: 1, shootInterval: 10 },
{ time: 230, type: 'small', variant: 2, position: 2, item: 1, shoots: 0, shootInterval: 3 },
{ time: 230, type: 'small', variant: 2, position: 4, item: 2, shoots: 0, shootInterval: 3 },
{ time: 230, type: 'small', variant: 8, position: 3, item: 0, shoots: 1, shootInterval: 10 },
{ time: 231, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 231, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
{ time: 231, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
{ time: 231, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
{ time: 231, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 232, type: 'small', variant: 6, position: 5, item: 1, shoots: 1, shootInterval: 3 },
{ time: 232, type: 'small', variant: 6, position: 1, item: 2, shoots: 1, shootInterval: 3 },
{ time: 233, type: 'small', variant: 5, position: 2, item: 1, shoots: 1, shootInterval: 3 },
{ time: 234, type: 'small', variant: 5, position: 4, item: 2, shoots: 1, shootInterval: 3 },
{ time: 235, type: 'small', variant: 5, position: 2, item: 1, shoots: 1, shootInterval: 3 },
{ time: 236, type: 'small', variant: 5, position: 4, item: 2, shoots: 1, shootInterval: 3 },
{ time: 237, type: 'small', variant: 5, position: 3, item: 1, shoots: 1, shootInterval: 3 },
{ time: 238, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
{ time: 240, type: 'small', variant: 4, position: 2, item: 2, shoots: 0, shootInterval: 3 },
{ time: 240, type: 'small', variant: 4, position: 3, item: 1, shoots: 0, shootInterval: 3 },
{ time: 240, type: 'small', variant: 4, position: 4, item: 2, shoots: 0, shootInterval: 3 },
{ time: 242, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
{ time: 245, type: 'large', variant: 1, position: 2, item: 2, shoots: 1, shootInterval: 1 },
{ time: 250, type: 'small', variant: 6, position: 4, item: 1, shoots: 1, shootInterval: 3 },
{ time: 260, type: 'large', variant: 1, position: 4, item: 1, shoots: 1, shootInterval: 1 },
{ time: 265, type: 'small', variant: 6, position: 2, item: 2, shoots: 1, shootInterval: 3 },
{ time: 270, type: 'small', variant: 5, position: 2, item: 2, shoots: 1, shootInterval: 3 },
{ time: 275, type: 'small', variant: 5, position: 4, item: 1, shoots: 1, shootInterval: 3 },
{ time: 280, type: 'small', variant: 5, position: 3, item: 2, shoots: 1, shootInterval: 3 },
{ time: 285, type: 'small', variant: 6, position: 4, item: 1, shoots: 1, shootInterval: 3 },
{ time: 290, type: 'small', variant: 6, position: 2, item: 2, shoots: 1, shootInterval: 3 },
// 以下、必要に応じてさらに敵の出現パターンを追加
];
3日目 まとめ
3日目の追加分はこのような内容でした。
- 大型ボス2追加
- 当たり判定の調整
- ゲームバランス調整
- 5分間のカウントダウンタイマー表示、5分攻撃を耐えたらクリア
今後の実装予定
- 大型ボス3(ラスボス)
- 追加のパワーアップとして追従ドローン(グラディ〇スのオプションや、ガン〇ムのファン〇ルのようなもの)※敵の弾を消せる予定
- ゲームバランス調整(難しいけど何度も挑戦したらぎりぎりクリアできる、爽快感がありつつ達成感もあるライン)
- ゲームクリアの判定(ラスボスを倒したらクリア)
- ゲームの進行状況の表示(現在地点表記 or ラスボス登場までの時間表記など)
以上、ROBO船長でした。では、また次回!
コメント