<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>3D Subway Runner Prototype</title> <style> body { margin: 0; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #111; } #ui { position: absolute; top: 20px; left: 20px; color: white; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); pointer-events: none; } #gameover { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #ff3333; font-size: 48px; font-weight: bold; text-align: center; text-shadow: 3px 3px 6px rgba(0,0,0,0.9); display: none; pointer-events: none; } #instructions { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: rgba(255,255,255,0.7); font-size: 16px; pointer-events: none; } </style> <!-- Include Three.js via CDN --> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> </head> <body> <div id="ui">Score: <span id="score">0</span></div> <div id="gameover">GAME OVER<br><span style="font-size: 20px; color: white;">Press Any Key to Restart</span></div> <div id="instructions">Use LEFT and RIGHT Arrow Keys to Switch Lanes</div> <script> // --- Game Variables --- let scene, camera, renderer; let player; let obstacles = []; let score = 0; let gameActive = true; // Lane configuration (Subway Surfers typically has 3 lanes) const LANES = [-3, 0, 3]; // Left, Middle, Right X coordinates let currentLane = 1; // Start in the middle lane (index 1) let targetX = LANES[currentLane]; const speed = 0.4; // Speed at which the world moves toward the player let obstacleSpawnTimer = 0; // --- Initialize Game --- init(); animate(); function init() { // 1. Create Scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a1a2e); scene.fog = new THREE.FogExp2(0x1a1a2e, 0.015); // 2. Create Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 4, 8); // Positioned slightly behind and above the player camera.lookAt(0, 1, -5); // 3. Create Renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; document.body.appendChild(renderer.domElement); // 4. Lights const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(5, 10, 7); dirLight.castShadow = true; scene.add(dirLight); // 5. Ground / Tracks const groundGeo = new THREE.PlaneGeometry(12, 1000); const groundMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 }); const ground = new THREE.Mesh(groundGeo, groundMat); ground.rotation.x = -Math.PI / 2; ground.position.z = -250; scene.add(ground); // Add simple visual lanes for (let i = 0; i < LANES.length; i++) { const trackGeo = new THREE.PlaneGeometry(0.2, 1000); const trackMat = new THREE.MeshBasicMaterial({ color: 0x555555 }); const track = new THREE.Mesh(trackGeo, trackMat); track.rotation.x = -Math.PI / 2; track.position.set(LANES[i], 0.01, -250); scene.add(track); } // 6. Player (A cool neon-blue cube) const playerGeo = new THREE.BoxGeometry(1.2, 1.8, 1.2); const playerMat = new THREE.MeshStandardMaterial({ color: 0x00f0ff, roughness: 0.2, metalness: 0.5 }); player = new THREE.Mesh(playerGeo, playerMat); player.position.set(targetX, 0.9, 0); // elevated by half its height scene.add(player); // 7. Event Listeners window.addEventListener('keydown', handleKeyDown); window.addEventListener('resize', onWindowResize); } // --- Controls --- function handleKeyDown(event) { if (!gameActive) { resetGame(); return; } if (event.key === "ArrowLeft") { if (currentLane > 0) { currentLane--; targetX = LANES[currentLane]; } } else if (event.key === "ArrowRight") { if (currentLane < LANES.length - 1) { currentLane++; targetX = LANES[currentLane]; } } } // --- Obstacle Spawning --- function spawnObstacle() { // Pick a random lane const laneIndex = Math.floor(Math.random() * LANES.length); const xPos = LANES[laneIndex]; // Setup temporary dimensions const height = Math.random() > 0.5 ? 3 : 1.5; // Tall hurdles or short blocks const obstacleGeo = new THREE.BoxGeometry(1.5, height, 1.5); const obstacleMat = new THREE.MeshStandardMaterial({ color: 0xff3333, roughness: 0.5 }); const obstacle = new THREE.Mesh(obstacleGeo, obstacleMat); obstacle.position.set(xPos, height / 2, -100); // Spawn far away scene.add(obstacle); obstacles.push(obstacle); } // --- Game Loop --- function animate() { requestAnimationFrame(animate); if (gameActive) { // Smoothly slide player to the target lane player.position.x = THREE.MathUtils.lerp(player.position.x, targetX, 0.2); // Increment score over time score += 1; document.getElementById('score').innerText = Math.floor(score / 5); // Spawn obstacles at intervals obstacleSpawnTimer++; if (obstacleSpawnTimer > 40) { // every ~40 frames spawnObstacle(); obstacleSpawnTimer = 0; } // Move obstacles toward player and handle collisions for (let i = obstacles.length - 1; i >= 0; i--) { const obs = obstacles[i]; obs.position.z += speed + (score * 0.0002); // Accelerates over time! // Simple AABB Collision Detection const playerBox = new THREE.Box3().setFromObject(player); const obsBox = new THREE.Box3().setFromObject(obs); if (playerBox.intersectsBox(obsBox)) { gameOver(); } // Remove obstacles that pass behind camera to save memory if (obs.position.z > 10) { scene.remove(obs); obstacles.splice(i, 1); } } } renderer.render(scene, camera); } // --- Game States --- function gameOver() { gameActive = false; document.getElementById('gameover').style.display = 'block'; } function resetGame() { // Remove existing obstacles obstacles.forEach(obs => scene.remove(obs)); obstacles = []; // Reset player and score currentLane = 1; targetX = LANES[currentLane]; player.position.set(targetX, 0.9, 0); score = 0; obstacleSpawnTimer = 0; document.getElementById('gameover').style.display = 'none'; gameActive = true; } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } </script> </body> </html>
Sign in to leave a comment
No completed page designs yet.
Completed design pages will appear here when they are ready to preview.
No user flows yet.
The User Flow Agent will generate per-persona navigation diagrams after SRD updates.
No completed page designs yet.
Completed design pages will appear here when they are ready to preview.
No user flows yet.
The User Flow Agent will generate per-persona navigation diagrams after SRD updates.
No comments yet. Be the first!