Three.js is the most popular 3D library for the web, and for good reason. It abstracts away the complexity of raw WebGL while giving you full control over scenes, lighting, materials, and animation. If you've never built a 3D game before, Three.js is the friendliest on-ramp.
Unlike game engines like Unity or Unreal, Three.js games run directly in the browser. No app store, no installer, no 2GB download — just a URL. That means anyone can play your game on any device with a modern browser.
By the end of this tutorial, you'll have a simple 3D game running in the browser: a player character that moves through a procedurally generated environment, collects items, and avoids obstacles. We'll cover:
Start with a clean HTML file. You don't need npm, webpack, or any build tools — Three.js works perfectly with a simple script tag or ES module import from a CDN.
Create three files: index.html, style.css, and game.js. The HTML sets up a full-screen canvas. The CSS makes the body margin-zero and overflow-hidden. The JavaScript is where the game lives.
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170/build/three.module.js' to skip npm entirely. Pin a specific version to avoid breaking changes.Every Three.js application starts with three objects: a Scene (the container for everything), a Camera (what the player sees), and a Renderer (what draws the scene to the screen).
Use a PerspectiveCamera with a field of view around 60-75 degrees. For games, you typically want the camera to follow the player, so store a reference you'll update each frame.
The renderer should be created with antialias: true for smoother edges. Set its size to window.innerWidth and window.innerHeight, and add a resize listener to handle window changes.
Good lighting makes the difference between a tech demo and a game that feels alive. Start with two lights:
0xfff4e0For materials, MeshStandardMaterial responds to light realistically. Use MeshBasicMaterial for UI elements that should always be visible regardless of lighting.
Track which keys are pressed using a Set that adds keys on keydown and removes them on keyup. In your animation loop, check the set and update the player position accordingly.
A common pattern for smooth movement:
speed constant (e.g., 5 units per second)deltaTime (the time between frames) so movement is consistent regardless of frameraterequestAnimationFrame for your game loop, never setInterval. It syncs to the display refresh rate and pauses when the tab is hidden (saving battery and CPU).For simple games, you don't need a physics engine. Axis-Aligned Bounding Box (AABB) collision detection works for most cases: check if two boxes overlap on all three axes.
Create a helper function that takes two objects with position and size, then returns true if they overlap. Call it each frame to check the player against collectibles and obstacles.
Instead of hand-placing every tree and rock, write a function that scatters objects randomly within bounds. Use seeded randomness if you want reproducible worlds.
Key technique: create objects in a pool and reuse them. When an object moves off-screen behind the player, teleport it ahead of them. This gives the illusion of an infinite world with a fixed number of mesh objects — critical for performance.
Track score, health, and game state in a plain JavaScript object. Render the UI with HTML/CSS overlaid on the canvas — this is much easier than rendering text in 3D and gives you full CSS styling control.
Game states to handle: menu, playing, paused, gameover. A simple state machine keeps your code organized as the game grows.
The Web Audio API lets you generate sounds procedurally — no audio files needed. Create short synthesized effects for collecting items (ascending tone), hitting obstacles (low buzz), and game over (descending sweep).
Always gate audio behind a user interaction (click or keypress) because browsers block autoplay. Start your AudioContext on the first user input.
Browser games that don't work on mobile miss half their audience. Add touch controls: a virtual joystick (two nested divs with touch event handlers) and tap-to-interact buttons.
Detect touch capability with 'ontouchstart' in window and show/hide the appropriate controls. Test on a real phone — emulators miss performance issues that real devices expose.
Your game is just static files — HTML, CSS, and JavaScript. Deploy to any static hosting service for free:
| Service | Free Tier | Deploy Method |
|---|---|---|
| Cloudflare Pages | Unlimited bandwidth | Git push or drag-and-drop |
| GitHub Pages | 1GB storage, 100GB/mo bandwidth | Git push |
| Netlify | 100GB/mo bandwidth | Git push or drag-and-drop |
| Vercel | 100GB/mo bandwidth | Git push |
Cloudflare Pages is the best choice for games because of unlimited bandwidth — a popular game can burn through other services' free tiers quickly.
.dispose() when removing objects.BufferGeometryUtils.mergeGeometries() to stay under 100 draw calls.Once your basic game works, try adding: particle effects (sparks, dust, rain), a day/night cycle, save/load with localStorage, and a leaderboard with a simple serverless function.
FarmHeart started as a simple Three.js prototype and grew into a full farming simulation. The same techniques in this tutorial — scene management, procedural generation, state machines — scale up to surprisingly complex games.
A cozy 3D farming game that runs right in your browser. No download, no signup.
Play Now