Skip to content

A 3D game with three.js

You will make a small arena where every player is a capsule that turns to face where it runs. Others see you move and turn smoothly. The finished, styled version is examples/games/arena3d.html.

Terminal window
npm install tickcli ws three
npx tickcli .

On the tickcli server, <script src="lib:three"></script> gives the page three.js as the global THREE, made from the installed package. Anywhere else, any build that sets window.THREE works.

<!doctype html><html><head><meta charset="utf-8"><title>Arena</title></head><body>
<script src="lib:three"></script>
<script src="lib:room"></script>
<script src="lib:game"></script>
<script src="lib:game3d"></script>
<script>
const R3 = TickcliThree, SIZE = 12, up = new THREE.Vector3(0, 1, 0);
let renderer, scene, camera, me, others, yaw = 0;
function capsule(color) {
const m = new THREE.Mesh(new THREE.CapsuleGeometry(0.45, 0.7, 6, 16), new THREE.MeshStandardMaterial({ color }));
m.position.y = 0.8; const o = new THREE.Group(); o.add(m); return o;
}
Game({
title: "Arena", context: "webgl", aspect: 16 / 10,
multiplayer: { max: 8, player: { x: [-SIZE, SIZE], z: [-SIZE, SIZE], qx: [-1, 1], qy: [-1, 1], qz: [-1, 1], qw: [-1, 1] } },
setup(g) {
renderer = renderer || R3.renderer(g); // a WebGLRenderer on the shell's canvas, made once
if (!scene) {
scene = new THREE.Scene(); scene.background = new THREE.Color(0x0b1020);
camera = new THREE.PerspectiveCamera(55, 1, 0.1, 100);
scene.add(new THREE.HemisphereLight(0xffffff, 0x334155, 2));
const floor = new THREE.Mesh(new THREE.PlaneGeometry(SIZE * 2, SIZE * 2), new THREE.MeshStandardMaterial({ color: 0x1e293b }));
floor.rotation.x = -Math.PI / 2; scene.add(floor);
me = capsule(0x60a5fa); scene.add(me);
others = R3.pool(scene, () => capsule(0xf87171)); // one capsule per other player
}
me.position.set((Math.random() - 0.5) * SIZE, 0, (Math.random() - 0.5) * SIZE);
},
update(g, dt) {
const a = g.axis();
if (a.x || a.y) { me.position.x += a.x * 6 * dt; me.position.z += a.y * 6 * dt; yaw = Math.atan2(a.x, a.y); }
me.quaternion.setFromAxisAngle(up, yaw);
g.me.set(R3.pose(me)); // {x, y, z, qx, qy, qz, qw}, rounded
},
draw(g) {
R3.resize(renderer, camera, g); // drawing size, pixel ratio, camera aspect
others.sync(g.players.filter((p) => !p.me)); // placed where they are now, eased
camera.position.set(me.position.x, 9, me.position.z + 11); camera.lookAt(me.position);
renderer.render(scene, camera);
},
});
</script></body></html>

Open it in two windows and press Start.

  • context: "webgl": the shell takes no 2D context. It still gives you the bar, the start and game-over screens, the lobby, keys and the touch pad; draw(g) runs every frame.
  • R3.resize follows the shell: when the window changes, or a slow device lowers the pixel ratio, the renderer and camera follow.
  • Rotations are quaternions. qx, qy, qz, qw are eased on other screens as one turn (slerp, the short way round, always of length 1). Declare each as [-1, 1]; they go out with 3 decimals.
  • Positions keep decimals. In webgl mode the host’s shared positions keep 2 decimals (metres to the centimetre); players’ fields follow their declared range.
  • Things the host moves ease too: g.shared.set({gem: R3.pose(gem)}) in host(), R3.place(gemMesh, g.shared.gem) in draw().