Subway Dash 3D
by LunarGalaxy87723 lines25.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, viewport-fit=cover">
<title>Subway Dash 3D</title>
<style>
html,body{margin:0;padding:0;overflow:hidden;background:#0a0f1e;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;touch-action:none;}
canvas{display:block;touch-action:none;}
#hud{position:fixed;top:0;left:0;width:100%;padding:14px 18px;box-sizing:border-box;display:flex;justify-content:space-between;align-items:flex-start;pointer-events:none;z-index:10;}
#score{color:#fff;font-size:28px;font-weight:800;text-shadow:0 2px 8px rgba(0,0,0,.6);letter-spacing:1px;}
#coins{color:#ffd54a;font-size:20px;font-weight:700;text-shadow:0 2px 8px rgba(0,0,0,.6);margin-top:4px;}
#fps{color:#7effc0;font-size:13px;font-weight:600;text-shadow:0 2px 6px rgba(0,0,0,.6);}
#speedbar{position:fixed;top:14px;left:50%;transform:translateX(-50%);color:#9fd8ff;font-size:12px;font-weight:600;text-shadow:0 2px 6px rgba(0,0,0,.6);z-index:10;}
#startScreen,#gameOverScreen{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 30%,rgba(30,40,70,.95),rgba(5,8,18,.98));z-index:20;color:#fff;text-align:center;padding:20px;box-sizing:border-box;}
#startScreen h1,#gameOverScreen h1{font-size:42px;margin:0 0 6px;background:linear-gradient(90deg,#ffd54a,#ff6a6a,#7effc0);-webkit-background-clip:text;background-clip:text;color:transparent;font-weight:900;letter-spacing:1px;}
#startScreen p,#gameOverScreen p{color:#b9c6e0;font-size:15px;margin:4px 0;max-width:320px;}
.btn{margin-top:22px;padding:16px 44px;font-size:19px;font-weight:800;color:#0a0f1e;background:linear-gradient(135deg,#ffd54a,#ffb84a);border:none;border-radius:40px;box-shadow:0 8px 24px rgba(255,180,50,.35);cursor:pointer;letter-spacing:.5px;}
.btn:active{transform:scale(.96);}
#finalScore{font-size:52px;font-weight:900;color:#ffd54a;margin:10px 0;}
#controls{position:fixed;bottom:0;left:0;width:100%;display:flex;z-index:10;}
.zone{flex:1;height:220px;}
.hidden{display:none !important;}
#swipeHint{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,.55);font-size:12px;z-index:10;pointer-events:none;letter-spacing:.5px;}
</style>
</head>
<body>
<div id="hud">
<div>
<div id="score">0 m</div>
<div id="coins">๐ช 0</div>
</div>
<div id="fps">120 fps</div>
</div>
<div id="swipeHint">Swipe left/right to switch lanes ยท up to jump ยท down to slide</div>
<div id="controls">
<div class="zone" id="zoneL"></div>
<div class="zone" id="zoneM"></div>
<div class="zone" id="zoneR"></div>
</div>
<div id="startScreen">
<h1>SUBWAY DASH 3D</h1>
<p>Sprint down the tracks, dodge trains and barriers, and grab every coin you can. Swipe or use arrow keys.</p>
<button class="btn" id="startBtn">START RUN</button>
</div>
<div id="gameOverScreen" class="hidden">
<h1>CRASHED</h1>
<div id="finalScore">0 m</div>
<p id="finalCoins">๐ช 0 collected</p>
<button class="btn" id="retryBtn">RUN AGAIN</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
"use strict";
// ---------- Core setup ----------
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x9fcfff, 0.018);
const camera = new THREE.PerspectiveCamera(62, window.innerWidth/window.innerHeight, 0.1, 500);
const renderer = new THREE.WebGLRenderer({antialias:true, powerPreference:"high-performance"});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', ()=>{
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ---------- Sky / lighting ----------
const skyGeo = new THREE.SphereGeometry(400, 24, 16);
const skyMat = new THREE.ShaderMaterial({
side: THREE.BackSide,
uniforms:{
top:{value:new THREE.Color(0x4fa8ff)},
bottom:{value:new THREE.Color(0xdcefff)}
},
vertexShader:`varying vec3 vPos; void main(){ vPos=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
fragmentShader:`varying vec3 vPos; uniform vec3 top; uniform vec3 bottom; void main(){ float h=normalize(vPos).y*0.5+0.5; gl_FragColor=vec4(mix(bottom,top,h),1.0);}`
});
scene.add(new THREE.Mesh(skyGeo, skyMat));
const hemi = new THREE.HemisphereLight(0xbfe0ff, 0x3a2d20, 0.65);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff2d8, 1.15);
sun.position.set(-10, 22, -8);
sun.castShadow = true;
sun.shadow.mapSize.set(1024,1024);
sun.shadow.camera.left=-18; sun.shadow.camera.right=18; sun.shadow.camera.top=18; sun.shadow.camera.bottom=-18;
sun.shadow.camera.near=1; sun.shadow.camera.far=60;
sun.shadow.bias=-0.0015;
scene.add(sun);
sun.target.position.set(0,0,-10);
scene.add(sun.target);
// ---------- Materials ----------
const skinMat = new THREE.MeshStandardMaterial({color:0xd9a679, roughness:0.65, metalness:0.02});
const shirtMat = new THREE.MeshStandardMaterial({color:0xe0433e, roughness:0.55, metalness:0.05});
const pantsMat = new THREE.MeshStandardMaterial({color:0x2c3550, roughness:0.6});
const shoeMat = new THREE.MeshStandardMaterial({color:0x1c1c1c, roughness:0.4});
const hairMat = new THREE.MeshStandardMaterial({color:0x3b2a20, roughness:0.7});
// r128 has no CapsuleGeometry (added in r142+) โ build an equivalent from a cylinder + two end caps
function makeCapsule(radius, length, mat){
const g = new THREE.Group();
const cyl = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, length, 8), mat);
cyl.castShadow = true;
g.add(cyl);
const capTop = new THREE.Mesh(new THREE.SphereGeometry(radius, 8, 6, 0, Math.PI*2, 0, Math.PI/2), mat);
capTop.position.y = length/2;
capTop.castShadow = true;
g.add(capTop);
const capBottom = new THREE.Mesh(new THREE.SphereGeometry(radius, 8, 6, 0, Math.PI*2, 0, Math.PI/2), mat);
capBottom.position.y = -length/2;
capBottom.rotation.x = Math.PI;
capBottom.castShadow = true;
g.add(capBottom);
return g;
}
// ---------- Build realistic-proportioned jointed character ----------
function buildCharacter(){
const g = new THREE.Group();
const rig = {};
// Hips pivot (root for legs+spine)
const hips = new THREE.Group();
hips.position.y = 1.0;
g.add(hips);
rig.hips = hips;
// Torso (tapered via two stacked boxes for a human-like ribcage/waist)
const torso = new THREE.Group();
torso.position.y = 0.02;
hips.add(torso);
const waist = new THREE.Mesh(new THREE.CylinderGeometry(0.145,0.165,0.28,10), pantsMat);
waist.position.y = 0.16;
waist.castShadow = true;
torso.add(waist);
const chest = new THREE.Mesh(new THREE.CylinderGeometry(0.185,0.155,0.42,10), shirtMat);
chest.position.y = 0.5;
chest.castShadow = true;
torso.add(chest);
// shoulders cap
const shoulders = new THREE.Mesh(new THREE.SphereGeometry(0.2,12,10), shirtMat);
shoulders.scale.set(1.15,0.6,0.85);
shoulders.position.y = 0.72;
shoulders.castShadow = true;
torso.add(shoulders);
// Neck + head
const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.06,0.07,0.1,8), skinMat);
neck.position.y = 0.8;
torso.add(neck);
const head = new THREE.Group();
head.position.y = 0.93;
torso.add(head);
const skull = new THREE.Mesh(new THREE.SphereGeometry(0.135,14,12), skinMat);
skull.scale.set(0.88,1,0.92);
skull.castShadow = true;
head.add(skull);
const jaw = new THREE.Mesh(new THREE.SphereGeometry(0.1,10,8), skinMat);
jaw.position.set(0,-0.08,0.02);
jaw.scale.set(0.8,0.65,0.8);
head.add(jaw);
const hair = new THREE.Mesh(new THREE.SphereGeometry(0.14,12,10,0,Math.PI*2,0,Math.PI*0.62), hairMat);
hair.position.y = 0.035;
head.add(hair);
// simple face hints
const noseGeo = new THREE.ConeGeometry(0.018,0.05,6);
const nose = new THREE.Mesh(noseGeo, skinMat);
nose.rotation.x = Math.PI/2.3;
nose.position.set(0,-0.01,0.13);
head.add(nose);
// Arm builder (upper+lower with elbow joint, shoulder joint pivot)
function buildArm(sign){
const shoulder = new THREE.Group();
shoulder.position.set(sign*0.225, 0.68, 0);
torso.add(shoulder);
const upperArm = makeCapsule(0.055,0.26,shirtMat);
upperArm.position.y = -0.15;
shoulder.add(upperArm);
const elbow = new THREE.Group();
elbow.position.y = -0.3;
shoulder.add(elbow);
const lowerArm = makeCapsule(0.045,0.24,skinMat);
lowerArm.position.y = -0.13;
elbow.add(lowerArm);
const hand = new THREE.Mesh(new THREE.SphereGeometry(0.05,8,8), skinMat);
hand.position.y = -0.27;
elbow.add(hand);
return {shoulder, elbow};
}
const armL = buildArm(1);
const armR = buildArm(-1);
// Leg builder (thigh+shin with hip and knee pivot)
function buildLeg(sign){
const hipJoint = new THREE.Group();
hipJoint.position.set(sign*0.11, 0.03, 0);
hips.add(hipJoint);
const thigh = makeCapsule(0.075,0.32,pantsMat);
thigh.position.y = -0.18;
hipJoint.add(thigh);
const knee = new THREE.Group();
knee.position.y = -0.36;
hipJoint.add(knee);
const shin = makeCapsule(0.06,0.3,pantsMat);
shin.position.y = -0.16;
knee.add(shin);
const foot = new THREE.Mesh(new THREE.BoxGeometry(0.09,0.06,0.2), shoeMat);
foot.position.set(0,-0.33,0.04);
foot.castShadow = true;
knee.add(foot);
return {hipJoint, knee};
}
const legL = buildLeg(1);
const legR = buildLeg(-1);
rig.torso = torso;
rig.armL = armL; rig.armR = armR;
rig.legL = legL; rig.legR = legR;
rig.head = head;
return {group:g, rig};
}
const player = buildCharacter();
player.group.position.set(0, 0, 0);
scene.add(player.group);
// ---------- Lane / track setup ----------
const LANES = [-2.2, 0, 2.2];
let laneIndex = 1;
let targetX = 0;
const trackGroup = new THREE.Group();
scene.add(trackGroup);
const groundMat = new THREE.MeshStandardMaterial({color:0x4a4f57, roughness:0.95});
const railMat = new THREE.MeshStandardMaterial({color:0x9a9a9a, roughness:0.4, metalness:0.6});
const tieMat = new THREE.MeshStandardMaterial({color:0x3a2c22, roughness:0.9});
const sideMat = new THREE.MeshStandardMaterial({color:0x6b7280, roughness:0.85});
const SEGMENT_LEN = 20;
const segments = [];
function buildSegment(zPos){
const seg = new THREE.Group();
seg.position.z = zPos;
const floor = new THREE.Mesh(new THREE.BoxGeometry(8, 0.4, SEGMENT_LEN), groundMat);
floor.position.y = -0.2;
floor.receiveShadow = true;
seg.add(floor);
const sideL = new THREE.Mesh(new THREE.BoxGeometry(0.6,1.4,SEGMENT_LEN), sideMat);
sideL.position.set(-4.3,0.5,0);
sideL.receiveShadow = true; sideL.castShadow = true;
seg.add(sideL);
const sideR = sideL.clone();
sideR.position.x = 4.3;
seg.add(sideR);
for(let t=-SEGMENT_LEN/2; t<SEGMENT_LEN/2; t+=1){
const tie = new THREE.Mesh(new THREE.BoxGeometry(2.6,0.08,0.5), tieMat);
tie.position.set(0,-0.001,t);
tie.receiveShadow = true;
seg.add(tie);
}
LANES.forEach(lx=>{
[-0.35,0.35].forEach(off=>{
const rail = new THREE.Mesh(new THREE.BoxGeometry(0.07,0.07,SEGMENT_LEN), railMat);
rail.position.set(lx+off, 0.03, 0);
rail.castShadow = true;
seg.add(rail);
});
});
trackGroup.add(seg);
segments.push(seg);
}
for(let i=0;i<6;i++) buildSegment(-i*SEGMENT_LEN + SEGMENT_LEN/2);
// Buildings for atmosphere
const buildingMats = [0x556b8f,0x6b5a7a,0x4a7a6b,0x8a6b4a].map(c=>new THREE.MeshStandardMaterial({color:c, roughness:0.8}));
const buildings = [];
function spawnBuilding(z, side){
const w = 3+Math.random()*3, h = 5+Math.random()*14, d = 3+Math.random()*3;
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w,h,d), buildingMats[Math.floor(Math.random()*buildingMats.length)]);
mesh.position.set(side*(7+Math.random()*6), h/2-0.3, z);
mesh.castShadow = true; mesh.receiveShadow = true;
scene.add(mesh);
buildings.push(mesh);
}
for(let i=0;i<20;i++){
spawnBuilding(-i*10+10, -1);
spawnBuilding(-i*10+15, 1);
}
// ---------- Obstacles / coins / trains ----------
const obstacles = [];
const coins = [];
const barrierMat = new THREE.MeshStandardMaterial({color:0xffb020, roughness:0.5, metalness:0.3});
const trainMat = new THREE.MeshStandardMaterial({color:0x2f7bd6, roughness:0.35, metalness:0.5});
const trainMat2 = new THREE.MeshStandardMaterial({color:0xe8e8ee, roughness:0.5});
const coinMat = new THREE.MeshStandardMaterial({color:0xffd54a, roughness:0.25, metalness:0.85, emissive:0x664400, emissiveIntensity:0.3});
const coinGeo = new THREE.CylinderGeometry(0.22,0.22,0.06,16);
function makeLowBarrier(lane){
const m = new THREE.Mesh(new THREE.BoxGeometry(1.6,0.7,0.35), barrierMat);
m.position.set(LANES[lane], 0.35, 0);
m.castShadow = true;
return {mesh:m, type:'jump', lane};
}
function makeHighBarrier(lane){
const g = new THREE.Group();
const bar = new THREE.Mesh(new THREE.BoxGeometry(1.6,0.18,0.3), barrierMat);
bar.position.y = 1.15;
bar.castShadow = true;
g.add(bar);
const postL = new THREE.Mesh(new THREE.BoxGeometry(0.1,1.15,0.1), barrierMat);
postL.position.set(-0.65,0.575,0);
const postR = postL.clone(); postR.position.x = 0.65;
g.add(postL, postR);
g.position.set(LANES[lane], 0, 0);
return {mesh:g, type:'duck', lane};
}
function makeTrain(lane){
const g = new THREE.Group();
const body = new THREE.Mesh(new THREE.BoxGeometry(1.9,1.9,5.5), trainMat);
body.position.y = 0.95;
body.castShadow = true;
g.add(body);
const stripe = new THREE.Mesh(new THREE.BoxGeometry(1.95,0.35,5.55), trainMat2);
stripe.position.y = 0.5;
g.add(stripe);
for(let i=-2;i<=2;i++){
const win = new THREE.Mesh(new THREE.BoxGeometry(0.05,0.5,0.7), new THREE.MeshStandardMaterial({color:0xbfe6ff, roughness:0.2, metalness:0.3}));
win.position.set(0.96,1.15,i*0.9);
g.add(win);
const win2 = win.clone(); win2.position.x = -0.96;
g.add(win2);
}
g.position.set(LANES[lane], 0, 0);
return {mesh:g, type:'block', lane};
}
function spawnObstacleSet(z){
const r = Math.random();
const lane = Math.floor(Math.random()*3);
if(r < 0.35){
const o = makeLowBarrier(lane);
o.mesh.position.z = z;
trackGroup.add(o.mesh);
o.z = z;
obstacles.push(o);
} else if(r < 0.6){
const o = makeHighBarrier(lane);
o.mesh.position.z = z;
trackGroup.add(o.mesh);
o.z = z;
obstacles.push(o);
} else {
const takenLanes = new Set();
const numTrains = Math.random()<0.4 ? 2 : 1;
for(let n=0;n<numTrains;n++){
let l = Math.floor(Math.random()*3);
let tries=0;
while(takenLanes.has(l) && tries<5){ l=Math.floor(Math.random()*3); tries++; }
takenLanes.add(l);
const o = makeTrain(l);
o.mesh.position.z = z;
trackGroup.add(o.mesh);
o.z = z;
obstacles.push(o);
}
}
// coins in a free lane
if(Math.random() < 0.75){
const freeLanes = [0,1,2];
const coinLane = freeLanes[Math.floor(Math.random()*3)];
const count = 4;
for(let i=0;i<count;i++){
const c = new THREE.Mesh(coinGeo, coinMat);
c.rotation.x = Math.PI/2;
c.position.set(LANES[coinLane], 0.9, z - i*1.1 - 2);
c.castShadow = true;
trackGroup.add(c);
coins.push({mesh:c, z:c.position.z, taken:false});
}
}
}
let nextSpawnZ = -25;
function ensureSpawns(playerZ){
while(nextSpawnZ > playerZ - 140){
spawnObstacleSet(nextSpawnZ);
nextSpawnZ -= 12 + Math.random()*6;
}
}
ensureSpawns(0);
// ---------- Input ----------
let touchStartX=0, touchStartY=0, touchActive=false;
function onDown(x,y){ touchStartX=x; touchStartY=y; touchActive=true; }
function onUp(x,y){
if(!touchActive) return;
touchActive=false;
const dx = x-touchStartX, dy = y-touchStartY;
if(Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30){
if(dx>0) changeLane(1); else changeLane(-1);
} else if(Math.abs(dy) > 30){
if(dy<0) doJump(); else doSlide();
}
}
document.getElementById('controls').addEventListener('touchstart', e=>{
const t=e.touches[0]; onDown(t.clientX,t.clientY);
},{passive:true});
document.getElementById('controls').addEventListener('touchend', e=>{
const t=e.changedTouches[0]; onUp(t.clientX,t.clientY);
},{passive:true});
document.addEventListener('mousedown', e=>{ if(gameState!=='playing') return; onDown(e.clientX,e.clientY); });
document.addEventListener('mouseup', e=>{ if(gameState!=='playing') return; onUp(e.clientX,e.clientY); });
window.addEventListener('keydown', e=>{
if(gameState!=='playing') return;
if(e.key==='ArrowLeft'||e.key==='a') changeLane(-1);
else if(e.key==='ArrowRight'||e.key==='d') changeLane(1);
else if(e.key==='ArrowUp'||e.key==='w'||e.key===' ') doJump();
else if(e.key==='ArrowDown'||e.key==='s') doSlide();
});
function changeLane(dir){
laneIndex = Math.max(0, Math.min(2, laneIndex+dir));
targetX = LANES[laneIndex];
}
// ---------- Player physics state ----------
let velY = 0;
let isJumping = false;
let isSliding = false;
let slideTimer = 0;
const GRAVITY = -28;
const JUMP_V = 9.5;
const GROUND_Y = 0;
function doJump(){
if(!isJumping && !isSliding){
isJumping = true;
velY = JUMP_V;
}
}
function doSlide(){
if(!isJumping && !isSliding){
isSliding = true;
slideTimer = 0.55;
}
}
// ---------- Game state ----------
let gameState = 'menu'; // menu, playing, over
let distance = 0;
let coinCount = 0;
let baseSpeed = 9;
let speed = baseSpeed;
let runTime = 0;
let gameOverAnim = 0;
const scoreEl = document.getElementById('score');
const coinsEl = document.getElementById('coins');
const fpsEl = document.getElementById('fps');
const startScreen = document.getElementById('startScreen');
const gameOverScreen = document.getElementById('gameOverScreen');
const finalScoreEl = document.getElementById('finalScore');
const finalCoinsEl = document.getElementById('finalCoins');
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('retryBtn').addEventListener('click', startGame);
function resetWorld(){
laneIndex = 1; targetX = 0;
velY = 0; isJumping=false; isSliding=false; slideTimer=0;
distance = 0; coinCount = 0; speed = baseSpeed; runTime = 0;
player.group.position.set(0,0,0);
player.group.rotation.z = 0;
player.rig.torso.rotation.x = 0;
obstacles.forEach(o=>trackGroup.remove(o.mesh));
obstacles.length = 0;
coins.forEach(c=>trackGroup.remove(c.mesh));
coins.length = 0;
nextSpawnZ = -25;
ensureSpawns(0);
}
function startGame(){
resetWorld();
gameState = 'playing';
startScreen.classList.add('hidden');
gameOverScreen.classList.add('hidden');
}
function endGame(){
gameState = 'over';
finalScoreEl.textContent = Math.floor(distance) + ' m';
finalCoinsEl.textContent = '๐ช ' + coinCount + ' collected';
setTimeout(()=> gameOverScreen.classList.remove('hidden'), 500);
}
// ---------- Camera rig ----------
camera.position.set(0, 3.2, 6);
let camShake = 0;
// ---------- Collision ----------
function checkCollisions(){
const pz = player.group.position.z;
for(let i=obstacles.length-1;i>=0;i--){
const o = obstacles[i];
if(o.mesh.position.z - pz > -1.4 && o.mesh.position.z - pz < 1.0){
const dx = Math.abs(player.group.position.x - LANES[o.lane]);
if(dx < 0.9){
if(o.type==='jump'){
if(!isJumping || player.group.position.y < 0.55){ crash(); return; }
} else if(o.type==='duck'){
if(!isSliding){ crash(); return; }
} else if(o.type==='block'){
crash(); return;
}
}
}
}
for(let i=coins.length-1;i>=0;i--){
const c = coins[i];
if(c.taken) continue;
const dz = c.mesh.position.z - pz;
if(dz > -1.0 && dz < 1.0){
const dx = Math.abs(player.group.position.x - c.mesh.position.x);
if(dx < 0.7){
c.taken = true;
c.mesh.visible = false;
coinCount++;
coinsEl.textContent = '๐ช ' + coinCount;
}
}
}
}
function crash(){
if(gameState!=='playing') return;
gameState = 'crashing';
camShake = 1;
setTimeout(()=> endGame(), 550);
}
// ---------- Animation loop ----------
let lastTime = performance.now();
let fpsAccum = 0, fpsFrames = 0, fpsLast = performance.now();
let runCycle = 0;
function animate(now){
requestAnimationFrame(animate);
let dt = (now - lastTime) / 1000;
lastTime = now;
dt = Math.min(dt, 0.05);
fpsFrames++;
if(now - fpsLast > 500){
fpsEl.textContent = Math.round(fpsFrames / ((now-fpsLast)/1000)) + ' fps';
fpsFrames = 0; fpsLast = now;
}
if(gameState==='playing' || gameState==='crashing'){
runTime += dt;
if(gameState==='crashing'){
// keep falling under gravity instead of freezing mid-air, and topple over
if(player.group.position.y > GROUND_Y){
velY += GRAVITY*dt;
player.group.position.y += velY*dt;
if(player.group.position.y <= GROUND_Y){
player.group.position.y = GROUND_Y;
velY = 0;
}
}
player.rig.torso.rotation.x += dt*4;
if(player.rig.torso.rotation.x > 1.4) player.rig.torso.rotation.x = 1.4;
player.group.rotation.z += dt*2.2;
}
if(gameState==='playing'){
speed = baseSpeed + Math.min(runTime*0.15, 9);
distance += speed*dt;
player.group.position.z -= speed*dt;
scoreEl.textContent = Math.floor(distance) + ' m';
// lane movement (smooth)
player.group.position.x += (targetX - player.group.position.x) * Math.min(1, dt*12);
// jump physics
if(isJumping){
velY += GRAVITY*dt;
player.group.position.y += velY*dt;
if(player.group.position.y <= GROUND_Y){
player.group.position.y = GROUND_Y;
isJumping = false; velY = 0;
}
}
// slide timer
if(isSliding){
slideTimer -= dt;
if(slideTimer<=0) isSliding=false;
}
checkCollisions();
ensureSpawns(player.group.position.z);
// cull passed obstacles/coins
const pz = player.group.position.z;
for(let i=obstacles.length-1;i>=0;i--){
if(obstacles[i].mesh.position.z - pz > 8){
trackGroup.remove(obstacles[i].mesh);
obstacles.splice(i,1);
}
}
for(let i=coins.length-1;i>=0;i--){
if(coins[i].mesh.position.z - pz > 8){
trackGroup.remove(coins[i].mesh);
coins.splice(i,1);
}
}
// recycle track segments
segments.forEach(seg=>{
if(seg.position.z - pz > SEGMENT_LEN){
seg.position.z -= SEGMENT_LEN*segments.length;
}
});
// recycle buildings
buildings.forEach(b=>{
if(b.position.z - pz > 15){
b.position.z -= 200;
}
});
}
// ---- run animation (cycle legs/arms) ----
const moving = gameState==='playing';
if(moving && !isSliding){
runCycle += dt * (7 + speed*0.35);
}
const cyc = runCycle;
const swing = isSliding ? 0 : Math.sin(cyc)*0.65;
const swingOpp = isSliding ? 0 : Math.sin(cyc+Math.PI)*0.65;
if(isSliding){
player.rig.torso.rotation.x = 1.05;
player.rig.hips.position.y = 0.55;
player.rig.legL.hipJoint.rotation.x = -0.3;
player.rig.legR.hipJoint.rotation.x = 0.9;
player.rig.legL.knee.rotation.x = 0.2;
player.rig.legR.knee.rotation.x = 1.1;
player.rig.armL.shoulder.rotation.x = -1.2;
player.rig.armR.shoulder.rotation.x = -1.2;
} else if(isJumping){
player.rig.torso.rotation.x = 0.12;
player.rig.hips.position.y = 1.0;
player.rig.legL.hipJoint.rotation.x = 0.7;
player.rig.legR.hipJoint.rotation.x = -0.4;
player.rig.legL.knee.rotation.x = 1.2;
player.rig.legR.knee.rotation.x = 0.3;
player.rig.armL.shoulder.rotation.x = -0.9;
player.rig.armR.shoulder.rotation.x = 0.6;
} else {
player.rig.torso.rotation.x = 0.08;
player.rig.hips.position.y = 1.0 + Math.abs(Math.sin(cyc*2))*0.03;
player.rig.legL.hipJoint.rotation.x = swing;
player.rig.legR.hipJoint.rotation.x = swingOpp;
player.rig.legL.knee.rotation.x = Math.max(0, -swing*1.3) + 0.15;
player.rig.legR.knee.rotation.x = Math.max(0, -swingOpp*1.3) + 0.15;
player.rig.armL.shoulder.rotation.x = swingOpp*0.8;
player.rig.armR.shoulder.rotation.x = swing*0.8;
player.rig.armL.elbow.rotation.x = Math.max(0,swingOpp)*0.6+0.2;
player.rig.armR.elbow.rotation.x = Math.max(0,swing)*0.6+0.2;
}
// coin spin
coins.forEach(c=>{ if(!c.taken) c.mesh.rotation.z += dt*3; });
// camera follow
const pz2 = player.group.position.z;
let camShakeX=0, camShakeY=0;
if(camShake>0){
camShake -= dt*2;
camShakeX = (Math.random()-0.5)*0.3*Math.max(0,camShake);
camShakeY = (Math.random()-0.5)*0.2*Math.max(0,camShake);
}
camera.position.x += ((player.group.position.x*0.5)+camShakeX - camera.position.x)*Math.min(1,dt*5);
camera.position.y += (3.2 + player.group.position.y*0.3 + camShakeY - camera.position.y)*Math.min(1,dt*5);
camera.position.z = pz2 + 6;
camera.lookAt(player.group.position.x*0.5, 1.1+player.group.position.y*0.3, pz2-4);
player.group.rotation.y = (targetX - player.group.position.x)*-0.15;
}
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
})();
</script>
</body>
</html>
Game Source: Subway Dash 3D
Creator: LunarGalaxy87
Libraries: three
Complexity: complex (723 lines, 25.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-dash-3d-lunargalaxy87" to link back to the original. Then publish at arcadelab.ai/publish.