-
-
Save shricodev/8e107dea81c9111a9828bddb6601b7fe to your computer and use it in GitHub Desktop.
Bike Racing (Developed by Gemini 2.5 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" /> | |
| <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