【ゲーム】JavaScript:16 ジャンピングバード

 ジャンピングバードは、画面左側を上下に跳ねる小鳥をスペースキーで操作し、右から迫ってくるパイプの間をくぐり抜けて得点を重ねるアクションゲームです。背景が左右にスクロールし、シンプルながらもタイミングが要求される爽快な遊びを楽しめます。

遊び方・操作方法

  • タイトル画面の「▶️ スタート」をクリックするとゲーム開始。
  • スペースキーを押すと小鳥が上向きにジャンプし、キーを放すと重力で下降します。
  • 画面右から出現するパイプの間をくぐりぬけるたびにスコアが1点増えます。
  • 小鳥が画面上下外にはみ出すか、パイプにぶつかるとゲームオーバー。

ルール

  • 制限時間はなく、パイプをくぐり抜け続けられる限りスコアが増えていきます。
  • 跳ねる高さはスペースキーを押すタイミングと回数で調整します。
  • 難易度はプレイヤー次第。ハイスコアを目指してタイミング調整を楽しみましょう。

🎮ゲームプレイ

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

16 ジャンピングバード

素材のダウンロード

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

jumping_bird_title.pngjumping_bird_bg.pngbird.png

ゲーム画面イメージ

プログラム全文(jumping_bird.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>🐦 ジャンピングバード 🐦</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      background-color: #87CEEB;
      font-family: Arial, sans-serif;
    }
    /* タイトル画面 */
    #title-screen {
      position: absolute;
      width: 100%;
      height: 100vh;
      background-color: #87CEEB;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      text-align: center;
    }
    #title-screen h1 {
      font-size: 48px;
      color: white;
      margin-bottom: 20px;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
    }
    #title-screen img.title-img {
      max-width: 50%;
      height: auto;
      margin-bottom: 20px;
    }
    #title-screen p {
      font-size: 20px;
      color: white;
      max-width: 80%;
      margin-bottom: 20px;
      text-shadow: 1px 1px 3px rgba(0,0,0,0.5);
    }
    #ranking {
      background-color: rgba(255,255,255,0.7);
      padding: 10px;
      border-radius: 8px;
      margin-bottom: 20px;
      width: 60%;
      max-width: 400px;
      text-align: center;
    }
    #ranking h2 {
      margin: 0 0 10px;
      font-size: 24px;
    }
    #ranking-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    #ranking-list li {
      font-size: 20px;
      color: #333;
      margin: 5px 0;
    }
    #start-button {
      font-size: 24px;
      padding: 10px 20px;
      border: none;
      border-radius: 8px;
      background-color: gold;
      color: black;
      cursor: pointer;
      box-shadow: 2px 2px 4px rgba(0,0,0,0.3);
    }

    /* ゲーム画面 */
    #game-container {
      position: absolute;
      width: 100%;
      height: 100vh;
      display: none;
      background-image: url('jumping_bird_bg.png');
      background-repeat: repeat-x;
      background-size: auto 100%;
    }
    #bird {
      position: absolute;
      width: 40px;
      height: 40px;
      top: 50%;
      left: 20%;
      transform: translate(-50%, -50%);
    }
    .pipe {
      position: absolute;
      width: 60px;
      background-color: green;
    }
    #score {
      position: absolute;
      top: 20px;
      left: 20px;
      font-size: 24px;
      color: white;
      background-color: rgba(0,0,0,0.5);
      padding: 5px 10px;
      border-radius: 5px;
    }
    #message-area {
      position: absolute;
      top: 40%;
      width: 100%;
      text-align: center;
      font-size: 36px;
      color: #FF4500;
      background-color: rgba(255,255,255,0.8);
      display: none;
      padding: 10px;
      border-radius: 10px;
    }
    #back-button {
      position: absolute;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      font-size: 24px;
      padding: 10px 20px;
      border: none;
      border-radius: 8px;
      background-color: #FF8C00;
      color: white;
      cursor: pointer;
      display: none;
      box-shadow: 2px 2px 4px rgba(0,0,0,0.3);
    }
  </style>
</head>
<body>
  <!-- タイトル画面 -->
  <div id="title-screen">
    <h1>🐦 ジャンピングバード 🐦</h1>
    <img class="title-img" src="jumping_bird_title.png" alt="ジャンピングバード タイトル">
    <p>🐤 スペースキーでジャンプしてパイプをよけよう!<br>パイプにぶつかるまでスコアを伸ばそう!</p>
    <div id="ranking">
      <h2>🏆 ランキング 🏆</h2>
      <ul id="ranking-list">
        <!-- JavaScriptで埋める -->
      </ul>
    </div>
    <button id="start-button">▶️ スタート</button>
  </div>

  <!-- ゲーム画面 -->
  <div id="game-container">
    <img id="bird" src="bird.png" alt="鳥">
    <div id="score">スコア: 0</div>
    <div id="message-area"></div>
    <button id="back-button">⬅️ タイトル画面へ戻る</button>
  </div>

  <script>
    const titleScreen   = document.getElementById('title-screen');
    const startButton   = document.getElementById('start-button');
    const rankingList   = document.getElementById('ranking-list');
    const gameContainer = document.getElementById('game-container');
    const bird          = document.getElementById('bird');
    const scoreDisplay  = document.getElementById('score');
    const messageArea   = document.getElementById('message-area');
    const backButton    = document.getElementById('back-button');

    let birdTop       = 50;
    let velocity      = 0;
    const gravity     = 0.1;
    const jumpStrength= -1.5;
    let isGameOver    = false;
    let score         = 0;
    let pipes         = [];
    let pipeGap       = 20;
    let bgX           = 0;
    const RANK_KEY    = 'jumpingBirdRanking';
    const medals      = ['🥇','🥈','🥉'];

    function loadRanking() {
      const stored = localStorage.getItem(RANK_KEY);
      return stored ? JSON.parse(stored) : [];
    }

    function saveRanking(ranks) {
      localStorage.setItem(RANK_KEY, JSON.stringify(ranks));
    }

    function renderRanking() {
      const ranks = loadRanking();
      rankingList.innerHTML = '';
      ranks.slice(0,3).forEach((s, i) => {
        if (s > 0) {
          const li = document.createElement('li');
          li.textContent = `${medals[i]} ${s}点`;
          rankingList.appendChild(li);
        }
      });
    }

    renderRanking();

    startButton.addEventListener('click', () => {
      titleScreen.style.display   = 'none';
      gameContainer.style.display = 'block';
      updateGame();
      setInterval(() => {
        if (!isGameOver) {
          createPipe();
          if (Math.random() < 0.3) setTimeout(createPipe, 800);
        }
      }, 2500);
    });

    function updateGame() {
      if (isGameOver) return;
      velocity += gravity;
      birdTop += velocity;
      if (birdTop > 100) { birdTop = 100; gameOver(); }
      if (birdTop < 0) birdTop = 0;
      bird.style.top = birdTop + '%';
      bgX -= 1;
      gameContainer.style.backgroundPositionX = bgX + 'px';
      movePipes();
      requestAnimationFrame(updateGame);
    }

    function createPipe() {
      if (isGameOver) return;
      const minGap = 8, maxGap = 30;
      pipeGap = Math.max(minGap, maxGap - score * 0.4);
      const topElem = document.createElement('div');
      const botElem = document.createElement('div');
      const height  = Math.random() * 40 + 20;
      topElem.classList.add('pipe');
      botElem.classList.add('pipe');
      topElem.style.height    = height + '%';
      topElem.style.top       = 0;
      topElem.style.right     = 0;
      botElem.style.height    = (100 - height - pipeGap) + '%';
      botElem.style.bottom    = 0;
      botElem.style.right     = 0;
      gameContainer.appendChild(topElem);
      gameContainer.appendChild(botElem);
      pipes.push({ top: topElem, bottom: botElem, x: 100 });
    }

    function movePipes() {
      pipes.forEach((pipe, i) => {
        pipe.x -= 1;
        pipe.top.style.right    = (100 - pipe.x) + '%';
        pipe.bottom.style.right = (100 - pipe.x) + '%';
        if (pipe.x < -10) {
          gameContainer.removeChild(pipe.top);
          gameContainer.removeChild(pipe.bottom);
          pipes.splice(i,1);
          score++;
          scoreDisplay.textContent = 'スコア: ' + score;
        }
        const b = bird.getBoundingClientRect();
        const t = pipe.top.getBoundingClientRect();
        const o = pipe.bottom.getBoundingClientRect();
        if (
          (b.right>t.left && b.left<t.right && b.bottom>t.top && b.top<t.bottom) ||
          (b.right>o.left && b.left<o.right && b.bottom>o.top && b.top<o.bottom)
        ) {
          gameOver();
        }
      });
    }

    document.addEventListener('keydown', e => {
      if (e.code==='Space' && !isGameOver) velocity = jumpStrength;
    });

    function gameOver() {
      if (isGameOver) return;
      isGameOver = true;
      // ランキング更新
      const allRanks = loadRanking();
      allRanks.push(score);
      allRanks.sort((a,b) => b - a);
      const newRanks = allRanks.filter(s => s>0).slice(0,3);
      saveRanking(newRanks);
      renderRanking();
      // メッセージ
      if (newRanks.includes(score)) {
        messageArea.textContent = `🎉 おめでとう!${score}点でランキングに入賞しました! 🎉`;
      } else {
        messageArea.textContent = `💥 ゲームオーバー! スコア: ${score}点`;
      }
      messageArea.style.display = 'block';
      backButton.style.display  = 'block';
    }

    backButton.addEventListener('click', () => {
      location.reload();
    });
  </script>
</body>
</html>

アルゴリズムの流れ

処理ステップ説明
タイトル表示タイトル画面にランキングを描画し、スタートボタンを待機
ゲーム開始タイトル非表示+ゲーム画面表示、updateGame() をキック、定期的にパイプ生成
updateGame()重力を速度に加算 → 位置更新 → 背景スクロール → movePipes() 呼び出し → 次フレームへ再帰呼出
createPipe()ランダムな高さの上パイプ・下パイプ要素を作成し右端に配置し配列に登録
movePipes()全パイプを左へ移動 → 範囲外のパイプは削除+得点加算 → 衝突判定 → 衝突時は gameOver()
衝突判定鳥とパイプ矩形の当たり判定(AABB)
gameOver()ループ停止 → スコアをランキングへ登録・保存 → 表示更新 → タイトルに戻るボタン表示

関数の役割

関数名引数返り値概要
loadRanking()なしArraylocalStorage から保存済みのスコア配列を読み出し。未定義なら空配列を返す。
saveRanking(ranks)Arrayなし引数のスコア配列を JSON 化して localStorage に保存
renderRanking()なしなし現在のスコア配列上位3件をタイトル画面のランキングリストに描画
updateGame()なしなしフレームごとに小鳥位置とパイプ移動を行い、画面を更新
createPipe()なしなし画面右端に新しい上/下パイプを生成して配置、管理リストに追加
movePipes()なしなし全パイプを左移動 → 画面外のパイプを削除 → 得点判定 → 衝突判定
gameOver()なしなしゲームフラグを立て、ランク更新、メッセージ表示、戻るボタンを出現

改造のポイント

  • 重力・ジャンプ強度 を調整してゲームスピードを変化させる
  • パイプ生成間隔やパイプ幅 を変更し、難易度を多段階化
  • 背景画像や鳥のスプライト を差し替え、見た目を一新
  • モバイル対応 でタッチ入力イベントを追加
  • 効果音・BGM を組み込んで演出を強化
  • ランキング保存数 や表示スタイルをカスタマイズして競争要素を拡張
  • 障害物パターン(動くパイプ、敵キャラなど)を増やして多様なステージを実装

 以上のように、各機能をモジュール化しているため、パラメータを変更したり関数を拡張するだけで手軽にカスタマイズが可能です。ぜひ自分好みのジャンピングバードを作ってみてください!