【ゲーム】JavaScript:51 スペースインベーダー

 「👾 スペースインベーダー」は、プレイヤーが自機を左右に動かし、上空から迫りくるインベーダー(👾)たちを撃ち落とす2Dシューティングゲームです。インベーダーをすべて倒すとステージクリア、より速くなった次ステージへ進めます。時々現れるUFO(🛸)を倒すとボーナス得点もゲットできます。

遊び方・操作方法

PCの場合

  • 左右移動:キーボードの ←(左)・→(右)キー
  • 弾を撃つ:スペースキー

スマートフォンの場合

  • 画面下の「←」「スペース」「→」ボタンで操作

ゲームのルール

  • 自機(🪆)を左右に動かし、インベーダー(👾)を全滅させるとステージクリア。
  • スペースキーで**弾(🔺)**を発射。自機からは1発ずつしか撃てません。
  • インベーダー(👾)は端まで到達すると一段下がります。
  • 敵の弾(🔻)やインベーダー本体に自機が当たるとゲームオーバー
  • UFO(🛸)は一定時間ごとに登場し、撃破でボーナス点。
  • ステージが進むごとに敵の動きが速くなります。

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

51 スペースインベーダー

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

space_invaders_title.pngspace_invaders_clear.pngspace_invaders_bg.png

ゲーム画面イメージ

プログラム全体(space_invaders.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>👾 スペースインベーダー 👾</title>
  <style>
    body {
      background: url('space_invaders_bg.png') no-repeat center center fixed;
      background-size: cover;
      font-family: 'Segoe UI', 'Yu Gothic', sans-serif;
      margin: 0;
      padding: 0;
    }
    .container {
      width: 800px;
      margin: 40px auto 0 auto;
      background: rgba(0,0,0,0.70);
      border-radius: 20px;
      box-shadow: 0 8px 24px rgba(0,0,0,0.3);
      min-height: 600px;
      padding-bottom: 20px;
      position: relative;
    }
    .main-title {
      font-size: 2.8rem;
      color: #fff;
      text-shadow: 2px 2px 6px #1e3a8a;
      letter-spacing: 0.1em;
      margin-top: 20px;
      margin-bottom: 18px;
      font-weight: bold;
      text-align: center;
      width: 100%;
    }
    .title-img, .clear-img {
      display: block;
      margin: 0 auto 20px auto;
      max-width: 400px;
      width: 75%;
    }
    .center {
      text-align: center;
    }
    .subtitle {
      font-size: 1.4rem;
      color: #ffeb3b;
      margin-bottom: 16px;
      text-shadow: 1px 1px 2px #333;
    }
    .rule-title {
      font-size: 1.3rem;
      color: #3cf4fc;
      text-align: center;
      margin-bottom: 6px;
      font-weight: bold;
    }
    .rules {
      color: #fff;
      font-size: 1.1rem;
      text-align: left;
      margin: 0 auto 20px auto;
      max-width: 90%;
      background: rgba(50,60,100,0.5);
      border-radius: 10px;
      padding: 16px;
      box-shadow: 0 2px 8px #2225;
    }
    .start-btn, .back-btn {
      display: block;
      margin: 28px auto 0 auto;
      padding: 14px 46px;
      font-size: 1.2rem;
      border: none;
      border-radius: 12px;
      background: linear-gradient(90deg, #50e3c2, #5f8df3);
      color: #fff;
      font-weight: bold;
      cursor: pointer;
      box-shadow: 0 2px 6px #2228;
      transition: background 0.2s;
    }
    .start-btn:hover, .back-btn:hover {
      background: linear-gradient(90deg, #5f8df3, #50e3c2);
    }
    .game-area {
      background: rgba(25,32,45,0.9);
      margin: 0 auto;
      border-radius: 12px;
      width: 760px;
      height: 534px;
      position: relative;
      overflow: hidden;
      box-shadow: 0 2px 12px #1117;
      border: 2px solid #444;
      margin-bottom: 16px;
    }
    .score {
      color: #fff;
      font-size: 1.1rem;
      background: rgba(60,60,100,0.8);
      border-radius: 8px;
      padding: 6px 18px;
      position: absolute;
      left: 14px;
      top: 14px;
      z-index: 10;
      letter-spacing: 0.1em;
      text-shadow: 1px 1px 2px #000;
    }
    .stage {
      color: #fff;
      font-size: 1.1rem;
      background: rgba(60,60,100,0.8);
      border-radius: 8px;
      padding: 6px 18px;
      position: absolute;
      right: 14px;
      top: 14px;
      z-index: 10;
      letter-spacing: 0.1em;
      text-shadow: 1px 1px 2px #000;
    }
    .msg-overlay {
      position: absolute;
      top: 0; left: 0; width: 100%; height: 100%;
      background: rgba(24,28,80,0.85);
      display: flex; align-items: center; justify-content: center;
      font-size: 2.1rem; color: #fff;
      text-shadow: 2px 2px 8px #111a;
      z-index: 50;
      border-radius: 12px;
      text-align: center;
      flex-direction: column;
    }
    .invader, .player, .bullet, .enemy-bullet, .ufo {
      position: absolute;
      text-align: center;
      user-select: none;
      pointer-events: none;
    }
    .invader {
      font-size: 2.3rem;
      transition: transform 0.07s;
    }
    .player {
      font-size: 2.3rem;
      transition: transform 0.06s;
      filter: drop-shadow(0 0 6px #0ff);
    }
    .bullet {
      font-size: 1.6rem;
      color: #ff5252;
      filter: drop-shadow(0 0 4px #f00);
    }
    .enemy-bullet {
      font-size: 1.6rem;
      color: #1cf745;
      filter: drop-shadow(0 0 4px #0f5);
    }
    .ufo {
      font-size: 2.5rem;
      text-shadow: 0 0 12px #f3f779;
      filter: drop-shadow(0 0 12px #fff176);
      z-index: 100;
    }
    .bonus {
      font-size: 1.3rem;
      color: #ffe931;
      background: rgba(40, 45, 12, 0.9);
      border-radius: 8px;
      padding: 4px 18px;
      box-shadow: 0 2px 8px #2228;
      position: absolute;
      left: 50%;
      transform: translateX(-50%);
      top: 80px;
      z-index: 120;
      animation: bonus-pop 1.2s linear;
    }
    @keyframes bonus-pop {
      0%   { opacity:0; top:120px; }
      10%  { opacity:1; top:80px; }
      90%  { opacity:1; top:80px;}
      100% { opacity:0; top:60px;}
    }
    @media (max-width: 900px) {
      .container { width: 98vw; min-width: 340px; }
      .game-area { width: 96vw; height: 54vw; min-width: 260px; min-height: 180px; }
    }
  </style>
</head>
<body>
  <div class="container" id="app"></div>
  <script>
    // ====== ゲーム設定 ======
    const WIDTH = 760;
    const HEIGHT = 534;
    const INVADER_ROWS = 3;
    const INVADER_COLS = 7;
    const INVADER_SIZE = 62;
    const PLAYER_SIZE = 60;
    const PLAYER_Y = HEIGHT - 56;
    const PLAYER_SPEED = 36;
    const BULLET_SPEED = 19;
    const ENEMY_BULLET_SPEED = 9;
    const INVADER_INIT_SPEED = 19;
    const INVADER_DROP = 38;
    const GAME_FPS = 60;
    const UFO_SPEED = 7;
    const UFO_BONUS = 500;
    let app = document.getElementById('app');

    // ========== タイトル画面 ==========
    function showTitleScreen() {
      app.innerHTML = `
        <div class="main-title">👾 スペースインベーダー 👾</div>
        <img src="space_invaders_title.png" class="title-img" alt="タイトル画像">
        <div class="subtitle center">~ 2Dシューティングゲーム ~</div>
        <div class="rule-title">🎮 ルール説明</div>
        <div class="rules">
          ・「🪆」を左右キー(← →)または「←」「→」ボタンで操作<br>
          ・スペースキーまたは「スペース」ボタンで弾を発射<br>
          ・「👾」が端に到達すると一段下がります<br>
          ・「👾」が全て倒れるとステージクリア、次ステージは敵が速くなります<br>
          ・「👾」や敵弾に当たるとゲームオーバー<br>
          ・時々1段目に「🛸」が登場。撃つとボーナス得点!
          <br><br>
          【スマホ操作】<br>
          ・「←」「→」「スペース」ボタンで操作できます
        </div>
        <button class="start-btn" onclick="startGame()">▶ ゲームスタート</button>
      `;
    }

    // ========== ゲーム終了画面 ==========
    function showGameOver(win, score, stage) {
      app.innerHTML = `
        <div class="main-title">👾 スペースインベーダー 👾</div>
        <img src="${win ? 'space_invaders_clear.png' : 'space_invaders_title.png'}" class="${win ? 'clear-img' : 'title-img'}" alt="タイトル画像">
        <div class="msg-overlay" style="position:relative; background:transparent; box-shadow:none; height:auto;">
          <div style="font-size:2.2rem; color:#fff;">${win ? '🎉 ステージクリア!' : '💥 ゲームオーバー!'}</div>
          <div style="margin:14px 0 6px 0; font-size:1.1rem;">スコア:<span style="color:#ffeb3b; font-weight:bold">${score}</span></div>
          <div style="margin-bottom:18px;font-size:1.1rem;">ステージ:${stage}</div>
          <button class="back-btn" onclick="showTitleScreen()">タイトル画面に戻る</button>
          ${win ? `<button class="start-btn" onclick="startGame(${stage+1},${score})">▶ 次のステージへ</button>` : ''}
        </div>
      `;
    }

    // ========== ゲーム開始処理 ==========
    function startGame(startStage=1, startScore=0) {
      // 変数宣言
      let stage = startStage;
      let score = startScore;
      let invaderSpeed = INVADER_INIT_SPEED + (stage-1)*7;
      let gameArea, player, invaders, bullets, enemyBullets, invaderDir, invaderDx, invaderDy;
      let leftPressed = false, rightPressed = false, spacePressed = false;
      let canShoot = true, isGameOver = false, isWin = false, canEnemyShoot = true;

      // UFO関連
      let ufo = null;       // UFOオブジェクト
      let ufoTimer = 0;     // UFO出現タイマー
      let ufoDir = 1;       // 1:右移動, -1:左移動
      let showBonus = false;
      let bonusTime = 0;

      // ゲーム画面描画
      app.innerHTML = `
        <div class="main-title">👾 スペースインベーダー 👾</div>
        <div class="game-area" id="gameArea"></div>
        <div class="score" id="score">スコア:${score}</div>
        <div class="stage" id="stage">ステージ:${stage}</div>
      `;
      gameArea = document.getElementById('gameArea');
      let scoreDiv = document.getElementById('score');
      let stageDiv = document.getElementById('stage');

      // プレイヤー初期化
      player = {
        x: WIDTH/2 - PLAYER_SIZE/2,
        y: PLAYER_Y,
        w: PLAYER_SIZE,
        h: PLAYER_SIZE,
        alive: true
      };

      // 敵の初期化
      invaders = [];
      for(let r=0; r<INVADER_ROWS; r++){
        for(let c=0; c<INVADER_COLS; c++){
          invaders.push({
            x: 68 + c*92,
            y: 48 + r*68,
            w: INVADER_SIZE,
            h: INVADER_SIZE,
            alive: true,
            id: 'invader-' + r + '-' + c
          });
        }
      }

      bullets = [];
      enemyBullets = [];
      invaderDx = invaderSpeed;
      invaderDy = 0;
      invaderDir = 1; // 1:右, -1:左

      // UFO出現タイミングを設定(5〜13秒後ランダム)
      function resetUfoTimer() {
        ufoTimer = GAME_FPS * (5 + Math.random()*8);
      }
      resetUfoTimer();

      // モバイル操作UI
      drawMobileControls();

      // キーイベント
      window.onkeydown = function(e){
        if(e.code === 'ArrowLeft'){ leftPressed = true; }
        if(e.code === 'ArrowRight'){ rightPressed = true; }
        if(e.code === 'Space'){ spacePressed = true; e.preventDefault(); }
      };
      window.onkeyup = function(e){
        if(e.code === 'ArrowLeft'){ leftPressed = false; }
        if(e.code === 'ArrowRight'){ rightPressed = false; }
        if(e.code === 'Space'){ spacePressed = false; }
      };

      // ========== ゲームループ ==========
      function gameLoop(){
        // プレイヤー操作
        if(leftPressed)  player.x -= PLAYER_SPEED;
        if(rightPressed) player.x += PLAYER_SPEED;
        player.x = Math.max(8, Math.min(WIDTH-player.w-8, player.x));

        // プレイヤー弾発射
        if(spacePressed && canShoot && player.alive){
          bullets.push({ x: player.x + player.w/2-8, y: player.y-22, w:16, h:24, from: 'player' });
          canShoot = false;
          setTimeout(()=>{ canShoot=true; }, 220);
        }
        // 弾移動
        for(let b of bullets){
          if(b.from === 'player') b.y -= BULLET_SPEED;
          else b.y += ENEMY_BULLET_SPEED + stage;
        }
        bullets = bullets.filter(b => b.y + b.h > 0 && b.y < HEIGHT);

        // 敵横移動・端到達時落下
        let livingInvs = invaders.filter(v=>v.alive);
        if(livingInvs.length > 0) {
          let invLeft = Math.min(...livingInvs.map(v=>v.x));
          let invRight = Math.max(...livingInvs.map(v=>v.x+v.w));
          // 端に到達
          if(invRight >= WIDTH-8 && invaderDir===1) { invaderDir=-1; invaderDy=INVADER_DROP; }
          if(invLeft <= 8 && invaderDir===-1) { invaderDir=1; invaderDy=INVADER_DROP; }
          for(let inv of invaders){
            if(inv.alive){
              inv.x += invaderDx * invaderDir / GAME_FPS;
              inv.y += invaderDy / GAME_FPS;
            }
          }
          invaderDy = 0;
        }

        // 敵弾発射:生存中の敵からランダム(一定間隔)
        if(canEnemyShoot && livingInvs.length > 0){
          // 最下段で生きてる敵のいずれかから発射
          let cols = {};
          for(let inv of livingInvs){
            let col = Math.round((inv.x-68)/92);
            if(!cols[col] || cols[col].y < inv.y) cols[col] = inv;
          }
          let shootInvaders = Object.values(cols);
          let shooter = shootInvaders[Math.floor(Math.random() * shootInvaders.length)];
          bullets.push({ x: shooter.x + shooter.w/2-6, y: shooter.y + shooter.h, w:12, h:20, from: 'enemy' });
          canEnemyShoot = false;
          setTimeout(()=>{ canEnemyShoot=true; }, 500 + Math.max(0, 1100 - stage*180) );
        }

        // ===== UFO出現制御 =====
        if(!ufo && --ufoTimer <= 0) {
          // 出現方向決定
          ufoDir = Math.random() < 0.5 ? 1 : -1;
          ufo = {
            x: ufoDir === 1 ? -62 : WIDTH+12,
            y: 6,
            w: 68,
            h: 44,
            alive: true,
            dir: ufoDir
          };
          resetUfoTimer();
        }
        // UFO移動
        if(ufo && ufo.alive){
          ufo.x += UFO_SPEED * ufo.dir;
          // 画面外に出たら消去
          if((ufo.dir === 1 && ufo.x > WIDTH+80) || (ufo.dir === -1 && ufo.x < -80)){
            ufo = null;
          }
        }

        // ===== 衝突判定 =====
        // 弾と敵
        for(let b of bullets){
          if(b.from === 'player'){
            for(let inv of invaders){
              if(inv.alive && isHit(b, inv)){
                inv.alive = false;
                b.y = -100;
                score += 50 + stage*10;
              }
            }
            // UFOヒット判定
            if(ufo && ufo.alive && isHit(b, ufo)){
              ufo.alive = false;
              b.y = -100;
              score += UFO_BONUS;
              showBonus = true;
              bonusTime = 0;
            }
          }
        }
        // 敵弾とプレイヤー
        for(let b of bullets){
          if(b.from === 'enemy' && player.alive && isHit(b, player)){
            player.alive = false;
            isGameOver = true;
            isWin = false;
          }
        }
        // 敵が自機ライン到達→ゲームオーバー
        for(let inv of invaders){
          if(inv.alive && inv.y+inv.h > player.y+16){
            isGameOver = true;
            isWin = false;
          }
        }
        // 敵全滅→クリア
        if(invaders.every(v=>!v.alive)){
          isGameOver = true;
          isWin = true;
        }

        // ===== 描画 =====
        drawGame();

        // スコア・ステージ
        scoreDiv.innerHTML = "スコア:" + score;
        stageDiv.innerHTML = "ステージ:" + stage;

        // UFOボーナス表示
        if(showBonus){
          bonusTime++;
          if(bonusTime > 70) showBonus = false;
        }

        // ===== ゲーム終了判定 =====
        if(isGameOver){
          setTimeout(()=>{
            showGameOver(isWin, score, stage);
          }, 950);
          return;
        }
        requestAnimationFrame(gameLoop);
      }

      // ========== 当たり判定 ==========
      function isHit(a, b){
        return a.x < b.x+b.w && a.x+a.w > b.x &&
               a.y < b.y+b.h && a.y+a.h > b.y;
      }

      // ========== ゲーム画面の描画 ==========
      function drawGame(){
        gameArea.innerHTML = '';
        // 敵
        for(let inv of invaders){
          if(inv.alive){
            let d = document.createElement('div');
            d.className = 'invader';
            d.style.left = inv.x+'px';
            d.style.top = inv.y+'px';
            d.innerText = '👾';
            gameArea.appendChild(d);
          }
        }
        // プレイヤー
        if(player.alive){
          let p = document.createElement('div');
          p.className = 'player';
          p.style.left = player.x+'px';
          p.style.top = player.y+'px';
          p.innerText = '🪆';
          gameArea.appendChild(p);
        }
        // プレイヤー・敵弾
        for(let b of bullets){
          let e = document.createElement('div');
          e.className = b.from === 'player' ? 'bullet' : 'enemy-bullet';
          e.style.left = b.x+'px';
          e.style.top = b.y+'px';
          e.innerText = b.from === 'player' ? '🔺' : '🔻';
          gameArea.appendChild(e);
        }
        // UFO
        if(ufo && ufo.alive){
          let u = document.createElement('div');
          u.className = 'ufo';
          u.style.left = ufo.x+'px';
          u.style.top = ufo.y+'px';
          u.innerText = '🛸';
          gameArea.appendChild(u);
        }
        // UFOボーナス
        if(showBonus){
          let b = document.createElement('div');
          b.className = 'bonus';
          b.innerText = `UFO撃破!+${UFO_BONUS}点`;
          gameArea.appendChild(b);
        }
      }

      // ========== スマホ用操作UI ==========
      function drawMobileControls(){
        if(window.innerWidth < 900){
          let controls = document.createElement('div');
          controls.style = "position:absolute; left:0; right:0; bottom:8px; width:98%; display:flex; justify-content:center; z-index:20;";
          controls.innerHTML = `
            <button id="leftBtn" style="margin-right:16px;font-size:1.7rem;border-radius:16px; width:66px; height:54px;">←</button>
            <button id="spaceBtn" style="margin:0 12px;font-size:1.7rem;border-radius:16px; width:86px; height:54px;">スペース</button>
            <button id="rightBtn" style="margin-left:16px;font-size:1.7rem;border-radius:16px; width:66px; height:54px;">→</button>
          `;
          app.querySelector('.game-area').appendChild(controls);

          // タッチ操作
          controls.querySelector('#leftBtn').addEventListener('touchstart',()=>{ leftPressed=true; }, false);
          controls.querySelector('#leftBtn').addEventListener('touchend',()=>{ leftPressed=false; }, false);
          controls.querySelector('#rightBtn').addEventListener('touchstart',()=>{ rightPressed=true; }, false);
          controls.querySelector('#rightBtn').addEventListener('touchend',()=>{ rightPressed=false; }, false);
          controls.querySelector('#spaceBtn').addEventListener('touchstart',()=>{
            spacePressed=true; setTimeout(()=>{spacePressed=false;},100);
          }, false);
        }
      }

      // ゲーム開始
      drawGame();
      gameLoop();
    }

    // ====== 初期画面 ======
    showTitleScreen();
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ処理内容
1. 初期表示タイトル画面(ルール説明・画像・開始ボタン)を表示
2. ゲーム開始ステージ・スコア初期化、インベーダー・自機・弾・UFO情報を初期化
3. 入力処理キー入力(またはボタン)で自機左右移動、弾発射
4. ゲームループ各キャラクターの移動、弾の発射・移動、UFO出現など
5. 当たり判定弾と敵、自機と敵弾・敵、弾とUFOの衝突判定
6. 描画各要素(自機・敵・弾・UFO)の位置に応じてDOM描画
7. ゲーム終了判定敵全滅ならステージクリア、敵or弾で自機がやられたらゲームオーバー
8. リザルト画面スコア・ステージ結果表示、タイトル画面or次ステージへボタン

主要関数・命令の解説

関数・変数名役割・説明
showTitleScreen()タイトル画面・ルール説明を表示
showGameOver(win, score, stage)ゲームオーバー or ステージクリア画面表示
startGame(startStage, startScore)ゲーム本体の初期化、ゲームループ開始
gameLoop()毎フレームごとの全体制御・衝突判定・描画更新
drawGame()画面内の自機・敵・弾・UFO・ボーナスのDOM描画
isHit(a, b)二つの矩形(オブジェクト)の当たり判定
drawMobileControls()スマホ用ボタンの描画とタッチイベント登録

関数の主な処理内容

関数名主な処理内容
showTitleScreen()ルール・画像・開始ボタンを描画
showGameOver(win, s, st)ゲーム結果・次のステージorタイトル復帰ボタンを描画
startGame(stage, score)ステージ・スコア初期化、キャラ情報リセット、ループ開始
gameLoop()各種移動/発射/当たり判定/敵UFO出現/ゲーム終了判定
drawGame()ゲームエリアのDOM再生成、自機・敵・弾・UFO描画
isHit(a, b)オブジェクト同士の当たり判定
drawMobileControls()スマホ向けの操作ボタンの描画・イベント設定

改造のポイントとアドバイス

  • 難易度調整
    INVADER_ROWSINVADER_COLSで敵の数を変更できます。
    INVADER_INIT_SPEEDINVADER_DROPで敵の動きを調整可能。
  • ライフ機能の追加
    ・プレイヤーのライフ(残機)を増やすと、初心者でも遊びやすくなります。
  • サウンドや効果の強化
    ・SEやBGMを加えるとレトロ感&臨場感UP!
  • 新しい敵やパターンの追加
    ・特殊な動きの敵や、ボス、アイテム出現などを実装できます。
  • スマホ最適化
    ・既にタッチボタン実装済みですが、見た目やレスポンス改善も面白いです。

アドバイス

このゲームはDOM描画ベースでシンプルに設計されており、カスタマイズも容易です。
初心者は弾や敵キャラの画像や、得点システムをアレンジしたり、上級者は自機・敵・UFOにアニメーションやエフェクトを追加してみましょう!

自分だけのスペースインベーダーを作ってみてください!