<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>Tilt Racing Game</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      background: #000;
      font-family: sans-serif;
      touch-action: none;
    }
    #ui {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      align-items: center;
      padding: 20px;
      box-sizing: border-box;
    }
    #score {
      color: #fff;
      font-size: 24px;
      font-weight: bold;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
    }
    #start-overlay {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0,0,0,0.7);
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      color: #fff;
      z-index: 10;
    }
    #start-btn {
      padding: 15px 30px;
      font-size: 20px;
      font-weight: bold;
      background: #ff4757;
      color: #fff;
      border: none;
      border-radius: 30px;
      cursor: pointer;
      box-shadow: 0 4px 10px rgba(0,0,0,0.3);
    }
  </style>
  <!-- Three.js CDN -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>

  <div id="ui">
    <div id="score">SCORE: 0</div>
  </div>

  <div id="start-overlay">
    <h1 style="margin-bottom: 10px;">傾きレースゲーム</h1>
    <p style="margin-bottom: 30px; text-align: center; line-height: 1.5;">
      スマホを左右に傾けてハンドル操作!<br>障害物を避けて走り抜けよう。
    </p>
    <button id="start-btn">スタート / センサー許可</button>
  </div>

  <script>
    let scene, camera, renderer;
    let player, track;
    let obstacles = [];
    let score = 0;
    let isPlaying = false;
    let tiltX = 0;

    const ROAD_WIDTH = 12;
    const SPEED = 0.6;

    function init() {
      // 1. シーン・カメラ・レンダラー設定
      scene = new THREE.Scene();
      scene.fog = new THREE.FogExp2(0xa0a0a0, 0.015);

      camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
      camera.position.set(0, 3, 7);
      camera.lookAt(0, 1, 0);

      renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setClearColor(0xa0a0a0);
      document.body.appendChild(renderer.domElement);

      // 2. 照明
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
      scene.add(ambientLight);

      const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
      dirLight.position.set(10, 20, 10);
      scene.add(dirLight);

      // 3. 道路(トラック)
      const trackGeo = new THREE.PlaneGeometry(ROAD_WIDTH, 1000);
      const trackMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
      track = new THREE.Mesh(trackGeo, trackMat);
      track.rotation.x = -Math.PI / 2;
      track.position.z = -450;
      scene.add(track);

      // 白線(サイドライン)
      const lineGeo = new THREE.PlaneGeometry(0.3, 1000);
      const lineMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
      const leftLine = new THREE.Mesh(lineGeo, lineMat);
      leftLine.position.set(-ROAD_WIDTH/2 + 0.3, 0.01, 0);
      track.add(leftLine);
      const rightLine = new THREE.Mesh(lineGeo, lineMat);
      rightLine.position.set(ROAD_WIDTH/2 - 0.3, 0.01, 0);
      track.add(rightLine);

      // 4. プレイヤー(自機)
      const playerGeo = new THREE.BoxGeometry(1.2, 0.6, 2.2);
      const playerMat = new THREE.MeshStandardMaterial({ color: 0xff4757 });
      player = new THREE.Mesh(playerGeo, playerMat);
      player.position.set(0, 0.3, 0);
      scene.add(player);

      // ウィンドウサイズ変更対応
      window.addEventListener('resize', onWindowResize);

      // スタートボタンイベント
      document.getElementById('start-btn').addEventListener('click', startGame);
    }

    // 傾きセンサーの権限要求&イベント登録
    function requestOrientationPermission() {
      if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
        // iOS 13+ 用の許可要求
        DeviceOrientationEvent.requestPermission()
          .then(response => {
            if (response === 'granted') {
              window.addEventListener('deviceorientation', handleOrientation);
            } else {
              alert('センサーの使用許可が得られませんでした。');
            }
          })
          .catch(console.error);
      } else {
        // Android / 古いiOS用
        window.addEventListener('deviceorientation', handleOrientation);
      }
    }

    // 傾きデータの取得
    function handleOrientation(event) {
      // gamma: 左右の傾き(-90 ~ 90)
      let g = event.gamma;
      if (g !== null) {
        // 感度調整(-30度〜30度を -1.0 〜 1.0 に正規化)
        tiltX = Math.max(-1, Math.min(1, g / 30));
      }
    }

    function startGame() {
      requestOrientationPermission();
      document.getElementById('start-overlay').style.display = 'none';
      score = 0;
      obstacles.forEach(obs => scene.remove(obs));
      obstacles = [];
      player.position.x = 0;
      isPlaying = true;
      animate();
    }

    function spawnObstacle() {
      if (Math.random() < 0.05) { // 生成確率
        const obsGeo = new THREE.BoxGeometry(1.5, 1.2, 1.5);
        const obsMat = new THREE.MeshStandardMaterial({ color: 0x2ed573 });
        const obstacle = new THREE.Mesh(obsGeo, obsMat);
        
        // 道路内のランダムな位置に配置
        const x = (Math.random() - 0.5) * (ROAD_WIDTH - 2);
        obstacle.position.set(x, 0.6, -80);
        scene.add(obstacle);
        obstacles.push(obstacle);
      }
    }

    function animate() {
      if (!isPlaying) return;

      requestAnimationFrame(animate);

      // 1. 自機の移動(傾きに基づく)
      player.position.x += tiltX * 0.25;
      
      // 移動制限(コースアウト防止)
      const limit = (ROAD_WIDTH / 2) - 0.8;
      if (player.position.x > limit) player.position.x = limit;
      if (player.position.x < -limit) player.position.x = -limit;

      // 自機を少し傾ける演出
      player.rotation.z = -tiltX * 0.2;

      // 2. 障害物の生成と移動
      spawnObstacle();

      for (let i = obstacles.length - 1; i >= 0; i--) {
        const obs = obstacles[i];
        obs.position.z += SPEED * 1.5;

        // 当たり判定 (簡易AABB)
        const dx = Math.abs(player.position.x - obs.position.x);
        const dz = Math.abs(player.position.z - obs.position.z);

        if (dx < 1.2 && dz < 1.5) {
          gameOver();
          return;
        }

        // 画面外(カメラの後ろ)に出たら削除
        if (obs.position.z > 5) {
          scene.remove(obs);
          obstacles.splice(i, 1);
          score += 100;
          document.getElementById('score').innerText = `SCORE: ${score}`;
        }
      }

      renderer.render(scene, camera);
    }

    function gameOver() {
      isPlaying = false;
      alert(`GAME OVER!\nスコア: ${score}`);
      document.getElementById('start-overlay').style.display = 'flex';
      document.getElementById('start-btn').innerText = 'リトライ';
    }

    function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    }

    window.onload = init;
  </script>
</body>
</html>

コメント

タイトルとURLをコピーしました