Subway Surfer 3D: Neon Rush
by BlazeShark731078 lines48.1 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Subway Surfer 3D: Neon Rush</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
body, html {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #0a0c16;
user-select: none;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
width: 100%;
height: 100%;
}
#webgl-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
.text-neon {
text-shadow: 0 0 10px rgba(59, 130, 246, 0.8), 0 0 20px rgba(59, 130, 246, 0.4);
}
.text-gold {
text-shadow: 0 0 10px rgba(234, 179, 8, 0.8), 0 0 20px rgba(234, 179, 8, 0.4);
}
</style>
</head>
<body>
<!-- HUD Overlay -->
<div id="hud" class="absolute top-0 left-0 w-full p-4 md:p-6 flex justify-between items-start pointer-events-none z-10 hidden">
<div class="flex flex-col gap-1">
<div class="bg-gray-900/80 backdrop-blur-md px-4 py-2 rounded-xl border border-blue-500/30 flex items-center gap-3">
<span class="text-xs font-bold text-blue-400 uppercase tracking-wider">Score</span>
<span id="score-val" class="text-2xl font-black text-white text-neon">0</span>
</div>
<div class="bg-gray-900/60 backdrop-blur-sm px-3 py-1 rounded-lg border border-gray-700/30 flex items-center gap-2 mt-1">
<span class="text-[10px] font-bold text-gray-400 uppercase">Multiplier</span>
<span id="multiplier-val" class="text-sm font-extrabold text-blue-300">x1.0</span>
</div>
</div>
<div class="flex flex-col items-end gap-2">
<div class="bg-gray-900/80 backdrop-blur-md px-4 py-2 rounded-xl border border-yellow-500/30 flex items-center gap-3">
<span class="text-xs font-bold text-yellow-500 uppercase tracking-wider">Coins</span>
<div class="flex items-center gap-1">
<svg class="w-5 h-5 text-yellow-400 animate-spin" style="animation-duration: 3s" fill="currentColor" viewBox="0 0 20 20"><path d="M8.433 7.418c.554-.589 1.448-.589 2.002 0l.207.22 1.343-1.43a3.91 3.91 0 00-5.517 0L4.126 8.59a3.91 3.91 0 000 5.517l1.342-1.43-.207-.22a1.411 1.411 0 010-2.002l3.172-3.172z"/><path d="M11.567 12.582c-.554.589-1.448.589-2.002 0l-.207-.22-1.343 1.43a3.91 3.91 0 005.517 0l2.342-2.38a3.91 3.91 0 000-5.517l-1.342 1.43.207.22a1.411 1.411 0 010 2.002l-3.172 3.172z"/></svg>
<span id="coins-val" class="text-2xl font-black text-yellow-400 text-gold">0</span>
</div>
</div>
<!-- Speed indicator -->
<div class="w-24 bg-gray-900/50 rounded-full h-1.5 overflow-hidden border border-gray-700/50">
<div id="speed-bar" class="bg-blue-500 h-full w-0 transition-all duration-300"></div>
</div>
</div>
</div>
<!-- UI Overlay Screens -->
<div id="ui-container" class="absolute inset-0 flex items-center justify-center bg-gray-950/80 z-20 transition-all duration-300">
<!-- Main / Start Menu -->
<div id="start-screen" class="max-w-md w-full mx-4 bg-gray-900/95 p-8 rounded-2xl border border-blue-500/40 text-center shadow-2xl flex flex-col items-center">
<h1 class="text-4xl md:text-5xl font-black text-white mb-2 tracking-tight">NEON <span class="text-blue-500 text-neon">RUNNER</span></h1>
<p class="text-xs text-blue-300/80 tracking-widest uppercase mb-8">Subway Surfers Clone 3D</p>
<div class="w-full bg-gray-950/80 rounded-xl p-4 border border-gray-800 mb-8 text-left text-sm text-gray-300">
<h3 class="text-blue-400 font-bold mb-2 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
HOW TO PLAY:
</h3>
<div class="grid grid-cols-2 gap-3 text-xs">
<div class="flex items-center gap-2">
<span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">A/D</span> or <span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">←/→</span>
<span>Change Lanes</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">W</span> or <span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">↑</span>
<span>Jump Over</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">S</span> or <span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">↓</span>
<span>Slide Under</span>
</div>
<div class="flex items-center gap-2">
<span class="text-gray-400">Mobile: Swipe Screen</span>
</div>
</div>
</div>
<button id="start-btn" class="w-full py-4 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-400 hover:to-indigo-500 text-white font-black text-lg rounded-xl shadow-lg hover:shadow-blue-500/20 transform hover:-translate-y-0.5 active:translate-y-0 transition-all duration-150 border-b-4 border-indigo-800">
START SURFING
</button>
<button id="toggle-audio-btn" class="mt-4 text-xs text-gray-400 hover:text-white transition flex items-center gap-1.5">
<span id="audio-icon">🔊</span> Music & Sound: ON
</button>
</div>
<!-- Game Over Screen -->
<div id="gameover-screen" class="max-w-md w-full mx-4 bg-gray-900/95 p-8 rounded-2xl border border-red-500/40 text-center shadow-2xl hidden flex flex-col items-center">
<div class="w-16 h-16 bg-red-500/10 rounded-full border border-red-500/30 flex items-center justify-center mb-4">
<svg class="w-8 h-8 text-red-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
</div>
<h2 class="text-3xl font-black text-white mb-1">CRASH DETECTED!</h2>
<p class="text-xs text-red-400 uppercase tracking-widest mb-6">Game Over</p>
<div class="grid grid-cols-2 gap-4 w-full mb-8">
<div class="bg-gray-950/80 rounded-xl p-4 border border-gray-800">
<span class="block text-xs text-gray-400 font-bold uppercase tracking-wider mb-1">Final Score</span>
<span id="final-score" class="text-3xl font-black text-blue-400 text-neon">0</span>
</div>
<div class="bg-gray-950/80 rounded-xl p-4 border border-gray-800">
<span class="block text-xs text-gray-400 font-bold uppercase tracking-wider mb-1">Coins Collected</span>
<span id="final-coins" class="text-3xl font-black text-yellow-400 text-gold">0</span>
</div>
</div>
<div id="highscore-banner" class="hidden w-full bg-yellow-500/10 border border-yellow-500/30 text-yellow-400 text-sm font-bold py-2 px-4 rounded-lg mb-6 flex items-center justify-center gap-2">
🏆 NEW PERSONAL HIGH SCORE!
</div>
<div class="w-full text-left mb-6 text-xs text-gray-400 border-t border-gray-800 pt-4">
<div class="flex justify-between mb-1">
<span>Personal Best Score:</span>
<span id="best-score-val" class="font-bold text-white">0</span>
</div>
<div class="flex justify-between">
<span>Total Coins Count:</span>
<span id="best-coins-val" class="font-bold text-white">0</span>
</div>
</div>
<button id="restart-btn" class="w-full py-4 bg-gradient-to-r from-red-500 to-pink-600 hover:from-red-400 hover:to-pink-500 text-white font-black text-lg rounded-xl shadow-lg hover:shadow-red-500/20 transform hover:-translate-y-0.5 active:translate-y-0 transition-all duration-150 border-b-4 border-pink-800">
RUN AGAIN
</button>
</div>
</div>
<!-- On-screen controls for mobile/tablet players -->
<div id="mobile-controls" class="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-4 z-10 opacity-30 hover:opacity-100 transition-opacity duration-300 md:hidden">
<button id="ctrl-left" class="w-14 h-14 bg-gray-900/80 border border-blue-500/40 rounded-xl flex items-center justify-center text-white text-xl active:bg-blue-600/50">←</button>
<div class="flex flex-col gap-2">
<button id="ctrl-up" class="w-14 h-14 bg-gray-900/80 border border-blue-500/40 rounded-xl flex items-center justify-center text-white text-xl active:bg-blue-600/50">↑</button>
<button id="ctrl-down" class="w-14 h-14 bg-gray-900/80 border border-blue-500/40 rounded-xl flex items-center justify-center text-white text-xl active:bg-blue-600/50">↓</button>
</div>
<button id="ctrl-right" class="w-14 h-14 bg-gray-900/80 border border-blue-500/40 rounded-xl flex items-center justify-center text-white text-xl active:bg-blue-600/50">→</button>
</div>
<!-- WebGL Canvas Container -->
<div id="webgl-container"></div>
<script>
// ==========================================
// AUDIO SYNTHESIZER (Web Audio API)
// ==========================================
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let soundEnabled = true;
function playSound(type) {
if (!soundEnabled) return;
try {
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'coin') {
osc.type = 'sine';
osc.frequency.setValueAtTime(587.33, now); // D5
osc.frequency.setValueAtTime(880, now + 0.08); // A5
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
} else if (type === 'jump') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(150, now);
osc.frequency.exponentialRampToValueAtTime(400, now + 0.15);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.25);
} else if (type === 'slide') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(250, now);
osc.frequency.exponentialRampToValueAtTime(80, now + 0.2);
gain.gain.setValueAtTime(0.12, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
osc.start(now);
osc.stop(now + 0.25);
} else if (type === 'crash') {
// Explosion noise
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, now);
osc.frequency.linearRampToValueAtTime(30, now + 0.5);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.6);
osc.start(now);
osc.stop(now + 0.6);
} else if (type === 'start') {
// Energizing chime
osc.type = 'triangle';
osc.frequency.setValueAtTime(261.63, now); // C4
osc.frequency.setValueAtTime(329.63, now + 0.1); // E4
osc.frequency.setValueAtTime(392.00, now + 0.2); // G4
osc.frequency.setValueAtTime(523.25, now + 0.3); // C5
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
osc.start(now);
osc.stop(now + 0.5);
}
} catch (e) {
console.warn('Audio play failed:', e);
}
}
// Handle Audio Toggle Setup
const toggleAudioBtn = document.getElementById('toggle-audio-btn');
const audioIcon = document.getElementById('audio-icon');
toggleAudioBtn.addEventListener('click', () => {
soundEnabled = !soundEnabled;
if (soundEnabled) {
audioIcon.textContent = '🔊';
toggleAudioBtn.innerHTML = '🔊 Music & Sound: ON';
if (audioCtx.state === 'suspended') audioCtx.resume();
} else {
audioIcon.textContent = '🔇';
toggleAudioBtn.innerHTML = '🔇 Music & Sound: OFF';
}
});
// ==========================================
// STATE & PERSISTENCE
// ==========================================
let state = {
isPlaying: false,
score: 0,
coins: 0,
speed: 80, // initial relative units per sec
maxSpeed: 200,
laneWidth: 4,
currentLane: 1, // 0 = Left, 1 = Center, 2 = Right
targetX: 0,
playerY: 0,
isJumping: false,
isSliding: false,
highScore: parseInt(localStorage.getItem('subway_high_score')) || 0,
totalCoins: parseInt(localStorage.getItem('subway_total_coins')) || 0
};
// ==========================================
// GRAPHICS & 3D ENVIRONMENT
// ==========================================
let scene, camera, renderer;
let playerGroup, boardMesh, bodyMesh;
let subwayRails = [];
let activeElements = []; // coins, obstacles, etc.
let spawnTimer = 0;
let gameClock;
let particleSystems = [];
// Setup colors
const colors = {
accent: 0x00f3ff,
train: 0xff0055,
coin: 0xffd700,
ground: 0x0c0e18,
laneMark: 0x22d3ee,
barrier: 0xf59e0b
};
function init3D() {
// Container
const container = document.getElementById('webgl-container');
// Scene
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0c16, 0.015);
scene.background = new THREE.Color(0x0a0c16); // Match the fog to prevent visual clipping
// Camera
camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 7, 10);
camera.lookAt(0, 2, -15);
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.45);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.85);
dirLight.position.set(10, 25, 10);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;
scene.add(dirLight);
// Neon ambient points
const neonBlueLight = new THREE.PointLight(0x00f3ff, 2, 50);
neonBlueLight.position.set(-10, 5, -20);
scene.add(neonBlueLight);
const neonPinkLight = new THREE.PointLight(0xff0055, 2, 50);
neonPinkLight.position.set(10, 5, -30);
scene.add(neonPinkLight);
// Generate Floor & Environment elements
createTrackGround();
createTunnelLights();
createPlayer();
}
// Create the running platform with three visible lanes
function createTrackGround() {
const groundGeo = new THREE.PlaneGeometry(30, 1000);
const groundMat = new THREE.MeshStandardMaterial({
color: colors.ground,
roughness: 0.8,
metalness: 0.2
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.z = -400;
ground.receiveShadow = true;
scene.add(ground);
// Visual lanes markers
for (let i = -1; i <= 1; i++) {
const laneX = i * state.laneWidth;
// Rail lines
const railGeo = new THREE.BoxGeometry(0.15, 0.1, 1000);
const railMat = new THREE.MeshStandardMaterial({
color: 0x475569,
metalness: 0.8,
roughness: 0.2
});
const railL = new THREE.Mesh(railGeo, railMat);
railL.position.set(laneX - 0.6, 0.05, -400);
scene.add(railL);
const railR = new THREE.Mesh(railGeo, railMat);
railR.position.set(laneX + 0.6, 0.05, -400);
scene.add(railR);
// Neon center lane dash markers
const markerGeo = new THREE.BoxGeometry(0.08, 0.02, 10);
const markerMat = new THREE.MeshBasicMaterial({ color: colors.laneMark });
for (let z = 0; z > -1000; z -= 30) {
const marker = new THREE.Mesh(markerGeo, markerMat);
marker.position.set(laneX, 0.02, z);
scene.add(marker);
subwayRails.push(marker);
}
}
// Create side walls / industrial looking grid pillars
const pillarGeo = new THREE.BoxGeometry(1, 15, 1);
const pillarMat = new THREE.MeshStandardMaterial({ color: 0x1e293b, roughness: 0.5 });
for (let z = 0; z > -1000; z -= 25) {
const pL = new THREE.Mesh(pillarGeo, pillarMat);
pL.position.set(-15, 7.5, z);
scene.add(pL);
subwayRails.push(pL);
const pR = new THREE.Mesh(pillarGeo, pillarMat);
pR.position.set(15, 7.5, z);
scene.add(pR);
subwayRails.push(pR);
const beamGeo = new THREE.BoxGeometry(32, 0.5, 0.5);
const beam = new THREE.Mesh(beamGeo, pillarMat);
beam.position.set(0, 15, z);
scene.add(beam);
subwayRails.push(beam);
}
}
function createTunnelLights() {
const lightGeo = new THREE.BoxGeometry(2, 0.1, 8);
const lightMat = new THREE.MeshBasicMaterial({ color: 0x3b82f6 });
for (let z = 0; z > -800; z -= 80) {
const neonPlank = new THREE.Mesh(lightGeo, lightMat);
neonPlank.position.set(0, 14.8, z);
scene.add(neonPlank);
subwayRails.push(neonPlank);
}
}
function createPlayer() {
playerGroup = new THREE.Group();
// Hoverboard
const boardGeo = new THREE.BoxGeometry(1.2, 0.15, 2.5);
const boardMat = new THREE.MeshStandardMaterial({
color: colors.accent,
roughness: 0.1,
metalness: 0.8,
emissive: colors.accent,
emissiveIntensity: 0.2
});
boardMesh = new THREE.Mesh(boardGeo, boardMat);
boardMesh.position.y = 0.3;
boardMesh.castShadow = true;
playerGroup.add(boardMesh);
// Thrusters
const thrusterGeo = new THREE.CylinderGeometry(0.15, 0.1, 0.4, 8);
const thrusterMat = new THREE.MeshBasicMaterial({ color: 0xff3b30 });
const thrusterL = new THREE.Mesh(thrusterGeo, thrusterMat);
thrusterL.rotation.x = Math.PI / 2;
thrusterL.position.set(-0.35, 0.2, 1.2);
playerGroup.add(thrusterL);
const thrusterR = thrusterL.clone();
thrusterR.position.x = 0.35;
playerGroup.add(thrusterR);
// Suit Body
const bodyGeo = new THREE.BoxGeometry(0.8, 1.2, 0.6);
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x1e1b4b, roughness: 0.4 });
bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
bodyMesh.position.set(0, 1.1, 0);
bodyMesh.castShadow = true;
playerGroup.add(bodyMesh);
// Chest decal
const logoGeo = new THREE.PlaneGeometry(0.4, 0.4);
const logoMat = new THREE.MeshBasicMaterial({ color: colors.accent, side: THREE.DoubleSide });
const logo = new THREE.Mesh(logoGeo, logoMat);
logo.position.set(0, 1.1, -0.31);
playerGroup.add(logo);
// Helmet
const headGeo = new THREE.BoxGeometry(0.6, 0.6, 0.6);
const headMat = new THREE.MeshStandardMaterial({ color: 0x312e81 });
const head = new THREE.Mesh(headGeo, headMat);
head.position.set(0, 2.0, 0);
head.castShadow = true;
playerGroup.add(head);
// Visor
const visorGeo = new THREE.BoxGeometry(0.5, 0.18, 0.1);
const visorMat = new THREE.MeshBasicMaterial({ color: 0x00f3ff });
const visor = new THREE.Mesh(visorGeo, visorMat);
visor.position.set(0, 2.05, -0.27);
playerGroup.add(visor);
const boardLight = new THREE.PointLight(0x00f3ff, 2.5, 4);
boardLight.position.set(0, 0, 0);
playerGroup.add(boardLight);
playerGroup.position.set(0, 0, 0);
scene.add(playerGroup);
}
// ==========================================
// OBSTACLES, COINS & DYNAMIC SPAWNING
// ==========================================
class GameElement {
constructor(type, lane) {
this.type = type; // 'train', 'low_barrier', 'high_barrier', 'coin'
this.lane = lane;
this.x = (lane - 1) * state.laneWidth;
this.z = -200;
this.mesh = new THREE.Group();
this.radius = 1.0;
this.hit = false;
this.createMesh();
}
createMesh() {
if (this.type === 'coin') {
const coinGeo = new THREE.TorusGeometry(0.5, 0.18, 8, 24);
const coinMat = new THREE.MeshStandardMaterial({
color: colors.coin,
metalness: 0.9,
roughness: 0.1,
emissive: 0x7a6000,
emissiveIntensity: 0.2
});
const coin = new THREE.Mesh(coinGeo, coinMat);
coin.castShadow = true;
this.mesh.add(coin);
this.mesh.position.set(this.x, 1.2, this.z);
this.radius = 0.8;
}
else if (this.type === 'train') {
const trainLength = 22;
const trainHeight = 5.5;
const trainGeo = new THREE.BoxGeometry(3.0, trainHeight, trainLength);
const trainMat = new THREE.MeshStandardMaterial({
color: colors.train,
metalness: 0.7,
roughness: 0.2,
});
const train = new THREE.Mesh(trainGeo, trainMat);
train.castShadow = true;
train.receiveShadow = true;
this.mesh.add(train);
const glassGeo = new THREE.BoxGeometry(2.8, 1.2, 0.5);
const glassMat = new THREE.MeshBasicMaterial({ color: 0x00f3ff });
const glass = new THREE.Mesh(glassGeo, glassMat);
glass.position.set(0, 1.8, trainLength/2 + 0.05);
this.mesh.add(glass);
const grillGeo = new THREE.BoxGeometry(2.2, 1.0, 0.2);
const grillMat = new THREE.MeshStandardMaterial({ color: 0x111827 });
const grill = new THREE.Mesh(grillGeo, grillMat);
grill.position.set(0, -1.5, trainLength/2 + 0.05);
this.mesh.add(grill);
const lightStripGeo = new THREE.BoxGeometry(0.05, 0.15, trainLength - 2);
const stripMat = new THREE.MeshBasicMaterial({ color: 0x00ffaa });
const stripL = new THREE.Mesh(lightStripGeo, stripMat);
stripL.position.set(-1.51, 1.0, 0);
this.mesh.add(stripL);
const stripR = stripL.clone();
stripR.position.x = 1.51;
this.mesh.add(stripR);
this.mesh.position.set(this.x, trainHeight / 2, this.z - trainLength/2);
this.radius = 2.0;
}
else if (this.type === 'low_barrier') {
const barGeo = new THREE.BoxGeometry(3.4, 1.2, 0.8);
const barMat = new THREE.MeshStandardMaterial({ color: colors.barrier, roughness: 0.5 });
const barrier = new THREE.Mesh(barGeo, barMat);
barrier.castShadow = true;
this.mesh.add(barrier);
const stripeGeo = new THREE.BoxGeometry(3.42, 0.2, 0.82);
const stripeMat = new THREE.MeshStandardMaterial({ color: 0x111827 });
const stripe1 = new THREE.Mesh(stripeGeo, stripeMat);
stripe1.position.y = 0.2;
this.mesh.add(stripe1);
const stripe2 = stripe1.clone();
stripe2.position.y = -0.3;
this.mesh.add(stripe2);
this.mesh.position.set(this.x, 0.6, this.z);
this.radius = 1.1;
}
else if (this.type === 'high_barrier') {
const frameGeo = new THREE.Group();
const barGeo = new THREE.BoxGeometry(3.6, 0.4, 0.6);
const barMat = new THREE.MeshStandardMaterial({ color: 0xef4444 });
const topBar = new THREE.Mesh(barGeo, barMat);
topBar.position.y = 2.4;
frameGeo.add(topBar);
const legGeo = new THREE.BoxGeometry(0.3, 2.4, 0.3);
const legMat = new THREE.MeshStandardMaterial({ color: 0x475569 });
const legL = new THREE.Mesh(legGeo, legMat);
legL.position.set(-1.6, 1.2, 0);
frameGeo.add(legL);
const legR = legL.clone();
legR.position.x = 1.6;
frameGeo.add(legR);
const signGeo = new THREE.BoxGeometry(1.5, 0.6, 0.1);
const signMat = new THREE.MeshBasicMaterial({ color: 0xffe600 });
const sign = new THREE.Mesh(signGeo, signMat);
sign.position.y = 1.8;
frameGeo.add(sign);
this.mesh.add(frameGeo);
this.mesh.position.set(this.x, 0, this.z);
this.radius = 1.3;
}
scene.add(this.mesh);
}
destroy() {
scene.remove(this.mesh);
this.mesh.traverse(child => {
if (child.isMesh) {
child.geometry.dispose();
if (Array.isArray(child.material)) {
child.material.forEach(m => m.dispose());
} else {
child.material.dispose();
}
}
});
}
}
function spawnElements(deltaTime) {
spawnTimer += deltaTime;
const spawnInterval = Math.max(0.7, 1.8 - (state.speed / 120));
if (spawnTimer >= spawnInterval) {
spawnTimer = 0;
const spawnLane = Math.floor(Math.random() * 3);
const rand = Math.random();
if (rand < 0.25) {
spawnCoinChain(spawnLane);
} else if (rand < 0.50) {
activeElements.push(new GameElement('train', spawnLane));
} else if (rand < 0.75) {
activeElements.push(new GameElement('low_barrier', spawnLane));
} else {
activeElements.push(new GameElement('high_barrier', spawnLane));
}
}
}
function spawnCoinChain(lane) {
const numCoins = Math.floor(Math.random() * 3) + 3;
const initialZ = -220;
for (let i = 0; i < numCoins; i++) {
const coin = new GameElement('coin', lane);
coin.z = initialZ - (i * 7);
coin.mesh.position.z = coin.z;
if (numCoins > 3 && i > 0 && i < numCoins - 1) {
coin.mesh.position.y = 2.8;
}
activeElements.push(coin);
}
}
// ==========================================
// PARTICLE EFFECTS ENGINE
// ==========================================
function createExplosion(pos, color, count = 20) {
const geom = new THREE.BufferGeometry();
const positions = [];
const velocities = [];
for (let i = 0; i < count; i++) {
positions.push(pos.x, pos.y, pos.z);
velocities.push(
(Math.random() - 0.5) * 12,
(Math.random() * 8) + 2,
(Math.random() - 0.5) * 12
);
}
geom.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: color,
size: 0.4,
transparent: true,
opacity: 1.0,
blending: THREE.AdditiveBlending
});
const pSystem = new THREE.Points(geom, mat);
scene.add(pSystem);
particleSystems.push({
system: pSystem,
velocities: velocities,
age: 0,
maxAge: 0.8
});
}
function updateParticles(deltaTime) {
for (let i = particleSystems.length - 1; i >= 0; i--) {
const p = particleSystems[i];
p.age += deltaTime;
if (p.age >= p.maxAge) {
scene.remove(p.system);
p.system.geometry.dispose();
p.system.material.dispose();
particleSystems.splice(i, 1);
continue;
}
const posArr = p.system.geometry.attributes.position.array;
for (let j = 0; j < posArr.length / 3; j++) {
posArr[j * 3] += p.velocities[j * 3] * deltaTime;
posArr[j * 3 + 1] += p.velocities[j * 3 + 1] * deltaTime;
posArr[j * 3 + 1] -= 9.8 * deltaTime;
posArr[j * 3 + 2] += p.velocities[j * 3 + 2] * deltaTime;
}
p.system.geometry.attributes.position.needsUpdate = true;
p.system.material.opacity = 1 - (p.age / p.maxAge);
}
}
// ==========================================
// INPUT CONTROLS SYSTEM
// ==========================================
function setupControls() {
window.addEventListener('keydown', (e) => {
if (!state.isPlaying) return;
switch (e.key.toLowerCase()) {
case 'arrowleft':
case 'a':
changeLane(-1);
break;
case 'arrowright':
case 'd':
changeLane(1);
break;
case 'arrowup':
case 'w':
jump();
break;
case 'arrowdown':
case 's':
slide();
break;
}
});
// Touch Swipes
let touchStartX = 0;
let touchStartY = 0;
const swipeThreshold = 35;
window.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
}, { passive: true });
window.addEventListener('touchend', (e) => {
if (!state.isPlaying) return;
const diffX = e.changedTouches[0].screenX - touchStartX;
const diffY = e.changedTouches[0].screenY - touchStartY;
if (Math.abs(diffX) > Math.abs(diffY)) {
if (Math.abs(diffX) > swipeThreshold) {
if (diffX > 0) changeLane(1);
else changeLane(-1);
}
} else {
if (Math.abs(diffY) > swipeThreshold) {
if (diffY < 0) jump();
else slide();
}
}
}, { passive: true });
document.getElementById('ctrl-left').addEventListener('click', () => changeLane(-1));
document.getElementById('ctrl-right').addEventListener('click', () => changeLane(1));
document.getElementById('ctrl-up').addEventListener('click', () => jump());
document.getElementById('ctrl-down').addEventListener('click', () => slide());
}
function changeLane(direction) {
let nextLane = state.currentLane + direction;
if (nextLane >= 0 && nextLane <= 2) {
state.currentLane = nextLane;
state.targetX = (state.currentLane - 1) * state.laneWidth;
boardMesh.rotation.z = -direction * 0.4;
setTimeout(() => {
if (boardMesh) boardMesh.rotation.z = 0;
}, 200);
playSound('slide');
}
}
let jumpVelocity = 0;
const gravity = -32;
function jump() {
if (state.isJumping || state.isSliding) return;
state.isJumping = true;
jumpVelocity = 14;
playSound('jump');
playerGroup.scale.set(0.9, 1.2, 0.9);
}
function slide() {
if (state.isJumping || state.isSliding) return;
state.isSliding = true;
playSound('slide');
playerGroup.scale.set(1.2, 0.4, 1.2);
playerGroup.position.y = 0.1;
setTimeout(() => {
if (state.isSliding) {
state.isSliding = false;
playerGroup.scale.set(1, 1, 1);
}
}, 650);
}
// ==========================================
// GAMEPLAY CYCLE & ENGINE
// ==========================================
function startGame() {
state.score = 0;
state.coins = 0;
state.speed = 80;
state.currentLane = 1;
state.targetX = 0;
state.playerY = 0;
state.isJumping = false;
state.isSliding = false;
state.isPlaying = true;
jumpVelocity = 0;
document.getElementById('score-val').textContent = "0";
document.getElementById('coins-val').textContent = "0";
document.getElementById('multiplier-val').textContent = "x1.0";
document.getElementById('speed-bar').style.width = "0%";
document.getElementById('ui-container').classList.add('hidden');
document.getElementById('hud').classList.remove('hidden');
activeElements.forEach(el => el.destroy());
activeElements = [];
particleSystems.forEach(p => scene.remove(p.system));
particleSystems = [];
playerGroup.position.set(0, 0, 0);
playerGroup.scale.set(1, 1, 1);
gameClock = new THREE.Clock();
playSound('start');
}
function gameOver() {
state.isPlaying = false;
playSound('crash');
state.totalCoins += state.coins;
localStorage.setItem('subway_total_coins', state.totalCoins);
let isNewHighScore = false;
if (state.score > state.highScore) {
state.highScore = state.score;
localStorage.setItem('subway_high_score', state.highScore);
isNewHighScore = true;
}
createExplosion(playerGroup.position, colors.train, 35);
createExplosion(playerGroup.position, colors.accent, 20);
document.getElementById('final-score').textContent = Math.floor(state.score);
document.getElementById('final-coins').textContent = state.coins;
document.getElementById('best-score-val').textContent = Math.floor(state.highScore);
document.getElementById('best-coins-val').textContent = state.totalCoins;
const highscoreBanner = document.getElementById('highscore-banner');
if (isNewHighScore) {
highscoreBanner.classList.remove('hidden');
} else {
highscoreBanner.classList.add('hidden');
}
document.getElementById('hud').classList.add('hidden');
document.getElementById('start-screen').classList.add('hidden');
document.getElementById('gameover-screen').classList.remove('hidden');
document.getElementById('ui-container').classList.remove('hidden');
}
function checkCollisions(playerBox, element) {
const elPos = new THREE.Vector3();
element.mesh.getWorldPosition(elPos);
let elMin, elMax;
if (element.type === 'coin') {
elMin = new THREE.Vector3(elPos.x - 0.6, elPos.y - 0.6, elPos.z - 0.6);
elMax = new THREE.Vector3(elPos.x + 0.6, elPos.y + 0.6, elPos.z + 0.6);
}
else if (element.type === 'train') {
elMin = new THREE.Vector3(elPos.x - 1.5, 0, elPos.z - 11);
elMax = new THREE.Vector3(elPos.x + 1.5, 5.5, elPos.z + 11);
}
else if (element.type === 'low_barrier') {
elMin = new THREE.Vector3(elPos.x - 1.7, 0, elPos.z - 0.4);
elMax = new THREE.Vector3(elPos.x + 1.7, 1.2, elPos.z + 0.4);
}
else if (element.type === 'high_barrier') {
elMin = new THREE.Vector3(elPos.x - 1.8, 1.6, elPos.z - 0.3);
elMax = new THREE.Vector3(elPos.x + 1.8, 2.6, elPos.z + 0.3);
}
const elBox = new THREE.Box3(elMin, elMax);
return playerBox.intersectsBox(elBox);
}
// ==========================================
// MAIN ANIMATION LOOP
// ==========================================
function animate() {
requestAnimationFrame(animate);
const deltaTime = gameClock ? Math.min(gameClock.getDelta(), 0.1) : 0.016;
// Constantly render and update background elements for seamless presentation
if (state.isPlaying) {
if (state.speed < state.maxSpeed) {
state.speed += 1.2 * deltaTime;
}
const speedPercentage = (state.speed / state.maxSpeed) * 100;
document.getElementById('speed-bar').style.width = `${speedPercentage}%`;
state.score += state.speed * 0.1 * deltaTime;
document.getElementById('score-val').textContent = Math.floor(state.score);
const mult = (1.0 + (state.speed - 80) / 40).toFixed(1);
document.getElementById('multiplier-val').textContent = `x${mult}`;
// Smooth lane change movement interpolation
playerGroup.position.x += (state.targetX - playerGroup.position.x) * 15 * deltaTime;
// Jump mechanics
if (state.isJumping) {
state.playerY += jumpVelocity * deltaTime;
jumpVelocity += gravity * deltaTime;
if (state.playerY <= 0) {
state.playerY = 0;
state.isJumping = false;
playerGroup.scale.set(1, 1, 1);
}
playerGroup.position.y = state.playerY;
}
// Dynamic Bounding Boxes
let pMin, pMax;
if (state.isSliding) {
pMin = new THREE.Vector3(playerGroup.position.x - 0.5, 0, playerGroup.position.z - 0.8);
pMax = new THREE.Vector3(playerGroup.position.x + 0.5, 0.6, playerGroup.position.z + 0.8);
} else {
pMin = new THREE.Vector3(playerGroup.position.x - 0.5, playerGroup.position.y, playerGroup.position.z - 0.8);
pMax = new THREE.Vector3(playerGroup.position.x + 0.5, playerGroup.position.y + 2.1, playerGroup.position.z + 0.8);
}
const playerBox = new THREE.Box3(pMin, pMax);
spawnElements(deltaTime);
// Run rails backwards to simulate forward velocity
subwayRails.forEach(mesh => {
mesh.position.z += state.speed * deltaTime;
if (mesh.position.z > 25) {
mesh.position.z -= 800;
}
});
// Update obstacles/coins
for (let i = activeElements.length - 1; i >= 0; i--) {
const el = activeElements[i];
el.mesh.position.z += state.speed * deltaTime;
if (el.type === 'coin') {
el.mesh.rotation.y += 3 * deltaTime;
}
if (!el.hit && checkCollisions(playerBox, el)) {
el.hit = true;
if (el.type === 'coin') {
state.coins += 1;
document.getElementById('coins-val').textContent = state.coins;
playSound('coin');
const splashPos = new THREE.Vector3();
el.mesh.getWorldPosition(splashPos);
createExplosion(splashPos, colors.coin, 8);
el.destroy();
activeElements.splice(i, 1);
} else {
gameOver();
break;
}
continue;
}
if (el.mesh.position.z > 15) {
el.destroy();
activeElements.splice(i, 1);
}
}
// Idle floating hoverboard effect
if (!state.isJumping && !state.isSliding) {
playerGroup.position.y = 0.15 + Math.sin(Date.now() * 0.008) * 0.08;
boardMesh.rotation.x = Math.sin(Date.now() * 0.01) * 0.05;
}
// Engine Sparks
if (Math.random() < 0.4) {
const tailPos = new THREE.Vector3(playerGroup.position.x, playerGroup.position.y + 0.2, playerGroup.position.z + 1.2);
createExplosion(tailPos, 0x00f3ff, 2);
}
} else {
// If the game isn't running, run tracks at slow idle pace for menu screen aesthetics
subwayRails.forEach(mesh => {
mesh.position.z += 8 * deltaTime;
if (mesh.position.z > 25) {
mesh.position.z -= 800;
}
});
if (playerGroup) {
playerGroup.position.y = 0.15 + Math.sin(Date.now() * 0.003) * 0.08;
boardMesh.rotation.x = Math.sin(Date.now() * 0.004) * 0.05;
}
}
updateParticles(deltaTime);
renderer.render(scene, camera);
}
window.addEventListener('resize', onWindowResize, false);
function onWindowResize() {
const container = document.getElementById('webgl-container');
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
}
// ==========================================
// INITIALIZATION
// ==========================================
window.onload = function() {
init3D();
setupControls();
gameClock = new THREE.Clock();
// Kick off the animation render loop immediately!
animate();
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', startGame);
};
</script>
</body>
</html>Game Source: Subway Surfer 3D: Neon Rush
Creator: BlazeShark73
Libraries: three
Complexity: complex (1078 lines, 48.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: subway-surfer-3d-neon-rush-blazeshark73" to link back to the original. Then publish at arcadelab.ai/publish.