Manager
View Site
Name
Type
React (.jsx)
HTML (.html)
Icon
Description
Code Editor
Revision History (0)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>Voxel Flight - WebVR Flying Game</title> <style> body { margin: 0; overflow: hidden; background-color: #87CEEB; /* Sky blue fallback */ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; user-select: none; } #canvas-container { width: 100vw; height: 100vh; display: block; } #ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; flex-direction: column; justify-content: space-between; padding: 20px; box-sizing: border-box; } #instructions { background: rgba(0, 0, 0, 0.5); color: white; padding: 15px; border-radius: 8px; max-width: 300px; pointer-events: auto; backdrop-filter: blur(5px); } h1 { margin: 0 0 10px 0; font-size: 1.2rem; color: #ffd700; text-transform: uppercase; letter-spacing: 1px; } p, ul { font-size: 0.9rem; margin: 0; line-height: 1.4; } ul { padding-left: 20px; margin-top: 5px; } .key { background: #444; padding: 2px 6px; border-radius: 4px; font-weight: bold; font-family: monospace; border-bottom: 2px solid #222; } #start-btn { display: inline-block; margin-top: 10px; padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; pointer-events: auto; } #start-btn:hover { background: #45a049; } /* Mobile Controls */ .joystick-zone { position: absolute; bottom: 30px; width: 120px; height: 120px; background: rgba(255, 255, 255, 0.1); border: 2px solid rgba(255, 255, 255, 0.2); border-radius: 50%; pointer-events: auto; display: none; /* Hidden on desktop by default */ } .joystick-knob { width: 50px; height: 50px; background: rgba(255, 255, 255, 0.5); border-radius: 50%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; } #left-stick { left: 30px; } #right-stick { right: 30px; } @media (max-width: 768px) { #instructions { font-size: 0.8rem; padding: 10px; max-width: 100%; background: rgba(0,0,0,0.2); } .desktop-hint { display: none; } .joystick-zone { display: block; } } #loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 1.5rem; font-weight: bold; text-shadow: 0 2px 4px rgba(0,0,0,0.5); transition: opacity 0.5s; } </style> </head> <body> <div id="loading">Generating World...</div> <div id="canvas-container"></div> <div id="ui-layer"> <div id="instructions"> <h1>Voxel Flight</h1> <p class="desktop-hint">Click Start to capture mouse.</p> <ul class="desktop-hint"> <li><span class="key">W</span> <span class="key">S</span> Move Forward/Back</li> <li><span class="key">A</span> <span class="key">D</span> Strafe</li> <li><span class="key">SPACE</span> Up | <span class="key">SHIFT</span> Down</li> <li><span class="key">E</span> Boost Speed</li> <li><span class="key">ESC</span> to Release Mouse</li> </ul> <p class="mobile-hint" style="display:none;">Use Left Stick to Move, Right Stick to Look.</p> <button id="start-btn">START FLIGHT</button> </div> <!-- Mobile Joysticks --> <div id="left-stick" class="joystick-zone"><div class="joystick-knob"></div></div> <div id="right-stick" class="joystick-zone"><div class="joystick-knob"></div></div> </div> <!-- Three.js --> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> // --- 1. Configuration & State --- const CONFIG = { worldSize: 128, // Size of the voxel grid (128x128) scale: 5, // Size of each block chunkHeight: 30, // Max height variance waterLevel: -5, colors: { water: 0x2980b9, sand: 0xf1c40f, grass: 0x2ecc71, forest: 0x27ae60, stone: 0x7f8c8d, snow: 0xffffff, cloud: 0xffffff }, speed: { normal: 30, boost: 80, rotation: 1.5 } }; let camera, scene, renderer; let blocksMesh, cloudsMesh; let lastTime = performance.now(); let isPointerLocked = false; let moveState = { forward: 0, back: 0, left: 0, right: 0, up: 0, down: 0, pitch: 0, yaw: 0 }; let velocity = new THREE.Vector3(); let isBoosting = false; // Simple pseudo-random noise generator (to avoid external deps) // Based on a simple sine wave combination for demonstration const Noise = { seed: Math.random() * 100, get: function(x, z) { let nx = x / 40; let nz = z / 40; // Combine a few frequencies let e = (1.00 * Math.sin(nx + this.seed) + Math.cos(nz + this.seed)) + (0.50 * Math.sin(2 * nx) + Math.cos(2 * nz)) + (0.25 * Math.sin(4 * nx) + Math.cos(4 * nz)); // Normalize roughly between -1 and 1 then scale return Math.pow(e, 1); } }; // --- 2. Initialization --- window.onload = function() { init(); generateWorld(); animate(); document.getElementById('loading').style.opacity = 0; setTimeout(() => document.getElementById('loading').remove(), 500); // Detect mobile for UI if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){ document.querySelector('.desktop-hint').style.display = 'none'; document.querySelector('.mobile-hint').style.display = 'block'; document.querySelectorAll('.desktop-hint').forEach(e => e.style.display = 'none'); } }; function init() { // Scene Setup const container = document.getElementById('canvas-container'); scene = new THREE.Scene(); scene.background = new THREE.Color(0x87CEEB); scene.fog = new THREE.Fog(0x87CEEB, 50, CONFIG.worldSize * CONFIG.scale * 0.8); // Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); // Start high up camera.position.set(0, 40, 0); // Renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); container.appendChild(renderer.domElement); // Lights const ambientLight = new THREE.AmbientLight(0xcccccc, 0.6); scene.add(ambientLight); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(100, 200, 50); scene.add(dirLight); // Event Listeners window.addEventListener('resize', onWindowResize, false); setupControls(); setupMobileControls(); } // --- 3. World Generation (Voxel) --- function generateWorld() { const geometry = new THREE.BoxGeometry(CONFIG.scale, CONFIG.scale, CONFIG.scale); // Move origin to bottom of cube so scaling height works nicely if we wanted variable height blocks // geometry.translate(0, CONFIG.scale/2, 0); const material = new THREE.MeshLambertMaterial({ color: 0xffffff }); const count = CONFIG.worldSize * CONFIG.worldSize; blocksMesh = new THREE.InstancedMesh(geometry, material, count); blocksMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // Static usually, but set dynamic just in case const dummy = new THREE.Object3D(); const color = new THREE.Color(); let i = 0; // Center the world around 0,0 const offset = (CONFIG.worldSize * CONFIG.scale) / 2; for (let x = 0; x < CONFIG.worldSize; x++) { for (let z = 0; z < CONFIG.worldSize; z++) { // Generate Height const noiseVal = Noise.get(x, z); // roughly -2 to 2 let height = Math.floor(noiseVal * 8); // Integer steps for minecraft look const posX = (x * CONFIG.scale) - offset; const posZ = (z * CONFIG.scale) - offset; const posY = height * CONFIG.scale; dummy.position.set(posX, posY, posZ); dummy.updateMatrix(); blocksMesh.setMatrixAt(i, dummy.matrix); // Biome Coloring if (posY <= CONFIG.waterLevel * CONFIG.scale) { // Water/Sand logic // Flatten water dummy.position.y = CONFIG.waterLevel * CONFIG.scale; dummy.updateMatrix(); blocksMesh.setMatrixAt(i, dummy.matrix); color.setHex(CONFIG.colors.water); } else if (posY < (CONFIG.waterLevel + 2) * CONFIG.scale) { color.setHex(CONFIG.colors.sand); } else if (posY < 6 * CONFIG.scale) { color.setHex(CONFIG.colors.grass); } else if (posY < 12 * CONFIG.scale) { color.setHex(CONFIG.colors.forest); } else if (posY < 18 * CONFIG.scale) { color.setHex(CONFIG.colors.stone); } else { color.setHex(CONFIG.colors.snow); } blocksMesh.setColorAt(i, color); i++; } } scene.add(blocksMesh); // --- Clouds --- const cloudGeo = new THREE.BoxGeometry(CONFIG.scale * 2, CONFIG.scale, CONFIG.scale * 2); const cloudMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 }); const cloudCount = 200; cloudsMesh = new THREE.InstancedMesh(cloudGeo, cloudMat, cloudCount); for (let c = 0; c < cloudCount; c++) { dummy.position.set( (Math.random() * CONFIG.worldSize * CONFIG.scale) - offset, (Math.random() * 30 + 60), // High up (Math.random() * CONFIG.worldSize * CONFIG.scale) - offset ); dummy.scale.set( Math.random() * 3 + 1, 1, Math.random() * 2 + 1 ); dummy.updateMatrix(); cloudsMesh.setMatrixAt(c, dummy.matrix); } scene.add(cloudsMesh); } // --- 4. Controls & Logic --- function setupControls() { const startBtn = document.getElementById('start-btn'); startBtn.addEventListener('click', () => { document.body.requestPointerLock(); }); document.addEventListener('pointerlockchange', () => { isPointerLocked = document.pointerLockElement === document.body; if (isPointerLocked) { document.getElementById('instructions').style.display = 'none'; } else { document.getElementById('instructions').style.display = 'block'; // Reset movement on unlock moveState.forward = 0; moveState.back = 0; moveState.left = 0; moveState.right = 0; moveState.up = 0; moveState.down = 0; } }); document.addEventListener('mousemove', (event) => { if (!isPointerLocked) return; // Mouse look (Yaw and Pitch) camera.rotation.y -= event.movementX * 0.002; camera.rotation.x -= event.movementY * 0.002; // Clamp pitch camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, camera.rotation.x)); // Reset Roll just in case camera.rotation.z = 0; }); document.addEventListener('keydown', (e) => { switch(e.code) { case 'KeyW': moveState.forward = 1; break; case 'KeyS': moveState.back = 1; break; case 'KeyA': moveState.left = 1; break; case 'KeyD': moveState.right = 1; break; case 'Space': moveState.up = 1; break; case 'ShiftLeft': moveState.down = 1; break; case 'KeyE': isBoosting = true; break; } }); document.addEventListener('keyup', (e) => { switch(e.code) { case 'KeyW': moveState.forward = 0; break; case 'KeyS': moveState.back = 0; break; case 'KeyA': moveState.left = 0; break; case 'KeyD': moveState.right = 0; break; case 'Space': moveState.up = 0; break; case 'ShiftLeft': moveState.down = 0; break; case 'KeyE': isBoosting = false; break; } }); } // Mobile Touch Controls (Virtual Joysticks) function setupMobileControls() { const leftStick = { zone: document.getElementById('left-stick'), knob: document.querySelector('#left-stick .joystick-knob'), active: false, origin: {x:0, y:0}, current: {x:0, y:0} }; const rightStick = { zone: document.getElementById('right-stick'), knob: document.querySelector('#right-stick .joystick-knob'), active: false, origin: {x:0, y:0}, current: {x:0, y:0} }; function handleTouch(stick, e, type) { e.preventDefault(); const touch = Array.from(e.changedTouches).find(t => t.target === stick.zone || stick.active); if (!touch) return; if (type === 'start') { stick.active = true; const rect = stick.zone.getBoundingClientRect(); stick.origin = { x: rect.left + rect.width/2, y: rect.top + rect.height/2 }; updateStick(stick, touch.clientX, touch.clientY); } else if (type === 'move' && stick.active) { updateStick(stick, touch.clientX, touch.clientY); } else if (type === 'end') { stick.active = false; stick.knob.style.transform = `translate(-50%, -50%)`; if(stick === leftStick) { moveState.forward = 0; moveState.back = 0; moveState.left = 0; moveState.right = 0; } } } function updateStick(stick, x, y) { const maxDist = 35; let dx = x - stick.origin.x; let dy = y - stick.origin.y; const dist = Math.sqrt(dx*dx + dy*dy); if(dist > maxDist) { dx = (dx / dist) * maxDist; dy = (dy / dist) * maxDist; } stick.knob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`; // Normalize -1 to 1 const nx = dx / maxDist; const ny = dy / maxDist; if (stick === leftStick) { moveState.forward = ny < -0.2 ? -ny : 0; moveState.back = ny > 0.2 ? ny : 0; moveState.right = nx > 0.2 ? nx : 0; moveState.left = nx < -0.2 ? -nx : 0; } else { // Right stick is look moveState.yaw = -nx * 0.05; moveState.pitch = -ny * 0.05; } } // Bind events ['touchstart', 'touchmove', 'touchend'].forEach(evt => { leftStick.zone.addEventListener(evt, (e) => handleTouch(leftStick, e, evt.replace('touch', ''))); rightStick.zone.addEventListener(evt, (e) => handleTouch(rightStick, e, evt.replace('touch', ''))); }); } // --- 5. Game Loop --- function animate() { requestAnimationFrame(animate); const time = performance.now(); const delta = (time - lastTime) / 1000; lastTime = time; // Movement Logic const actualSpeed = (isBoosting ? CONFIG.speed.boost : CONFIG.speed.normal) * delta; velocity.set(0, 0, 0); if (moveState.forward) velocity.z -= moveState.forward * actualSpeed; if (moveState.back) velocity.z += moveState.back * actualSpeed; if (moveState.left) velocity.x -= moveState.left * actualSpeed; if (moveState.right) velocity.x += moveState.right * actualSpeed; if (moveState.up) velocity.y += actualSpeed; if (moveState.down) velocity.y -= actualSpeed; // Mobile look update if (moveState.yaw || moveState.pitch) { camera.rotation.y += moveState.yaw; camera.rotation.x += moveState.pitch; camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, camera.rotation.x)); moveState.yaw = 0; moveState.pitch = 0; // Reset per frame } // Apply Rotation to Velocity velocity.applyQuaternion(camera.quaternion); camera.position.add(velocity); // Simple bounds check (wrap around world) const halfSize = (CONFIG.worldSize * CONFIG.scale) / 2; if (camera.position.x > halfSize) camera.position.x = -halfSize; if (camera.position.x < -halfSize) camera.position.x = halfSize; if (camera.position.z > halfSize) camera.position.z = -halfSize; if (camera.position.z < -halfSize) camera.position.z = halfSize; // Floor clamp (don't fall through bottom of world too deep) if (camera.position.y < -20) camera.position.y = -20; // Render renderer.render(scene, camera); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } </script> </body> </html>
No revisions found. Save the file to create a backup.
Delete
Update App