Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created June 15, 2025 09:55
Show Gist options
  • Save shricodev/64a26759fb01eb080f898f91acfe7d45 to your computer and use it in GitHub Desktop.
Save shricodev/64a26759fb01eb080f898f91acfe7d45 to your computer and use it in GitHub Desktop.
Bike Racing (Developed by OpenAI o3 Pro Model) - Blog Demo
<!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