Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created June 15, 2025 09:57
Show Gist options
  • Select an option

  • Save shricodev/8e107dea81c9111a9828bddb6601b7fe to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/8e107dea81c9111a9828bddb6601b7fe to your computer and use it in GitHub Desktop.
Bike Racing (Developed by Gemini 2.5 Pro Model) - Blog Demo
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Three.js Motorbike Racer</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
}
canvas {
display: block;
}
#hud {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: "Arial", sans-serif;
font-size: 24px;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
text-shadow: 2px 2px 4px #000000;
}
#instructions {
position: absolute;
bottom: 10px;
width: 100%;
text-align: center;
color: white;
font-family: "Arial", sans-serif;
font-size: 18px;
text-shadow: 2px 2px 4px #000000;
}
</style>
</head>
<body>
<div id="hud">
<div>Speed: <span id="speed">0</span> km/h</div>
<div>Position: <span id="position">4</span> / 4</div>
</div>
<div id="instructions">
W/S: Accelerate/Brake | A/D: Steer | Q/E: Kick Left/Right
</div>
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js";
// --- SCENE SETUP ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb); // Sky blue
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;
document.body.appendChild(renderer.domElement);
// --- LIGHTING ---
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(100, 100, 50);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// --- GAME CONSTANTS ---
const ROAD_WIDTH = 20;
const ROAD_LENGTH = 1000;
const NUM_ENEMIES = 3;
const LANE_WIDTH = ROAD_WIDTH / 4;
// --- GAME STATE ---
const keyboard = {};
let player = {
bike: null,
speed: 0,
maxSpeed: 280, // <<< CHANGED: Increased max speed slightly
acceleration: 2.5, // <<< CHANGED: Drastically increased acceleration
braking: 3, // <<< CHANGED: Increased braking power
drag: 0.99, // <<< CHANGED: Reduced drag to allow for higher top speed
steerSpeed: 0.2, // <<< CHANGED: Re-balanced steering for high speed
position: new THREE.Vector3(0, 0, 0), // Not world position, but progress
worldPosition: new THREE.Vector3(0, 0.8, 0),
isKickingLeft: false,
isKickingRight: false,
kickDuration: 150, // ms
kickTimer: 0,
health: 100,
totalDistance: 0,
};
let enemies = [];
let sceneryObjects = [];
let train;
// --- HELPER FUNCTIONS ---
function createMotorbike(color) {
const bike = new THREE.Group();
// Body
const bodyMat = new THREE.MeshStandardMaterial({
color,
metalness: 0.8,
roughness: 0.4,
});
const bodyGeom = new THREE.BoxGeometry(0.5, 0.5, 1.5);
const body = new THREE.Mesh(bodyGeom, bodyMat);
body.position.y = 0.5;
body.castShadow = true;
bike.add(body);
// Wheels
const wheelMat = new THREE.MeshStandardMaterial({
color: 0x111111,
metalness: 0.1,
roughness: 0.8,
});
const wheelGeom = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 24);
const frontWheel = new THREE.Mesh(wheelGeom, wheelMat);
frontWheel.rotation.z = Math.PI / 2;
frontWheel.position.set(0, 0.3, 0.7);
frontWheel.castShadow = true;
bike.add(frontWheel);
const rearWheel = new THREE.Mesh(wheelGeom, wheelMat);
rearWheel.rotation.z = Math.PI / 2;
rearWheel.position.set(0, 0.3, -0.7);
rearWheel.castShadow = true;
bike.add(rearWheel);
// Handlebars
const handleGeom = new THREE.CylinderGeometry(0.05, 0.05, 0.8);
const handlebars = new THREE.Mesh(handleGeom, wheelMat);
handlebars.rotation.y = Math.PI / 2;
handlebars.position.set(0, 0.8, 0.5);
bike.add(handlebars);
// Kick "legs" (invisible, for collision detection)
const kickLegGeom = new THREE.BoxGeometry(0.5, 0.2, 0.2);
const kickLegMat = new THREE.MeshBasicMaterial({ visible: false });
bike.kickLeft = new THREE.Mesh(kickLegGeom, kickLegMat);
bike.kickRight = new THREE.Mesh(kickLegGeom, kickLegMat);
bike.kickLeft.position.set(-0.5, 0.4, 0);
bike.kickRight.position.set(0.5, 0.4, 0);
bike.add(bike.kickLeft, bike.kickRight);
return bike;
}
// --- INITIALIZE GAME ELEMENTS ---
// 1. Player
player.bike = createMotorbike(0xff0000); // Red player bike
player.bike.position.set(0, 0.8, -5); // Start position
scene.add(player.bike);
// 2. Enemies
const enemyColors = [0x0000ff, 0x00ff00, 0xffff00]; // Blue, Green, Yellow
for (let i = 0; i < NUM_ENEMIES; i++) {
const bike = createMotorbike(enemyColors[i % enemyColors.length]);
const lane = (i - 1) * LANE_WIDTH;
bike.position.set(lane, 0.8, -(20 + i * 15));
scene.add(bike);
enemies.push({
bike,
speed: 220 + Math.random() * 50, // <<< CHANGED: Slightly faster enemies for a better challenge
targetX: lane,
steerFactor: 0.01 + Math.random() * 0.01,
wobble: 0,
wobbleSpeed: Math.random() * 0.05,
knockback: new THREE.Vector3(0, 0, 0),
totalDistance: 0,
});
}
// 3. Track & Ground
const groundMat = new THREE.MeshStandardMaterial({ color: 0x556b2f }); // Dark Olive Green
const groundGeom = new THREE.PlaneGeometry(1000, ROAD_LENGTH * 2);
const ground = new THREE.Mesh(groundGeom, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
const roadTexture = createRoadTexture();
const roadMat = new THREE.MeshStandardMaterial({ map: roadTexture });
const roadGeom = new THREE.PlaneGeometry(ROAD_WIDTH, ROAD_LENGTH * 2);
const road = new THREE.Mesh(roadGeom, roadMat);
road.rotation.x = -Math.PI / 2;
road.position.y = 0.01;
road.receiveShadow = true;
scene.add(road);
// Guard Rails
const railMat = new THREE.MeshStandardMaterial({
color: 0x888888,
metalness: 0.9,
roughness: 0.2,
});
const railGeom = new THREE.BoxGeometry(0.2, 0.5, ROAD_LENGTH * 2);
const leftRail = new THREE.Mesh(railGeom, railMat);
const rightRail = new THREE.Mesh(railGeom, railMat);
leftRail.position.set(-ROAD_WIDTH / 2 - 0.2, 0.25, 0);
rightRail.position.set(ROAD_WIDTH / 2 + 0.2, 0.25, 0);
leftRail.castShadow = true;
rightRail.castShadow = true;
scene.add(leftRail, rightRail);
// Track path definition
const path = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 1000),
new THREE.Vector3(0, 0, 500),
new THREE.Vector3(50, 0, 0),
new THREE.Vector3(20, 0, -500),
new THREE.Vector3(-80, 0, -1000),
new THREE.Vector3(-40, 0, -1500),
new THREE.Vector3(0, 0, -2000),
]);
path.curveType = "catmullrom";
path.tension = 0.5;
// 4. Scenery
function createTree() {
const tree = new THREE.Group();
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x8b4513 }); // Brown
const trunkGeom = new THREE.CylinderGeometry(0.2, 0.3, 2);
const trunk = new THREE.Mesh(trunkGeom, trunkMat);
trunk.castShadow = true;
tree.add(trunk);
const leavesMat = new THREE.MeshStandardMaterial({ color: 0x228b22 }); // Forest Green
const leavesGeom = new THREE.IcosahedronGeometry(1.5, 0);
const leaves = new THREE.Mesh(leavesGeom, leavesMat);
leaves.position.y = 2;
leaves.castShadow = true;
tree.add(leaves);
return tree;
}
function createFlower() {
const flower = new THREE.Group();
const stemMat = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const petalMat = new THREE.MeshBasicMaterial({ color: 0xff00ff });
const stemGeom = new THREE.CylinderGeometry(0.02, 0.02, 0.3);
const petalGeom = new THREE.SphereGeometry(0.1);
const stem = new THREE.Mesh(stemGeom, stemMat);
const petal = new THREE.Mesh(petalGeom, petalMat);
stem.position.y = 0.15;
petal.position.y = 0.35;
flower.add(stem, petal);
return flower;
}
for (let i = 0; i < 200; i++) {
const isTree = Math.random() > 0.3;
const obj = isTree ? createTree() : createFlower();
const side = Math.random() > 0.5 ? 1 : -1;
const distFromRoad = ROAD_WIDTH / 2 + 5 + Math.random() * 50;
obj.position.set(
side * distFromRoad,
isTree ? 1 : 0,
(Math.random() - 0.5) * ROAD_LENGTH * 2,
);
if (isTree) obj.rotation.y = Math.random() * Math.PI;
scene.add(obj);
sceneryObjects.push(obj);
}
// Hills
const hillGeom = new THREE.SphereGeometry(150, 32, 16);
const hillMat = new THREE.MeshStandardMaterial({ color: 0x6b8e23 }); // Olive drab
for (let i = 0; i < 10; i++) {
const hill = new THREE.Mesh(hillGeom, hillMat);
const side = Math.random() > 0.5 ? 1 : -1;
hill.position.set(
side * (200 + Math.random() * 300),
-100 + Math.random() * 40,
(Math.random() - 0.5) * ROAD_LENGTH * 2,
);
scene.add(hill);
sceneryObjects.push(hill);
}
// Clouds
const cloudGeom = new THREE.SphereGeometry(20, 16, 12);
const cloudMat = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.8,
});
for (let i = 0; i < 15; i++) {
const cloud = new THREE.Mesh(cloudGeom, cloudMat);
cloud.scale.set(
1 + Math.random(),
0.5 + Math.random() * 0.5,
1 + Math.random(),
);
cloud.position.set(
(Math.random() - 0.5) * 800,
100 + Math.random() * 50,
(Math.random() - 0.5) * ROAD_LENGTH * 2,
);
scene.add(cloud);
sceneryObjects.push(cloud);
}
// Train
train = new THREE.Group();
const trainMat = new THREE.MeshStandardMaterial({
color: 0x444444,
metalness: 1.0,
});
for (let i = 0; i < 5; i++) {
const carGeom = new THREE.BoxGeometry(4, 3, 10);
const car = new THREE.Mesh(carGeom, trainMat);
car.position.z = i * -12;
car.castShadow = true;
train.add(car);
}
train.position.set(ROAD_WIDTH, 1.5, -500);
scene.add(train);
// --- INPUT HANDLING ---
window.addEventListener(
"keydown",
(e) => (keyboard[e.key.toLowerCase()] = true),
);
window.addEventListener(
"keyup",
(e) => (keyboard[e.key.toLowerCase()] = false),
);
// --- GAME LOOP ---
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
// 1. Update Player
// Acceleration/Braking
if (keyboard["w"] || keyboard["arrowup"]) {
player.speed = Math.min(
player.maxSpeed,
player.speed + player.acceleration,
);
}
if (keyboard["s"] || keyboard["arrowdown"]) {
player.speed = Math.max(0, player.speed - player.braking);
}
player.speed *= player.drag; // Apply drag
// The effective speed used for movement calculation now incorporates delta for smoother, frame-rate independent motion
const effectiveSpeed = (player.speed / 60) * delta * 60; // <<< CHANGED: More robust speed calculation
// Steering
const steerAmount = effectiveSpeed * player.steerSpeed * delta; // <<< CHANGED: steering is now frame-rate independent
if (keyboard["a"] || keyboard["arrowleft"]) {
player.bike.position.x -= steerAmount;
player.bike.rotation.z = THREE.MathUtils.lerp(
player.bike.rotation.z,
Math.PI / 10,
0.1,
);
}
if (keyboard["d"] || keyboard["arrowright"]) {
player.bike.position.x += steerAmount;
player.bike.rotation.z = THREE.MathUtils.lerp(
player.bike.rotation.z,
-Math.PI / 10,
0.1,
);
}
// Auto-straighten steering
player.bike.rotation.z = THREE.MathUtils.lerp(
player.bike.rotation.z,
0,
0.1,
);
// Clamp player position to road
const roadLimit = ROAD_WIDTH / 2 - 0.5;
player.bike.position.x = THREE.MathUtils.clamp(
player.bike.position.x,
-roadLimit,
roadLimit,
);
// Kicking Logic
if (keyboard["q"] && !player.isKickingRight && !player.isKickingLeft) {
player.isKickingLeft = true;
player.kickTimer = Date.now();
}
if (keyboard["e"] && !player.isKickingLeft && !player.isKickingRight) {
player.isKickingRight = true;
player.kickTimer = Date.now();
}
if (player.isKickingLeft || player.isKickingRight) {
if (Date.now() - player.kickTimer > player.kickDuration) {
player.isKickingLeft = false;
player.isKickingRight = false;
}
}
// 2. Update World (Simulate Forward Motion)
const moveDistance = player.speed / 1000;
player.totalDistance += moveDistance;
// The world moves towards the player, not the other way around
const pathProgress = (player.totalDistance % 2000) / 2000;
const currentPoint = path.getPointAt(pathProgress);
const nextPoint = path.getPointAt((pathProgress + 0.01) % 1);
const pathAngle = Math.atan2(
nextPoint.x - currentPoint.x,
nextPoint.z - currentPoint.z,
);
// Move scenery
[...sceneryObjects, road, ground, leftRail, rightRail, train].forEach(
(obj) => {
obj.position.z += moveDistance;
// Reposition objects when they go behind camera
if (obj.position.z > camera.position.z + 100) {
obj.position.z -= ROAD_LENGTH * 2;
}
},
);
// Update road to follow path
road.position.x = -currentPoint.x;
ground.position.x = -currentPoint.x;
leftRail.position.x = -currentPoint.x - ROAD_WIDTH / 2 - 0.2;
rightRail.position.x = -currentPoint.x + ROAD_WIDTH / 2 + 0.2;
// Update player's world position for camera and collision
player.worldPosition.x = player.bike.position.x;
player.worldPosition.y = player.bike.position.y;
player.worldPosition.z = player.bike.position.z;
// 3. Update Enemies
enemies.forEach((enemy) => {
const enemyMoveDistance = enemy.speed / 1000;
enemy.totalDistance += enemyMoveDistance;
// AI Steering
enemy.wobble += enemy.wobbleSpeed;
const wobbleOffset = Math.sin(enemy.wobble) * LANE_WIDTH * 0.5;
const desiredX = enemy.targetX + wobbleOffset;
enemy.bike.position.x = THREE.MathUtils.lerp(
enemy.bike.position.x,
desiredX,
enemy.steerFactor,
);
// Apply kick knockback
enemy.bike.position.add(enemy.knockback);
enemy.knockback.multiplyScalar(0.9); // friction for knockback
// Keep enemies on the road
enemy.bike.position.x = THREE.MathUtils.clamp(
enemy.bike.position.x,
-roadLimit,
roadLimit,
);
// Update Z position relative to player
enemy.bike.position.z += moveDistance - enemyMoveDistance;
// Change lanes occasionally
if (Math.random() < 0.001) {
enemy.targetX = (Math.floor(Math.random() * 3) - 1) * LANE_WIDTH;
}
// Collision check for kicking
const distanceToPlayer = player.bike.position.distanceTo(
enemy.bike.position,
);
if (distanceToPlayer < 2.5) {
if (
player.isKickingLeft &&
player.bike.position.x > enemy.bike.position.x
) {
enemy.knockback.x -= 0.15; // <<< CHANGED: Stronger kick
player.isKickingLeft = false; // one kick per press
}
if (
player.isKickingRight &&
player.bike.position.x < enemy.bike.position.x
) {
enemy.knockback.x += 0.15; // <<< CHANGED: Stronger kick
player.isKickingRight = false;
}
}
});
// Train movement
train.position.z += moveDistance + 0.2; // slightly faster than max speed
if (train.position.z > 200) {
train.position.z = -ROAD_LENGTH - Math.random() * 1000;
train.position.x =
(ROAD_WIDTH / 2 + 10 + Math.random() * 10) *
(Math.random() > 0.5 ? 1 : -1);
}
// 4. Update Camera
const cameraOffset = new THREE.Vector3(0, 5, 10);
const targetPosition = player.bike.position.clone().add(cameraOffset);
camera.position.lerp(targetPosition, 0.1);
camera.lookAt(
player.bike.position.x,
player.bike.position.y + 1,
player.bike.position.z,
);
camera.rotation.z = -player.bike.rotation.z * 0.2; // Camera tilt
// 5. Update HUD
document.getElementById("speed").textContent = Math.round(player.speed);
const racers = [player, ...enemies];
racers.sort((a, b) => b.totalDistance - a.totalDistance);
const playerRank = racers.findIndex((r) => r === player) + 1;
document.getElementById("position").textContent =
`${playerRank} / ${racers.length}`;
// 6. Render
renderer.render(scene, camera);
}
// --- UTILITY FUNCTIONS ---
function createRoadTexture() {
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const ctx = canvas.getContext("2d");
// Dark gray road
ctx.fillStyle = "#444";
ctx.fillRect(0, 0, 128, 128);
// White dashed lines
ctx.fillStyle = "#fff";
ctx.strokeStyle = "#fff";
ctx.lineWidth = 3;
ctx.setLineDash([20, 15]);
// Center line
ctx.beginPath();
ctx.moveTo(64, 0);
ctx.lineTo(64, 128);
ctx.stroke();
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(1, ROAD_LENGTH / 4); // Repeat texture along the road
return texture;
}
// --- RESIZE HANDLER ---
window.addEventListener(
"resize",
() => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
},
false,
);
// --- START GAME ---
animate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment