-
-
Save shricodev/64a26759fb01eb080f898f91acfe7d45 to your computer and use it in GitHub Desktop.
Bike Racing (Developed by OpenAI o3 Pro 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" /> | |
<title>Three.js – Low-Poly Motorbike Road-Rash</title> | |
<style> | |
/* ... your styles ... */ | |
html, | |
body { | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
background: #87ceeb; | |
font-family: Arial; | |
} | |
#hud { | |
position: fixed; | |
top: 10px; | |
left: 10px; | |
color: #fff; | |
font-weight: bold; | |
text-shadow: 0 0 5px #000; | |
font-size: 15px; | |
line-height: 22px; | |
} | |
#hud span { | |
display: block; | |
} | |
</style> | |
</head> | |
<body> | |
<!-- ------------ IMPORT MAP (THE FIX) ---------------- --> | |
<!-- This must come before your module script --> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"three": "https://unpkg.com/[email protected]/build/three.module.js", | |
"three/examples/jsm/": "https://unpkg.com/[email protected]/examples/jsm/" | |
} | |
} | |
</script> | |
<div id="hud"> | |
<span id="speed">Speed: 0 km/h</span> | |
<span id="rank">Position: 1/4</span> | |
</div> | |
<!-- ------------ GAME CODE ---------------- --> | |
<script type="module"> | |
// Now you can use the "bare" specifiers you defined in the import map | |
import * as THREE from "three"; | |
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; | |
/* === GLOBAL CONSTANTS ================================================== */ | |
const MAX_SPEED = 160; // km/h | |
// ... the rest of your game code is unchanged and will now work ... | |
const ACCEL = 60; // km/h per sec | |
const BRAKE = 140; | |
const TURN_RATE = 1.8; // rad/s at full steer | |
const KICK_FORCE = 6; // m/s sideways impulse | |
const TRACK_LEN = 5000; // metres (wraps) | |
const TRACK_WIDTH = 10; | |
/* === CORE THREE OBJECTS =============================================== */ | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera( | |
60, | |
innerWidth / innerHeight, | |
0.1, | |
1000, | |
); | |
const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(innerWidth, innerHeight); | |
document.body.appendChild(renderer.domElement); | |
/* === LIGHTS ============================================================ */ | |
const hemi = new THREE.HemisphereLight(0xffffff, 0x444455, 0.9); | |
scene.add(hemi); | |
const dir = new THREE.DirectionalLight(0xffffff, 0.7); | |
dir.position.set(50, 100, -50); | |
dir.castShadow = false; | |
scene.add(dir); | |
/* === TRACK ============================================================== | |
We build one very long plane that we curve with a CatmullRom spline. | |
To keep things performant & “endless”, we teleport the player back to | |
z = 0 when he passes TRACK_LEN and do the same for every decoration. */ | |
const trackGroup = new THREE.Group(); | |
scene.add(trackGroup); | |
const trackPathPts = []; | |
let z = 0, | |
x = 0; | |
for (let i = 0; i <= TRACK_LEN; i += 200) { | |
x += (Math.random() - 0.5) * 40; // gentle curves | |
trackPathPts.push(new THREE.Vector3(x, 0, -i)); | |
} | |
const trackCurve = new THREE.CatmullRomCurve3(trackPathPts); | |
const shape = new THREE.Shape(); | |
shape.moveTo(-TRACK_WIDTH / 2, 0); | |
shape.lineTo(TRACK_WIDTH / 2, 0); | |
const extrude = new THREE.ExtrudeGeometry(shape, { | |
steps: trackPathPts.length * 2, | |
extrudePath: trackCurve, | |
}); | |
extrude.rotateX(-Math.PI / 2); | |
const matRoad = new THREE.MeshStandardMaterial({ | |
color: 0x303030, | |
roughness: 0.8, | |
}); | |
const road = new THREE.Mesh(extrude, matRoad); | |
road.receiveShadow = false; | |
trackGroup.add(road); | |
/* lane markers */ | |
const dashMat = new THREE.MeshBasicMaterial({ color: 0xffff00 }); | |
for (let i = 0; i < TRACK_LEN; i += 30) { | |
const g = new THREE.BoxGeometry(0.2, 0.1, 5); | |
const m = new THREE.Mesh(g, dashMat); | |
m.position.set(0, 0.01, -i - 2.5); | |
trackGroup.add(m); | |
} | |
/* guard rails */ | |
const railMat = new THREE.MeshBasicMaterial({ color: 0xffffff }); | |
for (let side = -1; side <= 1; side += 2) { | |
for (let i = 0; i < TRACK_LEN; i += 5) { | |
const seg = new THREE.BoxGeometry(0.3, 0.3, 4); | |
const rail = new THREE.Mesh(seg, railMat); | |
rail.position.set(side * (TRACK_WIDTH / 2 + 0.7), 0.2, -i - 2); | |
trackGroup.add(rail); | |
} | |
} | |
/* === SIMPLE LOW-POLY MODELS =========================================== */ | |
function makeBike(color = 0xff0000) { | |
const bike = new THREE.Group(); | |
const body = new THREE.Mesh( | |
new THREE.BoxGeometry(1.2, 0.4, 2.5), | |
new THREE.MeshStandardMaterial({ color }), | |
); | |
body.position.y = 0.6; | |
bike.add(body); | |
const wheelGeo = new THREE.CylinderGeometry(0.45, 0.45, 0.4, 12); | |
wheelGeo.rotateZ(Math.PI / 2); | |
const wheelMat = new THREE.MeshStandardMaterial({ color: 0x111111 }); | |
const w1 = new THREE.Mesh(wheelGeo, wheelMat); | |
const w2 = w1.clone(); | |
w1.position.set(0, -0.05, 1); | |
w2.position.set(0, -0.05, -1); | |
bike.add(w1, w2); | |
return bike; | |
} | |
/* === PLAYER OBJECT ===================================================== */ | |
class Player { | |
constructor() { | |
this.mesh = makeBike(0x00ccff); | |
scene.add(this.mesh); | |
this.speed = 0; // km/h | |
this.laneX = 0; // metres from centre | |
this.kickCooldown = 0; | |
} | |
update(dt, keys) { | |
/* acceleration / braking */ | |
if (keys["w"] || keys["arrowup"]) this.speed += ACCEL * dt; | |
else if (keys["s"] || keys["arrowdown"]) | |
this.speed -= BRAKE * dt * 0.5; | |
else this.speed -= ACCEL * 0.7 * dt; // rolling drag | |
this.speed = THREE.MathUtils.clamp(this.speed, 0, MAX_SPEED); | |
/* steering */ | |
let steer = 0; | |
if (keys["a"] || keys["arrowleft"]) steer += 1; | |
if (keys["d"] || keys["arrowright"]) steer -= 1; | |
const dx = (steer * TURN_RATE * dt * this.speed) / 100; // subtle at low speed | |
this.laneX = THREE.MathUtils.clamp( | |
this.laneX + dx, | |
-TRACK_WIDTH / 2 + 1, | |
TRACK_WIDTH / 2 - 1, | |
); | |
this.mesh.position.x = this.laneX; | |
/* forward motion – we leave the player mesh near z=0 and move world */ | |
const dz = ((this.speed * 1000) / 3600) * dt; // metres this frame | |
trackGroup.position.z += dz; | |
scenery.forEach((o) => (o.position.z += dz)); // move decor | |
enemies.forEach((e) => (e.mesh.position.z += dz)); // enemies too | |
/* keep everything inside numeric range */ | |
if (trackGroup.position.z > 200) { | |
trackGroup.position.z -= 200; | |
scenery.forEach((o) => (o.position.z -= 200)); | |
enemies.forEach((e) => (e.mesh.position.z -= 200)); | |
} | |
/* leaning animation */ | |
this.mesh.rotation.z = -steer * 0.4; | |
this.mesh.rotation.x = Math.sin(Date.now() * 0.02) * 0.02; | |
/* kick */ | |
this.kickCooldown -= dt; | |
if (this.kickCooldown < 0) { | |
if (keys["q"]) this.doKick(-1); | |
if (keys["e"]) this.doKick(1); | |
} | |
} | |
doKick(dir) { | |
const range = 2.2; | |
enemies.forEach((e) => { | |
const rel = e.mesh.position.clone().sub(this.mesh.position); | |
if ( | |
Math.abs(rel.z) < 1.5 && | |
dir * rel.x > 0 && | |
Math.abs(rel.x) < range | |
) { | |
e.sideVel += dir * KICK_FORCE; | |
e.stunned = 0.7; | |
} | |
}); | |
this.kickCooldown = 0.6; | |
} | |
} | |
/* === ENEMY AI ========================================================== */ | |
class Enemy { | |
constructor(color) { | |
this.mesh = makeBike(color); | |
this.pathPos = Math.random() * 150 + 30; // metres ahead | |
this.laneOffset = (Math.random() * 2 - 1) * (TRACK_WIDTH / 2 - 1.5); | |
this.speed = 120 + Math.random() * 15; // km/h | |
this.sideVel = 0; | |
this.stunned = 0; | |
scene.add(this.mesh); | |
} | |
update(dt) { | |
/* progress forward */ | |
const dz = ((this.speed * 1000) / 3600) * dt; | |
this.pathPos -= dz; // remember: world moves opposite | |
/* convert pathPos to world Z (negative) */ | |
this.mesh.position.z = -this.pathPos; | |
/* lane following / recovery */ | |
if (this.stunned > 0) { | |
this.stunned -= dt; | |
} else { | |
const diff = this.laneOffset - this.mesh.position.x; | |
this.sideVel += diff * dt * 1.5; | |
} | |
this.sideVel *= 0.9; | |
this.mesh.position.x += this.sideVel * dt; | |
/* lean */ | |
this.mesh.rotation.z = -this.sideVel * 0.4; | |
/* recycle when passes player */ | |
if (this.mesh.position.z > 5) { | |
this.pathPos += 400 + Math.random() * 100; | |
this.mesh.position.z = -this.pathPos; | |
this.laneOffset = (Math.random() * 2 - 1) * (TRACK_WIDTH / 2 - 1.5); | |
} | |
} | |
} | |
/* === DECOR / SCENERY =================================================== */ | |
const scenery = []; | |
function spawnTree(x, z) { | |
const g = new THREE.ConeGeometry(1.2, 3, 8); | |
const m = new THREE.MeshStandardMaterial({ color: 0x118833 }); | |
const tree = new THREE.Mesh(g, m); | |
tree.position.set(x, 1.5, z); | |
scene.add(tree); | |
scenery.push(tree); | |
} | |
function spawnFlower(x, z) { | |
const petal = new THREE.CircleGeometry(0.15, 6); | |
const m = new THREE.MeshBasicMaterial({ color: 0xff55ff }); | |
const f = new THREE.Mesh(petal, m); | |
f.rotation.x = -Math.PI / 2; | |
f.position.set(x, 0.05, z); | |
scene.add(f); | |
scenery.push(f); | |
} | |
function spawnCloud(z) { | |
const cloud = new THREE.Group(); | |
const geo = new THREE.SphereGeometry(1, 8, 6); | |
const mat = new THREE.MeshStandardMaterial({ | |
color: 0xffffff, | |
roughness: 0.9, | |
}); | |
const cnt = 3 + Math.floor(Math.random() * 3); | |
for (let i = 0; i < cnt; i++) { | |
const s = 0.8 + Math.random() * 0.6; | |
const puff = new THREE.Mesh(geo, mat); | |
puff.scale.set(s, s, s); | |
puff.position.set( | |
(Math.random() - 0.5) * 3, | |
Math.random() * 1, | |
(Math.random() - 0.5) * 3, | |
); | |
cloud.add(puff); | |
} | |
cloud.position.set( | |
(Math.random() * 2 - 1) * 50, | |
15 + Math.random() * 5, | |
z, | |
); | |
scene.add(cloud); | |
cloud.userData.speed = 2 + Math.random() * 1.5; | |
scenery.push(cloud); | |
} | |
function makeHill(x, z) { | |
const geo = new THREE.CylinderGeometry( | |
0, | |
8 + Math.random() * 6, | |
4, | |
8, | |
1, | |
); | |
const mat = new THREE.MeshStandardMaterial({ | |
color: 0x228822, | |
flatShading: true, | |
}); | |
const hill = new THREE.Mesh(geo, mat); | |
hill.position.set(x, 2, z); | |
scene.add(hill); | |
scenery.push(hill); | |
} | |
/* ---- trees / flowers / hills along first km -------------------------- */ | |
for (let i = 0; i < TRACK_LEN; i += 20) { | |
if (Math.random() < 0.5) | |
spawnTree(-TRACK_WIDTH / 2 - 2 - Math.random() * 4, -i); | |
if (Math.random() < 0.5) | |
spawnTree(TRACK_WIDTH / 2 + 2 + Math.random() * 4, -i); | |
for (let j = -1; j <= 1; j += 2) { | |
if (Math.random() < 0.3) | |
spawnFlower( | |
j * (TRACK_WIDTH / 2 + 1 + Math.random() * 2), | |
-i - 3 + Math.random() * 6, | |
); | |
} | |
if (i % 400 === 0) | |
makeHill((Math.random() < 0.5 ? -1 : 1) * 60, -i - 50); | |
} | |
for (let i = 0; i < 20; i++) { | |
spawnCloud(-i * 200); | |
} | |
/* === TRAIN ============================================================= */ | |
function makeTrain() { | |
const group = new THREE.Group(); | |
const mat = new THREE.MeshStandardMaterial({ color: 0xffaa00 }); | |
for (let i = 0; i < 4; i++) { | |
const c = new THREE.Mesh(new THREE.BoxGeometry(4, 2, 8), mat); | |
c.position.z = -i * 8 - 2; | |
group.add(c); | |
} | |
group.position.set(-TRACK_WIDTH / 2 - 8, 1, -200); | |
group.userData = { speed: 35 }; | |
scene.add(group); | |
scenery.push(group); | |
} | |
makeTrain(); | |
/* === INPUT HANDLING ==================================================== */ | |
const keys = {}; | |
addEventListener("keydown", (e) => (keys[e.key.toLowerCase()] = true)); | |
addEventListener("keyup", (e) => (keys[e.key.toLowerCase()] = false)); | |
/* === CAMERA ============================================================ */ | |
camera.position.set(0, 3, 4); | |
camera.lookAt(0, 0, -10); | |
/* === OPTIONAL ORBIT DEBUG ============================================= */ | |
// This part would have also caused the error if it was uncommented. | |
// Now, with the import map, it would work perfectly. | |
/* | |
const ctrl = new OrbitControls(camera, renderer.domElement); | |
ctrl.target.set(0,1,-10); | |
ctrl.update(); // It's good practice to call update() once after setting target | |
*/ | |
/* === INSTANCES ========================================================= */ | |
const player = new Player(); | |
const enemies = [ | |
new Enemy(0xff4444), | |
new Enemy(0x44ff44), | |
new Enemy(0x4444ff), | |
]; | |
/* === RANKING =========================================================== */ | |
function computeRank() { | |
let ahead = 0; | |
enemies.forEach((e) => { | |
if (e.mesh.position.z < player.mesh.position.z) ahead++; | |
}); | |
return ahead + 1; // player counted | |
} | |
/* === RESIZE ============================================================ */ | |
addEventListener("resize", () => { | |
camera.aspect = innerWidth / innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(innerWidth, innerHeight); | |
}); | |
/* === MAIN LOOP ========================================================= */ | |
let last = performance.now(); | |
function loop(now) { | |
const dt = Math.min(0.05, (now - last) * 0.001); | |
last = now; | |
player.update(dt, keys); | |
enemies.forEach((e) => e.update(dt)); | |
/* clouds & train movement */ | |
scenery.forEach((o) => { | |
if (o.userData.speed) { | |
o.position.z += o.userData.speed * dt; | |
if (o.position.z > 20) o.position.z -= 400; | |
} | |
}); | |
/* HUD */ | |
document.getElementById("speed").textContent = | |
`Speed: ${player.speed.toFixed(0)} km/h`; | |
document.getElementById("rank").textContent = | |
`Position: ${computeRank()}/4`; | |
renderer.render(scene, camera); | |
requestAnimationFrame(loop); | |
} | |
requestAnimationFrame(loop); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment