JavaScriptでゲーム作り

JavaScriptでシューティングゲームを作りました。
JavaScriptとHTMLはオンラインでもオフラインでもできます。
便利な所はコンパイル(プログラムをコンピュータがわかるように変換すること)がいらない事です。
コンパイルがいらない分、エラーがどこかわかりづらいこともあります。

ストーリー:

確認飛行物体と猫型宇宙生物が攻めてきた。
そんな時頼りになる最新型宇宙戦闘機が迎撃に。
地球の平和はあなたにかかってる。

ゲーム画面:


ゲームのメイン画面

猫型宇宙生物コニローをマウスの左クリックで迎撃します。


ボス出現

猫型宇宙生物が一定数出た後にボスが出現します。


エンディング

ボスの攻撃を見事かいくぐるとエンディングです。


ゲームオーバー画面

HPが0になるか、侵食率が30%になるとゲームオーバーです。
侵食率は敵が登場する度に増えていきます。
撃墜すると減ります。
得点は常時カウントされます。
侵食率の低さに応じて増えます。
敵や敵の弾に当たるとHPが減っていきます。
当たり方によってはダメージが大きくなります。

キャラクター:


最新型宇宙戦闘機スモールベイパー
武器はビーム


敵の猫型宇宙生物コニロー
弾を撃ち体当たりも仕掛けてきます


ボスの確認飛行物体IFO-dnp89.3です。
その巨体を活かして弾を撃ちながら体当たりをしてきます。

プログラム:

結構長くなりました。
音楽や効果音も鳴るようになっています。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>シューティングゲーム</title>
    
  <body>
    <canvas id="field" width="500" height="500"></canvas>
    <img id="back" src="back.png" style="display: none">
    <img id="enemy" src="enemy.png" style="display: none">
    <img id="ship" src="ship.png" style="display: none">
    <img id="boss" src="boss.png" style="display: none">
  </body>
  
    <script>
      "use strict";
      class Star {
        // 星クラス
        constructor() {
          this.x = Math.random() * 500; // x座標
          this.y = Math.random() * 500; // y座標
          this.r = Math.random() * 5 + 1; // 半径
        }
        tick() {
          this.y += this.r; // 下に移動
          if (this.y > 600) {
            // 画面下部にきたら上へ移動
            this.y -= 600;
          }
          drawCircle(this.x, this.y, this.r, "#ffd700");
        }
      }

      class Ship {
        // 自機クラス
        constructor() {
          this.img = document.getElementById("ship");
          this.x = 300;
          this.y = 500;
          this.sx = 0;
          this.sy = 0;
        }
        move(mouseX, mouseY) {
          this.sx = (mouseX - this.x) / 10; // マウスx方向
          this.sy = (mouseY - this.y) / 10; // マウスy方向
          if(this.x > 500){this.x -= 90;} //右端から自機が出ないようにする
          if(this.x < 90){this.x += 90;} //左端から自機が出ないようにする
          if(this.y > 500){this.y -=90;} //下端から自機が出ないようにする
          if(this.y < 90){this.y += 90;} //上端から自機が出ないようにする
        }
        tick() {
          this.x += this.sx; // 速度sx(座標x)
          this.y += this.sy; // 速度sy(座標y)

          ctx.drawImage(this.img, this.x - 50, this.y - 50);
        }
        shoot() {
          bullets.push(new Bullet(this.x, this.y, 0, -25, true)); // 弾を発射
        }
      }

      class Enemy {
        // 敵クラス
        constructor() {
         if(tekiflag < 50){
          this.img = document.getElementById("enemy");
          }
         if(tekiflag >= 50 && bossflag < 20){
          this.img = document.getElementById("boss");
          }
          this.x = Math.random() * 300 + 100; // x座標
          this.y = 0; // y座標
          this.sx = Math.random() * 5 - 2.5; // x方向最初の速度
          this.sy = Math.random() * 15 + 15; // y方向最初の速度
          this.shoot = false;
        }
        tick() {
          this.sy -= 1; // 減速する
          this.x += this.sx; // 座標xの速度sx
          this.y += this.sy; // 座標yの速度sy
          ctx.drawImage(this.img, this.x - 50, this.y - 50);

          if (this.shoot == false && this.sy < 0) {
            // 速度が上がったところで弾を発射
            let theta = Math.atan2(ship.y - this.y, ship.x - this.x);
            let sx = Math.cos(theta) * 10;
            let sy = Math.sin(theta) * 10;
            bullets.push(new Bullet(this.x, this.y, sx, sy, false));
            this.shoot = true;
          }
        }
      }

      class Bullet {
        // 弾のクラス
        constructor(x, y, sx, sy, isShip) {
          this.x = x;
          this.y = y;
          this.sx = sx;
          this.sy = sy;
          this.isShip = isShip; // 自機かどうか判定
        }
        tick() {
          this.x += this.sx;
          this.y += this.sy;
          drawCircle(this.x, this.y, 10, this.isShip ? "#1e90ff" : "#ff69b4");
        }
      }

      // (x,y)を中心に半径r、色colorの円を描画
      function drawCircle(x, y, r, color) {
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.arc(x, y, r, 0, Math.PI * 2);
        ctx.closePath();
        ctx.fill();
      }
      
      function drawScore() { //スコア表示
       ctx.font = "25px Arial";
       ctx.fillStyle = "#0095ff";
       ctx.fillText("Score:" + score, 350, 480);
      }
      
      function drawHp() { //HP表示
       ctx.font = "25px Arial";
       ctx.fillStyle = "#0095ff";
       ctx.fillText("HP:" + hp.toFixed(), 50, 480);
      }
      
      function Shinsyoku() { //侵食率
       ctx.font = "25px Arial";
       if(shi < 20){
       ctx.fillStyle = "#0095ff";
       }
       if(shi > 20){ //侵食率20%を超えた場合の色
       ctx.fillStyle = "#ff4500";
       }
       ctx.fillText("侵食率:" + shi +"%", 155, 480);
      }      
      
      function Ending() {
       main.currentTime = 0;
       main.volume = 0;
       main.pause();
       boss.currentTime = 0;
       boss.volume = 0;
       boss.pause();
       ending.volume = 0.3;
       ending.play();
       setTimeout(() => { clearInterval(timerId); }, 3000); //指定秒後に停止する
       ctx.fillStyle = "#ff9000";
       ctx.fillText("Mission Complete.Cnguratulations!", 70, 300);
      }

      let ctx; // 描画コンテキスト
      let ship; // 自機
      let enemy;
      let back; // 背景画像
      let count = 0;  // 敵出現用カウンタ
      let interval = 60; // 敵出現頻度
      let timerId;  // タイマー
      let bullets = []; // 弾のリスト
      let enemies = []; // 敵のリスト
      let bosses = []; //ボス敵のリスト
      const stars = []; // 星のリスト
      let score = 0;
      let hp = 100;
      let shi = 0; //侵食率
      let tekiflag = 0; //敵出現のフラグ
      let bossflag = 0; //ボス敵出現のフラグ
      let gameOver = false;
      
      //BGM
      const main = new Audio('amain.mp3'); //メイン
      const ending = new Audio('aending.mp3'); //エンディング
      const boss = new Audio('aboss.mp3'); //ボス敵
      const gameover = new Audio('agameover.mp3'); //ゲームオーバー
      const tama = new Audio('atama.mp3'); //弾発射音
      //

      onload = function () {
        ctx = document.getElementById("field").getContext("2d");
        ctx.font = "32px 'Times New Roman'";
        ship = new Ship();  // 自機オブジェクト
        enemy = new Enemy();
        back = document.getElementById("back");

        window.onpointermove = (e) => {
          ship.move(e.clientX, e.clientY);  // マウス移動で自機を移動
        };
        window.onpointerdown = (e) => {
          ship.shoot(); // マウスクリックで弾を発射
          if(bossflag < 20 && gameOver == false){
           tama.volume = 0.2;
           tama.currentTime = 0;
           tama.play();
          }
        };
        timerId = setInterval(tick, 50);  // タイマー開始
        for (let i = 0; i < 50; i++) {
          stars.push(new Star()); // 星を作成
        }
      };

      function tick() {
        count++;
        ctx.fillStyle = "black";
        ctx.fillRect(0, 0, 500, 500);
        ctx.drawImage(back, 0, 0); // 背景描画
        stars.forEach((s) => s.tick()); // 星の移動と描画
        ship.tick();
        drawScore(); //スコア表示
        drawHp(); //HPを表示
        Shinsyoku(); //侵食率を表示
        
        if(bossflag >= 20){Ending();} //ボス敵が20体出現したらエンディング
        
        if (count % interval == 0) {
         if(tekiflag < 50){ //敵出現フラグ、指定数以降出ない
          main.volume = 0.3; //メインBGMのボリューム
          main.play(); //メインBGMを再生
          tekiflag += 1; //敵出現フラグを加える
          enemies.push(new Enemy());  // intervalフレームごとに敵を作成
          interval = Math.max(5, interval - 5);
          }
         if(tekiflag >= 50){ //ボス敵出現フラグ、指定数以降出現
          main.currentTime = 0; //メインBGMの再生位置を最初にする
          main.volume = 0; //メインBGMのボリュームを0に
          main.pause(); //メインBGMを停止する
          boss.volume = 0.3;
          boss.play(); //ボス敵BGMを再生
          bossflag += 1;
          enemies.push(new Enemy());  // intervalフレームごとにボス敵を作成
          interval = Math.max(10, interval - 10);
         }
        }
        

        enemies.forEach((e) => {
        
          e.tick(); // 敵を移動
          if (dist(e, ship) < 30) { //衝突判定:敵との距離
            hp -= 20;
            
            if(hp < 0){
            hp = 0;
            gameOver = true;  // HPが0でゲームオーバー
            }
          }
        });
        //boss敵
        bosses.forEach((e) => {
          e.tick(); // 敵を移動
          if (dist(e, ship) < 30) { //衝突判定:敵との距離
            hp -= 20;
            
            if(hp < 0){
            hp = 0;
            gameOver = true;  // HPが0でゲームオーバー
            }
          }
        });        
        //boss敵ここまで
        bullets.forEach((b) => {
          
          b.tick(); // 弾丸移動
          if (!b.isShip && dist(b, ship) < 20) { //衝突判定:弾丸との距離
            hp -= 10;
            
            if(hp < 0){
            hp = 0;
            gameOver = true;  // HPが0でゲームオーバー
            }
          }

        });
        let eNum = enemies.length;
        enemies = enemies.filter((e) => { //敵リストのfilter関数
          return !bullets.some((b) => { //弾のsome関数
            return b.isShip && dist(e, b) < 50; // 弾丸と敵の衝突判定
          });
        });
        
        let el=enemies.length;
         
        shi=Math.round(el*100/69); //侵食率
        if(shi > 30){gameOver = true;} //侵食率30%でゲームオーバー
         score += Math.round((100-shi)/10); //スコア(侵食率の低さで獲得点数が増える)
     
        if (gameOver) { //ゲームオーバーフラグオンでゲームオーバー処理
          
          clearInterval(timerId);
          ctx.fillStyle = "#ff9000";
          ctx.fillText("GAME is OVER..Try Again", 90, 300); //テキストを表示
          main.pause();
          boss.pause();
          gameover.volume = 0.3;
          gameover.currentTime = 0;
          gameover.play();
        }
      }

      // 2つのオブジェクト間の距離を求める
      function dist(e0, e1) {
        return Math.sqrt(
          Math.abs(e0.x - e1.x) ** 2 + Math.abs(e0.y - e1.y) ** 2
        );
      }
    </script>
  </head>


</html>

HTMLの部分は表示する画像について<body>タグ内に記述しています。
残りは全てJavaScriptで<script>タグ内に記述しています。