【ゲーム】JavaScript:18 エアホッケー

 「エアホッケー」は、画面上の2つのパドルを操作し、中央のボールを相手ゴールにシュートしてポイントを競う対戦型スポーツゲームです。先に設定した最大スコア(デフォルトは5点)を獲得したプレイヤーが勝利となります。

遊び方・操作方法

  • 左パドル (Player1)W キーで上移動、S キーで下移動
  • 右パドル (Player2):矢印キー ↑↓ で上下移動
  • 開始:タイトル画面の「▶️ スタート」ボタンをクリック
  • リトライ:終了画面の「🔄 タイトル画面に戻る」ボタンをクリック

ルール

  1. ボールが画面左端を越えたら Player2 に1点、右端を越えたら Player1 に1点加算
  2. 先に5点(maxScore = 5)獲得したプレイヤーが勝利
  3. ボールは上下壁で跳ね返り、パドルに当たると反転
  4. 得点時はボールが中央にリセットされ、プレイ続行

🎮ゲームプレイ

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

18 エアホッケー

素材のダウンロード

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

air_hockey_bg.pngair_hockey_title.png

ゲーム画面イメージ

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

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>🏒 エアホッケー 🏒</title>
  <style>
    /* ========== 全体設定 ========== */
    body {
      margin: 0; /* ページの余白をなくす */
      overflow: hidden; /* スクロールバーを隠す */
      background: url('air_hockey_bg.png') no-repeat center center fixed;
      background-size: cover; /* 背景画像を画面いっぱいに表示 */
      font-family: Arial, sans-serif; /* フォント指定 */
    }

    /* ========== ゲームキャンバス ========== */
    #canvas {
      display: none; /* 初期状態では非表示 */
      position: absolute;
      top: 0;
      left: 0;
    }

    /* ========== スコア表示 ========== */
    #scoreBoard {
      position: absolute;
      top: 20px;
      left: 50%;
      transform: translateX(-50%);
      font-size: 24px;
      color: #fff;
      background: rgba(0, 0, 0, 0.6); /* 透過黒背景で視認性アップ */
      padding: 5px 10px;
      border-radius: 5px;
      display: none; /* 初期状態では非表示 */
      z-index: 1;
    }

    /* ========== タイトル画面 & 終了画面共通 ========== */
    #titleScreen, #endScreen {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.7); /* 黒透過背景 */
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      color: #fff;
      text-align: center;
    }

    /* タイトル画像をセンタリング */
    #titleScreen img {
      max-width: 50%;
      margin-bottom: 20px;
    }

    /* ボタン共通スタイル */
    #titleScreen button,
    #endScreen button {
      font-size: 1.2em;
      padding: 10px 20px;
      margin-top: 20px;
      cursor: pointer;
      background: #0a74da; /* 青系ボタン */
      border: none;
      color: #fff;
      border-radius: 5px;
    }

    /* ルール説明メッセージ */
    .message {
      background: rgba(0, 0, 0, 0.6); /* 透過黒背景 */
      padding: 10px;
      border-radius: 5px;
      margin: 5px 0;
      line-height: 1.4;
    }
  </style>
</head>
<body>
  <!-- ========== タイトル画面 ========== -->
  <div id="titleScreen">
    <!-- タイトルロゴ -->
    <img src="air_hockey_title.png" alt="🏒 エアホッケー 🏒">
    <!-- ルール説明見出し -->
    <h1 class="message">🎮 遊び方・ルール 🎮</h1>
    <!-- ルール説明テキスト -->
    <p class="message">
      • W/Sキーで左のパドルを上下に操作<br>
      • ↑/↓キーで右のパドルを上下に操作<br>
      • 先に5ポイント取った方が勝ち!
    </p>
    <!-- スタートボタン -->
    <button id="startBtn">▶️ スタート</button>
  </div>

  <!-- ========== スコア表示エリア ========== -->
  <div id="scoreBoard">Player1: 0  –  Player2: 0</div>
  <!-- ========== ゲームキャンバス ========== -->
  <canvas id="canvas"></canvas>

  <!-- ========== ゲーム終了画面 ========== -->
  <div id="endScreen" style="display:none;">
    <!-- 結果メッセージ -->
    <h1 id="endMessage" class="message"></h1>
    <!-- タイトルへ戻るボタン -->
    <button id="restartBtn">🔄 タイトル画面に戻る</button>
  </div>

  <script>
    // ======== 要素取得 ========
    const canvas      = document.getElementById('canvas');
    const ctx         = canvas.getContext('2d');
    const titleScreen = document.getElementById('titleScreen');
    const startBtn    = document.getElementById('startBtn');
    const endScreen   = document.getElementById('endScreen');
    const endMessage  = document.getElementById('endMessage');
    const restartBtn  = document.getElementById('restartBtn');
    const scoreBoard  = document.getElementById('scoreBoard');

    // ======== キャンバスサイズ調整 ========
    function resizeCanvas() {
      canvas.width  = window.innerWidth;
      canvas.height = window.innerHeight;
    }
    window.addEventListener('resize', resizeCanvas);
    resizeCanvas(); // 初回呼び出し

    // ======== パドルクラス ========
    class Paddle {
      /**
       * @param {number} x - パドルの初期X座標
       */
      constructor(x) {
        this.width  = 20;  // パドルの幅
        this.height = 100; // パドルの高さ
        this.x      = x;   // X座標
        // Y座標は画面中央に配置
        this.y      = canvas.height/2 - this.height/2;
        this.speed  = 6;   // 移動速度
        this.up     = false;   // 上移動フラグ
        this.down   = false;   // 下移動フラグ
      }
      // パドル描画
      draw() {
        ctx.fillStyle = '#fff';
        ctx.fillRect(this.x, this.y, this.width, this.height);
      }
      // キー入力に応じてY座標を変化
      update() {
        if (this.up   && this.y > 0)                          this.y -= this.speed;
        if (this.down && this.y + this.height < canvas.height) this.y += this.speed;
      }
    }

    // ======== ボールクラス ========
    class Ball {
      constructor() {
        this.radius = 10; // ボール半径
        this.reset();     // 初期位置・速度設定
      }
      // 中央に戻してランダム方向に飛ばす
      reset() {
        this.x = canvas.width/2;
        this.y = canvas.height/2;
        const angle = (Math.random()*Math.PI/4) - (Math.PI/8); // -22.5°〜+22.5°
        const dir   = Math.random() < 0.5 ? 1 : -1;            // 左右ランダム
        this.speed  = 5;                                      // 初速
        this.vx     = dir * this.speed * Math.cos(angle);
        this.vy     = this.speed * Math.sin(angle);
      }
      // ボール描画
      draw() {
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
        ctx.fillStyle = '#ff0'; // 黄色
        ctx.fill();
        ctx.closePath();
      }
      /**
       * @param {Paddle} p1 - 左パドル
       * @param {Paddle} p2 - 右パドル
       */
      update(p1, p2) {
        // 上下壁に当たったらY方向反転
        if (this.y - this.radius < 0 || this.y + this.radius > canvas.height) {
          this.vy = -this.vy;
        }
        // 左パドルとの当たり判定
        if (this.x - this.radius < p1.x + p1.width &&
            this.y > p1.y && this.y < p1.y + p1.height) {
          this.vx = -this.vx;
        }
        // 右パドルとの当たり判定
        if (this.x + this.radius > p2.x &&
            this.y > p2.y && this.y < p2.y + p2.height) {
          this.vx = -this.vx;
        }
        // 位置更新
        this.x += this.vx;
        this.y += this.vy;
      }
    }

    // ======== ゲームオブジェクト初期化 ========
    const player1  = new Paddle(20);                   // 左パドル
    const player2  = new Paddle(canvas.width - 40);   // 右パドル
    const ball     = new Ball();                      // ボール
    let score1 = 0, score2 = 0;                       // スコア
    const maxScore = 5;                               // 勝利条件スコア
    let animationId;                                  // アニメーションID保存用

    // ======== キー入力イベント ========
    window.addEventListener('keydown', e => {
      if (e.key === 'w')          player1.up   = true;
      if (e.key === 's')          player1.down = true;
      if (e.key === 'ArrowUp')    player2.up   = true;
      if (e.key === 'ArrowDown')  player2.down = true;
    });
    window.addEventListener('keyup', e => {
      if (e.key === 'w')          player1.up   = false;
      if (e.key === 's')          player1.down = false;
      if (e.key === 'ArrowUp')    player2.up   = false;
      if (e.key === 'ArrowDown')  player2.down = false;
    });

    // ======== スコア表示更新 ========
    function updateScore() {
      scoreBoard.textContent = `Player1: ${score1}  –  Player2: ${score2}`;
    }

    // ======== ゲーム終了処理 ========
    function endGame() {
      cancelAnimationFrame(animationId); // アニメループ停止
      const winner = score1 > score2 ? 'プレイヤー1' : 'プレイヤー2';
      endMessage.textContent = `🎉 ${winner} の勝ち! 🎉`;
      endScreen.style.display = 'flex'; // 終了画面表示
    }

    // ======== メインゲームループ ========
    function gameLoop() {
      // キャンバスクリア
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      // 背景を薄く暗くして見やすく
      ctx.fillStyle = 'rgba(0,0,0,0.3)';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      // オブジェクト更新・描画
      player1.update(); player1.draw();
      player2.update(); player2.draw();
      ball.update(player1, player2); ball.draw();

      // ゴール判定:左端を越えたらplayer2得点
      if (ball.x - ball.radius < 0) {
        score2++;
        updateScore();
        ball.reset();
      }
      // 右端を越えたらplayer1得点
      else if (ball.x + ball.radius > canvas.width) {
        score1++;
        updateScore();
        ball.reset();
      }

      // 勝利条件到達チェック
      if (score1 >= maxScore || score2 >= maxScore) {
        endGame();
        return; // 以降ループ停止
      }

      // 次フレーム呼び出し
      animationId = requestAnimationFrame(gameLoop);
    }

    // ======== スタートボタン処理 ========
    startBtn.addEventListener('click', () => {
      titleScreen.style.display = 'none';    // タイトル画面隠す
      canvas.style.display      = 'block';   // キャンバス表示
      scoreBoard.style.display  = 'block';   // スコア表示
      updateScore();                         // 初期スコア表示
      gameLoop();                            // ゲーム開始
    });

    // ======== 再スタート(タイトルに戻る)処理 ========
    restartBtn.addEventListener('click', () => {
      // 終了画面を隠してタイトル画面へ
      endScreen.style.display   = 'none';
      titleScreen.style.display = 'flex';
      // ゲーム画面を隠す
      canvas.style.display      = 'none';
      scoreBoard.style.display  = 'none';
      // スコアリセット&表示更新
      score1 = 0; score2 = 0;
      updateScore();
      // ボール位置リセット
      ball.reset();
    });
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ処理内容主な関数/命令
1. 初期化キャンバスサイズ設定、パドル・ボール・スコア変数を生成resizeCanvas(), new Paddle(), new Ball()
2. 入力受付キー押下/離上でパドルの上/下移動フラグを設定keydownkeyup イベント
3. メインループ開始描画クリア → 背景描画 → 各オブジェクト更新・描画gameLoop()
4. 衝突判定ボールと壁/パドルの当たり判定 → 方向反転Ball.update()
5. 得点判定ボールが左右端を越えたらスコア加算 → scoreBoard 更新 → ボールリセットupdateScore(), Ball.reset()
6. 勝利判定スコアが上限 (maxScore) に達したら終了画面を表示endGame()
7. 継続勝利条件未達の場合は requestAnimationFrame でループ継続requestAnimationFrame(gameLoop)

関数・クラスの詳しい解説

名称説明
resizeCanvas()ウィンドウサイズに合わせて <canvas> の幅・高さを調整
class Paddleパドルの大きさ・位置・速度・入力フラグを管理し、update() で移動、draw() で描画
class Ballボールの位置・速度を管理し、update(p1,p2) で衝突判定&移動、draw() で描画、reset() で初期位置に戻す
updateScore()scoreBoard 要素のテキストを現在のスコアに更新
endGame()アニメーションを停止し、勝者メッセージを表示 → 終了画面を表示
gameLoop()フレームごとの描画・更新を連続実行するメインループ

改造のポイント

  • 勝利条件の変更const maxScore = 5; の値を変えて、短期戦や長期戦に調整
  • パドル性能調整Paddle.speedwidthheight を変更し、操作感をカスタマイズ
  • ボール挙動の多様化Ball.reset() のランダム角度範囲を広げる・加速度を導入
  • 音声効果new Audio() で壁・パドル・得点時に効果音を再生
  • AI対戦モード:片方のパドルを自動追跡する AI ロジックを update() 内に実装
  • UI強化:スタート前のメニューや終了後のスコアランキング画面を追加

アドバイス:ゲームをより盛り上げるには、サウンドとエフェクトを組み合わせると同時に、パドルやボールに慣性や加速度を加えて、ビジュアルと操作感の両方を強化すると良いでしょう。ユーザーが自分の成長を実感できるランクシステムやカスタム設定画面を用意すると、リプレイ性も高まります!