TECH DETAILS

GTA Tunis - Day 6

Advanced Level

Table of Contents

01 / Open World & Camera

The world is larger than the viewport. The camera follows the player, showing only a portion of the world.

// World dimensions (larger than canvas) const worldWidth = 2000; const worldHeight = 2000; // Camera tracks player position const camera = { x: 0, y: 0 }; function updateCamera() { // Center camera on player camera.x = player.x - canvas.width / 2; camera.y = player.y - canvas.height / 2; // Clamp to world bounds camera.x = Math.max(0, Math.min(worldWidth - canvas.width, camera.x)); camera.y = Math.max(0, Math.min(worldHeight - canvas.height, camera.y)); } // Convert world coords to screen coords for drawing function worldToScreen(worldX, worldY) { return { x: worldX - camera.x, y: worldY - camera.y }; } // Only draw objects visible on screen (optimization) function isVisible(obj) { return obj.x + obj.width > camera.x && obj.x < camera.x + canvas.width && obj.y + obj.height > camera.y && obj.y < camera.y + canvas.height; }

02 / Car Physics

Cars have acceleration, deceleration, and steering that depends on speed. No physics library needed.

const car = { x: 500, y: 500, angle: 0, // Facing direction (radians) speed: 0, maxSpeed: 8, acceleration: 0.2, friction: 0.98 // Speed multiplier when not accelerating }; function updateCar() { // Acceleration / braking if (keys.up) { car.speed = Math.min(car.maxSpeed, car.speed + car.acceleration); } else if (keys.down) { car.speed = Math.max(-car.maxSpeed / 2, car.speed - car.acceleration); } else { // Apply friction when not accelerating car.speed *= car.friction; if (Math.abs(car.speed) < 0.1) car.speed = 0; } // Steering - turn rate depends on speed const turnRate = 0.05 * (car.speed / car.maxSpeed); if (keys.left) car.angle -= turnRate; if (keys.right) car.angle += turnRate; // Apply velocity based on angle car.x += Math.cos(car.angle) * car.speed; car.y += Math.sin(car.angle) * car.speed; }
Speed-Based Steering

Steering that depends on speed (slower = tighter turns) feels more realistic than constant turn rate.

03 / Procedural City Generation

The city is generated programmatically with a grid of roads and randomly placed buildings.

function generateCity() { const blockSize = 300; const roadWidth = 60; // Generate grid of roads for (let y = blockSize; y < worldHeight; y += blockSize) { roads.push({ x: 0, y: y - roadWidth/2, width: worldWidth, height: roadWidth, type: 'horizontal' }); } for (let x = blockSize; x < worldWidth; x += blockSize) { roads.push({ x: x - roadWidth/2, y: 0, width: roadWidth, height: worldHeight, type: 'vertical' }); } // Generate buildings in blocks (between roads) for (let bx = 50; bx < worldWidth - blockSize; bx += blockSize) { for (let by = 50; by < worldHeight - blockSize; by += blockSize) { const numBuildings = 2 + Math.floor(Math.random() * 3); for (let i = 0; i < numBuildings; i++) { buildings.push({ x: bx + Math.random() * (blockSize - 150), y: by + Math.random() * (blockSize - 150), width: 60 + Math.random() * 80, height: 60 + Math.random() * 80, color: ['#e8dcc4', '#f5f0e1', '#87ceeb'][Math.floor(Math.random() * 3)], hasDome: Math.random() > 0.7 // Tunisian architecture }); } } } }

04 / Police AI Chase

Police spawn when wanted level increases and chase the player using simple seek behavior.

function updatePolice() { // Spawn police based on wanted level if (police.length < wantedLevel * 2) { // Spawn off-screen, near player const angle = Math.random() * Math.PI * 2; const dist = 400; police.push({ x: player.x + Math.cos(angle) * dist, y: player.y + Math.sin(angle) * dist, speed: 2.5 }); } // Chase player for (const cop of police) { // Calculate angle to player const dx = player.x - cop.x; const dy = player.y - cop.y; cop.angle = Math.atan2(dy, dx); // Move toward player cop.x += Math.cos(cop.angle) * cop.speed; cop.y += Math.sin(cop.angle) * cop.speed; // Check if caught player const distance = Math.sqrt(dx*dx + dy*dy); if (distance < 30 && !player.inCar) { bustPlayer(); // Caught! } } } // Wanted level decays over time when not causing trouble if (wantedLevel > 0 && !isShooting) { wantedLevel -= 0.001; }

05 / Minimap Rendering

The minimap shows a scaled-down view of the world with player and police positions.

function drawMinimap() { const mmWidth = 100, mmHeight = 100; const mmX = canvas.width - mmWidth - 10; const mmY = 10; // Scale factors const scaleX = mmWidth / worldWidth; const scaleY = mmHeight / worldHeight; // Background ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(mmX, mmY, mmWidth, mmHeight); // Draw roads ctx.fillStyle = '#444'; for (const road of roads) { ctx.fillRect( mmX + road.x * scaleX, mmY + road.y * scaleY, road.width * scaleX, road.height * scaleY ); } // Player position (green dot) ctx.fillStyle = '#00ff00'; ctx.beginPath(); ctx.arc(mmX + player.x * scaleX, mmY + player.y * scaleY, 3, 0, Math.PI * 2); ctx.fill(); // Police positions (red dots) ctx.fillStyle = '#ff0000'; for (const cop of police) { ctx.beginPath(); ctx.arc(mmX + cop.x * scaleX, mmY + cop.y * scaleY, 2, 0, Math.PI * 2); ctx.fill(); } // Border ctx.strokeStyle = '#f39c12'; ctx.strokeRect(mmX, mmY, mmWidth, mmHeight); }

06 / Building Collision

When the player collides with a building, we push them out based on which side they hit.

function handleBuildingCollision() { for (const b of buildings) { if (player.x > b.x && player.x < b.x + b.width && player.y > b.y && player.y < b.y + b.height) { // Find building center const cx = b.x + b.width / 2; const cy = b.y + b.height / 2; // Player position relative to center const px = player.x - cx; const py = player.y - cy; // Push out on axis with less penetration if (Math.abs(px) > Math.abs(py)) { // Push horizontally player.x = px > 0 ? b.x + b.width + 5 // Push right : b.x - 5; // Push left } else { // Push vertically player.y = py > 0 ? b.y + b.height + 5 // Push down : b.y - 5; // Push up } } } }