このサイトはアフィリエイトリンクを含んでいます

【HTML5ゲーム開発】敵機に命を吹き込め!攻撃機能の実装方法を解説! Ver1.6

作ってみた!

HTML5とJavaScriptを使ったシューティングゲームの作り方を知りたいですか?今回は、敵機が自機を攻撃してくる機能を追加する方法を紹介しますよ。これで、もっとスリル満点で楽しいゲームになりますね。記事では、基本的な設定から始まり、敵の攻撃のプログラム、弾が当たったかどうかの判定まで、詳しく説明します。初心者の方でも安心して取り組めるように、一つひとつ丁寧に解説しますので、ぜひ挑戦してみてください。この記事を読んで、あなたのゲームをもっと面白くしちゃいましょう!さあ、一緒にHTML5とJavaScriptを使って、最高のシューティングゲームを作りましょう!


敵機の基本動作の実装

ゲームに登場する敵機は、プレイヤーにとって重要な挑戦要素ですね。ここでは、敵機の生成と動き、速度と方向の制御について説明します!

敵機の基本動作の実装

敵機の生成と動き

敵機はランダムに現れて、さまざまな動きをします。これによってゲームの難易度が上がり、プレイヤーは常に緊張感を持ってプレイできますね。敵機の出現位置や動きのパターンをランダムにするためには、乱数を使ったアルゴリズムがとても役立ちます。

例えば、敵機が画面の上部から現れて下に向かって移動する場合、画面の幅全体にわたる任意の位置から出現し、一定の速度で下方に移動します。また、ジグザグや曲線の動きをする敵機を作ることもできます。これらの動きは簡単な数学関数を使って実現できますよ。

コード例:敵機の生成

次に、JavaScriptを使って敵機を生成する例を見てみましょう。このコードでは、敵機が一定間隔で生成され、画面上に追加されていきます。

class Enemy {
    constructor(x, y, speed) {
        this.x = x;
        this.y = y;
        this.speed = speed;
        this.width = 50;
        this.height = 50;
    }

    move() {
        this.y += this.speed;
    }
}

function spawnEnemy() {
    const x = Math.random() * canvas.width; // 敵機の出現位置をランダムに
    const speed = 2 + Math.random() * 3; // 敵機の速度をランダムに
    const enemy = new Enemy(x, 0, speed);
    enemies.push(enemy);
}

setInterval(spawnEnemy, 1000); // 1秒ごとに敵機を生成

このコードでは、Enemyクラスを作成して、敵機の位置や速度を設定しています。moveメソッドで敵機の移動を処理し、spawnEnemy関数で敵機をランダムな位置に生成します。この関数は1秒ごとに実行され、ゲーム画面に次々と敵機が追加されます。

敵機の速度と方向制御

敵機の速度と移動方向の制御は、ゲームの難易度を調整する上で非常に重要です。速度は敵機の座標を変化させるスピードを指しますが、これに変化を持たせることで、ゲームのテンポが変わります。速い敵機はプレイヤーに素早い反応を要求し、遅い敵機はプレイヤーに対策を考える時間を与えます。

方向制御については、直進だけでなく、曲がる、ジグザグに動く、一時停止するなどの動きを組み合わせることで、敵機の動きを予測しにくくできます。これにより、ゲームのリプレイ性が向上し、プレイヤーは新しい攻略法を考える楽しみを味わえます。

これで、敵機の基本動作の実装についての説明は終わりです。

敵機の攻撃パターンの追加

敵機の攻撃パターンは、ゲームの難易度や戦略性を大きく左右する要素です。ここでは、敵機が弾を発射する機能の追加や、さまざまな攻撃パターンの実装方法について説明いたします。

攻撃パターン

敵機の弾の生成

敵機がプレイヤーを攻撃するために弾を発射する機能を追加することで、ゲームの緊張感が増します。この機能は、敵機が特定の間隔で弾を生成し、それが画面上を移動するようにすることで実現されます。以下は、JavaScriptでの敵機の弾生成の基本的なコード例です。

class Bullet {
    constructor(x, y, speed) {
        this.x = x;
        this.y = y;
        this.speed = speed;
        this.width = 5;
        this.height = 10;
    }

    move() {
        this.y += this.speed;
    }
}

function shootBullet(enemy) {
    const bullet = new Bullet(enemy.x + enemy.width / 2, enemy.y + enemy.height, 5);
    bullets.push(bullet);
}

setInterval(() => {
    enemies.forEach(enemy => shootBullet(enemy));
}, 2000); // 2秒ごとに敵機が弾を発射

このコードでは、Bulletクラスを定義し、敵機から弾が発射される動作を表現しています。shootBullet関数は、敵機から弾を発射する際に呼び出され、生成された弾はリストに追加されます。

攻撃パターンの種類

攻撃パターンにはいくつかの種類があります。以下はその主な例です:

  1. 追尾弾: プレイヤーの位置を追跡しながら飛ぶ弾です。プレイヤーに対して非常に高い脅威を与えます。
  2. 拡散弾: 複数の弾が広がるように発射されるパターンで、一度に広範囲を攻撃します。
  3. ランダム攻撃: 弾がランダムな方向に飛ぶため、予測が難しく、プレイヤーの回避を難しくします。

これらのパターンを実装することで、ゲームに多様性と挑戦をもたらします。

コード例:攻撃パターンの実装

次に、これらの攻撃パターンの具体的な実装例を見てみましょう。

function shootHomingBullet(enemy, player) {
    const angle = Math.atan2(player.y - enemy.y, player.x - enemy.x);
    const speed = 3;
    const bullet = new Bullet(enemy.x, enemy.y, speed);
    bullet.dx = Math.cos(angle) * speed;
    bullet.dy = Math.sin(angle) * speed;
    bullets.push(bullet);
}

function shootSpreadBullet(enemy) {
    const numBullets = 5;
    const spreadAngle = Math.PI / 4; // 45度の拡散
    for (let i = 0; i < numBullets; i++) {
        const angle = spreadAngle * (i / (numBullets - 1) - 0.5);
        const speed = 3;
        const bullet = new Bullet(enemy.x, enemy.y, speed);
        bullet.dx = Math.cos(angle) * speed;
        bullet.dy = Math.sin(angle) * speed;
        bullets.push(bullet);
    }
}

ここでは、shootHomingBullet関数で追尾弾の実装を、shootSpreadBullet関数で拡散弾の実装を示しています。追尾弾はプレイヤーの位置に向かって移動し、拡散弾は広範囲に弾をばらまく形で発射されます。

これらの実装により、敵機の攻撃パターンに多様性を持たせ、ゲームの戦略性を向上させることができます。

当たり判定と衝突処理

当たり判定と衝突処理

ゲームの当たり判定と衝突処理は、プレイヤーや敵キャラクターの行動や反応を決定する重要な要素です。この機能は、キャラクター同士や弾とキャラクターが衝突したときに、それがゲームプレイにどのような影響を与えるかを管理します。

当たり判定の基本的な仕組みは、オブジェクト同士が物理的に接触しているかどうかを判断することです。ゲーム内のオブジェクト(キャラクター、敵、弾など)は通常、矩形や円形の当たり判定領域を持っています。例えば、矩形の当たり判定を持つオブジェクト同士の衝突を判定するには、それぞれの矩形の各辺の位置を比較し、重なり合っているかどうかを確認します。一方、円形の当たり判定では、2つの円の中心間の距離が両方の半径の和よりも小さい場合に衝突と判定されます。

衝突処理は、判定された衝突に対してゲームがどのように応答するかを定義します。例えば、プレイヤーの弾が敵に命中した場合、敵のライフが減少し、一定のダメージを受けると設定できます。同様に、敵の弾がプレイヤーに命中した場合、プレイヤーのライフが減少するような処理が行われます。このような衝突処理を正確に行うことは、ゲームのバランスを保ち、公平なプレイ体験を提供するために非常に重要です。

また、高速で動くオブジェクトや複雑な形状のオブジェクトの場合、当たり判定の精度が低下することがあります。そのため、開発者は当たり判定を最適化し、必要に応じて精度を高める工夫をする必要があります。例えば、より小さな当たり判定領域を使用する、または当たり判定の頻度を増やすなどの方法があります。

当たり判定と衝突処理の実装は、ゲームエンジンや開発環境によって異なることが多いですが、基本的な考え方は共通です。これらの処理をうまく組み合わせることで、スムーズでリアルなゲームプレイを実現できます。

弾と敵機の当たり判定

シューティングゲームにおいて、弾と敵機の当たり判定はゲームの根幹を成す重要な要素です。この判定は、弾が敵機に命中したかどうかを判断し、ゲーム内で適切な反応を引き起こすために使用されます。当たり判定の基本的な考え方にはいくつかの方法がありますが、一般的に使用されるのは軸平行境界ボックス(AABB)法と円形境界法です。

軸平行境界ボックス(AABB)法は、敵機や弾を囲む矩形の位置を基に当たり判定を行う方法です。この方法では、各オブジェクトの矩形の左、右、上、下の辺の位置を比較し、これらが重なり合っているかどうかをチェックします。AABB法は計算が高速で、特にシンプルな矩形形状のオブジェクトに対して効果的です。しかし、形状が複雑であったり、角度がついたオブジェクトには対応しにくいという欠点もあります。

一方、円形境界法は、オブジェクトを囲む円形の境界を基に当たり判定を行う方法です。この方法では、2つのオブジェクトの中心間の距離がそれぞれの半径の和よりも小さい場合に衝突と判定します。円形境界法は、円形のオブジェクトや、回転を含む動作をするオブジェクトに対して適しています。これにより、より自然な衝突判定が可能となります。

これらの方法のいずれも、正確な当たり判定を実現するためには、オブジェクトのサイズや速度、形状などを適切に設定する必要があります。ゲームのバランスを保つためにも、当たり判定の精度は非常に重要です。プレイヤーにとって、弾が明確に敵機に当たったにもかかわらず、判定されない場合、ゲームの信頼性が損なわれる可能性があります。一方で、当たり判定が過剰に厳格すぎる場合もプレイヤーにとって不満となるでしょう。

このように、弾と敵機の当たり判定はゲーム開発において非常に重要な要素であり、その正確さとバランスがゲーム体験を大きく左右します。ゲーム開発者は、この判定ロジックを慎重に設計し、テストを通じて最適なバランスを見つけることが求められます。

プレイヤーと敵機の衝突判定

プレイヤーと敵機の衝突判定は、シューティングゲームにおいて重要な要素であり、プレイヤーのライフやゲーム進行に直接影響を与えます。この判定は、ゲームの難易度やプレイヤー体験に大きく影響するため、正確かつバランスの取れた実装が求められます。

プレイヤーキャラクターと敵機の衝突判定を実装する際には、オブジェクトの形状やサイズに基づいて適切な衝突領域を設定する必要があります。一般的には、プレイヤーキャラクターや敵機の形状に応じて、軸平行境界ボックス(AABB)や円形境界が使用されます。AABB法では、プレイヤーと敵機の周りに矩形のボックスを設定し、それらが重なり合うかどうかを確認します。この方法は、特に矩形や簡単な形状のオブジェクトに対して効率的です。

円形境界法では、各オブジェクトを囲む円形の境界を設定し、その中心間の距離が半径の和よりも小さい場合に衝突と判定します。この方法は、特にキャラクターが回転する場合や、円形に近い形状のオブジェクトに対して有効です。円形の当たり判定は、オブジェクトが常に一定の形状を維持しない場合にも、より柔軟に対応できます。

衝突判定が行われた際の処理も重要です。例えば、プレイヤーキャラクターが敵機と衝突した場合、通常はプレイヤーのライフが減少し、特定の条件が満たされるとゲームオーバーとなります。また、敵機が破壊される、または消滅するなどのエフェクトが追加されることで、視覚的なフィードバックが強化され、プレイヤーの没入感が向上します。

衝突判定の精度とリアクションは、ゲームの難易度やプレイヤーの満足度に大きく関わります。過剰に厳しい判定はプレイヤーにとって不満の原因となる可能性がありますが、逆に緩すぎる判定もゲームのチャレンジ性を損ないます。これらのバランスを適切に調整することが、ゲーム開発における重要な課題となります。

これらの要素を考慮しながら、プレイヤーと敵機の衝突判定は、ゲーム全体の設計と調和した形で実装されるべきです。これにより、プレイヤーは一貫したルールとフィードバックを通じてゲームに対する理解を深め、より楽しむことができます。

衝突時の処理とエフェクト

シューティングゲームにおいて、プレイヤーキャラクターや敵機が衝突した際の処理とエフェクトは、ゲームの視覚的魅力とプレイ感に大きな影響を与えます。これらの要素は、衝突の結果をプレイヤーに明確に伝え、ゲームの進行やスコアリングに関与します。

まず、衝突が発生した際の視覚エフェクトについてです。視覚エフェクトは、衝突のインパクトを強調し、プレイヤーに対して瞬時にフィードバックを提供します。一般的なエフェクトには、爆発、フラッシュ、スパークなどが含まれます。例えば、敵機がプレイヤーの弾に当たった場合、爆発エフェクトが表示されることがよくあります。これにより、プレイヤーは視覚的に敵機が破壊されたことを確認できます。同様に、プレイヤーのキャラクターがダメージを受けた場合、画面の一部が赤く点滅するなどのエフェクトが使われることがあります。

次に、衝突時のゲーム内処理についてです。衝突が検出されると、ゲームはその結果に基づいてさまざまな処理を実行します。例えば、プレイヤーが敵機に命中した場合、通常はスコアが増加します。スコアリングはゲームの進行において重要な役割を果たし、プレイヤーに達成感を与えます。また、特定の敵機を破壊することでアイテムがドロップしたり、新たな敵が出現したりする場合もあります。

逆に、プレイヤーが敵機や弾に当たった場合には、プレイヤーのライフが減少することが一般的です。ライフがゼロになるとゲームオーバーとなり、ゲームの終了条件が満たされます。このようなシステムは、プレイヤーに対して挑戦を提供し、ゲームの緊張感を高めます。

さらに、衝突後の状態遷移も重要です。例えば、敵機が破壊された後の消失エフェクトや、プレイヤーキャラクターの一時的な無敵状態などがあります。これらの要素は、ゲームのバランスを調整し、プレイヤーが次のアクションを考える時間を提供します。

これらの衝突時の処理とエフェクトは、ゲームデザインの一部として慎重に考慮され、実装されるべきです。視覚的なフィードバックとゲーム内の結果は、プレイヤーの没入感を高め、ゲームのリプレイ性を向上させる重要な要素です。

パフォーマンスと最適化

ゲーム開発において、パフォーマンスの最適化は非常に重要な課題です。特にアクションやシューティングゲームでは、フレームレートの安定性や滑らかな動作がプレイヤーの体験に大きく影響します。ゲームがカクついたり、遅延が発生したりすると、プレイヤーは没入感を失い、ゲームの評価にも影響を与える可能性があります。そのため、メモリ管理や処理の最適化は不可欠です。このセクションでは、オブジェクトプールの活用、requestAnimationFrameを用いたゲームループの最適化、そしてデバッグとテストの手法について詳しく説明します。

オブジェクトプールの活用

オブジェクトプールは、ゲームのパフォーマンスを向上させるための効果的な技術です。ゲーム内で頻繁に生成・破棄されるオブジェクト、例えば弾丸、エフェクト、NPCなどを管理するために使用されます。オブジェクトプールの主な目的は、新しいオブジェクトの生成と不要になったオブジェクトの破棄によるメモリ割り当てとガベージコレクションの負荷を削減することです。

具体的には、必要なオブジェクトを予め一定数プールに用意しておき、ゲームプレイ中に新しいオブジェクトが必要になった場合、そのプールから利用可能なオブジェクトを取り出して使用します。使用後のオブジェクトは再度プールに戻され、再利用されます。これにより、頻繁なオブジェクト生成によるメモリアロケーションを回避し、パフォーマンスの向上が図られます。

オブジェクトプールの導入は、特に弾幕シューティングやリアルタイムストラテジーなど、多数のオブジェクトが動的に生成されるゲームにおいて効果的です。例えば、シューティングゲームでは一度に数百発もの弾丸が発射されることがありますが、オブジェクトプールを使用することで、これらの弾丸オブジェクトの生成と破棄のコストを大幅に削減できます。

requestAnimationFrameによるゲームループの最適化

requestAnimationFrameは、ブラウザベースのゲームやアニメーションにおいて、スムーズなフレームレートを実現するための標準的な方法です。従来、JavaScriptではsetIntervalsetTimeoutを使用して一定間隔で描画処理を行っていましたが、これらの方法ではフレームレートが不安定になりがちで、またバッテリー消費が多いという欠点がありました。

requestAnimationFrameは、ブラウザのリフレッシュレートに同期して描画を行うため、フレームの間隔が一定で、よりスムーズなアニメーションを実現します。このメソッドは、バックグラウンドタブでは自動的に更新頻度を下げるため、リソースの消費も抑えることができます。

ゲームループにおいてrequestAnimationFrameを利用することで、アニメーションや動的なオブジェクトの動きをより自然で滑らかに見せることができます。特に、高速で移動するオブジェクトや、多数のエフェクトが表示されるシーンにおいて、その効果は顕著です。これにより、プレイヤーは快適なゲーム体験を享受でき、ゲーム全体のクオリティが向上します。

デバッグとテスト方法

ゲーム開発におけるデバッグとテストは、最終的な製品の品質を確保するための重要な工程です。これらのプロセスを通じて、バグを検出し、修正することで、安定したゲームプレイを提供することができます。デバッグの際には、エラーログの確認、デバッガーツールの使用、または特定の機能やイベントのシミュレーションが行われます。

テスト方法としては、ユニットテスト、統合テスト、システムテスト、プレイテストが一般的です。ユニットテストは、個々のモジュールや機能が単独で正しく動作するかを検証します。統合テストは、複数のモジュールが組み合わさったときに、正しく連携して動作するかを確認します。システムテストは、システム全体としての機能やパフォーマンスを評価します。

プレイテストは、実際のプレイヤーがゲームをプレイすることで、全体的なバランスやゲームプレイの質を評価します。この過程で得られたフィードバックは、ゲームの改善に役立てられます。また、プレイテストを通じて発見されたバグや不具合は、リリース前に修正されるため、製品の品質向上に大いに貢献します。

デバッグとテストは、開発プロセスの早い段階から繰り返し行われるべきです。これにより、バグの早期発見と修正が可能になり、最終的なリリース時のトラブルを防ぐことができます。これらの工程を徹底することで、高品質で信頼性の高いゲームを提供することができます。

まとめ

あんちゃん
あんちゃん

Ver1.6では、敵機の攻撃機能がたくさん改善されましたよ。これにより、ゲームがもっと楽しくなって、プレイヤーのみなさんが挑戦しがいのあるものになりました!新しく追加された追尾弾や拡散弾、そしてランダムな攻撃のおかげで、ゲームがさらにワクワクするものになりましたね。これで、敵の動きがもっと予測できなくなって、プレイヤーの皆さんは様々な戦略を考えなきゃいけなくなりました。

また、攻撃の頻度やスピードも調整されて、どんなレベルのプレイヤーさんでも楽しめるようになったんです。特に、初心者の方からベテランの方まで、みんなが楽しめるように難易度がうまくバランス取られているのがいいですね。さらに、攻撃時のエフェクトも追加されて、ゲームの雰囲気がもっと盛り上がるようになりました!

でも、いくつか課題もありますね。一部のプレイヤーさんからは、「ちょっと難しすぎるかも!」という声も聞かれました。これには、攻撃パターンをさらに調整して、難易度設定をもっと細かくする必要がありそうです。それから、新しい攻撃パターンについてのチュートリアルももっと分かりやすくすると、みなさんが楽しんでプレイしやすくなると思います。

これからのアップデートでは、こういった課題を解決しながら、さらにバランスの取れたゲーム体験を提供することを目指します!プレイヤーさんの声をたくさん聞いて、もっと楽しいゲームにしていくので、ぜひフィードバックをくださいね!

これからもみんなで一緒に楽しめるゲームを作っていきましょう!

Ver1.6 敵機に攻撃機能を追加 まとめ
あんちゃん
あんちゃん

いよいよ、敵機へ攻撃機能を実装したコードだよ。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Shooting Game Ver1.6</title>
  <style>
    body, html {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
      display: flex;
      flex-direction: column;
      background-color: black;
    }
    #scoreContainer {
      font-size: 20px;
      background-color: rgba(255, 255, 255, 0.7);
      padding: 10px;
      position: fixed; /* スコアとスタートボタンを画面上部に固定 */
      top: 0;
      width: 100%;
      display: flex;
      justify-content: space-between;
      box-sizing: border-box;
      z-index: 10; /* スコアとスタートボタンがキャンバスの上に表示されるようにする */
    }
    #startButton {
      flex: none;
    }
    #gameContainer {
      position: relative;
      width: 100%;
      height: calc(100% - 160px); /* 上下の帯分の高さを引く */
      margin-top: 60px; /* 上部の帯の高さ */
      margin-bottom: 100px; /* 下部の帯の高さ */
      display: flex;
      justify-content: center;
      align-items: center;
    }
    #gameCanvas {
      width: 100%;
      height: 100%;
      display: block;
      object-fit: contain; /* 縦横比を維持する */
    }
    #finalScore {
      font-size: 30px;
      color: red;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background-color: rgba(255, 255, 255, 0.7);
      padding: 10px;
      display: none;
      width: 100%;
      text-align: center;
    }
    #controls {
      position: fixed; /* ボタンを画面下部に固定 */
      bottom: 0;
      width: 100%;
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      align-items: center;
      padding: 10px 20px; /* ボタンをキャンバスから離すためにパディングを追加 */
      box-sizing: border-box;
      z-index: 10; /* ボタンがキャンバスの上に表示されるようにする */
      background-color: rgba(0, 0, 0, 0.5); /* ボタンエリアの背景色を追加 */
    }
    .control-button {
      width: 60px;
      height: 60px;
      background-color: rgba(255, 255, 255, 0.7);
      border: none;
      border-radius: 50%;
      font-size: 20px;
      font-weight: bold;
      color: black;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    #moveControl {
      width: 140px;
      display: flex;
      justify-content: space-between;
    }
    .connected-button {
      border-radius: 0;
      flex-grow: 1;
      margin: 0;
    }
    .connected-button:first-child {
      border-top-left-radius: 50%;
      border-bottom-left-radius: 50%;
    }
    .connected-button:last-child {
      border-top-right-radius: 50%;
      border-bottom-right-radius: 50%;
    }
  </style>
</head>
<body>
  <div id="scoreContainer">
    <div id="scoreDisplay">スコア: 0</div>
    <button id="startButton" onclick="startGame()">START</button>
  </div>
  <div id="gameContainer">
    <canvas id="gameCanvas" width="480" height="600"></canvas>
    <div id="finalScore">GAME OVER<br>SCORE: 0</div>
  </div>
  <div id="controls">
    <button class="control-button" id="fireButton">&#x1F52B;</button>
    <div id="moveControl">
      <button class="control-button connected-button" id="leftButton">&larr;</button>
      <button class="control-button connected-button" id="rightButton">&rarr;</button>
    </div>
  </div>
  <script>
    // ゲーム設定
    const canvas = document.getElementById("gameCanvas");
    const ctx = canvas.getContext("2d");

    function resizeCanvas() {
      const container = document.getElementById('gameContainer');
      const width = container.clientWidth;
      const height = width * (600 / 480);
      canvas.width = width;
      canvas.height = height;
      shipX = canvas.width / 2 - shipWidth / 2;
      shipY = canvas.height - shipHeight - 10;
      ship = { x: shipX, y: shipY, width: shipWidth, height: shipHeight, dx: 0 };
    }

    window.addEventListener('resize', resizeCanvas);
    window.addEventListener('load', resizeCanvas);

    // アニメーションフレームID
    let animationFrameId;

    // 背景の星を描画する関数
    function drawStars() {
      const numStars = 100;
      for (let i = 0; i < numStars; i++) {
        const x = Math.random() * canvas.width;
        const y = Math.random() * canvas.height;
        const radius = Math.random() * 1.5;
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, Math.PI * 2);
        ctx.fillStyle = "white";
        ctx.fill();
      }
    }

    // 自機画像の読み込み
    const shipImage = new Image();
    shipImage.src = 'https://tokodomo.xyz/wp-content/uploads/2024/07/jiki.png';

    const shipWidth = 50;
    const shipHeight = 50;
    let shipX = canvas.width / 2 - shipWidth / 2;
    let shipY = canvas.height - shipHeight - 10;

    let ship = { x: shipX, y: shipY, width: shipWidth, height: shipHeight, dx: 0 };
    let bullets = [];
    let enemyBullets = []; // 敵の弾
    let enemies = [];
    let explosions = [];
    let score = 0;
    let gameOver = false;
    let gameOverTimeout;

    // 敵機画像の読み込み
    const enemyImage = new Image();
    enemyImage.src = 'https://tokodomo.xyz/wp-content/uploads/2024/07/teki.png';

    // 爆発エフェクト画像の読み込み
    const explosionImages = [
      new Image(),
      new Image()
    ];
    explosionImages[0].src = 'https://tokodomo.xyz/wp-content/uploads/2024/07/shooting_ver1.4_bom1.png'; // 1枚目の画像のパスを指定
    explosionImages[1].src = 'https://tokodomo.xyz/wp-content/uploads/2024/07/shooting_ver1.4_bom2.png'; // 2枚目の画像のパスを指定

    // 爆発エフェクトの設定
    const explosionFrameWidth = 64; // 各フレームの幅
    const explosionFrameHeight = 64; // 各フレームの高さ
    const explosionFrameCount = 16; // フレーム数

    const keys = {}; // キーの状態を追跡するためのオブジェクト

    document.addEventListener("keydown", keyDownHandler);
    document.addEventListener("keyup", keyUpHandler);
    document.addEventListener("keypress", keyPressHandler);

    function keyDownHandler(e) {
      keys[e.key] = true; // キーの状態を更新

      if (keys["ArrowRight"] && !keys["ArrowLeft"]) {
        ship.dx = 5;
      } else if (keys["ArrowLeft"] && !keys["ArrowRight"]) {
        ship.dx = -5;
      } else if (keys["ArrowRight"] && keys["ArrowLeft"]) {
        ship.dx = 0; // 両方のキーが押された場合は停止
      }
    }

    function keyUpHandler(e) {
      keys[e.key] = false; // キーの状態を更新

      if (keys["ArrowRight"] && !keys["ArrowLeft"]) {
        ship.dx = 5;
      } else if (keys["ArrowLeft"] && !keys["ArrowRight"]) {
        ship.dx = -5;
      } else {
        ship.dx = 0; // どちらのキーも押されていない場合は停止
      }
    }

    function keyPressHandler(e) {
      if (e.key === "z" || e.key === "Z") {
        bullets.push({ x: ship.x + ship.width / 2 - 2.5, y: ship.y, width: 5, height: 10, dy: -5 });
      }
    }

    function fireBullet(e) {
      e.preventDefault(); // ここでイベントを防ぐ
      bullets.push({ x: ship.x + ship.width / 2 - 2.5, y: ship.y, width: 5, height: 10, dy: -5 });
    }

    // モバイル用ボタンのイベントリスナーを追加
    if (window.matchMedia("(max-width: 600px)").matches) {
      document.getElementById("fireButton").addEventListener("touchstart", fireBullet);

      const moveControl = document.getElementById("moveControl");
      let initialTouchX = null;
      const touches = {}; // タッチイベントを管理するためのオブジェクト

      moveControl.addEventListener("touchstart", (e) => {
        for (const touch of e.touches) {
          touches[touch.identifier] = touch.clientX;
        }
      });

      moveControl.addEventListener("touchmove", (e) => {
        for (const touch of e.touches) {
          const previousX = touches[touch.identifier];
          if (previousX !== undefined) {
            const deltaX = touch.clientX - previousX;
            ship.x += deltaX * 0.9; // スワイプの移動量を増やす
            if (ship.x < 0) ship.x = 0;
            if (ship.x + ship.width > canvas.width) ship.x = canvas.width - ship.width;
            touches[touch.identifier] = touch.clientX;
          }
        }
        e.preventDefault(); // 拡大・縮小を防ぐ
      });

      moveControl.addEventListener("touchend", (e) => {
        for (const touch of e.changedTouches) {
          delete touches[touch.identifier];
        }
      });

      // 拡大・縮小を防ぐ
      window.addEventListener("touchstart", function(e) {
        if (e.touches.length > 1) {
          e.preventDefault();
        }
      }, { passive: false });

      window.addEventListener("gesturestart", function(e) {
        e.preventDefault();
      });
      window.addEventListener("gesturechange", function(e) {
        e.preventDefault();
      });
      window.addEventListener("gestureend", function(e) {
        e.preventDefault();
      });
    }

    function drawShip() {
      ctx.drawImage(shipImage, ship.x, ship.y, ship.width, ship.height);
    }

    function drawBullets() {
      ctx.fillStyle = "#FF0000";
      bullets.forEach((bullet, index) => {
        ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
        bullet.y += bullet.dy;
        if (bullet.y < 0) {
          bullets.splice(index, 1);
        }
      });
    }

    function drawEnemyBullets() {
      ctx.fillStyle = "#FFFF00";
      enemyBullets.forEach((bullet, index) => {
        ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
        bullet.y += bullet.dy;
        if (bullet.y > canvas.height) {
          enemyBullets.splice(index, 1);
        }
      });
    }

    function drawEnemies() {
      enemies.forEach((enemy, index) => {
        ctx.drawImage(enemyImage, enemy.x, enemy.y, enemy.width, enemy.height);
        enemy.y += enemy.dy;
        if (enemy.y > canvas.height) {
          enemies.splice(index, 1);
        }
        // 敵機が弾を撃つ
        if (Math.random() < 0.01) {
          enemyBullets.push({ x: enemy.x + enemy.width / 2 - 2.5, y: enemy.y + enemy.height, width: 5, height: 10, dy: 5 });
        }
      });
    }

    function createEnemies() {
      if (Math.random() < 0.02) {
        let enemyX = Math.random() * (canvas.width - 30);
        enemies.push({ x: enemyX, y: 0, width: 30, height: 30, dy: 2 });
      }
    }

    function drawExplosions() {
      explosions.forEach((explosion, index) => {
        const frame = Math.floor(explosion.frame / 4); // フレームをスローダウン
        if (frame >= explosionFrameCount) {
          explosions.splice(index, 1); // 爆発アニメーション終了
          return;
        }

        // ランダムに爆発画像を選択
        const image = explosionImages[Math.floor(Math.random() * explosionImages.length)];

        // 爆発画像の中心を敵機の中心に合わせる
        const explosionX = explosion.x - (explosion.width / 2);
        const explosionY = explosion.y - (explosion.height / 2);

        ctx.drawImage(
          image,
          0, 0, image.width, image.height,
          explosionX, explosionY, explosion.width, explosion.height
        );
        explosion.frame++;
        explosion.y += 1; // 爆発が少し下に流れるようにする
      });
    }

    function detectCollisions() {
      bullets.forEach((bullet, bulletIndex) => {
        enemies.forEach((enemy, enemyIndex) => {
          if (
            bullet.x < enemy.x + enemy.width &&
            bullet.x + bullet.width > enemy.x &&
            bullet.y < enemy.y + enemy.height &&
            bullet.y + bullet.height > enemy.y
          ) {
            bullets.splice(bulletIndex, 1);
            enemies.splice(enemyIndex, 1);
            score += 10;
            updateScore();
            // ランダムに爆発画像を選択し、爆発の大きさを敵機のサイズにリサイズ
            const useFirstImage = Math.random() < 0.5;
            explosions.push({
              x: enemy.x + enemy.width / 2,
              y: enemy.y + enemy.height / 2,
              width: enemy.width,
              height: enemy.height,
              frame: 0,
              useFirstImage
            });
          }
        });
      });

      enemies.forEach((enemy, enemyIndex) => {
        if (
          ship.x < enemy.x + enemy.width &&
          ship.x + ship.width > enemy.x &&
          ship.y < enemy.y + enemy.height &&
          ship.y + ship.height > enemy.y
        ) {
          // 自機が敵機にぶつかったときの処理
          gameOver = true;
          // ランダムに爆発画像を選択し、爆発の大きさを自機のサイズにリサイズ
          const useFirstImage = Math.random() < 0.5;
          explosions.push({
            x: ship.x + ship.width / 2,
            y: ship.y + ship.height / 2,
            width: ship.width,
            height: ship.height,
            frame: 0,
            useFirstImage
          });
          // 2秒後にゲームオーバー画面を表示
          gameOverTimeout = setTimeout(displayFinalScore, 2000);
        }
      });

      enemyBullets.forEach((bullet, bulletIndex) => {
        if (
          bullet.x < ship.x + ship.width &&
          bullet.x + bullet.width > ship.x &&
          bullet.y < ship.y + ship.height &&
          bullet.y + bullet.height > ship.y
        ) {
          // 敵の弾が自機に当たったときの処理
          gameOver = true;
          // ランダムに爆発画像を選択し、爆発の大きさを自機のサイズにリサイズ
          const useFirstImage = Math.random() < 0.5;
          explosions.push({
            x: ship.x + ship.width / 2,
            y: ship.y + ship.height / 2,
            width: ship.width,
            height: ship.height,
            frame: 0,
            useFirstImage
          });
          // 2秒後にゲームオーバー画面を表示
          gameOverTimeout = setTimeout(displayFinalScore, 2000);
        }
      });
    }

    function updateScore() {
      document.getElementById("scoreDisplay").innerText = `スコア: ${score}`;
    }

    function displayFinalScore() {
      const finalScoreElement = document.getElementById("finalScore");
      finalScoreElement.innerText = `GAME OVER\nSCORE: ${score}`;
      finalScoreElement.style.display = "block";
    }

    function update() {
      if (gameOver && explosions.length === 0) {
        cancelAnimationFrame(animationFrameId);
        return;
      }
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      drawStars();

      if (!gameOver) {
        ship.x += ship.dx;
        if (ship.x < 0) ship.x = 0;
        if (ship.x + ship.width > canvas.width) ship.x = canvas.width - ship.width;

        drawShip();
      }

      drawBullets();
      drawEnemyBullets();
      drawEnemies();
      drawExplosions(); // 爆発エフェクトを描画
      createEnemies();
      detectCollisions();

      animationFrameId = requestAnimationFrame(update);
    }

    function startGame() {
      gameOver = false;
      score = 0;
      ship = { x: shipX, y: shipY, width: shipWidth, height: shipHeight, dx: 0 };
      bullets = [];
      enemyBullets = [];
      enemies = [];
      explosions = [];
      document.getElementById("finalScore").style.display = "none";
      clearTimeout(gameOverTimeout); // 既存のタイムアウトをクリア
      cancelAnimationFrame(animationFrameId); // 既存のアニメーションフレームをキャンセル
      update();
    }

    // ゲーム開始準備
    document.addEventListener("DOMContentLoaded", (event) => {
      drawStars();
      resizeCanvas();
    });
  </script>
</body>
</html>

コメント

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