Building a First‑Person Shooting Game with Three.js
This tutorial walks through creating a complete first‑person shooter in the browser using Three.js, covering scene setup, floor and lighting, random moving targets, player controls, shooting mechanics, bullet physics, hit detection, explosion effects, and the main animation loop.
The article starts by motivating programmers to build a small shooting game for fun and skill improvement, then outlines the five core elements of a shooter: scene, targets, player, trajectory, and explosion effect.
Scene creation : A basic Three.js program requires a renderer, a scene, and a camera. The code creates these objects and adds a window‑resize listener to keep the view consistent.
const renderer = new THREE.WebGLRenderer();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);
adjustWindowSize();
window.addEventListener("resize", adjustWindowSize);
function adjustWindowSize() {
camera.aspect = window.innerWidth / window.innerHeight;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
renderer.render(scene, camera);
}Floor and lighting : A large plane serves as the ground and a directional light simulates sunlight.
const floorGeometry = new THREE.PlaneGeometry(100, 100);
const floorMaterial = new THREE.MeshBasicMaterial({color: 0x808080, side: THREE.DoubleSide});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 7.5);
scene.add(light);Target creation : Targets are random‑colored boxes placed at random positions without overlapping. Each target receives a small random velocity.
function createTarget() {
const targetGeometry = new THREE.BoxGeometry(2, 2, 2);
const targetMaterial = new THREE.MeshBasicMaterial({color: new THREE.Color(Math.random(), Math.random(), Math.random())});
const target = new THREE.Mesh(targetGeometry, targetMaterial);
let validPosition = false;
while (!validPosition) {
target.position.set(Math.random() * 100 - 50, 1, Math.random() * 100 - 50);
validPosition = true;
for (let i = 0; i < targets.length; i++) {
const other = targets[i];
if (target.position.distanceTo(other.position) < 5) { validPosition = false; break; }
}
}
target.velocity = new THREE.Vector3((Math.random() - 0.5) * 0.1, 0, (Math.random() - 0.5) * 0.1);
targets.push(target);
scene.add(target);
}Updating target positions : Each frame moves targets according to their velocity and reverses direction when they hit the floor boundaries.
function updateTargets() {
targets.forEach(target => {
target.position.add(target.velocity);
if (target.position.x > 50 || target.position.x < -50) target.velocity.x = -target.velocity.x;
if (target.position.z > 50 || target.position.z < -50) target.velocity.z = -target.velocity.z;
});
}Player controls : PointerLockControls lock the mouse pointer, while keyboard listeners set movement flags and trigger shooting.
const controls = new THREE.PointerLockControls(camera, document.body);
document.addEventListener("click", () => controls.lock());
const move = {forward:false, backward:false, left:false, right:false};
function onKeyDown(event) {
switch(event.code) {
case "ArrowUp": move.forward = true; break;
case "ArrowDown": move.backward = true; break;
case "ArrowLeft": move.left = true; break;
case "ArrowRight": move.right = true; break;
case "Space": shoot(); break;
}
}
function onKeyUp(event) {
switch(event.code) {
case "ArrowUp": move.forward = false; break;
case "ArrowDown": move.backward = false; break;
case "ArrowLeft": move.left = false; break;
case "ArrowRight": move.right = false; break;
}
}
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);Updating player position each frame based on the movement flags.
function updatePlayer() {
const speed = 0.1;
const direction = new THREE.Vector3();
if (move.forward) direction.z += speed;
if (move.backward) direction.z -= speed;
if (move.left) direction.x -= speed;
if (move.right) direction.x += speed;
controls.moveRight(direction.x);
controls.moveForward(direction.z);
}Shooting : When the space bar is pressed, a small yellow sphere is spawned at the camera position, given an initial velocity in the camera direction, and a gravity vector.
function shoot() {
const bulletGeometry = new THREE.SphereGeometry(0.1, 8, 8);
const bulletMaterial = new THREE.MeshBasicMaterial({color: 0xffff00});
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
bullet.position.copy(controls.getObject().position);
bullet.velocity = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
bullet.gravity = new THREE.Vector3(0, -0.001, 0);
bullets.push(bullet);
scene.add(bullet);
}Bullet update : Each frame applies gravity, moves the bullet, and removes it when it travels beyond a distance of 100 units.
function updateBullets() {
bullets = bullets.filter(bullet => {
bullet.velocity.add(bullet.gravity);
bullet.position.add(bullet.velocity);
if (bullet.position.length() >= 100) { scene.remove(bullet); return false; }
return true;
});
}Hit detection : Checks every bullet against every target; if the distance is less than 1, an explosion is created, the target and bullet are removed, the score is increased, and a new target is spawned.
function checkHit() {
targets.forEach((target, tIdx) => {
bullets.forEach((bullet, bIdx) => {
const distance = target.position.distanceTo(bullet.position);
if (distance < 1) {
createExplosion(target.position);
scene.remove(target);
scene.remove(bullet);
targets.splice(tIdx, 1);
bullets.splice(bIdx, 1);
score += 10;
scoreElement.innerText = `分数: ${score}`;
createTarget();
}
});
});
}Explosion effect : Uses a particle system (PointsMaterial) with a pre‑loaded texture; the particles are removed after 100 ms.
const explosionTexture = new THREE.TextureLoader().load("./expose.jpg");
function createExplosion(position) {
const material = new THREE.PointsMaterial({size:1, map:explosionTexture, blending:THREE.AdditiveBlending, depthWrite:false, transparent:true, color:0xff4500});
const geometry = new THREE.BufferGeometry();
const vertices = [];
for (let i = 0; i < 100; i++) {
const p = new THREE.Vector3();
p.x = position.x + (Math.random() - 0.5) * 5;
p.y = position.y + (Math.random() - 0.5) * 5;
p.z = position.z + (Math.random() - 0.5) * 5;
vertices.push(p.x, p.y, p.z);
}
geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
const explosion = new THREE.Points(geometry, material);
scene.add(explosion);
setTimeout(() => scene.remove(explosion), 100);
}Initialization and animation loop : The init function creates a number of targets and starts the animate loop, which updates the player, bullets, targets, checks hits, and renders the scene each frame.
function init() {
for (let i = 0; i < 20; i++) createTarget();
animate();
}
function animate() {
requestAnimationFrame(animate);
updatePlayer();
updateBullets();
updateTargets();
checkHit();
renderer.render(scene, camera);
}
init();The article concludes with reflections on the development experience, noting the challenges of UI modeling in Three.js and the personal satisfaction of creating a playable mini‑game.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.