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


画像

・テキストファイルを新規作成して、(index.html)
 以下のソースコードをコピペして保存し、ファイルをクリックすると、
 webブラウザが起動してゲームが遊べます。↓

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Image Booster 2.1 - Glass Road</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; z-index: 10; }
        
        #view-mode { position: absolute; top: 20px; right: 20px; font-size: 20px; color: #0f0; }
        #stage-info { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); font-size: 20px; color: #ff0; }
        #score-info { position: absolute; top: 20px; left: 20px; font-size: 24px; color: #fff; }

        /* 集中線エフェクト */
        #speed-lines {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background: repeating-conic-gradient(transparent 0deg, transparent 2deg, rgba(255, 255, 255, 0.1) 2.1deg, transparent 2.5deg);
            opacity: 0;
            transition: opacity 0.2s ease-out;
            pointer-events: none;
            z-index: 5;
            mix-blend-mode: screen;
        }

        #hud-bottom { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); width: 85%; display: none; flex-direction: row; justify-content: space-around; }
        .gauge-container { width: 30%; text-align: center; }
        .gauge-label { font-size: 18px; margin-bottom: 5px; }
        .bar-bg { width: 100%; height: 12px; border: 2px solid #fff; border-radius: 6px; overflow: hidden; }
        .bar-fill { height: 100%; width: 100%; transition: width 0.1s ease-out; }

        #hp-bg { background: #ff0000; }
        #hp-fill { background: #adff2f; }
        #speed-bg { background: #000080; }
        #speed-fill { background: #00ffff; }
        #time-bg { background: #b8860b; }
        #time-fill { background: #ffff00; }

        #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="speed-lines"></div>
    <div id="ui-layer">
        <div id="score-info">SCORE: 0</div>
        <div id="stage-info">STAGE: 1 / 8</div>
        <div id="view-mode">VIEW: 3RD PERSON</div>
        <div id="hud-bottom">
            <div class="gauge-container">
                <div class="gauge-label" id="hp-txt">HP: 10</div>
                <div class="bar-bg" id="hp-bg"><div id="hp-fill" class="bar-fill"></div></div>
            </div>
            <div class="gauge-container">
                <div class="gauge-label" id="speed-txt">SPEED: 0</div>
                <div class="bar-bg" id="speed-bg"><div id="speed-fill" class="bar-fill"></div></div>
            </div>
            <div class="gauge-container">
                <div class="gauge-label" id="time-txt">TIME: 60</div>
                <div class="bar-bg" id="time-bg"><div id="time-fill" class="bar-fill"></div></div>
            </div>
        </div>
        <div id="center-text"></div>
    </div>

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

        const MAX_HP = 10, TUBE_R = 15, PLANE_W = 12, MAX_TIME = 60;
        // 速度と加速を倍増
        const MAX_SPEED = 5.0; 
        const ACCEL = 0.1;
        const TOTAL_STAGES = 8;
        
        let scene, camera, renderer;
        let gameState = "TITLE", viewMode = "F3"; 
        let score = 0, hp = MAX_HP, timeLeft = MAX_TIME;
        let currentStage = 1;
        
        let player = { 
            mesh: null, angle: 0, x: 0, z: 0, 
            vz: 0.2, vAngle: 0, vx: 0, 
            altitude: 0, jumpV: 0,
            surge: 0, bank: 0 
        };
        
        let worldObjects = [], debris = [], stars, groundTube, groundPlane;
        let keys = {};
        let nextSpawnZ = 50;
        let tubeMaterial;

        function init() {
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000005);
            scene.fog = new THREE.Fog(0x000005, 50, 600); // 霧の範囲を広げて高速走行に対応

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

            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
            scene.add(new THREE.AmbientLight(0xffffff, 0.6));
            const sun = new THREE.DirectionalLight(0xffffff, 1.0);
            sun.position.set(0, 20, 10);
            scene.add(sun);

            createStars();

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

            const canvas = document.createElement('canvas');
            canvas.width = 512; canvas.height = 512;
            const ctx = canvas.getContext('2d');
            ctx.fillStyle = '#000000'; ctx.fillRect(0,0,512,512);
            ctx.strokeStyle = '#00ffff'; ctx.lineWidth = 4;
            for(let i=0; i<=512; i+=64) {
                ctx.moveTo(i, 0); ctx.lineTo(i, 512);
                ctx.moveTo(0, i); ctx.lineTo(512, i);
            }
            ctx.stroke();

            const gridTex = new THREE.CanvasTexture(canvas);
            gridTex.wrapS = THREE.RepeatWrapping;
            gridTex.wrapT = THREE.RepeatWrapping;
            gridTex.repeat.set(12, 100); 

            tubeMaterial = new THREE.MeshPhongMaterial({ 
                color: 0x444455, 
                map: gridTex,
                side: THREE.BackSide, 
                transparent: true, 
                opacity: 0.6,
                shininess: 100
            });

            groundTube = new THREE.Mesh(
                new THREE.CylinderGeometry(TUBE_R, TUBE_R, 400000, 32, 1, true),
                tubeMaterial
            );
            groundTube.rotation.x = Math.PI / 2;
            scene.add(groundTube);

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

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

        function createStars() {
            const starGeo = new THREE.BufferGeometry();
            const starCount = 3000;
            const posArray = new Float32Array(starCount * 3);
            for(let i=0; i<starCount * 3; i++) {
                posArray[i] = (Math.random() - 0.5) * 1000; 
            }
            starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
            const starMat = new THREE.PointsMaterial({color: 0xffffff, size: 1.0, transparent: true});
            stars = new THREE.Points(starGeo, starMat);
            scene.add(stars);
        }

        function updateStars() {
            if(stars) {
                stars.position.z = player.z;
                stars.rotation.z += 0.001; 
            }
            if(tubeMaterial) {
                tubeMaterial.map.offset.y = -player.z * 0.01; 
            }
        }

        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" || gameState === "ALL_CLEAR") {
                if(gameState !== "TITLE") {
                    score = 0; 
                    currentStage = 1;
                }
                changeState("STAGE_START");
            }
            else if (gameState === "STAGE_CLEAR") changeState("STAGE_START");
            else if (gameState === "STAGE_START") changeState("PLAYING");
            else if (gameState === "PLAYING" && player.altitude <= 0.01) {
                player.jumpV = 0.8; 
            }
        }

        function changeState(s) {
            gameState = s;
            document.getElementById('hud-bottom').style.display = (s === "PLAYING") ? "flex" : "none";
            const menu = document.getElementById('center-text');
            document.getElementById('stage-info').innerText = `STAGE: ${currentStage} / ${TOTAL_STAGES}`;
            
            document.getElementById('speed-lines').style.opacity = 0;

            if(s === "TITLE") menu.innerHTML = "<h1>Image Booster 2.1</h1><p>PRESS SPACE TO START</p>";
            else if(s === "STAGE_START") { 
                menu.innerHTML = "<h1>STAGE "+currentStage+"</h1><p>SPACE: GO!</p>"; 
                resetPlayerPos(); 
            }
            else if(s === "PLAYING") menu.innerHTML = "";
            else if(s === "STAGE_CLEAR") {
                if (currentStage >= TOTAL_STAGES) {
                    changeState("ALL_CLEAR");
                } else {
                    menu.innerHTML = "<h1 style='color:#0f0;'>STAGE CLEAR!</h1><p>SCORE: "+score+"</p><p>SPACE FOR NEXT STAGE</p>";
                    currentStage++;
                }
            }
            else if(s === "ALL_CLEAR") {
                menu.innerHTML = "<h1 style='color:#0ff;'>ALL CLEARED!</h1><p>FINAL SCORE: "+score+"</p><p>THANK YOU FOR PLAYING</p><p>SPACE TO TITLE</p>";
            }
            else if(s === "GAMEOVER") menu.innerHTML = "<h1 style='color:red;'>GAME OVER</h1><p>SCORE: "+score+"</p><p>SPACE TO RESTART</p>";
        }

        function resetPlayerPos() {
            player.angle = 0; player.x = 0; player.z = 0; player.vz = 0.5; player.altitude = 0; player.jumpV = 0;
            player.surge = 0; player.bank = 0;
            
            // HPのリセット条件を整理
            hp = MAX_HP;
            timeLeft = MAX_TIME;
            
            worldObjects.forEach(obj => scene.remove(obj.mesh));
            debris.forEach(d => scene.remove(d));
            worldObjects = [];
            debris = [];
            nextSpawnZ = 50;
        }

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

        function updatePhysics() {
            if (keys['ArrowUp']) {
                player.vz += ACCEL; // 加速力アップ
                document.getElementById('speed-lines').style.opacity = (player.vz / MAX_SPEED);
                player.surge = THREE.MathUtils.lerp(player.surge, 3.0, 0.05);
            } else {
                player.vz *= 0.995;
                document.getElementById('speed-lines').style.opacity = 0;
                if (keys['ArrowDown']) {
                    player.vz *= 0.90;
                    player.surge = THREE.MathUtils.lerp(player.surge, -2.0, 0.05);
                } else {
                    player.surge = THREE.MathUtils.lerp(player.surge, 0, 0.05);
                }
            }
            player.vz = Math.max(0.1, Math.min(player.vz, MAX_SPEED));

            player.altitude += player.jumpV;
            if (player.altitude > 0) player.jumpV -= 0.03;
            else { player.altitude = 0; player.jumpV = 0; }

            let targetBank = 0;
            if (viewMode === "F2") {
                if (keys['ArrowLeft']) { player.vx = THREE.MathUtils.lerp(player.vx, 1.2, 0.1); targetBank = 0.5; }
                else if (keys['ArrowRight']) { player.vx = THREE.MathUtils.lerp(player.vx, -1.2, 0.1); targetBank = -0.5; }
                else player.vx *= 0.9;
                
                player.x = Math.max(-PLANE_W, Math.min(PLANE_W, player.x + player.vx));
                player.bank = THREE.MathUtils.lerp(player.bank, targetBank, 0.1);
                player.mesh.position.set(player.x, 0.5 + player.altitude, player.z + player.surge);
                player.mesh.rotation.set(0, 0, (player.vx * 0.3) + player.bank);
            } else {
                if (keys['ArrowLeft']) { player.vAngle = THREE.MathUtils.lerp(player.vAngle, 0.12, 0.1); targetBank = 0.8; }
                else if (keys['ArrowRight']) { player.vAngle = THREE.MathUtils.lerp(player.vAngle, -0.12, 0.1); targetBank = -0.8; }
                else player.vAngle *= 0.9;
                
                player.angle += player.vAngle;
                player.bank = THREE.MathUtils.lerp(player.bank, targetBank, 0.1);
                const r = TUBE_R - 0.5 - player.altitude;
                player.mesh.position.set(Math.sin(player.angle)*r, -Math.cos(player.angle)*r, player.z + player.surge);
                player.mesh.rotation.set(0, 0, player.angle + player.bank);
            }
            
            player.z += player.vz;
            
            // タイムが0になった瞬間にクリア判定
            timeLeft -= 1/60;
            if (timeLeft <= 0) {
                timeLeft = 0;
                changeState("STAGE_CLEAR");
            }
            if (hp <= 0) {
                hp = 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 - 20);
                camera.lookAt(player.x, 1, player.z + 40);
            } else { 
                const camDist = 20, camHeight = 6;
                const coreX = Math.sin(player.angle) * (TUBE_R - 0.5);
                const coreY = -Math.cos(player.angle) * (TUBE_R - 0.5);
                camera.position.x = coreX - Math.sin(player.angle) * camHeight;
                camera.position.y = coreY + Math.cos(player.angle) * camHeight;
                camera.position.z = player.z - camDist;
                camera.up.set(-coreX, -coreY, 0).normalize();
                camera.lookAt(coreX, coreY, player.z + 30);
            }
        }

        function updateObjects() {
            let spawnDist = Math.max(8, 20 - (currentStage * 2.0)); 
            if (player.z + 400 > nextSpawnZ) { 
                spawnObject(nextSpawnZ); 
                nextSpawnZ += Math.random() * 5 + (spawnDist);
            }
            for (let i = worldObjects.length - 1; i >= 0; i--) {
                const obj = worldObjects[i];
                if(obj.flying) {
                    obj.mesh.position.add(obj.vel);
                    obj.mesh.rotation.x += 0.1;
                    obj.mesh.rotation.y += 0.1;
                } else if (player.mesh.position.distanceTo(obj.mesh.position) < (obj.type === 'hurdle' ? 4.5 : 3.0)) {
                    handleCollision(obj, i);
                    if(!obj.flying && obj.type !== "block" && obj.type !== "hurdle") {
                        scene.remove(obj.mesh);
                        worldObjects.splice(i, 1);
                        continue;
                    }
                }
                if (obj.mesh.position.z < player.z - 100) {
                    scene.remove(obj.mesh);
                    worldObjects.splice(i, 1);
                }
            }
        }

        function spawnObject(z) {
            let hurdleRate = Math.min(0.5, 0.15 + (currentStage * 0.05));
            let type = (Math.random() < 0.1) ? "score" : (Math.random() < 0.05) ? "heal" : "block";
            if (Math.random() < hurdleRate) type = "hurdle";

            const colors = { block: 0x884422, hurdle: 0xff3300, score: 0xffff00, heal: 0x00ff00 };
            let geo = (type === 'hurdle') ? new THREE.BoxGeometry(4,4,4) : ( (type==='score'||type==='heal') ? new THREE.BoxGeometry(1.5,1.5,1.5) : new THREE.BoxGeometry(2.5,2.5,2.5) );
            const mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ color: colors[type] }));
            
            if (viewMode === "F2") mesh.position.set((Math.random()-0.5)*PLANE_W*2, (type==='hurdle'?2:1), z);
            else {
                const a = Math.random()*Math.PI*2;
                const offset = (type==='hurdle') ? 2 : (type==='block' ? 1.2 : 0.7);
                mesh.position.set(Math.sin(a)*(TUBE_R - offset), -Math.cos(a)*(TUBE_R - offset), z);
                mesh.rotation.z = a;
            }
            scene.add(mesh);
            worldObjects.push({ mesh, type, flying: false, vel: new THREE.Vector3() });
        }

        function handleCollision(obj, index) {
            const isMaxSpeed = player.vz >= (MAX_SPEED - 0.5);
            if (obj.type === "block") {
                if (isMaxSpeed) {
                    createExplosion(obj.mesh.position, obj.mesh.material.color, 1.2);
                    scene.remove(obj.mesh);
                    worldObjects.splice(index, 1);
                    score += 20; showPopText("BREAK! +20", "#ffaa00");
                } else {
                    obj.flying = true;
                    obj.vel.set((Math.random()-0.5)*2, 2, 5);
                    score += 10; showPopText("+10", "#ffaa00");
                }
            } else if (obj.type === "hurdle") {
                if (isMaxSpeed) {
                    hp -= 1; showPopText("OUCH! HP -1", "#ff5500");
                    obj.flying = true;
                    obj.vel.set((Math.random()-0.5)*3, 3, 6);
                    player.vz *= 0.85;
                } else {
                    hp -= 2; showPopText("CRASH! HP -2", "#ff0000");
                    obj.flying = true;
                    obj.vel.set(0, 2, 2);
                    player.vz *= 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 createExplosion(pos, color, scaleFactor) {
            const count = 10 * scaleFactor;
            for(let i=0; i<count; i++) {
                const size = 0.5 * scaleFactor;
                const mesh = new THREE.Mesh(new THREE.BoxGeometry(size, size, size), new THREE.MeshPhongMaterial({ color }));
                mesh.position.copy(pos);
                scene.add(mesh);
                const vel = new THREE.Vector3((Math.random()-0.5)*2, Math.random()*2, 3.0+Math.random()*4);
                debris.push({ mesh, vel, life: 60 });
            }
        }

        function updateDebris() {
            for(let i = debris.length - 1; i >= 0; i--) {
                const d = debris[i];
                d.mesh.position.add(d.vel);
                d.life--;
                if(d.life <= 0) {
                    scene.remove(d.mesh);
                    debris.splice(i, 1);
                }
            }
        }

        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 = "40%";
            document.getElementById('ui-layer').appendChild(div);
            setTimeout(() => div.remove(), 1000);
        }

        function updateUI() {
            document.getElementById('score-info').innerText = "SCORE: " + score;
            document.getElementById('hp-txt').innerText = "HP: " + hp;
            document.getElementById('hp-fill').style.width = Math.max(0, (hp / MAX_HP * 100)) + "%";
            
            document.getElementById('speed-txt').innerText = "SPEED: " + (player.vz * 40).toFixed(0);
            document.getElementById('speed-fill').style.width = (player.vz / MAX_SPEED * 100) + "%";
            document.getElementById('speed-fill').style.backgroundColor = (player.vz >= MAX_SPEED - 0.5) ? "#fff" : "#00ffff";

            document.getElementById('time-txt').innerText = "TIME: " + Math.max(0, timeLeft).toFixed(0);
            document.getElementById('time-fill').style.width = (timeLeft / MAX_TIME * 100) + "%";
        }

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

【操作説明】

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

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

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

【おまけ】

・チューブ系のゲームを検索してみたところ、
 やはりいくつかあったので紹介しておきます。

1981 Atari Tempest Arcade 

・これはレースゲームというか、シューティングみたいな感じですね。

1983 ジャイラス コナミ

・これもシューティングゲームで、やや滑らかに回る感じ。

Bullfrog - Tube - 1995

・かなりレースゲームっぽい感じになってきました。

Ballistics (2001) - PC

これは障害物を避けていくパターンで、一応、レースゲームなのかな?

Tube Slider (2003) GameCube

・これは任天堂のレースゲームで、
 「F-ZERO」の後継シリーズとして開発されていたとのこと。

Space Giraffe(2007)Llamasoft

・これはまたシューティングゲームで、テンペストっぽい感じ。

「Race The Sun」Flippfly (2013)

・これはチューブ系ではないものの、平面モードはわりと近いかも。

「Thumper」Drool(2016)

・これはリズム系のゲームで、
 タイミングを合わせると障害物を回避できる。

「Tunnel Rush」(2024?)

・これも障害物を避けまくるゲーム。

とまあ、似たような雰囲気のゲームは
これまでにもあったみたいなんですが、
ぶっ壊しに主軸をおいたゲームでは無かったみたいなので、
まあまあセーフだったりするのかも知れない。(^^;

3Dのこういう真正面を向いたゲームは、
合わせるのが難しいので、
気軽に遊べるのがいいのではないだろうか。uu

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
買うたび 抽選 ※条件・上限あり \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