【ゲーム】JavaScript:72 フロッグクロッシング

 「🚦🐸 フロッグクロッシング 🐸🚦」は、十字キーで操作するカエルを道路と歩道の障害物を避けながら、画面上部の安全地帯へ導くアクションゲームです。プレイヤーは車や歩行者に衝突しないようにタイミングよく移動し、クリアするたびにスコアが増加、速度が上がることで難易度が上がっていきます。

遊び方と操作方法

  • 操作方法: キーボードの十字キー(↑↓←→)でカエルを上下左右に移動。
  • 目的: 画面上部(SAFE_ZONE_H)に到達すると +1 スコア。次のラウンドでは移動速度が上がる。
  • ゲームオーバー: カエル(🐸)が車(🚗/🚙)または歩行者(🚶)に衝突した瞬間。

ルール

  1. カエルは画面下部の安全地帯からスタート。
  2. 道路レーン(LANE_COUNT = 5)には一定間隔で車が出現。
  3. 歩道レーン(SIDEWALK_LANE_INDEX)には歩行者が出現。
  4. 衝突判定は矩形(a.x < b.x + b.w かつ a.x + a.w > b.x ...)で厳密に行う。
  5. クリア時に速度(speed)を speed += SPEED_INC だけ上昇。

🎮ゲームプレイ

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

72 フロッグクロッシング

素材のダウンロード

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

frog_crossing_title.pngfrog_crossing_bg.png

ゲーム画面イメージ

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

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>🚦🐸 フロッグクロッシング 🐸🚦</title>
  <style>
    html,body{
      width:100%; height:100%; margin:0; padding:0;
      background:url('frog_crossing_bg.png') no-repeat center center fixed;
      background-size:cover;
      font-family:'Yu Gothic', 'Meiryo', sans-serif;
      overflow:auto;
    }
    #wrapper{
      width:800px;
      margin:20px auto 0 auto;
      position:relative;
      box-sizing:border-box;
    }
    .screen{display:none; width:100%; height:100%; box-sizing:border-box;}
    #titleScreen{display:block; text-align:center; color:#fff; padding-top:20px;}
    #titleScreen img{display:block; margin:0 auto 20px auto; max-width:70%; height:auto;}
    .main-title {
      font-size:2.0rem;
      font-weight:bold;
      line-height:1.1;
      letter-spacing:0.08em;
      margin-bottom:18px;
      text-shadow:
        0 0 8px #fff,
        0 0 2px #fff,
        2px 2px 0 #000,
        -2px 2px 0 #000,
        2px -2px 0 #000,
        -2px -2px 0 #000;
      background:rgba(0,0,0,0.28);
      border-radius:12px;
      padding:0.1em 0.4em;
      display:inline-block;
    }
    .rule-panel{background:rgba(0,0,0,0.6); padding:20px; border-radius:10px; margin:0 auto; max-width:90%;}
    .rule-panel h2{margin-top:0; text-align:center;}
    .rule-panel p{text-align:left; margin:0 0 10px 0; line-height:1.6;}
    .btn{display:inline-block; padding:12px 24px; margin-top:20px; font-size:1.1rem; font-weight:bold; color:#fff; background:#4CAF50; border:none; border-radius:8px; cursor:pointer; transition:opacity .2s;}
    .btn:hover{opacity:0.8;}
    #gameScreen{position:relative; color:#fff; user-select:none;}
    #gameCanvas{display:block; margin:0 auto; background:transparent; border:4px solid #fff; border-radius:4px;}
    #scoreBoard{position:absolute; top:10px; left:50%; transform:translateX(-50%); background:rgba(0,0,0,0.6); padding:6px 12px; border-radius:6px; font-size:1.1rem;}
    #gameOverScreen{text-align:center; color:#fff; padding-top:20px;}
    #gameOverScreen .message{font-size:2rem; font-weight:bold; margin-bottom:20px;}
    #rankingBoard{
      margin:24px auto 0 auto; background:rgba(0,0,0,0.6);
      border-radius:10px; padding:16px 32px; max-width:350px;
    }
    #rankingBoard h2{margin-top:0; color:#FFD700;}
    #rankingBoard ul{margin:0; padding-left:0; text-align:left; list-style:none;}
    #rankingBoard li{font-size:1.15em; margin-bottom:2px;}
  </style>
</head>
<body>
  <div id="wrapper">
    <!-- ==================== タイトル画面 ==================== -->
    <div id="titleScreen" class="screen">
      <img src="frog_crossing_title.png" alt="ゲームタイトル">
      <h1 class="main-title">🚦🐸 フロッグクロッシング 🐸🚦</h1>
      <div class="rule-panel">
        <h2>📜 ルール説明 📜</h2>
        <p>・カエル (<span style="font-size:1.2em;">🐸</span>) を十字キーで操作し、道路と歩道を渡って上端の安全地帯へたどり着きましょう。</p>
        <p>・車 (<span style="font-size:1.2em;">🚗</span>, <span style="font-size:1.2em;">🚙</span>) および 歩行者 (<span style="font-size:1.2em;">🚶</span>) に触れるとゲームオーバーです。</p>
        <p>・無事に渡るとスコア +1。時間が経つにつれて車や歩行者の速度が上がります。</p>
      </div>
      <button id="startBtn" class="btn">▶️ スタート</button>
    </div>
    <!-- ==================== ゲーム画面 ==================== -->
    <div id="gameScreen" class="screen">
      <div id="scoreBoard">スコア: <span id="score">0</span></div>
      <canvas id="gameCanvas" width="800" height="500"></canvas>
    </div>
    <!-- ==================== ゲーム終了画面 ==================== -->
    <div id="gameOverScreen" class="screen">
      <div class="message" id="resultMessage"></div>
      <div id="rankingBoard"></div>
      <button id="returnBtn" class="btn">🔙 タイトル画面に戻る</button>
    </div>
  </div>
<script>
/*******************************************
 *   🚦🐸 フロッグクロッシング メイン処理   *
 *******************************************/
(() => {
  // ========== 要素取得 ==========
  const titleScreen   = document.getElementById('titleScreen');
  const gameScreen    = document.getElementById('gameScreen');
  const gameOverScreen= document.getElementById('gameOverScreen');
  const startBtn      = document.getElementById('startBtn');
  const returnBtn     = document.getElementById('returnBtn');
  const scoreSpan     = document.getElementById('score');
  const resultMessage = document.getElementById('resultMessage');
  const rankingBoard  = document.getElementById('rankingBoard');
  const canvas        = document.getElementById('gameCanvas');
  const ctx           = canvas.getContext('2d');

  // ========== 定数設定 ==========
  const LANE_COUNT    = 5;
  const LANE_HEIGHT   = 60;
  const SAFE_ZONE_H   = 60;
  const FROG_SIZE     = 40;
  const INITIAL_SPEED = 2;
  const SPEED_INC     = 0.2;
  const CAR_SPAWN_GAP = 200;

  // 歩道レーン
  const SIDEWALK_LANE_INDEX = LANE_COUNT;
  const SIDEWALK_HEIGHT = 60;
  const SIDEWALK_Y = canvas.height - SAFE_ZONE_H - SIDEWALK_HEIGHT;

  // ========== ゲーム用変数 ==========
  let frog, cars, pedestrians, score, speed, frameId, running, carSpawnTimers, pedestrianSpawnTimer;

  // ========== ランキング管理 ==========
  const RANKING_KEY = "frog_crossing_rankings";
  function saveScore(newScore) {
    let rankings = JSON.parse(localStorage.getItem(RANKING_KEY) || "[]");
    rankings.push(newScore);
    rankings = rankings.sort((a,b)=>b-a).slice(0,5);
    localStorage.setItem(RANKING_KEY, JSON.stringify(rankings));
    return rankings;
  }
  function loadRanking() {
    return JSON.parse(localStorage.getItem(RANKING_KEY) || "[]");
  }
  function showRankingBoard(rankings) {
    const rankLabel = ["1位", "2位", "3位", "4位", "5位"];
    let html = `<h2>🏆 スコアランキング 🏆</h2><ul>`;
    for(let i=0; i<5; i++) {
      html += `<li>${rankLabel[i]}: ${rankings[i] !== undefined ? rankings[i] : "0"}</li>`;
    }
    html += `</ul>`;
    rankingBoard.innerHTML = html;
  }

  // ========== オブジェクト生成関数 ==========
  function createFrog(){
    return {
      x: (canvas.width - FROG_SIZE)/2,
      // 歩道より下(最下部安全地帯の中央)
      y: canvas.height - SAFE_ZONE_H - FROG_SIZE/2 + (SAFE_ZONE_H - FROG_SIZE)/2,
      w: FROG_SIZE,
      h: FROG_SIZE
    };
  }
  function createCar(lane){
    const dir = lane % 2 === 0 ? 1 : -1;
    const y   = SAFE_ZONE_H + lane * LANE_HEIGHT + (LANE_HEIGHT - 40)/2;
    const w   = 80;
    const x   = dir === 1 ? -w : canvas.width + w;
    const emoji = dir === 1 ? "🚗" : "🚙";
    return {x, y, w, h:40, dir, emoji};
  }
  function createPedestrian(){
    const y = SIDEWALK_Y + 10;
    const w = 38, h = 38;
    const x = canvas.width + 20;
    return {x, y, w, h, dir: -1, emoji: "🚶"};
  }

  // ========== ゲーム初期化 ==========
  function initGame(){
    frog  = createFrog();
    cars  = [];
    pedestrians = [];
    score = 0;
    speed = INITIAL_SPEED;
    carSpawnTimers = Array(LANE_COUNT).fill(0);
    pedestrianSpawnTimer = 0;
    scoreSpan.textContent = score;
    running = true;
    cancelAnimationFrame(frameId);
    frameId = requestAnimationFrame(gameLoop);
  }

  // ========== メインループ ==========
  function gameLoop(){
    update();
    render();
    if(running) frameId = requestAnimationFrame(gameLoop);
  }

  // ========== 更新処理 ==========
  function update(){
    for(const car of cars){
      if(rectHit(frog, car)){ gameOver(); return; }
    }
    for(const p of pedestrians){
      if(rectHit(frog, p)){ gameOver(); return; }
    }
    for(let lane=0; lane<LANE_COUNT; lane++){
      carSpawnTimers[lane] -= 1;
      if(carSpawnTimers[lane] <= 0){
        cars.push(createCar(lane));
        carSpawnTimers[lane] = CAR_SPAWN_GAP / speed + Math.random()*100;
      }
    }
    cars.forEach(car => car.x += car.dir * speed);
    cars = cars.filter(car => car.x < canvas.width + car.w && car.x > -car.w);

    pedestrianSpawnTimer -= 1;
    if(pedestrianSpawnTimer <= 0){
      pedestrians.push(createPedestrian());
      pedestrianSpawnTimer = 200 / speed + Math.random()*100;
    }
    pedestrians.forEach(p => p.x += p.dir * (speed*0.9));
    pedestrians = pedestrians.filter(p => p.x > -p.w);

    checkClear();
  }

  // ========== 描画処理 ==========
  function render(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.fillStyle = '#555';
    ctx.fillRect(0, SAFE_ZONE_H, canvas.width, LANE_COUNT * LANE_HEIGHT);
    ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.setLineDash([10,10]);
    for(let i=1;i<LANE_COUNT;i++){
      const y = SAFE_ZONE_H + i*LANE_HEIGHT;
      ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(canvas.width,y); ctx.stroke();
    }
    ctx.setLineDash([]);
    ctx.fillStyle = '#2E7D32';
    ctx.fillRect(0,0,canvas.width,SAFE_ZONE_H);
    ctx.fillRect(0,canvas.height-SAFE_ZONE_H,canvas.width,SAFE_ZONE_H);

    ctx.fillStyle = '#eee';
    ctx.fillRect(0, SIDEWALK_Y, canvas.width, SIDEWALK_HEIGHT);
    for(let i=0; i<canvas.width; i+=48){
      ctx.fillStyle = '#fff';
      ctx.fillRect(i, SIDEWALK_Y+16, 24, 12);
    }
    cars.forEach(car => {
      ctx.font = "40px serif";
      ctx.textBaseline = "top";
      ctx.textAlign = "left";
      ctx.fillText(car.emoji, car.x+10, car.y);
    });
    pedestrians.forEach(p => {
      ctx.font = "38px serif";
      ctx.textBaseline = "top";
      ctx.textAlign = "left";
      ctx.fillText(p.emoji, p.x, p.y);
    });
    ctx.font = "38px serif";
    ctx.textBaseline = "top";
    ctx.textAlign = "left";
    ctx.fillText("🐸", frog.x+2, frog.y);
  }

  // ========== 衝突 / クリア判定 ==========
  function rectHit(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 checkClear(){
    if(frog.y < SAFE_ZONE_H){
      score++;
      scoreSpan.textContent = score;
      speed += SPEED_INC;
      frog = createFrog();
    }
  }

  // ========== 入力処理 ==========
  window.addEventListener('keydown', e => {
    if(!running) return;
    const MOVE = 40;
    switch(e.code){
      case 'ArrowUp':   frog.y -= MOVE; break;
      case 'ArrowDown': frog.y += MOVE; break;
      case 'ArrowLeft': frog.x -= MOVE; break;
      case 'ArrowRight':frog.x += MOVE; break;
    }
    frog.x = Math.max(0, Math.min(canvas.width - frog.w, frog.x));
    frog.y = Math.max(0, Math.min(canvas.height - frog.h, frog.y));
    // 歩道の下まで行けないよう制限(ただし安全地帯内はOK)
    if(frog.y > canvas.height - frog.h) frog.y = canvas.height - frog.h;
  });

  // ========== ゲームオーバー ==========
  function gameOver(){
    running = false;
    cancelAnimationFrame(frameId);
    resultMessage.innerHTML = `💥 ゲームオーバー 💥<br>あなたのスコア: <strong>${score}</strong>`;
    const rankings = saveScore(score);
    showRankingBoard(rankings);
    gameScreen.style.display = 'none';
    gameOverScreen.style.display = 'block';
  }

  // ========== 画面遷移ボタン ==========
  startBtn.addEventListener('click', () => {
    titleScreen.style.display  = 'none';
    gameOverScreen.style.display = 'none';
    gameScreen.style.display   = 'block';
    initGame();
  });
  returnBtn.addEventListener('click', () => {
    gameOverScreen.style.display = 'none';
    titleScreen.style.display    = 'block';
    const rankings = loadRanking();
    showRankingBoard(rankings);
  });

  document.addEventListener('DOMContentLoaded',()=>{
    showRankingBoard(loadRanking());
  });
})();
</script>
</body>
</html>

アルゴリズムの流れ

以下の表では、メインの処理ステップと関連関数をまとめています。

ステップ処理内容関連関数
1. 初期化カエル・車・歩行者の配列を生成し、スコアと速度をリセットinitGame()
2. メインループ毎フレーム update()render() を実行し継続判定gameLoop()
3. 衝突判定カエルと車/歩行者の矩形重なりを判定し、衝突ならゲーム終了rectHit()
4. 出現処理各レーンに応じたタイマー管理で車/歩行者を定周期で生成createCar(), createPedestrian()
5. 移動更新生成オブジェクトの座標を速度・方向(dir)に応じて更新update 内
6. 画面描画背景・レーンライン・カエル・車・歩行者を canvas API で描画render()
7. クリア判定カエルが画面上部の安全地帯に到達したかをチェックし、クリア時にスコアと速度増加checkClear()
8. ゲーム終了衝突検出後にアニメーションを停止し、スコア保存・ランキング表示を行うgameOver(), saveScore(), showRankingBoard()

関数の解説

関数名引数戻り値処理内容
createFrog()なし{x,y,w,h}カエルの初期位置(画面下部中央)、サイズを設定して返す。
createCar(lane)lane (0〜4){x,y,w,h,dir,emoji}指定レーンに合わせて左右どちらかから出現する車のオブジェクトを生成。ランダム間隔で呼び出し。
createPedestrian()なし{x,y,w,h,dir,emoji}歩道レーン端から歩行者を生成。速度は車よりやや遅い。
rectHit(a,b)a,b (矩形情報)booleana と b の矩形重なりを論理演算で判定。
checkClear()なしなしカエルの y 座標が安全地帯上部より小さいか確認。クリアならスコア増加と速度アップ。
saveScore(newScore)newScore (数値)rankings(配列)localStorage へスコアを追加・ソート・上位5件に絞り込み保存し、最新ランキングを返す。
loadRanking()なしrankings(配列)localStorage から保存済みランキングを取得して返す。存在しなければ空配列。
showRankingBoard(ranking)ranking(配列)なしHTML を組み立ててスコアランキングを画面に表示。テンプレート文字列を活用。

改造のポイント

  • カエルや車のデザイン変更createFrog(), createCar()emoji プロパティを任意の画像や絵文字に変更可能。
  • レーン数や速度調整LANE_COUNT, INITIAL_SPEED, SPEED_INC を変えることでゲームの難易度やスピード感を調節できます。
  • 出現間隔やパターンCAR_SPAWN_GAPpedestrianSpawnTimer の計算式に乱数やレベル別の制御を加えると多彩な演出が可能。
  • UI強化: スコアボードやゲームオーバーメッセージを CSS アニメーションやトランジションで装飾する。

アドバイス
 まずは小さなパラメータ(速度増加量やレーン数)を変えて挙動を確認し、徐々にエフェクトやBGMを追加するとバランス良く拡張できます。