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

【更新機歴】

・2025/12/22 バージョン3.0公開。
・2025/12/24 バージョン4.0公開。
・2025/12/26 バージョン5.0公開。…トンネルの外側を追加。

画像
トンネルの内側を走行中のようす(三人称視点時)
画像
フラットモード
画像
トンネルの外側を走行中のようす(三人称視点時)



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

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Image Booster 5.0 Custom+</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: radial-gradient(circle, transparent 30%, rgba(255, 255, 255, 0.1) 80%),
                        repeating-conic-gradient(transparent 0deg, transparent 2deg, rgba(100, 255, 150, 0.3) 2.1deg, transparent 2.5deg);
            opacity: 0;
            transition: opacity 0.1s 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; align-items: flex-end; }
        .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; background: rgba(0,0,0,0.5); }
        .bar-fill { height: 100%; width: 100%; transition: width 0.05s ease-out; }

        #hp-fill { background: #adff2f; box-shadow: 0 0 10px #adff2f; }
        
        #speed-val { font-size: 48px; line-height: 1; color: #00ffff; text-shadow: 0 0 10px #00ffff; }
        #speed-unit { font-size: 20px; color: #aaa; margin-left: 5px; }
        #speed-bg { }
        #speed-fill { background: #00ffff; }
        
        #time-bg { }
        #time-fill { background: #ffcc00; }

        #center-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; text-align: center; }
        h1 { font-size: 60px; margin: 0; color: #0f0; text-shadow: 0 0 20px #0f0; }
        p { font-size: 24px; margin: 10px 0; color: #fff; }
        .pop-text { position: absolute; font-weight: bold; font-size: 40px; animation: moveUpFade 0.8s forwards; transform: translate(-50%, -50%); z-index: 999; }
        @keyframes moveUpFade { 0% { transform: translate(-50%, 0) scale(1); opacity: 1; } 100% { transform: translate(-50%, -100px) scale(1.5); 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">HP: <span id="hp-num" style="font-size: 32px;">10</span></div>
                <div class="bar-bg" id="hp-bg"><div id="hp-fill" class="bar-fill"></div></div>
            </div>
            <div class="gauge-container">
                <div style="margin-bottom:5px;">
                    <span id="speed-val">0</span><span id="speed-unit">km/h</span>
                </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">TIME: <span id="time-num" style="font-size: 32px;">1:00</span></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';

        // --- Sound Manager ---
        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        let noiseBuffer = null;
        let windSource = null, windGain = null, windFilter = null;
        let jetOsc = null, jetGain = null;

        function createNoiseBuffer() {
            const bufferSize = audioCtx.sampleRate * 2.0; 
            const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
            const data = buffer.getChannelData(0);
            for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
            return buffer;
        }

        const sound = {
            init: () => { if (!noiseBuffer) noiseBuffer = createNoiseBuffer(); },
            startEngine: () => {
                if (windSource) return;
                windSource = audioCtx.createBufferSource();
                windSource.buffer = noiseBuffer;
                windSource.loop = true;
                windFilter = audioCtx.createBiquadFilter();
                windFilter.type = 'bandpass';
                windFilter.Q.value = 1.0;
                windFilter.frequency.value = 400;
                windGain = audioCtx.createGain();
                windGain.gain.value = 0;
                windSource.connect(windFilter); windFilter.connect(windGain); windGain.connect(audioCtx.destination);
                windSource.start();

                jetOsc = audioCtx.createOscillator();
                jetOsc.type = 'sawtooth';
                jetOsc.frequency.value = 800;
                jetGain = audioCtx.createGain();
                jetGain.gain.value = 0;
                jetOsc.connect(jetGain); jetGain.connect(audioCtx.destination);
                jetOsc.start();
            },
            updateEngine: (speedRatio, isBoosting) => {
                if (!windSource) return;
                const now = audioCtx.currentTime;
                const targetFreq = 400 + (speedRatio * 800);
                const targetVol = Math.min(0.15, speedRatio * 0.1); 
                windFilter.frequency.setTargetAtTime(targetFreq, now, 0.1);
                windGain.gain.setTargetAtTime(targetVol, now, 0.1);

                const jetVol = isBoosting ? 0.05 : 0.0;
                const jetFreq = 1000 + (speedRatio * 2000);
                jetGain.gain.setTargetAtTime(jetVol, now, 0.2);
                jetOsc.frequency.setTargetAtTime(jetFreq, now, 0.1);
            },
            play: (type) => {
                if (audioCtx.state === 'suspended') audioCtx.resume();
                const now = audioCtx.currentTime;
                
                if (type === 'crash') {
                    const src = audioCtx.createBufferSource(); src.buffer = noiseBuffer;
                    const f = audioCtx.createBiquadFilter(); f.type = 'lowpass'; 
                    f.frequency.setValueAtTime(800, now); f.frequency.exponentialRampToValueAtTime(100, now + 1.2);
                    const g = audioCtx.createGain(); g.gain.setValueAtTime(1.0, now); g.gain.exponentialRampToValueAtTime(0.01, now + 1.2);
                    src.connect(f); f.connect(g); g.connect(audioCtx.destination); src.start(now); src.stop(now + 1.2);
                } else if (type === 'heal') {
                    const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
                    osc.connect(g); g.connect(audioCtx.destination); osc.type = 'sine';
                    osc.frequency.setValueAtTime(600, now); osc.frequency.exponentialRampToValueAtTime(2000, now+0.4);
                    g.gain.setValueAtTime(0.2, now); g.gain.linearRampToValueAtTime(0, now+0.4);
                    osc.start(now); osc.stop(now+0.4);
                } else if (type === 'boost_dash') {
                    const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
                    osc.connect(g); g.connect(audioCtx.destination); osc.type = 'triangle';
                    osc.frequency.setValueAtTime(150, now); osc.frequency.exponentialRampToValueAtTime(50, now + 0.3);
                    g.gain.setValueAtTime(0.5, now); g.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
                    osc.start(now); osc.stop(now + 0.3);
                } else if (type === 'jump') {
                    const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
                    osc.connect(g); g.connect(audioCtx.destination); osc.type = 'triangle';
                    osc.frequency.setValueAtTime(150, now); osc.frequency.linearRampToValueAtTime(300, now+0.2);
                    g.gain.setValueAtTime(0.3, now); g.gain.exponentialRampToValueAtTime(0.01, now+0.3);
                    osc.start(now); osc.stop(now+0.3);
                } else if (type === 'land') {
                    const src = audioCtx.createBufferSource(); src.buffer = noiseBuffer;
                    const f = audioCtx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.setValueAtTime(200, now);
                    const g = audioCtx.createGain(); g.gain.setValueAtTime(0.6, now); g.gain.exponentialRampToValueAtTime(0.01, now+0.15);
                    src.connect(f); f.connect(g); g.connect(audioCtx.destination); src.start(now); src.stop(now+0.15);
                } else if (type === 'coin') {
                    const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
                    osc.connect(g); g.connect(audioCtx.destination); osc.type = 'square';
                    osc.frequency.setValueAtTime(1200, now); osc.frequency.setValueAtTime(1800, now+0.05);
                    g.gain.setValueAtTime(0.1, now); g.gain.exponentialRampToValueAtTime(0.01, now+0.1);
                    osc.start(now); osc.stop(now+0.1);
                } else if (type === 'ui') {
                    const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
                    osc.connect(g); g.connect(audioCtx.destination); osc.frequency.setValueAtTime(880, now);
                    g.gain.setValueAtTime(0.1, now); g.gain.exponentialRampToValueAtTime(0.01, now+0.1);
                    osc.start(now); osc.stop(now+0.1);
                }
            }
        };

        // --- Config ---
        const MAX_HP = 10, TUBE_R = 18, MAX_TIME = 60;
        const NORMAL_MAX_SPEED = 5.0; 
        const BOOST_MAX_SPEED = 20.0;
        const NORMAL_ACCEL = 0.08;
        const BOOST_ACCEL = 0.5;
        const TOTAL_STAGES = 8;
        
        let scene, camera, renderer;
        let gameState = "TITLE", viewMode = "F3"; 
        let score = 0, stageScore = 0; 
        let 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,
            isBoosting: false,
            dashOffset: 0, 
            wasAirborne: false,
            barrier: null, 
            shadow: null,
            barrierRadius: 3.5 
        };
        
        let worldObjects = [], debris = [], stars;
        let keys = {};
        let nextSpawnZ = 50;

        // --- Tunnel & Curve Logic ---
        const TILE_SEGMENT_LENGTH = 8; 
        const VISIBLE_SEGMENTS = 40;   
        const TILES_PER_RING = 16;
        let tunnelSegments = [];       
        
        function getCurve(z) {
            const scale = 0.002;
            let x = Math.sin(z * scale) * 120 + Math.sin(z * scale * 2.3) * 60;
            let y = Math.cos(z * scale * 0.7) * 90 + Math.sin(z * scale * 1.5) * 50;
            
            const sharpCurveInterval = 4000;
            const sharpCurveWidth = 400;
            const segmentInCycle = z % sharpCurveInterval;
            if (segmentInCycle < sharpCurveWidth) {
                const t = segmentInCycle / sharpCurveWidth; 
                const sharpness = Math.sin(t * Math.PI) * 150; 
                y += sharpness;
                x += Math.sin(z * 0.003) * 30;
            }
            return new THREE.Vector3(x, y, z);
        }

        function getBasis(z) {
            const origin = getCurve(z);
            const forwardPoint = getCurve(z + 1.0);
            const T = new THREE.Vector3().subVectors(forwardPoint, origin).normalize();
            const tempUp = new THREE.Vector3(0, 1, 0);
            const R = new THREE.Vector3().crossVectors(T, tempUp).normalize();
            const U = new THREE.Vector3().crossVectors(R, T).normalize();
            return { origin, T, U, R };
        }

        function getSectionPosition(angle, radius, basis, mode) {
            // F2: フラット展開
            if (mode === "F2") {
                let a = angle;
                while (a > Math.PI) a -= Math.PI * 2;
                while (a < -Math.PI) a += Math.PI * 2;

                const spread = TUBE_R * Math.PI; // 全周の展開幅
                const localX = (a / Math.PI) * spread; // 角度を横位置にリニア変換
                const localY = -radius; // 半径を縦位置に(地面が下)

                // F2モードでは完全にフラットな座標系を使用(basis依存なし)
                return new THREE.Vector3(localX, localY, basis.origin.z);
            } 
            
            // F1, F3, F4, F5: 円筒座標
            const localX = Math.sin(angle) * radius;
            const localY = -Math.cos(angle) * radius;
            
            return basis.origin.clone()
                .addScaledVector(basis.R, localX)
                .addScaledVector(basis.U, localY);
        }

        function getGrassColor() {
            const hue = 0.2 + (Math.random() * 0.15); 
            const sat = 0.2 + (Math.random() * 0.3);  
            const val = 0.3 + (Math.random() * 0.3);  
            return new THREE.Color().setHSL(hue, sat, val);
        }

        function isGap(segmentIndex) {
            if (segmentIndex < 10) return false;
            return (segmentIndex % 25) >= 23; 
        }

        function init() {
            sound.init();
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000000);
            scene.fog = new THREE.Fog(0x000000, 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(80, window.innerWidth / window.innerHeight, 0.1, 1000);
            
            scene.add(new THREE.AmbientLight(0xffffff, 0.4));
            const sun = new THREE.DirectionalLight(0xffffee, 1.0);
            sun.position.set(0, 100, -50);
            scene.add(sun);
            const rim = new THREE.DirectionalLight(0x00aaff, 0.8);
            rim.position.set(0, -50, 50);
            scene.add(rim);

            createStars();

            // --- Player Model ---
            const playerGeo = new THREE.Group();
            const body = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshPhongMaterial({ color: 0x00ffff, emissive: 0x004444 }));
            playerGeo.add(body);

            const thrustGeo = new THREE.ConeGeometry(0.8, 8, 8);
            const thrustMat = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.7 });
            const thrust = new THREE.Mesh(thrustGeo, thrustMat);
            thrust.rotation.x = Math.PI / 2;
            thrust.position.z = -5;
            playerGeo.add(thrust);
            player.thrust = thrust;

            const barrierGeo = new THREE.IcosahedronGeometry(3.5, 1);
            const barrierMat = new THREE.MeshPhongMaterial({ color: 0x00ffff, transparent: true, opacity: 0.3, wireframe: true, emissive: 0x00aaaa, shininess: 100 });
            const barrier = new THREE.Mesh(barrierGeo, barrierMat);
            barrier.position.z = 3.5; 
            playerGeo.add(barrier);
            player.barrier = barrier;

            player.mesh = playerGeo;
            scene.add(player.mesh);

            // --- 影 (Shadow) ---
            // 修正: サイズを自機幅に合わせて縮小 (2.0)
            const shadowGeo = new THREE.PlaneGeometry(2.0, 2.0);
            const shadowMat = new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.6 });
            player.shadow = new THREE.Mesh(shadowGeo, shadowMat);
            scene.add(player.shadow);

            initTunnel();

            window.addEventListener('keydown', (e) => {
                const views = ["F1", "F2", "F3", "F4", "F5"];
                if(views.includes(e.code)) {
                    e.preventDefault();
                    viewMode = e.code;
                    updateViewLabel();
                }
                keys[e.code] = true;
                if(e.code === 'Space') { e.preventDefault(); handleSpace(); }
                if(e.code === 'KeyX') handleJump();
                if(e.code === 'KeyZ' && !player.isBoosting && gameState === 'PLAYING') {
                    player.dashOffset = 0; 
                    sound.play('boost_dash');
                }
            });
            window.addEventListener('keyup', (e) => { keys[e.code] = false; });
            
            updateViewLabel();
            changeState("TITLE");
            animate();
        }

        function initTunnel() {
            const tileWidth = (TUBE_R * 2 * Math.PI) / TILES_PER_RING;
            const boxGeo = new THREE.BoxGeometry(tileWidth * 1.02, 0.15, TILE_SEGMENT_LENGTH * 1.02);
            for (let i = 0; i < VISIBLE_SEGMENTS; i++) {
                const segmentGroup = new THREE.Group();
                segmentGroup.userData = { zIndex: i }; 
                for (let j = 0; j < TILES_PER_RING; j++) {
                    const mat = new THREE.MeshPhongMaterial({ 
                        color: getGrassColor(),
                        side: THREE.DoubleSide // 両面描画(F4/F5対応)
                    });
                    const tile = new THREE.Mesh(boxGeo, mat);
                    segmentGroup.add(tile);
                }
                tunnelSegments.push(segmentGroup);
                scene.add(segmentGroup);
            }
        }

        function updateTunnel() {
            const playerSegmentIndex = Math.floor(player.z / TILE_SEGMENT_LENGTH);
            const startSegment = playerSegmentIndex - 5; 

            tunnelSegments.forEach((seg) => {
                let segIdx = seg.userData.zIndex;
                
                if (segIdx < startSegment) {
                    segIdx += VISIBLE_SEGMENTS;
                    seg.userData.zIndex = segIdx;
                    seg.children.forEach(tile => {
                        tile.material.color.copy(getGrassColor());
                    });
                }

                const gap = isGap(segIdx);
                seg.visible = !gap;

                const zPos = segIdx * TILE_SEGMENT_LENGTH;
                const basis = getBasis(zPos);
                
                seg.children.forEach((tile, j) => {
                    const angle = (j / TILES_PER_RING) * Math.PI * 2;
                    const pos = getSectionPosition(angle, TUBE_R, basis, viewMode);
                    tile.position.copy(pos);

                    if (viewMode === "F2") {
                        // フラット展開時:完全に平面として扱い、すべてのタイルを同じ角度に
                        tile.rotation.set(0, 0, 0);
                        
                        // 同じセグメント内のすべてのタイルを同じ向きに統一
                        // Z軸(進行方向)を基準に、X-Y平面に平行に配置
                        const forward = new THREE.Vector3(0, 0, 1); // 常にZ軸方向
                        const up = new THREE.Vector3(0, 1, 0);      // 常にY軸方向
                        const right = new THREE.Vector3(1, 0, 0);   // 常にX軸方向
                        
                        const m = new THREE.Matrix4();
                        m.makeBasis(right, up, forward);
                        tile.rotation.setFromRotationMatrix(m);
                    } else {
                        // --- 修正: シリンダー状に整列 ---
                        // F4, F5(外側走行)の場合は外向き、それ以外は内向き
                        const isOutside = (viewMode === "F4" || viewMode === "F5");
                        
                        // 進行方向ベクトル
                        const forward = basis.T.clone();
                        
                        // 各タイルの法線ベクトル(中心からの方向)
                        const tileLocalX = Math.sin(angle);
                        const tileLocalY = -Math.cos(angle);
                        const normal = new THREE.Vector3()
                            .addScaledVector(basis.R, tileLocalX)
                            .addScaledVector(basis.U, tileLocalY)
                            .normalize();
                        
                        // 外側なら外向き、内側なら内向き
                        const up = isOutside ? normal : normal.clone().multiplyScalar(-1);
                        
                        // 接線方向(円周方向)
                        const tangent = new THREE.Vector3().crossVectors(up, forward).normalize();
                        
                        // 再計算して直交化
                        const orthoForward = new THREE.Vector3().crossVectors(tangent, up).normalize();
                        
                        const m = new THREE.Matrix4();
                        m.makeBasis(tangent, up, orthoForward);
                        tile.rotation.setFromRotationMatrix(m);
                    }
                });
            });
        }

        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.5, transparent: true, opacity: 0.8});
            stars = new THREE.Points(starGeo, starMat);
            scene.add(stars);
        }

        function updateStars() {
            if(stars) {
                stars.position.set(0, 0, player.z); 
                stars.rotation.z += 0.0002; 
            }
        }

        function updateViewLabel() {
            const labels = { 
                "F1": "1ST PERSON (INNER)", 
                "F2": "FLAT MODE", 
                "F3": "3RD PERSON (INNER)",
                "F4": "1ST PERSON (OUTER)",
                "F5": "3RD PERSON (OUTER)"
            };
            document.getElementById('view-mode').innerText = "VIEW: " + labels[viewMode];
        }

        function handleSpace() {
            if (gameState === "TITLE" || gameState === "GAMEOVER" || gameState === "ALL_CLEAR") {
                sound.play('ui');
                sound.startEngine(); 
                if(gameState !== "TITLE") { score = 0; currentStage = 1; }
                changeState("STAGE_START");
            }
            else if (gameState === "STAGE_CLEAR") { 
                sound.play('ui'); 
                currentStage++;
                changeState("STAGE_START"); 
            }
            else if (gameState === "STAGE_START") { sound.play('ui'); changeState("PLAYING"); }
        }

        function handleJump() {
            if (gameState === "PLAYING" && player.altitude <= 0.01) {
                sound.play('jump');
                player.jumpV = 0.8; 
                player.wasAirborne = true;
            }
        }

        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 5.0</h1><p style='color:#adff2f;'>ROCK & ROLL</p><p>PRESS SPACE TO START</p><p style='font-size:18px; color:#aaa;'>[UP]: ACCEL / [DOWN]: BRAKE / [Z]: BOOST / [X]: JUMP</p>";
            else if(s === "STAGE_START") { 
                stageScore = 0; 
                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>STAGE SCORE: "+stageScore+"</p><p>SPACE FOR NEXT STAGE</p>"; 
                }
            }
            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; player.dashOffset = 0;
            hp = MAX_HP; timeLeft = MAX_TIME;
            
            worldObjects.forEach(obj => scene.remove(obj.mesh));
            debris.forEach(d => scene.remove(d));
            worldObjects = []; debris = [];
            nextSpawnZ = 50;
            
            tunnelSegments.forEach((seg, i) => { seg.userData.zIndex = i; });
        }

        function animate() {
            requestAnimationFrame(animate);
            if (gameState === "PLAYING") {
                updatePhysics();
                updateObjects();
                updateDebris();
            }
            
            if(gameState === "PLAYING") {
                const ratio = player.vz / BOOST_MAX_SPEED;
                sound.updateEngine(ratio, player.isBoosting);
            } else sound.updateEngine(0, false);

            updateTunnel(); 
            updatePlayerVisuals(); 
            updateStars();
            renderer.render(scene, camera);
        }

        function updatePhysics() {
            let targetMax = NORMAL_MAX_SPEED;
            let currentAccel = 0;
            player.isBoosting = false;

            if (keys['KeyZ']) {
                player.isBoosting = true;
                targetMax = BOOST_MAX_SPEED;
                currentAccel = BOOST_ACCEL;
                player.surge = THREE.MathUtils.lerp(player.surge, 6.0, 0.05);
                if(player.dashOffset < 15.0) player.dashOffset = THREE.MathUtils.lerp(player.dashOffset, 20.0, 0.2); 
            } 
            else if (keys['ArrowUp']) {
                targetMax = NORMAL_MAX_SPEED;
                currentAccel = NORMAL_ACCEL;
                player.surge = THREE.MathUtils.lerp(player.surge, 2.0, 0.05);
                player.dashOffset = THREE.MathUtils.lerp(player.dashOffset, 0, 0.1);
            } 
            else {
                currentAccel = 0;
                player.surge = THREE.MathUtils.lerp(player.surge, 0, 0.05);
                player.dashOffset = THREE.MathUtils.lerp(player.dashOffset, 0, 0.1);
            }

            if (keys['ArrowDown']) {
                player.vz *= 0.92;
                player.surge = THREE.MathUtils.lerp(player.surge, -2.0, 0.1);
            }

            if (currentAccel > 0) {
                if (player.vz < targetMax) player.vz += currentAccel;
                else player.vz *= 0.98;
            } else player.vz *= 0.98;
            player.vz = Math.max(0.1, player.vz);

            const lineOpacity = Math.max(0, (player.vz - 8) / 10.0);
            document.getElementById('speed-lines').style.opacity = lineOpacity;

            player.altitude += player.jumpV;
            
            const currentSegIdx = Math.floor(player.z / TILE_SEGMENT_LENGTH);
            const inGap = isGap(currentSegIdx);

            if (inGap) {
                player.jumpV -= 0.03;
            } else {
                if (player.altitude > 0) player.jumpV -= 0.03;
                else { 
                    if (player.wasAirborne) { sound.play('land'); player.wasAirborne = false; }
                    player.altitude = 0; player.jumpV = 0; 
                }
            }
            
            if (player.altitude < -30) {
                hp = 0; 
                changeState("GAMEOVER");
            }

            let steerFactor = 1.0;
            if (player.vz > 10.0) steerFactor = 0.6; 
            
            // F2モードは直感的な左右移動なので少し感度を上げる
            if (viewMode === "F2") steerFactor *= 1.8; // 1.5倍の速度に調整

            let targetBank = 0;
            
            // F4/F5(外側走行)では左右が逆になるので符号を反転
            // F2モードでも座標系が違うので反転が必要
            const isOutside = (viewMode === "F4" || viewMode === "F5");
            const isFlat = (viewMode === "F2");
            const steerDirection = (isOutside || isFlat) ? -1 : 1;
            
            if (keys['ArrowLeft']) { 
                player.vAngle = THREE.MathUtils.lerp(player.vAngle, -0.12 * steerFactor * steerDirection, 0.1); 
                targetBank = -0.8; 
            }
            else if (keys['ArrowRight']) { 
                player.vAngle = THREE.MathUtils.lerp(player.vAngle, 0.12 * steerFactor * steerDirection, 0.1); 
                targetBank = 0.8; 
            }
            else player.vAngle *= 0.9;
            
            player.angle += player.vAngle;

            // F2モードの端の制限(フラット表示時の壁)
            if (viewMode === "F2") {
                const limit = Math.PI; // 180度
                if (player.angle > limit) { player.angle = limit; player.vAngle = 0; }
                else if (player.angle < -limit) { player.angle = -limit; player.vAngle = 0; }
            }

            player.bank = THREE.MathUtils.lerp(player.bank, targetBank, 0.1);
            player.z += player.vz;
            
            timeLeft -= 1/60;
            if (timeLeft <= 0) { timeLeft = 0; changeState("STAGE_CLEAR"); }
            if (hp <= 0) { hp = 0; changeState("GAMEOVER"); }

            updateUI();
        }

        function updatePlayerVisuals() {
            const visualZ = player.z + player.surge + player.dashOffset;
            const basis = getBasis(visualZ);

            // F4, F5 (外側走行) の場合は半径計算を変える
            const isOutside = (viewMode === "F4" || viewMode === "F5");
            
            // 外側なら TUBE_R + altitude, 内側なら TUBE_R - altitude
            // ただしタイル厚み等考慮して微調整
            const r = isOutside 
                ? TUBE_R + 1.5 + player.altitude 
                : TUBE_R - 1.5 - player.altitude;

            const pos = getSectionPosition(player.angle, r, basis, viewMode);
            const center = basis.origin.clone();

            let playerUp;
            if (viewMode === "F2") {
                playerUp = new THREE.Vector3(0, 1, 0); // F2は常にY軸上向き(固定)
            } else if (isOutside) {
                // 外側走行:中心からプレイヤー方向が「上」
                playerUp = new THREE.Vector3().subVectors(pos, center).normalize();
            } else {
                // 内側走行:プレイヤーから中心方向が「上」(内向き重力)
                playerUp = new THREE.Vector3().subVectors(center, pos).normalize();
            }

            let playerForward;
            if (viewMode === "F2") {
                playerForward = new THREE.Vector3(0, 0, 1); // F2は常にZ軸方向(固定)
            } else {
                playerForward = basis.T.clone();
            }

            player.mesh.position.copy(pos);

            const m = new THREE.Matrix4();
            const right = new THREE.Vector3().crossVectors(playerForward, playerUp).normalize();
            const orthoUp = new THREE.Vector3().crossVectors(right, playerForward).normalize();
            
            m.makeBasis(right, orthoUp, playerForward);
            player.mesh.rotation.setFromRotationMatrix(m);
            player.mesh.rotateZ(player.bank);

            player.mesh.visible = (viewMode !== "F1" && viewMode !== "F4");

            // スラスター
            if (player.thrust) {
                if (player.vz < 0.1) {
                    player.thrust.visible = false;
                } else {
                    player.thrust.visible = true;
                    const speedRatio = player.vz / BOOST_MAX_SPEED;
                    player.thrust.scale.z = 0.5 + speedRatio * 1.5; 
                    player.thrust.material.opacity = 0.4 + speedRatio * 0.4;
                    if (player.isBoosting) player.thrust.material.color.setHex(0xff00ff); 
                    else player.thrust.material.color.setHex(0xff6600); 
                    player.thrust.scale.x = 0.8 + Math.random() * 0.4;
                    player.thrust.scale.y = 0.8 + Math.random() * 0.4;
                }
            }

            if (player.barrier) {
                const hpRatio = Math.max(0, hp / MAX_HP);
                const targetScale = 0.6 + (hpRatio * 1.55);
                player.barrier.scale.setScalar(targetScale);
                player.barrier.rotation.y += Math.PI;
                player.barrier.rotation.z += Math.PI;
                player.barrierRadius = 3.5 * targetScale;
                if (player.isBoosting) {
                    player.barrier.material.color.setHex(0xff0000); 
                    player.barrier.material.emissive.setHex(0x550000);
                } else {
                    player.barrier.material.color.setHex(0x00ffff); 
                    player.barrier.material.emissive.setHex(0x004444);
                }
            }

            // --- 影の処理修正 ---
            if (player.shadow) {
                const currentSegIdx = Math.floor(player.z / TILE_SEGMENT_LENGTH);
                const inGap = isGap(currentSegIdx);
                const isFirstPerson = (viewMode === "F1" || viewMode === "F4");

                if (!isFirstPerson && !inGap) {
                    player.shadow.visible = true;
                    
                    // 影の位置:地面(TUBE_R)の表面
                    // 外側モードなら TUBE_R + 0.1, 内側なら TUBE_R - 0.2
                    const groundR = isOutside ? TUBE_R + 0.2 : TUBE_R - 0.2;
                    const groundPos = getSectionPosition(player.angle, groundR, basis, viewMode);
                    player.shadow.position.copy(groundPos);
                    
                    // 影の回転:地面の法線に合わせる
                    // PlaneGeometryはZ軸+が正面。
                    if (viewMode === "F2") {
                         // F2: 平面なのでシンプルに上を向く
                         player.shadow.rotation.set(-Math.PI/2, 0, 0); 
                    } else {
                        // シリンダー: 中心を見て調整
                        player.shadow.lookAt(center);
                        if(isOutside) {
                            // 外側:影の面が外を向く必要がある。lookAt(center)だと内向き。
                            player.shadow.rotateY(Math.PI); 
                        }
                    }

                    // ジャンプ時の影サイズ制御: 単純に小さくする (opacityは変えない)
                    const distFromGround = Math.max(0, player.altitude);
                    // 最小0.2倍まで縮小
                    const shadowScale = Math.max(0.2, 1.0 - distFromGround * 0.1); 
                    player.shadow.scale.setScalar(shadowScale);
                    player.shadow.material.opacity = 0.6; // 固定

                } else {
                    player.shadow.visible = false;
                }
            }

            updateCamera(pos, orthoUp, playerForward, visualZ, isOutside);
        }

        function updateCamera(playerPos, playerUp, playerForward, visualZ, isOutside) {
            const shake = player.isBoosting ? (Math.random()-0.5)*0.3 : 0; 
            
            if (viewMode === "F1" || viewMode === "F4") {
                // 1ST PERSON
                const eyePos = playerPos.clone().addScaledVector(playerForward, 2.0).addScaledVector(playerUp, 0.5);
                camera.position.copy(eyePos);
                camera.position.y += shake; 
                camera.up.copy(playerUp);
                const target = eyePos.clone().add(playerForward);
                camera.lookAt(target);
            } 
            else if (viewMode === "F2") { 
                // FLAT MODE:自機のやや背後上に固定
                const camPos = playerPos.clone();
                camPos.z -= 30; // 背後
                camPos.y += 25; // 上
                
                camera.position.copy(camPos);
                camera.up.set(0, 1, 0); 
                
                // 自機の少し前方を見る
                const target = playerPos.clone();
                target.z += 10;
                camera.lookAt(target); 
            } 
            else { 
                // 3RD PERSON
                const camDist = 20 + (player.vz * 1.0); 
                const camHeight = 6 + (player.vz * 0.2);
                
                const camPos = playerPos.clone()
                    .addScaledVector(playerForward, -camDist)
                    .addScaledVector(playerUp, camHeight);    
                
                camera.position.copy(camPos);
                camera.position.addScaledVector(camera.position.clone().cross(playerForward).normalize(), shake);
                camera.up.copy(playerUp);
                
                const target = playerPos.clone().addScaledVector(playerForward, 20);
                camera.lookAt(target);
            }
        }

        function updateObjects() {
            let spawnDist = (Math.max(10, 30 - (currentStage * 2.0)) + (player.vz * 2.0)) * 0.2; 
            
            if (player.z + 600 > nextSpawnZ) { 
                spawnObject(nextSpawnZ); 
                nextSpawnZ += Math.random() * 10 + spawnDist;
            }

            for (let i = worldObjects.length - 1; i >= 0; i--) {
                const obj = worldObjects[i];
                
                if(obj.flying) {
                    obj.localPos.add(obj.vel);
                    obj.mesh.rotation.x += 0.1;
                    obj.mesh.rotation.y += 0.1;
                } 

                const objVisualZ = obj.z; 
                const basis = getBasis(objVisualZ);
                
                let worldPos;
                if (obj.flying) {
                    worldPos = basis.origin.clone().add(obj.localPos); 
                } else {
                    // オブジェクトが地面に固定されている場合
                    const isOutside = (viewMode === "F4" || viewMode === "F5");
                    
                    worldPos = getSectionPosition(obj.angle, obj.r, basis, viewMode);
                    
                    if (viewMode === "F2") {
                        // F2モード:タイルと同じフラットな角度
                        const forward = new THREE.Vector3(0, 0, 1); // 常にZ軸方向
                        const up = new THREE.Vector3(0, 1, 0);      // 常にY軸方向
                        const right = new THREE.Vector3(1, 0, 0);   // 常にX軸方向
                        
                        const m = new THREE.Matrix4();
                        m.makeBasis(right, up, forward);
                        obj.mesh.rotation.setFromRotationMatrix(m);
                    } else {
                        // F1, F3, F4, F5モード:地面の接平面に平行
                        
                        // 地面の法線ベクトル(地面に対して垂直な方向)
                        const objLocalX = Math.sin(obj.angle);
                        const objLocalY = -Math.cos(obj.angle);
                        const groundNormal = new THREE.Vector3()
                            .addScaledVector(basis.R, objLocalX)
                            .addScaledVector(basis.U, objLocalY)
                            .normalize();
                        
                        // 外側なら外向き、内側なら内向き(地面から見た上方向)
                        const up = isOutside ? groundNormal : groundNormal.clone().multiplyScalar(-1);
                        
                        // 進行方向ベクトル(トンネルの接線方向)
                        const forward = basis.T.clone();
                        
                        // 地面の接平面に沿った右方向ベクトル
                        // 地面に平行な方向 = 法線に垂直な方向
                        const right = new THREE.Vector3().crossVectors(up, forward).normalize();
                        
                        // 地面に平行な前方向ベクトルを再計算(直交化)
                        const parallelForward = new THREE.Vector3().crossVectors(right, up).normalize();
                        
                        // オブジェクトの回転行列を設定
                        // right: 地面に平行な横方向
                        // up: 地面の法線方向(地面に垂直)
                        // parallelForward: 地面に平行な前方向
                        const m = new THREE.Matrix4();
                        m.makeBasis(right, up, parallelForward);
                        obj.mesh.rotation.setFromRotationMatrix(m);
                    }
                }
                
                obj.mesh.position.copy(worldPos);

                const dz = Math.abs(player.z - obj.z);
                let collisionThreshold = 4.0;
                if (obj.type === "block" && player.barrierRadius) collisionThreshold = player.barrierRadius; 

                if (dz < collisionThreshold && !obj.flying) {
                    const dist = player.mesh.position.distanceTo(obj.mesh.position);
                    if (dist < collisionThreshold) { 
                        handleCollision(obj, i);
                        if(!obj.flying && obj.type !== "block" && obj.type !== "hurdle") {
                            scene.remove(obj.mesh);
                            worldObjects.splice(i, 1);
                            continue;
                        }
                    }
                }
                if (obj.z < player.z - 50) {
                    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.15) ? "score" : (Math.random() < 0.08) ? "heal" : "block";
            if (Math.random() < hurdleRate) type = "hurdle";

            // グリッドスナップ用の角度計算
            // 全周16分割とかではなく、ランダムだがブロックサイズで整列させる
            // TILES_PER_RING = 16 なので、それに合わせる
            const seg = Math.floor(Math.random() * TILES_PER_RING);
            let tubeAngle = (seg / TILES_PER_RING) * Math.PI * 2;
            
            let length = (type === 'block') ? 3 : 1; 

            for(let i = 0; i < length; i++) {
                // --- 修正: Z方向の位置を固定間隔で整列 ---
                let zOffset = z + (i * 3.0); 

                let stackHeight = (type === 'block') ? Math.floor(Math.random() * (1 + currentStage)) + 1 : 1;
                if(stackHeight > 5) stackHeight = 5;

                for (let k = 0; k < stackHeight; k++) {
                    let geo;
                    let blockSize = 2.5; 

                    if (type === 'hurdle') { geo = new THREE.BoxGeometry(4,4,4); blockSize=4.0; }
                    else if (type === 'heal') { geo = new THREE.BoxGeometry(1.5,1.5,1.5); blockSize=1.5; }
                    else { geo = new THREE.BoxGeometry(2.5,2.5,2.5); blockSize=2.5; }

                    let color;
                    if (type === 'block') {
                        const gVal = 0.3 + Math.random() * 0.5; 
                        color = new THREE.Color(gVal, gVal, gVal);
                    } else {
                        const colors = { hurdle: 0xff3300, score: 0xffff00, heal: 0x00ff00 };
                        color = new THREE.Color(colors[type]);
                    }

                    // オブジェクトのマテリアルも両面描画にしておく
                    const mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ color: color, side: THREE.DoubleSide }));
                    
                    let baseR = TUBE_R;
                    
                    // F4, F5 (外側走行モード) の場合は、オブジェクトも外側に配置
                    const isOutside = (viewMode === "F4" || viewMode === "F5");
                    
                    if (isOutside) {
                        // 外側モード:TUBE_Rより外側に配置
                        if (type === 'hurdle') baseR += 2;
                        else if (type === 'block') {
                            baseR += (1.25 + (k * 2.5)); 
                        }
                        else if (type === 'score') baseR += 1.25; 
                        else baseR += 0.7;
                    } else {
                        // 内側モード(従来通り):TUBE_Rより内側に配置
                        if (type === 'hurdle') baseR -= 2;
                        else if (type === 'block') {
                            baseR -= (1.25 + (k * 2.5)); 
                        }
                        else if (type === 'score') baseR -= 1.25; 
                        else baseR -= 0.7;
                    }

                    const lx = Math.sin(tubeAngle) * baseR;
                    const ly = -Math.cos(tubeAngle) * baseR;

                    scene.add(mesh);
                    worldObjects.push({ 
                        mesh, type, flying: false, 
                        z: zOffset, 
                        angle: tubeAngle,
                        r: baseR,
                        localPos: new THREE.Vector3(lx, ly, 0),
                        vel: new THREE.Vector3() 
                    });
                }
            }
        }

        function handleCollision(obj, index) {
            const isHighSpeed = player.vz >= (NORMAL_MAX_SPEED + 2.0);

            if (obj.type === "block") {
                if (isHighSpeed) {
                    sound.play('crash'); 
                    createExplosion(obj.mesh.position, obj.mesh.material.color, 1.5);
                    scene.remove(obj.mesh);
                    worldObjects.splice(index, 1);
                    const pts = 50; score += pts; stageScore += pts; 
                    showPopText("SMASH! +50", "#ffaa00");
                } else {
                    sound.play('crash');
                    createExplosion(obj.mesh.position, obj.mesh.material.color, 0.8);
                    obj.flying = true;
                    obj.vel.set((Math.random()-0.5)*2, 2, 5);
                    const pts = 10; score += pts; stageScore += pts;
                    showPopText("+10", "#ffaa00");
                    player.vz *= 0.8;
                }
            } else if (obj.type === "hurdle") {
                sound.play('crash');
                if (player.vz > 15.0) { 
                    hp -= 1; showPopText("IMPACT! HP -1", "#ff5500");
                    obj.flying = true;
                    obj.vel.set((Math.random()-0.5)*5, 5, 10);
                    player.vz *= 0.7;
                } 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") { 
                sound.play('coin');
                const pts = 100; score += pts; stageScore += pts;
                showPopText("+100", "#ffff00"); 
                scene.remove(obj.mesh);
                worldObjects.splice(index, 1);
            }
            else if (obj.type === "heal") { 
                sound.play('heal');
                hp = Math.min(MAX_HP, hp + 1); showPopText("RECOVER!", "#00ff00"); 
                scene.remove(obj.mesh);
                worldObjects.splice(index, 1);
            }
        }

        function createExplosion(pos, color, scaleFactor) {
            const count = 8 * scaleFactor;
            for(let i=0; i<count; i++) {
                const size = 0.6 * 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)*10, (Math.random()-0.5)*10, (Math.random()-0.5)*10);
                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.mesh.rotation.x += 0.2;
                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(), 800);
        }

        function updateUI() {
            document.getElementById('score-info').innerText = "SCORE: " + score;
            document.getElementById('hp-num').innerText = hp;
            document.getElementById('hp-fill').style.width = Math.max(0, (hp / MAX_HP * 100)) + "%";
            
            const spd = (player.vz * 40).toFixed(0);
            document.getElementById('speed-val').innerText = spd;
            
            const speedRatio = player.vz / BOOST_MAX_SPEED;
            document.getElementById('speed-fill').style.width = Math.min(100, speedRatio * 100) + "%";
            
            if (player.isBoosting) document.getElementById('speed-fill').style.backgroundColor = "#ff00ff";
            else document.getElementById('speed-fill').style.backgroundColor = "#00ff88";

            const remainingTime = Math.max(0, timeLeft);
            const minutes = Math.floor(remainingTime / 60);
            const seconds = Math.floor(remainingTime % 60);
            const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
            document.getElementById('time-num').innerText = timeStr;
            document.getElementById('time-fill').style.width = (timeLeft / MAX_TIME * 100) + "%";
        }

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

・ダウンロードされる方はこちら。↓

【操作説明】

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

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

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

・灰色ブロック … バリアで破壊可能。+10点。
・赤色ブロック … バリアでは防げず、HPにダメージ。
・黄緑色ブロック … HPを回復。
・黄色ブロック … +100点。

・HPが減るとバリアが小さくなる。
・HPがゼロになるとゲームオーバー。

【おまけ】

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

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?)

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

「Redout」(2016)34BigThings 

「Redout 2」(2022)34BigThings 

・これはレースゲームですが、
ブーストが付いていて1000km/hまで加速可能。

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

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

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

ピックアップされています

便利なツール

  • 40本

コメント

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