-
-
Save shricodev/59a3af7c2e211f7e8d69d2a2a069d4dc to your computer and use it in GitHub Desktop.
o3 - Blog SimCity Simulation - Part 1
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" /> | |
<title>Mini‑SimCity</title> | |
<link rel="stylesheet" href="style.css" /> | |
</head> | |
<body> | |
<div id="hud"> | |
<span id="modeTxt">Mode: Road</span> | | |
<span id="popTxt">Population: 0</span> | |
<div id="buttons"> | |
<button data-mode="road">Road</button> | |
<button data-mode="res">Residential</button> | |
<button data-mode="com">Commercial</button> | |
<button data-mode="ind">Industrial</button> | |
<button data-mode="lake">Lake</button> | |
</div> | |
</div> | |
<canvas id="scene"></canvas> | |
<script type="module" src="main.js"></script> | |
</body> | |
</html> |
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
import * as THREE from "https://unpkg.com/[email protected]/build/three.module.js"; | |
import { OrbitControls } from "https://unpkg.com/[email protected]/examples/jsm/controls/OrbitControls.js?module"; | |
/* Mini‑SimCity main.js */ | |
const CELL = 5; // cell = 5×5 three‑js units | |
const GRID = 20; // 20×20 cells | |
const HALF = GRID * CELL * 0.5; | |
const POP_PER_RES = 10; // each residential cube = 10 people | |
// Build‑time state | |
let mode = "road"; | |
let population = 0; | |
const cells = [...Array(GRID)].map(() => Array(GRID).fill(null)); // null | 'road' | 'res' | 'com' | 'ind' | 'lake' | |
// Basic THREE setup --------------------------------------------------------- | |
const canvas = document.getElementById("scene"); | |
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); | |
renderer.setPixelRatio(devicePixelRatio); | |
renderer.setSize(innerWidth, innerHeight); | |
const scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0xaee0ff); | |
const camera = new THREE.PerspectiveCamera( | |
60, | |
innerWidth / innerHeight, | |
1, | |
1000, | |
); | |
camera.position.set(60, 60, 60); | |
const controls = new OrbitControls(camera, renderer.domElement); | |
controls.enableDamping = true; | |
// Lights | |
scene.add(new THREE.AmbientLight(0xffffff, 0.6)); | |
const sun = new THREE.DirectionalLight(0xffffff, 0.6); | |
sun.position.set(50, 100, 20); | |
scene.add(sun); | |
// Ground grid (helper) | |
const gHelper = new THREE.GridHelper(GRID * CELL, GRID, 0x444444, 0xcccccc); | |
gHelper.position.y = 0.01; // just above zero so it does not z‑fight | |
scene.add(gHelper); | |
// Pre‑baked materials | |
const Mats = { | |
road: new THREE.MeshStandardMaterial({ color: 0x333333 }), | |
res: new THREE.MeshStandardMaterial({ color: 0x4caf50 }), | |
com: new THREE.MeshStandardMaterial({ color: 0x03a9f4 }), | |
ind: new THREE.MeshStandardMaterial({ color: 0xffc107 }), | |
lake: new THREE.MeshStandardMaterial({ | |
color: 0x2196f3, | |
transparent: true, | |
opacity: 0.7, | |
}), | |
}; | |
// Interaction / placement ---------------------------------------------------- | |
window.addEventListener("resize", () => { | |
renderer.setSize(innerWidth, innerHeight); | |
camera.aspect = innerWidth / innerHeight; | |
camera.updateProjectionMatrix(); | |
}); | |
const raycaster = new THREE.Raycaster(); | |
const mouse = new THREE.Vector2(); | |
canvas.addEventListener("pointerdown", placeStructure); | |
function placeStructure(ev) { | |
// Convert click to normalized device coordinates | |
const rect = canvas.getBoundingClientRect(); | |
mouse.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1; | |
mouse.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1; | |
raycaster.setFromCamera(mouse, camera); | |
// Ground plane at y=0 | |
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); | |
const pt = new THREE.Vector3(); | |
raycaster.ray.intersectPlane(plane, pt); | |
if (!pt) return; | |
// Convert world coords → grid indices | |
const gx = Math.floor((pt.x + HALF) / CELL); | |
const gz = Math.floor((pt.z + HALF) / CELL); | |
if (gx < 0 || gx >= GRID || gz < 0 || gz >= GRID) return; | |
// Already filled? | |
if (cells[gx][gz]) return; | |
cells[gx][gz] = mode; | |
spawnMesh(gx, gz, mode); | |
if (mode === "res") { | |
population += POP_PER_RES; | |
refreshHUD(); | |
} | |
} | |
function spawnMesh(gx, gz, type) { | |
const y = type === "road" ? 0.1 : CELL / 2; | |
let h = type === "road" ? 0.2 : CELL; | |
if (type === "lake") { | |
h = 0.2; | |
} | |
const geo = new THREE.BoxGeometry(CELL, h, CELL); | |
const mesh = new THREE.Mesh(geo, Mats[type]); | |
mesh.position.set( | |
gx * CELL - HALF + CELL / 2, | |
h / 2, | |
gz * CELL - HALF + CELL / 2, | |
); | |
scene.add(mesh); | |
if (type === "road") { | |
roads.push(mesh); | |
} | |
} | |
// UI ------------------------------------------------------------------------ | |
document.querySelectorAll("#buttons button").forEach((btn) => { | |
btn.onclick = () => { | |
mode = btn.dataset.mode; | |
refreshHUD(); | |
}; | |
}); | |
function refreshHUD() { | |
document.getElementById("modeTxt").textContent = document.getElementById( | |
"modeTxt", | |
).textContent = `Mode: ${mode[0].toUpperCase()}${mode.slice(1)}`; | |
document.getElementById("popTxt").textContent = | |
`Population: ${population.toLocaleString()}`; | |
document.getElementById("cashTxt").textContent = | |
`Cash: $${cash.toLocaleString()}`; | |
} | |
// Simple traffic / pedestrians ---------------------------------------------- | |
const roads = []; // store road meshes to know where to drive | |
const cars = []; | |
const walkers = []; | |
const CarGeo = new THREE.BoxGeometry(2, 1, 4); | |
const CarMat = new THREE.MeshStandardMaterial({ color: 0xff5252 }); | |
const WalkGeo = new THREE.BoxGeometry(1, 2, 1); | |
const WalkMat = new THREE.MeshStandardMaterial({ color: 0xffffff }); | |
// spawn some moving agents every few seconds | |
setInterval(() => { | |
if (roads.length) { | |
spawnCar(); | |
} | |
}, 3000); | |
setInterval(() => { | |
if (roads.length) { | |
spawnWalker(); | |
} | |
}, 5000); | |
function spawnCar() { | |
// choose random road segment and direction | |
const seg = roads[(Math.random() * roads.length) | 0]; | |
const car = new THREE.Mesh(CarGeo, CarMat.clone()); | |
car.position.copy(seg.position); | |
car.position.y = 0.6; | |
car.userData.dir = Math.random() < 0.5 ? "x" : "z"; | |
car.userData.speed = 0.5 + Math.random() * 0.5; | |
cars.push(car); | |
scene.add(car); | |
} | |
function spawnWalker() { | |
const walk = new THREE.Mesh(WalkGeo, WalkMat.clone()); | |
walk.position.set( | |
((Math.random() * GRID) | 0) * CELL - HALF + CELL / 2, | |
1, | |
((Math.random() * GRID) | 0) * CELL - HALF + CELL / 2, | |
); | |
walk.userData.speed = 0.2 + Math.random() * 0.2; | |
walk.userData.target = new THREE.Vector3( | |
((Math.random() * GRID) | 0) * CELL - HALF + CELL / 2, | |
1, | |
((Math.random() * GRID) | 0) * CELL - HALF + CELL / 2, | |
); | |
walkers.push(walk); | |
scene.add(walk); | |
} | |
// Animation loop ------------------------------------------------------------ | |
function animate() { | |
requestAnimationFrame(animate); | |
const dt = 0.016; // fake delta‑time | |
// Move cars | |
cars.forEach((c) => { | |
if (c.userData.dir === "x") { | |
c.position.x += c.userData.speed; | |
} else { | |
c.position.z += c.userData.speed; | |
} | |
// Despawn off edge | |
if ( | |
Math.abs(c.position.x) > HALF + 10 || | |
Math.abs(c.position.z) > HALF + 10 | |
) { | |
scene.remove(c); | |
cars.splice(cars.indexOf(c), 1); | |
} | |
}); | |
// Move walkers | |
walkers.forEach((w) => { | |
const tgt = w.userData.target; | |
const dir = tgt.clone().sub(w.position).setY(0); | |
if (dir.length() < 0.5) { | |
w.userData.target.set( | |
((Math.random() * GRID) | 0) * CELL - HALF + CELL / 2, | |
1, | |
((Math.random() * GRID) | 0) * CELL - HALF + CELL / 2, | |
); | |
} else { | |
dir.normalize().multiplyScalar(w.userData.speed); | |
w.position.add(dir); | |
} | |
}); | |
controls.update(); | |
renderer.render(scene, camera); | |
} | |
animate(); |
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
html, | |
body { | |
margin: 0; | |
height: 100%; | |
overflow: hidden; | |
font-family: sans-serif; | |
} | |
#scene { | |
display: block; | |
width: 100%; | |
height: 100%; | |
} | |
#hud { | |
position: fixed; | |
top: 8px; | |
left: 8px; | |
background: #0009; | |
color: #fff; | |
padding: 6px 10px; | |
border-radius: 4px; | |
z-index: 10; | |
} | |
#buttons button { | |
margin: 2px; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment