メインコンテンツへスキップ
見出し画像

HTMLのみで簡単に作れるブラウザ3Dゲーム

    🧠「Three.js」という3Dライブラリ

    このゲームは、「Three.js」という3Dライブラリを使って、ブラウザだけで作られています。専門的なソフトをインストールしなくても、このHTMLファイル一つで動くのが特徴です。

    「3D空間で動くカラフルな的を、マウスでクリックして撃ち落とす」という、とてもシンプルなシューティングゲームです。

    画像

    🎯 基本ルール(超シンプル)

    1. 制限時間は30秒:右上のタイマーがゼロになるまでプレイします。

    2. 的をクリックで破壊:3D空間に浮かぶ色とりどりの球体(的)を、マウスの十字線で狙ってクリックすると、弾が当たって的が消えます。

    3. スコアを競う:的を1つ破壊するごとに、左上のスコアが1増えます。時間内にどれだけ多くの的を壊せるかが勝負です。

    🎮 操作方法

    • 射撃:的をクリックするだけ(照準は画面中央に固定)。

    • 視点移動:マウスをドラッグすると、カメラが回転し、周囲の的を探せます。

    • 再プレイ:ゲームオーバー時に表示される「もう一度」ボタンをクリック。

    画像

    ✨ ゲームの見どころ

    • 的が動く:的はゆっくりとしたランダムな方向に動き、壁で跳ね返るので、狙いを定めるのに少しコツがいります。

    • 派手なエフェクト:的を撃つと、その色に合わせた小さな火花(パーティクル)が飛び散ります。

    • カラフルなビジュアル:的はランダムな色で生成され、宇宙空間のような美しい背景に浮かんでいます。

    • 奥行きのある3D空間:マウスドラッグで視点を変えられるので、的の位置を立体的に把握する必要があります。

    デスクトップにindex.htmlで保存して、クリックしてブラウザで開くだけで遊べるゲームとなります。何も知識がなくても、パソコンあれば1分で試すことができます。

    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>3Dシューティング - ベクトル空間でエイム練習</title>
        <style>
            body { margin: 0; overflow: hidden; font-family: 'Segoe UI', sans-serif; }
            #ui {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                pointer-events: none;
                z-index: 10;
            }
            #score {
                position: absolute;
                top: 20px;
                left: 20px;
                background: rgba(0,0,0,0.7);
                color: #ffaa00;
                padding: 12px 24px;
                border-radius: 30px;
                font-size: 24px;
                font-weight: bold;
                border: 1px solid rgba(255,170,0,0.3);
                backdrop-filter: blur(5px);
                pointer-events: none;
            }
            #score span {
                color: white;
                font-weight: normal;
            }
            #timer {
                position: absolute;
                top: 20px;
                right: 20px;
                background: rgba(0,0,0,0.7);
                color: #66ffaa;
                padding: 12px 24px;
                border-radius: 30px;
                font-size: 24px;
                font-weight: bold;
                border: 1px solid rgba(102,255,170,0.3);
                backdrop-filter: blur(5px);
                pointer-events: none;
            }
            #timer span {
                color: white;
                font-weight: normal;
            }
            #message {
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                color: white;
                font-size: 48px;
                font-weight: bold;
                text-shadow: 0 0 30px rgba(0,0,0,0.9);
                background: rgba(0,0,0,0.6);
                padding: 30px 60px;
                border-radius: 20px;
                border: 2px solid rgba(255,255,255,0.2);
                backdrop-filter: blur(10px);
                text-align: center;
                pointer-events: none;
                opacity: 0;
                transition: opacity 0.5s;
            }
            #message.show {
                opacity: 1;
            }
            #message .sub {
                font-size: 20px;
                color: #aaa;
                margin-top: 10px;
            }
            #crosshair {
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 40px;
                height: 40px;
                pointer-events: none;
                opacity: 0.8;
            }
            #crosshair::before,
            #crosshair::after {
                content: '';
                position: absolute;
                background: rgba(255,255,255,0.7);
            }
            #crosshair::before {
                width: 2px;
                height: 100%;
                left: 50%;
                transform: translateX(-50%);
            }
            #crosshair::after {
                width: 100%;
                height: 2px;
                top: 50%;
                transform: translateY(-50%);
            }
            #crosshair .dot {
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 4px;
                height: 4px;
                background: #ff4444;
                border-radius: 50%;
            }
            #restart-btn {
                position: absolute;
                bottom: 40px;
                left: 50%;
                transform: translateX(-50%);
                background: #ffaa00;
                color: #000;
                border: none;
                padding: 12px 40px;
                font-size: 18px;
                font-weight: bold;
                border-radius: 30px;
                cursor: pointer;
                pointer-events: all;
                box-shadow: 0 4px 20px rgba(255,170,0,0.3);
                transition: transform 0.2s, box-shadow 0.2s;
                opacity: 0;
                transition: opacity 0.5s, transform 0.2s;
            }
            #restart-btn.show {
                opacity: 1;
            }
            #restart-btn:hover {
                transform: translateX(-50%) scale(1.05);
                box-shadow: 0 6px 30px rgba(255,170,0,0.5);
            }
            #hint {
                position: absolute;
                bottom: 100px;
                left: 50%;
                transform: translateX(-50%);
                color: rgba(255,255,255,0.4);
                font-size: 14px;
                background: rgba(0,0,0,0.3);
                padding: 6px 20px;
                border-radius: 20px;
                pointer-events: none;
                white-space: nowrap;
            }
        </style>
    </head>
    <body>
        <div id="ui">
            <div id="score">🎯 <span id="score-value">0</span></div>
            <div id="timer">⏱️ <span id="timer-value">30</span>s</div>
            <div id="crosshair"><div class="dot"></div></div>
            <div id="message">
                <div id="msg-title">GAME OVER</div>
                <div class="sub" id="msg-sub">スコア: 0</div>
            </div>
            <button id="restart-btn">🔄 もう一度</button>
            <div id="hint">🖱️ クリックでターゲットを撃つ · ドラッグで回転</div>
        </div>
    
    <!-- Three.js -->
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.128.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.128.0/examples/jsm/"
            }
        }
    </script>
    
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    
        // --- ゲーム状態 ---
        const state = {
            score: 0,
            time: 30,
            isPlaying: false,
            targets: [],
            spawnInterval: null,
            timerInterval: null,
        };
    
        // --- DOM要素 ---
        const scoreEl = document.getElementById('score-value');
        const timerEl = document.getElementById('timer-value');
        const messageEl = document.getElementById('message');
        const msgTitle = document.getElementById('msg-title');
        const msgSub = document.getElementById('msg-sub');
        const restartBtn = document.getElementById('restart-btn');
    
        // --- シーン設定 ---
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x0a0a1a);
    
        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 2, 8);
        camera.lookAt(0, 0, 0);
    
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        renderer.shadowMap.enabled = true;
        document.body.prepend(renderer.domElement);
    
        // --- コントロール ---
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        controls.autoRotate = false;
        controls.minDistance = 3;
        controls.maxDistance = 20;
        controls.target.set(0, 0, 0);
        controls.update();
    
        // --- 照明 ---
        const ambient = new THREE.AmbientLight(0x404060);
        scene.add(ambient);
        const dirLight = new THREE.DirectionalLight(0xffeedd, 1);
        dirLight.position.set(5, 10, 7);
        scene.add(dirLight);
        const fillLight = new THREE.DirectionalLight(0x4488ff, 0.3);
        fillLight.position.set(-5, 0, 5);
        scene.add(fillLight);
    
        // --- 床と星 ---
        const grid = new THREE.GridHelper(12, 20, 0x4488ff, 0x224488);
        grid.position.y = -1.5;
        scene.add(grid);
    
        // 星
        const starGeo = new THREE.BufferGeometry();
        const starCount = 2000;
        const starPos = new Float32Array(starCount * 3);
        for (let i = 0; i < starCount * 3; i++) {
            starPos[i] = (Math.random() - 0.5) * 200;
        }
        starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
        const starMat = new THREE.PointsMaterial({ color: 0x88aaff, size: 0.08, transparent: true });
        const stars = new THREE.Points(starGeo, starMat);
        scene.add(stars);
    
        // --- 中心にマーカー ---
        const center = new THREE.Mesh(
            new THREE.SphereGeometry(0.1, 8, 8),
            new THREE.MeshBasicMaterial({ color: 0xffaa44 })
        );
        scene.add(center);
    
        // --- レイキャスター (クリック検出) ---
        const raycaster = new THREE.Raycaster();
        const mouse = new THREE.Vector2();
    
        // --- 敵(ターゲット)生成関数 ---
        function spawnTarget() {
            if (!state.isPlaying) return;
    
            // ランダムな位置(範囲: -4〜4, yは0〜3)
            const x = (Math.random() - 0.5) * 8;
            const y = Math.random() * 3 + 0.5;
            const z = (Math.random() - 0.5) * 8;
    
            // 色: ランダムな派手な色
            const hue = Math.random();
            const color = new THREE.Color().setHSL(hue, 0.8, 0.6);
    
            const size = 0.3 + Math.random() * 0.3;
            const sphere = new THREE.Mesh(
                new THREE.SphereGeometry(size, 12, 12),
                new THREE.MeshStandardMaterial({
                    color: color,
                    emissive: color,
                    emissiveIntensity: 0.2,
                    metalness: 0.2,
                    roughness: 0.4,
                })
            );
            sphere.position.set(x, y, z);
            sphere.userData.isTarget = true;
            sphere.userData.velocity = new THREE.Vector3(
                (Math.random() - 0.5) * 0.02,
                (Math.random() - 0.5) * 0.02,
                (Math.random() - 0.5) * 0.02
            );
            sphere.castShadow = true;
    
            // リング(オーラ)
            const ring = new THREE.Mesh(
                new THREE.RingGeometry(size * 1.4, size * 1.8, 16),
                new THREE.MeshBasicMaterial({
                    color: color,
                    transparent: true,
                    opacity: 0.3,
                    side: THREE.DoubleSide,
                })
            );
            ring.position.copy(sphere.position);
            ring.lookAt(camera.position);
            sphere.userData.ring = ring;
            scene.add(ring);
    
            scene.add(sphere);
            state.targets.push(sphere);
        }
    
        // --- ゲーム開始 ---
        function startGame() {
            // リセット
            state.score = 0;
            state.time = 30;
            state.isPlaying = true;
            state.targets = [];
    
            // 既存のターゲットを削除
            scene.children.forEach(child => {
                if (child.userData && child.userData.isTarget) {
                    scene.remove(child);
                    if (child.userData.ring) {
                        scene.remove(child.userData.ring);
                    }
                }
            });
    
            // UI更新
            scoreEl.textContent = '0';
            timerEl.textContent = '30';
            messageEl.classList.remove('show');
            restartBtn.classList.remove('show');
            document.getElementById('hint').style.opacity = '1';
    
            // 最初に5個配置
            for (let i = 0; i < 5; i++) {
                spawnTarget();
            }
    
            // 定期生成 (0.8秒ごと)
            if (state.spawnInterval) clearInterval(state.spawnInterval);
            state.spawnInterval = setInterval(() => {
                if (state.isPlaying) {
                    spawnTarget();
                    // 最大15個までに制限
                    if (state.targets.length > 15) {
                        const old = state.targets.shift();
                        scene.remove(old);
                        if (old.userData && old.userData.ring) {
                            scene.remove(old.userData.ring);
                        }
                    }
                }
            }, 800);
    
            // タイマー (1秒ごと)
            if (state.timerInterval) clearInterval(state.timerInterval);
            state.timerInterval = setInterval(() => {
                state.time--;
                timerEl.textContent = state.time;
                if (state.time <= 0) {
                    endGame();
                }
            }, 1000);
        }
    
        // --- ゲーム終了 ---
        function endGame() {
            state.isPlaying = false;
            if (state.spawnInterval) clearInterval(state.spawnInterval);
            if (state.timerInterval) clearInterval(state.timerInterval);
    
            msgTitle.textContent = '💥 GAME OVER';
            msgSub.textContent = `スコア: ${state.score} 体破壊!`;
            messageEl.classList.add('show');
            restartBtn.classList.add('show');
            document.getElementById('hint').style.opacity = '0';
        }
    
        // --- クリック(射撃)処理 ---
        renderer.domElement.addEventListener('click', (event) => {
            if (!state.isPlaying) return;
    
            // マウス座標を正規化
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    
            raycaster.setFromCamera(mouse, camera);
    
            // シーン内のターゲット球体を取得
            const meshes = [];
            scene.children.forEach(child => {
                if (child.isMesh && child.userData && child.userData.isTarget) {
                    meshes.push(child);
                }
            });
    
            const intersects = raycaster.intersectObjects(meshes);
    
            if (intersects.length > 0) {
                const hit = intersects[0].object;
                if (hit.userData.isTarget) {
                    // 破壊!
                    const idx = state.targets.indexOf(hit);
                    if (idx !== -1) {
                        state.targets.splice(idx, 1);
                    }
                    // リングも削除
                    if (hit.userData.ring) {
                        scene.remove(hit.userData.ring);
                    }
                    scene.remove(hit);
    
                    // スコア加算
                    state.score++;
                    scoreEl.textContent = state.score;
    
                    // 小さな爆発エフェクト(パーティクル)
                    createExplosion(hit.position.clone(), hit.material.color.clone());
                }
            }
        });
    
        // --- 爆発エフェクト ---
        function createExplosion(position, color) {
            const count = 20;
            const particles = [];
            for (let i = 0; i < count; i++) {
                const geo = new THREE.SphereGeometry(0.04, 4, 4);
                const mat = new THREE.MeshBasicMaterial({
                    color: color,
                    transparent: true,
                    opacity: 1,
                });
                const p = new THREE.Mesh(geo, mat);
                p.position.copy(position);
                const dir = new THREE.Vector3(
                    (Math.random() - 0.5) * 0.15,
                    (Math.random() - 0.5) * 0.15,
                    (Math.random() - 0.5) * 0.15
                );
                p.userData.vel = dir;
                p.userData.life = 1.0;
                scene.add(p);
                particles.push(p);
            }
    
            // アニメーションループで消えるように
            const explodeInterval = setInterval(() => {
                let alive = false;
                particles.forEach(p => {
                    p.position.add(p.userData.vel);
                    p.userData.vel.multiplyScalar(0.98);
                    p.userData.life -= 0.03;
                    p.material.opacity = p.userData.life;
                    p.scale.setScalar(p.userData.life);
                    if (p.userData.life > 0) alive = true;
                });
                if (!alive) {
                    clearInterval(explodeInterval);
                    particles.forEach(p => scene.remove(p));
                }
            }, 16);
        }
    
        // --- アニメーション ---
        function animate() {
            requestAnimationFrame(animate);
    
            // ターゲットをゆっくり動かす
            state.targets.forEach(t => {
                t.position.x += t.userData.velocity.x;
                t.position.y += t.userData.velocity.y;
                t.position.z += t.userData.velocity.z;
                // 壁で跳ね返り
                if (Math.abs(t.position.x) > 4.5) t.userData.velocity.x *= -1;
                if (t.position.y > 3.5 || t.position.y < 0.2) t.userData.velocity.y *= -1;
                if (Math.abs(t.position.z) > 4.5) t.userData.velocity.z *= -1;
                // リングも追従
                if (t.userData.ring) {
                    t.userData.ring.position.copy(t.position);
                    t.userData.ring.lookAt(camera.position);
                }
            });
    
            stars.rotation.y += 0.0002;
            controls.update();
            renderer.render(scene, camera);
        }
        animate();
    
        // --- リサイズ ---
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
    
        // --- 再起動ボタン ---
        restartBtn.addEventListener('click', () => {
            startGame();
        });
    
        // --- ゲームスタート! ---
        startGame();
    
        console.log('🎯 3Dシューティングゲーム開始!');
        console.log('🖱️ ターゲットをクリックして撃ち落とせ!');
    </script>
    </body>
    </html>


    あなたへのおすすめ