-
-
Save shricodev/9fc4d5727fa1fe66a2de485647fc79db to your computer and use it in GitHub Desktop.
Bike Racing (Developed by Claude Opus 4 Model) - Blog Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>3D Motorbike Racing Game</title> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
font-family: Arial, sans-serif; | |
} | |
#gameCanvas { | |
width: 100%; | |
height: 100vh; | |
display: block; | |
} | |
#hud { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
color: white; | |
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); | |
font-size: 20px; | |
font-weight: bold; | |
} | |
#controls { | |
position: absolute; | |
bottom: 20px; | |
left: 20px; | |
color: white; | |
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); | |
font-size: 14px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="hud"> | |
<div>Speed: <span id="speed">0</span> km/h</div> | |
<div>Position: <span id="position">1</span>/6</div> | |
</div> | |
<div id="controls"> | |
W/S - Accelerate/Brake | A/D - Steer | Q/E - Kick Left/Right | |
</div> | |
<script type="module"> | |
import * as THREE from "https://unpkg.com/[email protected]/build/three.module.js"; | |
// Game Configuration | |
const CONFIG = { | |
playerMaxSpeed: 150, | |
playerAcceleration: 80, | |
playerBraking: 120, | |
playerSteering: 2, | |
enemySpeed: 100, | |
kickForce: 15, | |
trackLength: 2000, | |
trackWidth: 30, | |
cameraHeight: 8, | |
cameraDistance: 20, | |
}; | |
// Game State | |
const gameState = { | |
keys: {}, | |
playerBike: null, | |
enemyBikes: [], | |
playerSpeed: 0, | |
playerPosition: 0, | |
clouds: [], | |
trees: [], | |
flowers: [], | |
train: null, | |
trainDirection: 1, | |
}; | |
// Scene Setup | |
const scene = new THREE.Scene(); | |
scene.fog = new THREE.Fog(0x87ceeb, 100, 500); | |
const camera = new THREE.PerspectiveCamera( | |
75, | |
window.innerWidth / window.innerHeight, | |
0.1, | |
1000, | |
); | |
const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
document.body.appendChild(renderer.domElement); | |
// Lighting | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(50, 100, 50); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.camera.left = -100; | |
directionalLight.shadow.camera.right = 100; | |
directionalLight.shadow.camera.top = 100; | |
directionalLight.shadow.camera.bottom = -100; | |
scene.add(directionalLight); | |
// Create Bike Geometry | |
function createBike(color = 0xff0000) { | |
const bikeGroup = new THREE.Group(); | |
// Body | |
const bodyGeometry = new THREE.BoxGeometry(1, 1, 3); | |
const bodyMaterial = new THREE.MeshPhongMaterial({ color: color }); | |
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
body.position.y = 0.5; | |
body.castShadow = true; | |
bikeGroup.add(body); | |
// Wheels | |
const wheelGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.3, 16); | |
const wheelMaterial = new THREE.MeshPhongMaterial({ color: 0x333333 }); | |
const frontWheel = new THREE.Mesh(wheelGeometry, wheelMaterial); | |
frontWheel.rotation.z = Math.PI / 2; | |
frontWheel.position.set(0, 0, 1.2); | |
frontWheel.castShadow = true; | |
bikeGroup.add(frontWheel); | |
const backWheel = new THREE.Mesh(wheelGeometry, wheelMaterial); | |
backWheel.rotation.z = Math.PI / 2; | |
backWheel.position.set(0, 0, -1.2); | |
backWheel.castShadow = true; | |
bikeGroup.add(backWheel); | |
// Rider (using basic geometry instead of CapsuleGeometry) | |
const riderGroup = new THREE.Group(); | |
// Rider body (cylinder) | |
const bodyGeom = new THREE.CylinderGeometry(0.3, 0.3, 1.2, 8); | |
const bodyMat = new THREE.MeshPhongMaterial({ color: 0x333333 }); | |
const riderBody = new THREE.Mesh(bodyGeom, bodyMat); | |
riderBody.position.y = 0.6; | |
riderGroup.add(riderBody); | |
// Rider head (sphere) | |
const headGeom = new THREE.SphereGeometry(0.3, 8, 6); | |
const headMat = new THREE.MeshPhongMaterial({ color: 0xffdbca }); | |
const riderHead = new THREE.Mesh(headGeom, headMat); | |
riderHead.position.y = 1.5; | |
riderGroup.add(riderHead); | |
// Helmet | |
const helmetGeom = new THREE.SphereGeometry(0.35, 8, 6); | |
const helmetMat = new THREE.MeshPhongMaterial({ color: color }); | |
const helmet = new THREE.Mesh(helmetGeom, helmetMat); | |
helmet.position.y = 1.5; | |
helmet.scale.y = 0.8; | |
riderGroup.add(helmet); | |
riderGroup.position.y = 1; | |
riderGroup.rotation.x = Math.PI / 6; | |
bikeGroup.add(riderGroup); | |
// Add handlebars | |
const handlebarGeom = new THREE.CylinderGeometry(0.05, 0.05, 1.5); | |
const handlebarMat = new THREE.MeshPhongMaterial({ color: 0x222222 }); | |
const handlebars = new THREE.Mesh(handlebarGeom, handlebarMat); | |
handlebars.rotation.z = Math.PI / 2; | |
handlebars.position.set(0, 1, 0.8); | |
bikeGroup.add(handlebars); | |
bikeGroup.userData = { | |
velocity: new THREE.Vector3(), | |
kickCooldown: 0, | |
wobble: 0, | |
}; | |
return bikeGroup; | |
} | |
// Create Track | |
function createTrack() { | |
const trackGroup = new THREE.Group(); | |
// Road segments | |
for (let i = 0; i < CONFIG.trackLength / 100; i++) { | |
const roadGeometry = new THREE.PlaneGeometry(CONFIG.trackWidth, 100); | |
const roadMaterial = new THREE.MeshPhongMaterial({ | |
color: 0x444444, | |
side: THREE.DoubleSide, | |
}); | |
const roadSegment = new THREE.Mesh(roadGeometry, roadMaterial); | |
roadSegment.rotation.x = -Math.PI / 2; | |
roadSegment.position.z = i * 100; | |
roadSegment.receiveShadow = true; | |
trackGroup.add(roadSegment); | |
// Lane markers (dashed lines) | |
for (let j = -1; j <= 1; j++) { | |
if (j !== 0) { | |
for (let k = 0; k < 10; k++) { | |
const lineGeometry = new THREE.PlaneGeometry(0.5, 8); | |
const lineMaterial = new THREE.MeshPhongMaterial({ | |
color: 0xffffff, | |
}); | |
const line = new THREE.Mesh(lineGeometry, lineMaterial); | |
line.rotation.x = -Math.PI / 2; | |
line.position.set( | |
(j * CONFIG.trackWidth) / 3, | |
0.01, | |
i * 100 + k * 10, | |
); | |
trackGroup.add(line); | |
} | |
} | |
} | |
} | |
// Guard rails | |
const railGeometry = new THREE.BoxGeometry(1, 2, CONFIG.trackLength); | |
const railMaterial = new THREE.MeshPhongMaterial({ color: 0x888888 }); | |
const leftRail = new THREE.Mesh(railGeometry, railMaterial); | |
leftRail.position.set( | |
-CONFIG.trackWidth / 2 - 1, | |
1, | |
CONFIG.trackLength / 2, | |
); | |
leftRail.castShadow = true; | |
trackGroup.add(leftRail); | |
const rightRail = new THREE.Mesh(railGeometry, railMaterial); | |
rightRail.position.set( | |
CONFIG.trackWidth / 2 + 1, | |
1, | |
CONFIG.trackLength / 2, | |
); | |
rightRail.castShadow = true; | |
trackGroup.add(rightRail); | |
return trackGroup; | |
} | |
// Create Environment | |
function createEnvironment() { | |
// Sky | |
scene.background = new THREE.Color(0x87ceeb); | |
// Ground | |
const groundGeometry = new THREE.PlaneGeometry( | |
1000, | |
CONFIG.trackLength, | |
); | |
const groundMaterial = new THREE.MeshPhongMaterial({ color: 0x4b7c4b }); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.position.y = -0.1; | |
ground.position.z = CONFIG.trackLength / 2; | |
ground.receiveShadow = true; | |
scene.add(ground); | |
// Clouds | |
for (let i = 0; i < 20; i++) { | |
const cloudGroup = new THREE.Group(); | |
for (let j = 0; j < 5; j++) { | |
const cloudGeometry = new THREE.SphereGeometry( | |
Math.random() * 10 + 5, | |
8, | |
6, | |
); | |
const cloudMaterial = new THREE.MeshPhongMaterial({ | |
color: 0xffffff, | |
transparent: true, | |
opacity: 0.7, | |
}); | |
const cloudPart = new THREE.Mesh(cloudGeometry, cloudMaterial); | |
cloudPart.position.set( | |
Math.random() * 20 - 10, | |
Math.random() * 5, | |
Math.random() * 10 - 5, | |
); | |
cloudGroup.add(cloudPart); | |
} | |
cloudGroup.position.set( | |
Math.random() * 200 - 100, | |
Math.random() * 50 + 50, | |
Math.random() * CONFIG.trackLength, | |
); | |
gameState.clouds.push(cloudGroup); | |
scene.add(cloudGroup); | |
} | |
// Trees | |
for (let i = 0; i < 100; i++) { | |
const treeGroup = new THREE.Group(); | |
// Trunk | |
const trunkGeometry = new THREE.CylinderGeometry(1, 1.5, 8); | |
const trunkMaterial = new THREE.MeshPhongMaterial({ | |
color: 0x8b4513, | |
}); | |
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial); | |
trunk.position.y = 4; | |
trunk.castShadow = true; | |
treeGroup.add(trunk); | |
// Leaves (multiple cones for fuller look) | |
const leavesGeometry = new THREE.ConeGeometry(5, 10, 8); | |
const leavesMaterial = new THREE.MeshPhongMaterial({ | |
color: 0x228b22, | |
}); | |
const leaves = new THREE.Mesh(leavesGeometry, leavesMaterial); | |
leaves.position.y = 12; | |
leaves.castShadow = true; | |
treeGroup.add(leaves); | |
const leaves2 = new THREE.Mesh(leavesGeometry, leavesMaterial); | |
leaves2.position.y = 10; | |
leaves2.scale.set(0.8, 0.8, 0.8); | |
treeGroup.add(leaves2); | |
const side = Math.random() > 0.5 ? 1 : -1; | |
treeGroup.position.set( | |
side * (CONFIG.trackWidth / 2 + Math.random() * 30 + 10), | |
0, | |
Math.random() * CONFIG.trackLength, | |
); | |
gameState.trees.push(treeGroup); | |
scene.add(treeGroup); | |
} | |
// Flowers | |
for (let i = 0; i < 200; i++) { | |
const flowerGroup = new THREE.Group(); | |
// Stem | |
const stemGeometry = new THREE.CylinderGeometry(0.1, 0.1, 2); | |
const stemMaterial = new THREE.MeshPhongMaterial({ color: 0x228b22 }); | |
const stem = new THREE.Mesh(stemGeometry, stemMaterial); | |
stem.position.y = 1; | |
flowerGroup.add(stem); | |
// Petals | |
const petalGeometry = new THREE.SphereGeometry(0.5, 6, 4); | |
const colors = [0xff1493, 0xffff00, 0x00ced1, 0xff6347, 0xda70d6]; | |
const petalMaterial = new THREE.MeshPhongMaterial({ | |
color: colors[Math.floor(Math.random() * colors.length)], | |
}); | |
// Center | |
const centerGeometry = new THREE.SphereGeometry(0.3, 6, 4); | |
const centerMaterial = new THREE.MeshPhongMaterial({ | |
color: 0xffff00, | |
}); | |
const center = new THREE.Mesh(centerGeometry, centerMaterial); | |
center.position.y = 2; | |
flowerGroup.add(center); | |
// Petals around center | |
for (let j = 0; j < 5; j++) { | |
const petal = new THREE.Mesh(petalGeometry, petalMaterial); | |
const angle = (j / 5) * Math.PI * 2; | |
petal.position.set(Math.cos(angle) * 0.7, 2, Math.sin(angle) * 0.7); | |
flowerGroup.add(petal); | |
} | |
const side = Math.random() > 0.5 ? 1 : -1; | |
flowerGroup.position.set( | |
side * (CONFIG.trackWidth / 2 + Math.random() * 10 + 5), | |
0, | |
Math.random() * CONFIG.trackLength, | |
); | |
gameState.flowers.push(flowerGroup); | |
scene.add(flowerGroup); | |
} | |
// Train track | |
const railGroup = new THREE.Group(); | |
const railGeometry = new THREE.BoxGeometry(2, 0.5, CONFIG.trackLength); | |
const railMaterial = new THREE.MeshPhongMaterial({ color: 0x654321 }); | |
const leftTrainRail = new THREE.Mesh(railGeometry, railMaterial); | |
leftTrainRail.position.set(-60, 0, CONFIG.trackLength / 2); | |
railGroup.add(leftTrainRail); | |
const rightTrainRail = new THREE.Mesh(railGeometry, railMaterial); | |
rightTrainRail.position.set(-55, 0, CONFIG.trackLength / 2); | |
railGroup.add(rightTrainRail); | |
scene.add(railGroup); | |
// Train | |
const trainGroup = new THREE.Group(); | |
// Train cars | |
for (let i = 0; i < 5; i++) { | |
const carGeometry = new THREE.BoxGeometry(8, 6, 20); | |
const carMaterial = new THREE.MeshPhongMaterial({ | |
color: i === 0 ? 0xff0000 : 0x0000ff, | |
}); | |
const car = new THREE.Mesh(carGeometry, carMaterial); | |
car.position.z = i * 22; | |
car.position.y = 3; | |
car.castShadow = true; | |
trainGroup.add(car); | |
// Windows | |
const windowGeometry = new THREE.PlaneGeometry(2, 2); | |
const windowMaterial = new THREE.MeshPhongMaterial({ | |
color: 0x87ceeb, | |
}); | |
for (let w = 0; w < 3; w++) { | |
const windowLeft = new THREE.Mesh(windowGeometry, windowMaterial); | |
windowLeft.position.set(-4.1, 3, i * 22 + (w - 1) * 5); | |
windowLeft.rotation.y = Math.PI / 2; | |
trainGroup.add(windowLeft); | |
const windowRight = new THREE.Mesh(windowGeometry, windowMaterial); | |
windowRight.position.set(4.1, 3, i * 22 + (w - 1) * 5); | |
windowRight.rotation.y = -Math.PI / 2; | |
trainGroup.add(windowRight); | |
} | |
// Wheels | |
for (let j = 0; j < 4; j++) { | |
const wheelGeometry = new THREE.CylinderGeometry(1, 1, 10); | |
const wheelMaterial = new THREE.MeshPhongMaterial({ | |
color: 0x333333, | |
}); | |
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); | |
wheel.rotation.z = Math.PI / 2; | |
wheel.position.set( | |
5 * (j < 2 ? 1 : -1), | |
0, | |
i * 22 + (j % 2) * 8 - 4, | |
); | |
trainGroup.add(wheel); | |
} | |
} | |
trainGroup.position.set(-57.5, 0, 0); | |
gameState.train = trainGroup; | |
scene.add(trainGroup); | |
} | |
// Initialize Game | |
function init() { | |
// Create track | |
const track = createTrack(); | |
scene.add(track); | |
// Create environment | |
createEnvironment(); | |
// Create player bike | |
gameState.playerBike = createBike(0xff0000); | |
gameState.playerBike.position.set(0, 0.5, 10); | |
scene.add(gameState.playerBike); | |
// Create enemy bikes | |
const enemyColors = [0x0000ff, 0x00ff00, 0xffff00, 0xff00ff, 0x00ffff]; | |
for (let i = 0; i < 5; i++) { | |
const enemyBike = createBike(enemyColors[i]); | |
enemyBike.position.set((i - 2) * 5, 0.5, Math.random() * 50 + 20); | |
enemyBike.userData.lane = i - 2; | |
enemyBike.userData.baseSpeed = | |
CONFIG.enemySpeed + Math.random() * 20 - 10; | |
enemyBike.userData.aggressiveness = Math.random(); | |
gameState.enemyBikes.push(enemyBike); | |
scene.add(enemyBike); | |
} | |
// Position camera | |
updateCamera(); | |
} | |
// Update Camera | |
function updateCamera() { | |
const bikePosition = gameState.playerBike.position; | |
const bikeRotation = gameState.playerBike.rotation; | |
camera.position.x = | |
bikePosition.x - Math.sin(bikeRotation.y) * CONFIG.cameraDistance; | |
camera.position.y = bikePosition.y + CONFIG.cameraHeight; | |
camera.position.z = | |
bikePosition.z - Math.cos(bikeRotation.y) * CONFIG.cameraDistance; | |
camera.lookAt(bikePosition.x, bikePosition.y + 2, bikePosition.z); | |
} | |
// Handle player input | |
function handleInput(deltaTime) { | |
const player = gameState.playerBike; | |
// Acceleration/Braking | |
if (gameState.keys["w"] || gameState.keys["W"]) { | |
gameState.playerSpeed += CONFIG.playerAcceleration * deltaTime; | |
} | |
if (gameState.keys["s"] || gameState.keys["S"]) { | |
gameState.playerSpeed -= CONFIG.playerBraking * deltaTime; | |
} | |
// Clamp speed | |
gameState.playerSpeed = Math.max( | |
0, | |
Math.min(CONFIG.playerMaxSpeed, gameState.playerSpeed), | |
); | |
// Natural deceleration | |
if (!gameState.keys["w"] && !gameState.keys["W"]) { | |
gameState.playerSpeed *= 0.98; | |
} | |
// Steering with lean effect | |
if ( | |
(gameState.keys["a"] || gameState.keys["A"]) && | |
gameState.playerSpeed > 0 | |
) { | |
player.rotation.y += | |
CONFIG.playerSteering * | |
deltaTime * | |
(gameState.playerSpeed / CONFIG.playerMaxSpeed); | |
player.rotation.z = Math.min(0.3, player.rotation.z + deltaTime * 2); | |
} else if ( | |
(gameState.keys["d"] || gameState.keys["D"]) && | |
gameState.playerSpeed > 0 | |
) { | |
player.rotation.y -= | |
CONFIG.playerSteering * | |
deltaTime * | |
(gameState.playerSpeed / CONFIG.playerMaxSpeed); | |
player.rotation.z = Math.max(-0.3, player.rotation.z - deltaTime * 2); | |
} else { | |
player.rotation.z *= 0.9; | |
} | |
// Apply movement | |
const forward = new THREE.Vector3(0, 0, 1); | |
forward.applyQuaternion(player.quaternion); | |
player.position.add( | |
forward.multiplyScalar(gameState.playerSpeed * deltaTime), | |
); | |
// Keep on track | |
player.position.x = Math.max( | |
-CONFIG.trackWidth / 2 + 2, | |
Math.min(CONFIG.trackWidth / 2 - 2, player.position.x), | |
); | |
player.position.z = Math.max( | |
0, | |
Math.min(CONFIG.trackLength - 10, player.position.z), | |
); | |
// Kicking | |
if (player.userData.kickCooldown > 0) { | |
player.userData.kickCooldown -= deltaTime; | |
} | |
if ( | |
(gameState.keys["q"] || gameState.keys["Q"]) && | |
player.userData.kickCooldown <= 0 | |
) { | |
performKick(player, -1); | |
player.userData.kickCooldown = 0.5; | |
} | |
if ( | |
(gameState.keys["e"] || gameState.keys["E"]) && | |
player.userData.kickCooldown <= 0 | |
) { | |
performKick(player, 1); | |
player.userData.kickCooldown = 0.5; | |
} | |
gameState.playerPosition = player.position.z; | |
} | |
// Perform kick | |
function performKick(attacker, direction) { | |
const attackerPos = attacker.position; | |
const kickRange = 5; | |
gameState.enemyBikes.forEach((enemy) => { | |
const distance = attackerPos.distanceTo(enemy.position); | |
if (distance < kickRange) { | |
// Apply kick force | |
enemy.userData.wobble = 1; | |
enemy.position.x += direction * CONFIG.kickForce; | |
// Add some rotation for effect | |
enemy.rotation.z += direction * 0.3; | |
// Temporary speed reduction | |
enemy.userData.baseSpeed *= 0.8; | |
setTimeout(() => { | |
enemy.userData.baseSpeed /= 0.8; | |
}, 2000); | |
} | |
}); | |
} | |
// Update enemy AI | |
function updateEnemyAI(deltaTime) { | |
gameState.enemyBikes.forEach((enemy, index) => { | |
// Basic forward movement | |
enemy.position.z += enemy.userData.baseSpeed * deltaTime; | |
// Lane changing based on aggressiveness | |
if (Math.random() < enemy.userData.aggressiveness * 0.01) { | |
enemy.userData.lane += Math.random() > 0.5 ? 1 : -1; | |
enemy.userData.lane = Math.max( | |
-2, | |
Math.min(2, enemy.userData.lane), | |
); | |
} | |
// Smooth lane positioning | |
const targetX = enemy.userData.lane * 5; | |
enemy.position.x += (targetX - enemy.position.x) * 0.05; | |
// Wobble recovery | |
if (enemy.userData.wobble > 0) { | |
enemy.userData.wobble -= deltaTime * 2; | |
enemy.rotation.z *= 0.9; | |
} | |
// Keep on track | |
enemy.position.x = Math.max( | |
-CONFIG.trackWidth / 2 + 2, | |
Math.min(CONFIG.trackWidth / 2 - 2, enemy.position.x), | |
); | |
// Wrap around | |
if (enemy.position.z > CONFIG.trackLength - 10) { | |
enemy.position.z = 10; | |
} | |
if (enemy.position.z < 0) { | |
enemy.position.z = CONFIG.trackLength - 20; | |
} | |
// Avoid collisions with other enemies | |
gameState.enemyBikes.forEach((other, otherIndex) => { | |
if (index !== otherIndex) { | |
const dist = enemy.position.distanceTo(other.position); | |
if (dist < 5) { | |
const avoidDir = enemy.position | |
.clone() | |
.sub(other.position) | |
.normalize(); | |
enemy.position.add(avoidDir.multiplyScalar(0.1)); | |
} | |
} | |
}); | |
// Try to catch up to player if behind | |
const playerZ = gameState.playerBike.position.z; | |
if (enemy.position.z < playerZ - 50) { | |
enemy.userData.baseSpeed = CONFIG.enemySpeed * 1.2; | |
} else if (enemy.position.z > playerZ + 50) { | |
enemy.userData.baseSpeed = CONFIG.enemySpeed * 0.9; | |
} | |
}); | |
} | |
// Update environment | |
function updateEnvironment(deltaTime) { | |
// Animate clouds | |
gameState.clouds.forEach((cloud) => { | |
cloud.position.x += deltaTime * 5; | |
if (cloud.position.x > 150) { | |
cloud.position.x = -150; | |
} | |
cloud.rotation.y += deltaTime * 0.1; | |
}); | |
// Animate train | |
if (gameState.train) { | |
gameState.train.position.z += | |
deltaTime * 50 * gameState.trainDirection; | |
if (gameState.train.position.z > CONFIG.trackLength) { | |
gameState.trainDirection = -1; | |
} else if (gameState.train.position.z < -100) { | |
gameState.trainDirection = 1; | |
} | |
// Train sound effect (visual cue) | |
const playerZ = gameState.playerBike.position.z; | |
const trainZ = gameState.train.position.z; | |
if (Math.abs(playerZ - trainZ) < 100) { | |
// Make train more visible when near | |
gameState.train.scale.y = 1 + Math.sin(Date.now() * 0.01) * 0.05; | |
} | |
} | |
} | |
// Update HUD | |
function updateHUD() { | |
document.getElementById("speed").textContent = Math.round( | |
gameState.playerSpeed, | |
); | |
// Calculate position | |
const allBikes = [ | |
{ bike: gameState.playerBike, z: gameState.playerBike.position.z }, | |
...gameState.enemyBikes.map((bike) => ({ bike, z: bike.position.z })), | |
]; | |
allBikes.sort((a, b) => b.z - a.z); | |
const position = | |
allBikes.findIndex((b) => b.bike === gameState.playerBike) + 1; | |
document.getElementById("position").textContent = position; | |
} | |
// Animation loop | |
let lastTime = 0; | |
function animate(currentTime) { | |
requestAnimationFrame(animate); | |
const deltaTime = (currentTime - lastTime) / 1000; | |
lastTime = currentTime; | |
if (deltaTime < 0.1) { | |
// Prevent huge jumps | |
handleInput(deltaTime); | |
updateEnemyAI(deltaTime); | |
updateEnvironment(deltaTime); | |
updateCamera(); | |
updateHUD(); | |
} | |
renderer.render(scene, camera); | |
} | |
// Event listeners | |
window.addEventListener("keydown", (e) => { | |
gameState.keys[e.key] = true; | |
}); | |
window.addEventListener("keyup", (e) => { | |
gameState.keys[e.key] = false; | |
}); | |
window.addEventListener("resize", () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
// Start game | |
init(); | |
animate(0); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment