Your first field in the browser with Three.js

Semih2 min read

How do you set up the first scene of a browser game? A step-by-step start with Three.js for camera, light, a low-poly field and clickable plots.

In a browser game, the first scene is where players decide “this game is fast”. In this post we’ll build a low-poly field with Three.js and make its plots clickable.

Scene, camera, light

Farm games usually use a camera looking down at a slight angle. An orthographic camera has no perspective shrink, so the field reads like a board on a table.

import * as THREE from 'three';

const scene = new THREE.Scene();
const aspect = innerWidth / innerHeight;
const camera = new THREE.OrthographicCamera(-10 * aspect, 10 * aspect, 10, -10, 0.1, 100);
camera.position.set(12, 14, 12);
camera.lookAt(0, 0, 0);

scene.add(new THREE.HemisphereLight(0xffffff, 0x6b8f5a, 1.1));
const sun = new THREE.DirectionalLight(0xfff1d6, 1.4);
sun.position.set(6, 10, 4);
scene.add(sun);

Laying out the plots

Each plot is a thin box. If we draw the same geometry hundreds of times, an InstancedMesh brings it down to a single draw call.

const plot = new THREE.BoxGeometry(1.8, 0.2, 1.8);
const soil = new THREE.MeshLambertMaterial({ color: 0x8a5a3b });
const field = new THREE.InstancedMesh(plot, soil, 9);
const m = new THREE.Matrix4();
for (let i = 0; i < 9; i++) {
  m.setPosition((i % 3) * 2 - 2, 0, Math.floor(i / 3) * 2 - 2);
  field.setMatrixAt(i, m);
}
scene.add(field);

Clicking: which plot?

A raycaster sends a ray from a point on the screen into the scene. With an InstancedMesh, the hit’s instanceId gives us the plot number.

const ray = new THREE.Raycaster();
addEventListener('pointerdown', (e) => {
  const p = new THREE.Vector2((e.clientX / innerWidth) * 2 - 1, -(e.clientY / innerHeight) * 2 + 1);
  ray.setFromCamera(p, camera);
  const hit = ray.intersectObject(field)[0];
  if (hit) selectPlot(hit.instanceId);
});

Three rules for performance

  • If you draw the same object again and again, use instancing.
  • Only enable shadows on the light that needs them; it’s the most expensive setting on mobile.
  • Compress textures and load the scene after the rest of the page has opened.

These steps are the skeleton of a game. The loop, quests and economy are built on top. For a live example, open Farmie.

More from the rulebook

  1. 2 min read

    60 FPS in a browser game: smooth scenes without cooking the phone

    Practical notes on draw calls, shadows, textures and pixel ratio for keeping a Three.js browser game smooth on mid-range phones.

  2. 3 min read

    WHMCS or WiseCP? 6 questions to ask when a hosting company picks a panel

    What to look at when a company selling hosting and domains chooses between WHMCS and WiseCP. There's no winner, only the right questions.

Chance

Got a project in mind? Let’s open the box: tell me what you want to build and I’ll prepare a proposal with the scope and a roadmap.