| name | threejs-game-dev |
| description | Three.js game development - scene setup, asset loading from local zips/folders, vehicle physics, city generation, camera systems, collision, HUD. Use when creating 3D games, loading model assets, building interactive experiences, or setting up game loops with Three.js. |
Three.js Game Development Skill
Comprehensive skill for building 3D games with Three.js using local 3D model assets (GLTF, FBX, OBJ from extracted zip archives).
Quick Start — Vite + Three.js Game
npm create vite@latest my-game -- --template vanilla
cd my-game
npm install three
npm run dev
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const sun = new THREE.DirectionalLight(0xffffff, 1.2);
sun.position.set(50, 50, 50);
sun.castShadow = true;
scene.add(sun);
const loader = new GLTFLoader();
loader.load('/models/city/gltf/building_A.gltf', (gltf) => {
const model = gltf.scene;
model.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });
scene.add(model);
});
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
renderer.render(scene, camera);
}
animate();
Game Loop Architecture
class Game {
constructor() {
this.clock = new THREE.Clock();
this.scene = new THREE.Scene();
this.fixedTimeStep = 1 / 60;
this.accumulator = 0;
this.state = 'loading';
}
update(delta) {
this.accumulator += delta;
while (this.accumulator >= this.fixedTimeStep) {
this.fixedUpdate(this.fixedTimeStep);
this.accumulator -= this.fixedTimeStep;
}
}
fixedUpdate(dt) {
}
render() {
this.renderer.render(this.scene, this.camera);
}
loop() {
requestAnimationFrame(() => this.loop());
const delta = this.clock.getDelta();
if (this.state === 'playing') this.update(delta);
this.render();
}
}
Asset Loading from Extracted Zips
Place extracted zip contents in public/models/. Vite serves public/ as static root.
public/
models/
city/ ← KayKit City Builder (GLTF files)
cars/ ← Low Poly Cars (OBJ + textures)
GLTF Loading (preferred for web)
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const gltfLoader = new GLTFLoader();
function loadGLTF(url) {
return new Promise((resolve, reject) => {
gltfLoader.load(url, resolve, undefined, reject);
});
}
const gltf = await loadGLTF('/models/city/gltf/building_A.gltf');
scene.add(gltf.scene);
FBX Loading
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
const fbxLoader = new FBXLoader();
fbxLoader.load('/models/city/fbx/building_A.fbx', (object) => {
object.scale.setScalar(0.01);
object.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });
scene.add(object);
});
OBJ + MTL Loading
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
const mtlLoader = new MTLLoader();
mtlLoader.setPath('/models/cars/');
mtlLoader.load('model.mtl', (materials) => {
materials.preload();
const objLoader = new OBJLoader();
objLoader.setMaterials(materials);
objLoader.setPath('/models/cars/');
objLoader.load('model.obj', (object) => { scene.add(object); });
});
Loading Manager with Progress
const manager = new THREE.LoadingManager();
manager.onProgress = (url, loaded, total) => {
const pct = (loaded / total * 100).toFixed(0);
document.getElementById('loading-bar').style.width = pct + '%';
};
manager.onLoad = () => { document.getElementById('loading-screen').style.display = 'none'; };
const gltfLoader = new GLTFLoader(manager);
Vehicle Physics (Arcade Style)
class Car {
constructor(mesh) {
this.mesh = mesh;
this.speed = 0;
this.maxSpeed = 30;
this.acceleration = 15;
this.braking = 30;
this.friction = 5;
this.turnSpeed = 2.5;
this.steerAngle = 0;
this.velocity = new THREE.Vector3();
}
update(dt, input) {
if (input.forward) this.speed += this.acceleration * dt;
else if (input.backward) this.speed -= this.braking * dt;
else this.speed -= Math.sign(this.speed) * this.friction * dt;
this.speed = THREE.MathUtils.clamp(this.speed, -this.maxSpeed * 0.3, this.maxSpeed);
if (Math.abs(this.speed) < 0.1) this.speed = 0;
if (Math.abs(this.speed) > 0.5) {
const turnFactor = this.speed > 0 ? 1 : -1;
if (input.left) this.mesh.rotation.y += this.turnSpeed * dt * turnFactor;
if (input.right) this.mesh.rotation.y -= this.turnSpeed * dt * turnFactor;
}
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.mesh.quaternion);
this.velocity.copy(forward.multiplyScalar(this.speed));
this.mesh.position.add(this.velocity.clone().multiplyScalar(dt));
}
}
City Generation
class City {
constructor(scene) {
this.scene = scene;
this.tileSize = 2;
this.buildings = [];
this.roads = [];
}
async generate(loader) {
const buildingNames = ['building_A','building_B','building_C','building_D','building_E','building_F','building_G','building_H'];
const roadNames = ['road_straight','road_corner','road_junction','road_tsplit','road_straight_crossing'];
const propNames = ['bench','bush','streetlight','trafficlight_A','firehydrant','dumpster'];
const models = {};
for (const name of [...buildingNames, ...roadNames, ...propNames]) {
const gltf = await loadGLTF(`/models/city/gltf/${name}.gltf`);
models[name] = gltf.scene;
}
const gridSize = 5;
for (let x = -gridSize; x <= gridSize; x++) {
for (let z = -gridSize; z <= gridSize; z++) {
if (Math.abs(x) <= 1 || Math.abs(z) <= 1) {
this.placeRoad(models, x, z);
} else {
this.placeBuilding(models, buildingNames, x, z);
}
}
}
}
placeBuilding(models, names, x, z) {
const name = names[Math.floor(Math.random() * names.length)];
const building = models[name].clone();
building.position.set(x * this.tileSize, 0, z * this.tileSize);
building.rotation.y = (Math.floor(Math.random() * 4)) * Math.PI / 2;
building.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });
this.scene.add(building);
this.buildings.push(building);
}
placeRoad(models, x, z) {
const road = models['road_straight'].clone();
road.position.set(x * this.tileSize, 0, z * this.tileSize);
if (Math.abs(x) <= 1) road.rotation.y = Math.PI / 2;
road.traverse(c => { if (c.isMesh) { c.receiveShadow = true; } });
this.scene.add(road);
this.roads.push(road);
}
}
Third-Person Chase Camera
class ChaseCamera {
constructor(camera, target) {
this.camera = camera;
this.target = target;
this.offset = new THREE.Vector3(0, 5, 10);
this.lookOffset = new THREE.Vector3(0, 1, 0);
this.smoothSpeed = 5;
this.currentPosition = new THREE.Vector3();
this.currentLookAt = new THREE.Vector3();
}
update(dt) {
const desiredPos = new THREE.Vector3()
.copy(this.offset)
.applyQuaternion(this.target.quaternion)
.add(this.target.position);
this.currentPosition.lerp(desiredPos, this.smoothSpeed * dt);
this.camera.position.copy(this.currentPosition);
const lookTarget = this.target.position.clone().add(this.lookOffset);
this.currentLookAt.lerp(lookTarget, this.smoothSpeed * dt);
this.camera.lookAt(this.currentLookAt);
}
}
Collision Detection (AABB)
class CollisionSystem {
constructor() {
this.obstacles = [];
}
addObstacle(mesh) {
const box = new THREE.Box3().setFromObject(mesh);
this.obstacles.push({ mesh, box });
}
checkCollision(playerMesh, playerRadius = 1) {
const playerPos = playerMesh.position;
const playerBox = new THREE.Box3().setFromCenterAndSize(
playerPos, new THREE.Vector3(playerRadius * 2, 2, playerRadius * 2)
);
for (const obs of this.obstacles) {
if (playerBox.intersectsBox(obs.box)) {
return { hit: true, obstacle: obs };
}
}
return { hit: false };
}
resolveCollision(playerMesh, collision) {
if (!collision.hit) return;
const obsCenter = new THREE.Vector3();
collision.obstacle.box.getCenter(obsCenter);
const pushDir = playerMesh.position.clone().sub(obsCenter).normalize();
playerMesh.position.add(pushDir.multiplyScalar(0.5));
}
}
HUD Overlay
class HUD {
constructor() {
this.container = document.createElement('div');
this.container.id = 'hud';
this.container.innerHTML = `
<div id="speed-display">0 km/h</div>
<div id="controls-hint">WASD/Arrows to drive</div>
`;
document.body.appendChild(this.container);
}
updateSpeed(speed) {
const kmh = Math.abs(Math.round(speed * 3.6));
document.getElementById('speed-display').textContent = kmh + ' km/h';
}
}
Input Handler
class InputHandler {
constructor() {
this.keys = {};
window.addEventListener('keydown', e => this.keys[e.code] = true);
window.addEventListener('keyup', e => this.keys[e.code] = false);
}
get forward() { return this.keys['KeyW'] || this.keys['ArrowUp']; }
get backward() { return this.keys['KeyS'] || this.keys['ArrowDown']; }
get left() { return this.keys['KeyA'] || this.keys['ArrowLeft']; }
get right() { return this.keys['KeyD'] || this.keys['ArrowRight']; }
get brake() { return this.keys['Space']; }
}
Lighting Setup for Games
scene.add(new THREE.HemisphereLight(0x87CEEB, 0x362907, 0.5));
const sun = new THREE.DirectionalLight(0xffeedd, 1.5);
sun.position.set(50, 80, 50);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.left = -50;
sun.shadow.camera.right = 50;
sun.shadow.camera.top = 50;
sun.shadow.camera.bottom = -50;
sun.shadow.camera.near = 0.1;
sun.shadow.camera.far = 200;
sun.shadow.bias = -0.001;
scene.add(sun);
scene.fog = new THREE.FogExp2(0xC8DCE8, 0.015);
scene.background = new THREE.Color(0x87CEEB);
Responsive Canvas
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
Performance Tips for Games
- Use instancing for repeated objects (trees, props)
- Pool objects instead of create/destroy
- LOD for distance-based mesh switching
- Frustum culling is enabled by default
- Merge static geometries for terrain/buildings
- Limit shadow casters — only nearby objects
- Use
requestAnimationFrame — never setInterval
- Dispose unused resources — geometry, materials, textures
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
matrix.setPosition(x, y, z);
instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
See Also