JavaScriptで作る簡単なポップバルーンゲーム

## 概要
この記事では、HTML、CSS、JavaScriptを使用した簡単なブラウザゲーム「ポップバルーン」の作り方を紹介します。移動する風船（ターゲット）をクリックしてスコアを稼ぐシンプルなゲームですが、複数の移動パターンや responsive デザインを実装しています。

## 特徴
- 30秒間の制限時間
- 3種類の移動パターン（ランダム、円運動、ジグザグ）
- レスポンシブデザイン対応
- ハイスコア機能
- スマートフォン・タブレット対応（タッチイベント）

このゲームは Markdown AI で作成、公開しています。

https://mdown.ai/

## デモ
![1000005317.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/49dd204c-2826-859e-fc01-3b7586bd34f3.gif)

[実際にプレイしてみる](https://mdown.ai/content/e77861ea-9e59-4e75-b538-d7025bc4c020
)

<!-- open属性なし -->
<details><summary>サンプルコードはこちら</summary>

```html
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      margin: 0;
      padding: 15px;
      box-sizing: border-box;
    }
    .game-container {
      position: relative;
      width: 100%;
      max-width: 600px;
      height: 400px;
      margin: 0 auto;
      background: #f0f0f0;
      border: 2px solid #333;
      overflow: hidden;
    }
    @media (max-width: 600px) {
      .game-container {
        height: 300px;
      }
      body {
        padding: 10px;
      }
    }
    .target {
      position: absolute;
      border-radius: 50%;
      cursor: pointer;
      transition: width 0.3s, height 0.3s, background-color 0.2s;
    }
    .target:hover {
      filter: brightness(1.2);
    }
    .score-panel {
      position: absolute;
      top: 10px;
      left: 10px;
      font-size: 16px;
      font-family: Arial, sans-serif;
      background: rgba(255, 255, 255, 0.8);
      padding: 5px;
      border-radius: 5px;
    }
    .timer {
      position: absolute;
      top: 10px;
      right: 10px;
      font-size: 16px;
      font-family: Arial, sans-serif;
      color: #e53e3e;
      background: rgba(255, 255, 255, 0.8);
      padding: 5px;
      border-radius: 5px;
    }
    .game-over {
      position: absolute;
      top: 40%;
      left: 50%;
      transform: translate(-50%, -50%);
      font-size: 32px;
      font-family: Arial, sans-serif;
      color: #e53e3e;
      display: none;
      text-align: center;
      background: rgba(255, 255, 255, 0.9);
      padding: 20px;
      border-radius: 10px;
      width: 80%;
      max-width: 300px;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    }
    .start-button {
      position: absolute;
      top: 60%;
      left: 50%;
      transform: translate(-50%, -50%);
      padding: 10px 20px;
      font-size: 18px;
      background: #48BB78;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    }
    .start-button:hover {
      background: #38A169;
    }
    @media (max-width: 400px) {
      .score-panel, .timer {
        font-size: 14px;
      }
      .game-over {
        font-size: 24px;
        padding: 15px;
      }
      .start-button {
        font-size: 16px;
      }
    }
  </style>
</head>
<body>
  <div class="game-container">
    <div class="score-panel">
      スコア: <span id="scoreValue">0</span><br>
      ハイスコア: <span id="highScore">0</span>
    </div>
    <div class="timer">残り時間: <span id="timeLeft">30</span>秒</div>
    <div id="target" class="target"></div>
    <div class="game-over" id="gameOver">
      ゲームオーバー！
    </div>
    <button id="startButton" class="start-button">ゲームスタート</button>
  </div>

  <script>
    const target = document.getElementById('target');
    const scoreElement = document.getElementById('scoreValue');
    const highScoreElement = document.getElementById('highScore');
    const timeLeftElement = document.getElementById('timeLeft');
    const gameOverElement = document.getElementById('gameOver');
    const startButton = document.getElementById('startButton');
    
    let score = 0;
    let highScore = 0;
    let timeLeft = 30;
    let gameInterval;
    let movePattern = 0;
    let isGameRunning = false;
    let currentX = 0;
    let currentY = 0;
    let patternTime = 0;

    const colors = ['#4299E1', '#F56565', '#48BB78', '#ECC94B', '#9F7AEA'];
    const sizes = [30, 40, 50, 60];

    function getRandomPosition() {
      const container = document.querySelector('.game-container');
      const targetSize = parseInt(target.style.width);
      const maxX = container.clientWidth - targetSize;
      const maxY = container.clientHeight - targetSize;
      return {
        x: Math.random() * maxX,
        y: Math.random() * maxY
      };
    }

    function moveTarget() {
      const container = document.querySelector('.game-container');
      const targetSize = parseInt(target.style.width);
      const maxX = container.clientWidth - targetSize;
      const maxY = container.clientHeight - targetSize;
      
      switch(movePattern) {
        case 0: // ランダム移動
          const pos = getRandomPosition();
          currentX = pos.x;
          currentY = pos.y;
          break;
        case 1: // 円運動
          patternTime += 0.1;
          currentX = maxX/2 + Math.cos(patternTime) * (maxX/3);
          currentY = maxY/2 + Math.sin(patternTime) * (maxY/3);
          break;
        case 2: // ジグザグ
          patternTime += 0.1;
          currentX = (maxX/2) * (1 + Math.sin(patternTime));
          currentY = (maxY/2) * (1 + Math.cos(patternTime * 3));
          break;
      }

      // 画面内に収まるように位置を制限
      currentX = Math.max(0, Math.min(currentX, maxX));
      currentY = Math.max(0, Math.min(currentY, maxY));

      target.style.left = currentX + 'px';
      target.style.top = currentY + 'px';
      
      const newSize = sizes[Math.floor(Math.random() * sizes.length)];
      const colorIndex = Math.floor(Math.random() * colors.length);
      
      target.style.width = newSize + 'px';
      target.style.height = newSize + 'px';
      target.style.backgroundColor = colors[colorIndex];
      
      if (Math.random() < 0.1) {
        movePattern = Math.floor(Math.random() * 3);
      }
    }

    function updateTimer() {
      timeLeft--;
      timeLeftElement.textContent = timeLeft;
      
      if (timeLeft <= 0) {
        endGame();
      }
    }

    function startGame() {
      score = 0;
      timeLeft = 30;
      isGameRunning = true;
      scoreElement.textContent = score;
      timeLeftElement.textContent = timeLeft;
      gameOverElement.style.display = 'none';
      startButton.style.display = 'none';
      target.style.display = 'block';
      
      target.style.width = '50px';
      target.style.height = '50px';
      moveTarget();
      
      gameInterval = setInterval(updateTimer, 1000);
    }

    function endGame() {
      isGameRunning = false;
      clearInterval(gameInterval);
      target.style.display = 'none';
      gameOverElement.style.display = 'block';
      startButton.style.display = 'block';
      startButton.textContent = 'もう一度プレイ';
      
      if (score > highScore) {
        highScore = score;
        highScoreElement.textContent = highScore;
      }
    }

    target.addEventListener('click', () => {
      if (!isGameRunning) return;
      
      score++;
      scoreElement.textContent = score;
      moveTarget();
    });

    startButton.addEventListener('click', startGame);

    // タッチデバイス対応
    target.addEventListener('touchstart', (e) => {
      e.preventDefault(); // デフォルトの動作を防止
      if (!isGameRunning) return;
      
      score++;
      scoreElement.textContent = score;
      moveTarget();
    });

    // 初期表示設定
    target.style.display = 'none';
  </script>
</body>
</html>
```
</details>

## 実装の解説

### 1. HTML構造
ゲームの基本構造は非常にシンプルです。

```html
<div class="game-container">
  <div class="score-panel">
    スコア: <span id="scoreValue">0</span><br>
    ハイスコア: <span id="highScore">0</span>
  </div>
  <div class="timer">残り時間: <span id="timeLeft">30</span>秒</div>
  <div id="target" class="target"></div>
  <div class="game-over" id="gameOver">
    ゲームオーバー！
  </div>
  <button id="startButton" class="start-button">ゲームスタート</button>
</div>
```

### 2. CSSスタイリング
重要なポイントとなるCSSの一部を紹介します。

```css
.game-container {
  position: relative;
  width: 100%;
  max-width: 600px;
  height: 400px;
  margin: 0 auto;
  background: #f0f0f0;
  border: 2px solid #333;
  overflow: hidden;
}

.target {
  position: absolute;
  border-radius: 50%;
  cursor: pointer;
  transition: width 0.3s, height 0.3s, background-color 0.2s;
}
```
### 3. JavaScriptの実装
ゲームの主要な機能を実現するJavaScriptコードの解説です。

#### 3.1 移動パターンの実装
3種類の移動パターンを実装しています。

```javascript
function moveTarget() {
  switch(movePattern) {
    case 0: // ランダム移動
      const pos = getRandomPosition();
      currentX = pos.x;
      currentY = pos.y;
      break;
    case 1: // 円運動
      patternTime += 0.1;
      currentX = maxX/2 + Math.cos(patternTime) * (maxX/3);
      currentY = maxY/2 + Math.sin(patternTime) * (maxY/3);
      break;
    case 2: // ジグザグ
      patternTime += 0.1;
      currentX = (maxX/2) * (1 + Math.sin(patternTime));
      currentY = (maxY/2) * (1 + Math.cos(patternTime * 3));
      break;
  }
}
```
#### 3.2 タッチデバイス対応
スマートフォンやタブレットでも遊べるように、タッチイベントを実装しています。

```javascript
target.addEventListener('touchstart', (e) => {
  e.preventDefault();
  if (!isGameRunning) return;
  
  score++;
  scoreElement.textContent = score;
  moveTarget();
});
```
## 改善点とカスタマイズのアイデア
1. 効果音の追加
2. 難易度選択機能の実装
3. パワーアップアイテムの追加
4. アニメーション効果の強化
5. ソーシャルシェア機能の追加

## 実装上の工夫
見た目以上にシンプルな実装になっています。requestAnimationFrameを使用せずともsetIntervalとCSS transitionの組み合わせで滑らかな動きを実現し、三角関数を使って複雑な動きを表現しています。また、10%の確率で移動パターンが変更されることで、プレイヤーは次の動きを予測しづらく、シンプルながらも高いゲーム性を実現しています。

## まとめ
このゲームは、JavaScriptでインタラクティブなWebアプリケーションを作る良い練習になります。モバイル対応やアニメーション、タイマー処理など、実践的なWeb開発の要素を含んでいます。

## 参考リンク
- [MDN - requestAnimationFrame](https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame)
- [MDN - Touch events](https://developer.mozilla.org/ja/docs/Web/API/Touch_events)