Need Game Art? Edit Game Trailers Launch Your Dev Site Sell Your Merch
Need Game Art? Sell Your Merch

Three.js Third Person Shooter Tutorial: Character, Camera, and Shooting

Updated July 2026
A basic third person shooter is one of the best projects for learning Three.js game development because it touches every core system at once: a controllable character, a camera that follows without fighting the player, movement that respects where the camera is looking, and shooting built on raycasting. This tutorial builds each piece in order, starting from an empty scene and ending with a character that can run, aim, shoot targets, and get chased by a simple enemy. Every step works with modern Three.js using ES modules.

Third person differs from first person in one fundamental way: the camera and the character are separate objects with separate rotations. In a first person game the camera is the character's eyes, so first-person controls bind them together. In third person, the camera orbits behind the character, the character rotates toward where it is moving or aiming, and the two must stay coordinated without being locked together. Most of the work in this tutorial is managing that relationship cleanly.

Step 1: Set Up the Scene

Create a standard Three.js foundation: a Scene, a PerspectiveCamera with a 60 to 75 degree field of view, and a WebGLRenderer with antialiasing enabled, sized to the window and appended to the page. Add a HemisphereLight for soft ambient fill and a DirectionalLight for shadows and definition. If you have not built this base before, the getting started guide covers the boilerplate in detail.

For the level, a large PlaneGeometry rotated flat serves as the ground, and a handful of BoxGeometry meshes scattered around act as walls and cover. Add three or four brightly colored boxes or spheres as shooting targets. Keep every mesh you want to be shootable in a dedicated array, something like shootables, because the raycaster in step 5 will test against that list instead of the whole scene. That one decision keeps hit detection fast and prevents bullets from hitting things like the ground or particle effects.

Step 2: Add the Player Character

Start with a placeholder: a CapsuleGeometry mesh about two units tall, parented to a Group that represents the player. The group is the object your movement code will translate and rotate, which means everything you build in the next three steps keeps working when you replace the capsule with a real model. That separation between the logical player object and its visual mesh is a habit worth forming early.

When you are ready for a real character, load an animated glTF model with GLTFLoader and drop it inside the same group. Free rigged characters with run, idle, and shoot animations are easy to find, and the model loading guide walks through the loader setup. Drive the animations with an AnimationMixer: play the idle clip when velocity is near zero and blend to the run clip when the character moves, using crossFadeTo over a tenth of a second or so. Smooth blending matters more for game feel than the quality of the animations themselves.

Step 3: Build the Follow Camera

The classic third person camera sits behind and slightly above the character, offset a little to one side for an over-the-shoulder framing. Store that offset as a vector, something like 1.5 units right, 2 units up, and 4 units back in the character's local space. Each frame, compute the desired camera position by applying the current orbit rotation to the offset and adding the character's position, then move the camera toward that point with camera.position.lerp(target, 0.1). The lerp gives the camera a soft, slightly delayed follow that feels far better than rigid attachment.

Mouse input rotates the orbit, not the character. Track yaw and pitch angles from movementX and movementY deltas using the Pointer Lock API, exactly as in a first person setup, and clamp pitch so the camera can neither go underground nor flip over the top. Then point the camera at a spot slightly above the character's head with camera.lookAt.

The last problem is walls. If the player backs the character against a wall, the desired camera position ends up inside or behind geometry. The standard fix is a raycast from the character's head toward the desired camera position: if it hits level geometry before reaching the full offset distance, place the camera at the hit point instead, pulled in a small margin so it does not intersect the surface. This camera-collision raycast is what separates a professional feeling third person camera from one that constantly clips through walls.

Step 4: Add Character Movement

Track WASD key state in a simple object updated by keydown and keyup listeners, and process it in the game loop rather than in the event handlers. Movement must be camera-relative: pressing W should move the character away from the camera, not along the world Z axis. Take the camera's forward direction, flatten it by zeroing the Y component, normalize it, and derive the right vector from a cross product with world up. Combine the two according to which keys are held, normalize the result, and scale by run speed times delta time.

Now rotate the character to face where it is going. Compute the target angle from the movement direction with Math.atan2(dir.x, dir.z) and rotate the player group toward it gradually, either by interpolating the angle or by building a target quaternion and using quaternion.rotateTowards with a turn speed in radians per second. The character swinging around smoothly as the player changes direction is the signature look of third person movement.

Shooters add one twist: while the player holds aim (commonly right mouse button), the character should stop facing its movement direction and instead face where the camera points, so it can strafe sideways and backpedal while keeping the reticle on target. Implement it as a simple boolean: aiming locks character yaw to camera yaw, releasing returns to face-your-movement. Blending a slower walk animation while aiming completes the effect.

Step 5: Implement Aiming and Shooting

Shooting in a third person game is almost always a camera raycast, not a projectile from the gun barrel. Draw a small crosshair fixed at the center of the screen with HTML or CSS, and on fire, cast a Raycaster from the camera through that center point using raycaster.setFromCamera with normalized device coordinates of 0, 0. Test against the shootables array with intersectObjects and take the first hit. This guarantees the shot lands exactly where the crosshair sits, which is what players expect.

Gate firing behind a cooldown timer, around 0.12 to 0.2 seconds per shot for a rifle feel, accumulated with delta time. Give every shot visible feedback even when it misses: a brief muzzle flash (a small emissive sprite or point light that lives for two or three frames), a tracer line from the gun toward the hit point drawn with a thin stretched mesh or Line, and a small impact marker at the hit location. Feedback is most of what makes shooting feel good, and all of it is cheap to build.

Step 6: Detect Hits and React

Give each target a health value in its userData, for example target.userData.health = 3, when you create it. On a raycast hit, read the intersected object, walk up its parent chain if your targets are groups, and subtract damage. React immediately: flash the material's emissive color for a few frames, scale the target down briefly, or nudge it in the direction of the shot. When health reaches zero, remove the mesh from the scene and from the shootables array, then respawn it somewhere else after a delay so the range never empties.

Keep score in a simple counter rendered in an HTML overlay. Updating DOM text is fine at this scale and keeps UI out of the WebGL scene. If you later want floating damage numbers or hit markers in world space, project the hit position to screen coordinates and position absolutely placed elements over the canvas.

Step 7: Add a Simple Enemy

A basic enemy needs only two states: idle and chase. Each frame, measure the distance from the enemy to the player. Beyond a detection radius it idles in place; inside the radius it moves toward the player by normalizing the vector between them and scaling by an enemy speed slower than the player's run speed, rotating to face its movement just like the player character does. When it gets within a touch distance, damage the player and knock the enemy back a step so contact is not continuous.

Make the enemy shootable by adding it to the same shootables array with its own health in userData, and give it a hit reaction, a stagger pause of a quarter second works well, so shots visibly matter. This tiny state machine is genuinely the seed of real enemy behavior, and when you outgrow it, the game AI and NPC guides cover pathfinding, smarter state machines, and AI-driven behavior.

Step 8: Wire It Into the Game Loop

Order matters in the per-frame update. Read the clock delta once, then update in this sequence: input state into movement (step 4), character position and rotation, camera follow and collision (step 3), aiming and fire cooldowns (step 5), enemies (step 7), animation mixers, and finally render. Updating the camera after the character prevents the one-frame lag jitter that appears when the order is reversed. The game loop guide covers delta timing and fixed-step updates in depth if you want to harden this structure.

From here the project grows in whichever direction interests you: swap the box level for a real environment, add physics with Rapier for gravity and proper collision, or add more weapon types with different cooldowns and damage. Each system you built in this tutorial stays useful as the game gets bigger.

Key Takeaway

A third person shooter in Three.js is five coordinated systems: a player group that moves camera-relative and rotates toward movement or aim, an orbiting follow camera smoothed with lerp and protected by a collision raycast, camera-center raycast shooting with cooldowns and visual feedback, health-based hit detection through userData, and a two-state enemy. Build them in that order and every step is testable before the next begins.