Tech Details

City Carnage - Day 14

Three.js + Physics

Table of Contents

01. Three.js Scene Setup

The game uses Three.js to create an immersive 3D city environment. We set up the scene with proper lighting, shadows, and fog for atmosphere.

Basic Scene Structure

// Create the 3D scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a1a2e); scene.fog = new THREE.Fog(0x1a1a2e, 50, 150); // Perspective camera for 3D view camera = new THREE.PerspectiveCamera( 60, // Field of view 1, // Aspect ratio (updated on resize) 0.1, // Near clipping plane 500 // Far clipping plane ); // WebGL renderer with shadows renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;

Lighting Setup

We use multiple light sources to create a realistic city atmosphere:

// Ambient light for base illumination const ambient = new THREE.AmbientLight(0x404060, 0.6); scene.add(ambient); // Directional light (sun) with shadows const sun = new THREE.DirectionalLight(0xffffcc, 0.8); sun.position.set(50, 100, 50); sun.castShadow = true; // Shadow map configuration for quality sun.shadow.mapSize.width = 2048; sun.shadow.mapSize.height = 2048; sun.shadow.camera.near = 10; sun.shadow.camera.far = 300; // Point lights for street lamps for (let i = 0; i < 20; i++) { const light = new THREE.PointLight(0xffaa44, 0.5, 30); light.position.set( (Math.random() - 0.5) * CITY_SIZE, 8, (Math.random() - 0.5) * CITY_SIZE ); scene.add(light); }
Performance Tip

Fog not only adds atmosphere but also improves performance by hiding distant objects that would otherwise be rendered.

02. Car Physics System

The car uses a simplified physics model that feels responsive while remaining easy to control. We track position, rotation, and velocity.

Car State Object

const carState = { x: 0, // World X position z: 0, // World Z position rotation: 0, // Y-axis rotation (radians) speed: 0, // Current speed maxSpeed: 1.5, // Maximum forward speed acceleration: 0.03, deceleration: 0.01, turnSpeed: 0.04, friction: 0.98 // Speed decay when not accelerating };

Physics Update Loop

function updateCar() { // Acceleration input if (keys.up) { carState.speed += carState.acceleration; } if (keys.down) { carState.speed -= carState.acceleration; } // Clamp speed (reverse is slower) carState.speed = Math.max( -carState.maxSpeed / 2, Math.min(carState.maxSpeed, carState.speed) ); // Apply friction when coasting if (!keys.up && !keys.down) { carState.speed *= carState.friction; } // Steering (only effective when moving) if (Math.abs(carState.speed) > 0.01) { const turnFactor = carState.speed > 0 ? 1 : -1; if (keys.left) carState.rotation += carState.turnSpeed * turnFactor; if (keys.right) carState.rotation -= carState.turnSpeed * turnFactor; } // Update position based on speed and rotation carState.x += Math.sin(carState.rotation) * carState.speed; carState.z += Math.cos(carState.rotation) * carState.speed; // Apply to 3D mesh car.position.x = carState.x; car.position.z = carState.z; car.rotation.y = carState.rotation; }

Car Movement Vector

Speed * sin(rotation) = X velocity

03. Procedural City Generation

The city is generated procedurally using a grid-based approach. Roads form a grid pattern, and buildings fill the blocks between them.

City Parameters

const CITY_SIZE = 200; // Total city size const BLOCK_SIZE = 30; // Size of each city block const ROAD_WIDTH = 10; // Width of roads

Road Grid Generation

function createRoads() { const roadMat = new THREE.MeshLambertMaterial({ color: 0x333344 }); // Create grid of roads for (let x = -CITY_SIZE/2; x <= CITY_SIZE/2; x += BLOCK_SIZE + ROAD_WIDTH) { // Vertical roads const roadV = new THREE.Mesh( new THREE.PlaneGeometry(ROAD_WIDTH, CITY_SIZE), roadMat ); roadV.rotation.x = -Math.PI / 2; roadV.position.set(x, 0.01, 0); scene.add(roadV); // Add center line markings const lineMat = new THREE.MeshBasicMaterial({ color: 0xffff44 }); const line = new THREE.Mesh( new THREE.PlaneGeometry(0.3, CITY_SIZE), lineMat ); line.rotation.x = -Math.PI / 2; line.position.set(x, 0.02, 0); scene.add(line); } }

Building Generation

function createBuildings() { const colors = [0x445566, 0x556677, 0x667788]; for (let x = -CITY_SIZE/2 + ROAD_WIDTH; x < CITY_SIZE/2; x += BLOCK_SIZE + ROAD_WIDTH) { for (let z = -CITY_SIZE/2 + ROAD_WIDTH; z < CITY_SIZE/2; z += BLOCK_SIZE + ROAD_WIDTH) { // Multiple buildings per block const numBuildings = 2 + Math.floor(Math.random() * 3); for (let i = 0; i < numBuildings; i++) { const height = 10 + Math.random() * 40; const width = 5 + Math.random() * 10; const depth = 5 + Math.random() * 10; const building = new THREE.Mesh( new THREE.BoxGeometry(width, height, depth), new THREE.MeshLambertMaterial({ color: colors[Math.floor(Math.random() * colors.length)] }) ); // Random position within block building.position.set( x + (Math.random() - 0.5) * (BLOCK_SIZE - width), height / 2, z + (Math.random() - 0.5) * (BLOCK_SIZE - depth) ); building.castShadow = true; scene.add(building); } } } }
Design Pattern

By storing building dimensions in userData, we can later use them for precise collision detection without recalculating.

04. Pedestrian AI & Animation

Pedestrians are simple 3D humanoid figures with autonomous movement and walking animations.

Pedestrian Creation

function createPedestrian(x, z) { const ped = new THREE.Group(); // Random appearance const skinColors = [0xf5d0c5, 0xd4a574, 0x8b5a3c]; const shirtColors = [0xff6b6b, 0x4ecdc4, 0xffe66d]; // Body parts const body = new THREE.Mesh( new THREE.BoxGeometry(0.6, 1, 0.4), new THREE.MeshLambertMaterial({ color: shirtColor }) ); body.position.y = 1.2; ped.add(body); const head = new THREE.Mesh( new THREE.SphereGeometry(0.25, 8, 8), new THREE.MeshLambertMaterial({ color: skinColor }) ); head.position.y = 2; ped.add(head); // Legs for animation const legL = new THREE.Mesh(legGeo, legMat); legL.position.set(-0.15, 0.35, 0); ped.add(legL); // Store movement data const angle = Math.random() * Math.PI * 2; ped.userData = { vx: Math.cos(angle) * 0.02, vz: Math.sin(angle) * 0.02, walkPhase: Math.random() * Math.PI * 2, hit: false }; return ped; }

Walking Animation

function updatePedestrians() { pedestrians.forEach(ped => { if (ped.userData.hit) return; // Move pedestrian ped.position.x += ped.userData.vx; ped.position.z += ped.userData.vz; // Walking animation using sine wave ped.userData.walkPhase += 0.1; const legL = ped.children[3]; const legR = ped.children[4]; // Opposite leg movement creates walking motion legL.rotation.x = Math.sin(ped.userData.walkPhase) * 0.3; legR.rotation.x = -Math.sin(ped.userData.walkPhase) * 0.3; // Face movement direction ped.rotation.y = Math.atan2(ped.userData.vx, ped.userData.vz); // Random direction changes if (Math.random() < 0.005) { const angle = Math.random() * Math.PI * 2; ped.userData.vx = Math.cos(angle) * 0.02; ped.userData.vz = Math.sin(angle) * 0.02; } }); }

05. Collision Detection

Collision detection uses transformed bounding boxes to accurately detect hits even when the car is rotated.

Rotated Bounding Box Collision

function checkCollisions() { // Transform pedestrian position to car's local space const cos = Math.cos(carState.rotation); const sin = Math.sin(carState.rotation); pedestrians.forEach(ped => { if (ped.userData.hit) return; // Distance from car center const dx = ped.position.x - carState.x; const dz = ped.position.z - carState.z; // Transform to car's local coordinate space const localX = dx * cos + dz * sin; const localZ = -dx * sin + dz * cos; // Check against car dimensions in local space if (Math.abs(localX) < 1.5 && Math.abs(localZ) < 2.5) { // Collision detected! ped.userData.hit = true; // Score based on speed const points = 100 + Math.floor(Math.abs(carState.speed) * 100); score += points; } }); }

Coordinate Transformation

World Coords -> Rotation Matrix -> Local Coords

Ragdoll Hit Animation

function animateHit(ped, forceX, forceZ) { let vy = 0.3; // Initial upward velocity let vx = forceX + (Math.random() - 0.5) * 0.2; let vz = forceZ + (Math.random() - 0.5) * 0.2; let rotX = (Math.random() - 0.5) * 0.3; let rotZ = (Math.random() - 0.5) * 0.3; function animFrame() { ped.position.x += vx; ped.position.y += vy; ped.position.z += vz; ped.rotation.x += rotX; ped.rotation.z += rotZ; vy -= 0.02; // Gravity if (ped.position.y > 0) { requestAnimationFrame(animFrame); } else { ped.position.y = 0; // Remove after delay, respawn new pedestrian } } animFrame(); }
Physics Note

The force direction uses the car's rotation to push pedestrians in the direction of travel, creating realistic impact physics.

06. Camera Following System

The camera follows behind the car using smooth interpolation, creating a cinematic chase-cam feel.

Third-Person Camera

// Camera follows behind the car const camDist = 20; // Distance behind car const camHeight = 12; // Height above ground // Calculate camera position based on car rotation camera.position.x = carState.x - Math.sin(carState.rotation) * camDist; camera.position.z = carState.z - Math.cos(carState.rotation) * camDist; camera.position.y = camHeight; // Always look at the car camera.lookAt(carState.x, 2, carState.z);

Camera Math Explained

The camera position is calculated using trigonometry to place it behind the car regardless of which direction the car is facing:

Camera Position Calculation

Car Position - Forward * Distance = Camera Position
Enhancement Idea

For smoother camera movement, you could add lerp (linear interpolation) to gradually move the camera to its target position instead of snapping directly.