ブラウザで遊べるドライブゲームImageBooster

【操作説明】

・左右キー … 自機を左右に移動。
・上キー … アクセル。
・下キー … ブレーキ。
・スペースキー … ジャンプ。

・F1キー … チューブモードの一人称視点に切り替え。
・F2キー … 平面モードに切り替え。
・F3キー … チューブモードの三人称視点に切り替え。

・障害物を避けて、高得点を目指しましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Image Booster 1.0 - Chase Camera Fixed</title>
    <style>
        body { margin: 0; overflow: hidden; background: #000; font-family: 'Arial Black', sans-serif; }
        #ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; color: white; text-shadow: 2px 2px 4px #000; }
        #hud { position: absolute; top: 20px; left: 20px; text-align: left; display: none; font-size: 22px; line-height: 1.2; }
        #center-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; text-align: center; }
        h1 { font-size: 60px; margin: 0; color: #0ff; }
        .pop-text { position: absolute; font-weight: bold; font-size: 38px; animation: moveUpFade 1s forwards; transform: translate(-50%, -50%); z-index: 999; }
        @keyframes moveUpFade { 0% { transform: translate(-50%, 0); opacity: 1; } 100% { transform: translate(-50%, -150px); opacity: 0; } }
    </style>
    <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } } </script>
</head>
<body>
    <div id="ui-layer">
        <div id="hud">
            <div id="view-mode" style="color:#0f0">VIEW: 3RD PERSON</div>
            <div id="hp-disp">HP: 10</div>
            <div id="score-disp">SCORE: 0</div>
            <div id="time-disp">TIME: 180.00</div>
        </div>
        <div id="center-text"></div>
    </div>

    <script type="module">
        import * as THREE from 'three';

        const MAX_HP = 10;
        const TUBE_R = 15;
        const PLANE_W = 12;

        let scene, camera, renderer;
        let gameState = "TITLE", viewMode = "F3"; 
        let score = 0, hp = MAX_HP, timeLeft = 180;
        let isBoost = false;
        
        let player = { mesh: null, angle: 0, x: 0, z: 0, vz: 0.2, vAngle: 0, vx: 0 };
        let worldObjects = [], groundTube, groundPlane;
        let keys = {};
        let nextSpawnZ = 50;

        function init() {
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000005);
            scene.fog = new THREE.Fog(0x000005, 50, 500);

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

            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);

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

            player.mesh = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.6, 2.5), new THREE.MeshPhongMaterial({ color: 0x00aaff }));
            scene.add(player.mesh);

            groundTube = new THREE.Mesh(
                new THREE.CylinderGeometry(TUBE_R, TUBE_R, 200000, 32, 1, true),
                new THREE.MeshPhongMaterial({ color: 0x333344, side: THREE.BackSide, wireframe: true })
            );
            groundTube.rotation.x = Math.PI / 2;
            scene.add(groundTube);

            groundPlane = new THREE.Mesh(
                new THREE.PlaneGeometry(100, 200000),
                new THREE.MeshPhongMaterial({ color: 0x111111 })
            );
            groundPlane.rotation.x = -Math.PI / 2;
            groundPlane.visible = false;
            scene.add(groundPlane);

            window.addEventListener('keydown', (e) => {
                keys[e.code] = true;
                if(["F1", "F2", "F3"].includes(e.code)) {
                    e.preventDefault();
                    viewMode = e.code;
                    updateViewLabel();
                }
                if(e.code === 'Space') handleSpace();
            });
            window.addEventListener('keyup', (e) => keys[e.code] = false);
            
            updateViewLabel();
            changeState("TITLE");
            animate();
        }

        function updateViewLabel() {
            const labels = { "F1": "1ST PERSON", "F2": "PLANE MODE", "F3": "3RD PERSON" };
            document.getElementById('view-mode').innerText = "VIEW: " + labels[viewMode];
            groundTube.visible = (viewMode !== "F2");
            groundPlane.visible = (viewMode === "F2");
        }

        function handleSpace() {
            if (gameState === "TITLE" || gameState === "GAMEOVER") changeState("STAGE_START");
            else if (gameState === "STAGE_START") changeState("PLAYING");
        }

        function changeState(s) {
            gameState = s;
            document.getElementById('hud').style.display = (s === "PLAYING") ? "block" : "none";
            const menu = document.getElementById('center-text');
            if(s === "TITLE") menu.innerHTML = "<h1>Image Booster 1.0</h1><p>PRESS SPACE TO START</p>";
            else if(s === "STAGE_START") { menu.innerHTML = "<h1>READY?</h1><p>SPACE: GO!</p>"; resetPlayer(); }
            else if(s === "PLAYING") menu.innerHTML = "";
            else if(s === "GAMEOVER") menu.innerHTML = "<h1 style='color:red;'>GAME OVER</h1><p>SCORE: "+score+"</p><p>SPACE TO RESTART</p>";
        }

        function resetPlayer() {
            player.angle = 0; player.x = 0; player.z = 0; player.vz = 0.5;
            hp = MAX_HP; score = 0; timeLeft = 180; isBoost = false;
            worldObjects.forEach(obj => scene.remove(obj.mesh));
            worldObjects = [];
            nextSpawnZ = 50;
        }

        function animate() {
            requestAnimationFrame(animate);
            if (gameState === "PLAYING") {
                updatePhysics();
                updateObjects();
            }
            updateCamera();
            renderer.render(scene, camera);
        }

        function updatePhysics() {
            if (keys['ArrowUp']) player.vz += (isBoost ? 0.08 : 0.04);
            else { isBoost = false; player.vz *= 0.995; }
            if (keys['ArrowDown']) player.vz *= 0.90;
            player.vz = Math.max(0.1, Math.min(player.vz, 2.5));

            if (viewMode === "F2") {
                if (keys['ArrowLeft']) player.vx = THREE.MathUtils.lerp(player.vx, 0.7, 0.1);
                else if (keys['ArrowRight']) player.vx = THREE.MathUtils.lerp(player.vx, -0.7, 0.1);
                else player.vx *= 0.9;
                player.x = Math.max(-PLANE_W, Math.min(PLANE_W, player.x + player.vx));
                player.mesh.position.set(player.x, 0.5, player.z);
                player.mesh.rotation.set(0, 0, player.vx * 0.3);
            } else {
                if (keys['ArrowLeft']) player.vAngle = THREE.MathUtils.lerp(player.vAngle, 0.08, 0.1);
                else if (keys['ArrowRight']) player.vAngle = THREE.MathUtils.lerp(player.vAngle, -0.08, 0.1);
                else player.vAngle *= 0.9;
                player.angle += player.vAngle;
                
                player.mesh.position.set(
                    Math.sin(player.angle) * (TUBE_R - 0.5), 
                    -Math.cos(player.angle) * (TUBE_R - 0.5), 
                    player.z
                );
                player.mesh.rotation.set(0, 0, player.angle);
            }
            player.z += player.vz;
            timeLeft -= 1/60;
            if (hp <= 0 || timeLeft <= 0) changeState("GAMEOVER");
            updateUI();
        }

        function updateCamera() {
            player.mesh.visible = (viewMode !== "F1");

            if (viewMode === "F1") { 
                camera.position.copy(player.mesh.position);
                camera.up.set(-player.mesh.position.x, -player.mesh.position.y, 0).normalize();
                camera.lookAt(player.mesh.position.x, player.mesh.position.y, player.z + 100);

            } else if (viewMode === "F2") { 
                camera.up.set(0, 1, 0);
                camera.position.set(player.x, 6, player.z - 15);
                camera.lookAt(player.x, 1, player.z + 30);
            } else { 
                // 【F3 三人称視点】自機の少し手前の上から見下ろす
                const camDist = 18; // 自機からの後方距離
                const camHeight = 5; // チューブ中心方向(上空)への高さ
                
                // カメラ位置:自機の位置から、中心方向に浮かせ、さらに後方に引く
                camera.position.x = player.mesh.position.x - Math.sin(player.angle) * camHeight;
                camera.position.y = player.mesh.position.y + Math.cos(player.angle) * camHeight;
                camera.position.z = player.z - camDist;
                
                // カメラの上向きは「チューブの中心」
                camera.up.set(-player.mesh.position.x, -player.mesh.position.y, 0).normalize();
                
                // 注視点は自機の少し前方
                camera.lookAt(player.mesh.position.x, player.mesh.position.y, player.z + 20);
            }
        }

        function updateObjects() {
            if (player.z + 250 > nextSpawnZ) { spawnObject(nextSpawnZ); nextSpawnZ += 5; }
            for (let i = worldObjects.length - 1; i >= 0; i--) {
                const obj = worldObjects[i];
                if(obj.flying) {
                    obj.mesh.position.add(obj.vel);
                } else if (player.mesh.position.distanceTo(obj.mesh.position) < 2.5) {
                    handleCollision(obj);
                    if(obj.type !== "block" && obj.type !== "hurdle") {
                        scene.remove(obj.mesh);
                        worldObjects.splice(i, 1);
                        continue;
                    }
                }
                if (obj.mesh.position.z < player.z - 50) {
                    scene.remove(obj.mesh);
                    worldObjects.splice(i, 1);
                }
            }
        }

        function spawnObject(z) {
            let type = (Math.random() < 0.1) ? "score" : (Math.random() < 0.05) ? "heal" : "block";
            if (Math.random() < 0.15) type = "hurdle";
            const colors = { block: 0x884422, hurdle: 0xff3300, score: 0xffff00, heal: 0x00ff00 };
            const mesh = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshPhongMaterial({ color: colors[type] }));
            
            if (viewMode === "F2") {
                mesh.position.set((Math.random()-0.5)*PLANE_W*2, 1, z);
            } else {
                const a = Math.random()*Math.PI*2;
                mesh.position.set(Math.sin(a)*(TUBE_R - 1.0), -Math.cos(a)*(TUBE_R - 1.0), z);
                mesh.rotation.z = a;
            }
            scene.add(mesh);
            worldObjects.push({ mesh, type, flying: false, vel: new THREE.Vector3() });
        }

        function handleCollision(obj) {
            if (obj.type === "block") {
                score += 10; showPopText("+10", "#ffaa00"); 
                obj.flying = true; obj.vel.set((Math.random()-0.5)*2, 2, 3);
            } else if (obj.type === "hurdle") {
                hp--; showPopText("HP -1", "#ff0000"); 
                obj.flying = true; obj.vel.set(0, 1, 0.5); 
            } else if (obj.type === "score") { score += 100; showPopText("+100", "#ffff00"); }
            else if (obj.type === "heal") { hp = Math.min(MAX_HP, hp + 1); showPopText("HP +1", "#00ff00"); }
        }

        function showPopText(text, color) {
            const div = document.createElement('div');
            div.className = 'pop-text';
            div.style.color = color;
            div.innerText = text;
            div.style.left = "50%";
            div.style.top = "50%";
            document.getElementById('ui-layer').appendChild(div);
            setTimeout(() => div.remove(), 1000);
        }

        function updateUI() {
            document.getElementById('hp-disp').innerText = "HP: " + hp;
            document.getElementById('score-disp').innerText = "SCORE: " + score;
            document.getElementById('time-disp').innerText = "TIME: " + Math.max(0, timeLeft).toFixed(2);
        }

        init();
    </script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
買うたび 抽選 ※条件・上限あり \note クリエイター感謝祭ポイントバックキャンペーン/最大全額もどってくる! 12.1 月〜1.14 水 まで
ブラウザで遊べるドライブゲームImageBooster|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1