Super Soldier Battle Dev Log
THREE.JS FPS CONTROLS:
FOUR FIXES FROM A REAL BROWSER SHOOTER
My shooter's mouse look, first shot, trigger and jump all had small bugs that players felt right away. Here is what was wrong and how I fixed each one.
By Shane Brumback · · Beginner friendly
Three.js FPS Controls in One Paragraph
Three.js FPS controls are usually built on the PointerLockControls addon, which locks the mouse and turns its movement into camera yaw and pitch. To make them hold up in a real game, clamp the pitch a little short of straight up and down, ignore the click that acquires the pointer lock, keep the trigger in sync with the real mouse button every frame, and keep horizontal momentum while the player is in the air.
Three.js FPS controls are the first thing a player touches in my browser shooter, Super Soldier Battle, and the first thing they judge. Graphics can be rough around the edges. Controls can't. If the mouse look feels off or the gun doesn't fire when you click, people leave before they ever see the map.
This week I went back through the game's first-person controls and fixed four small bugs that each made the game feel a little broken. None of them showed up in a quick test. All of them showed up the moment real people played. Here is what was wrong, what I changed, and how you can avoid the same problems in your own Three.js FPS game.
What Are Three.js FPS Controls?
In Three.js, FPS controls are usually built on the PointerLockControls addon. When the player clicks Play, the browser hides the cursor and locks the pointer to the page. From then on, every mouse movement turns the camera: left and right is yaw, up and down is pitch. The addon also gives you moveForward and moveRight helpers that move along the direction you are facing.
Everything else is yours to write: WASD movement, sprint, jump, gravity, firing, and all the little rules that make it feel good. That is where the bugs live.
If you are just getting started, my free guides on Three.js pointer lock controls basics and keyboard inputs for first person games cover the setup. This post is about what happens after that.
Three.js PointerLockControls vs FirstPersonControls
People often ask which one to use. Here is the short version:
| PointerLockControls | FirstPersonControls | |
|---|---|---|
| Cursor | Hidden and locked | Visible |
| Turning | Unlimited mouse movement | Based on cursor position on screen |
| Best for | Shooters and first-person games | Fly-throughs and demos |
| Needs a click to start | Yes (browser rule) | No |
| Mobile | No pointer lock, needs touch controls | Works, but not for aiming |
For a shooter, PointerLockControls is the right choice. Super Soldier Battle uses it on desktop.
Fix 1: Why Does the Three.js Camera Flip When You Look Straight Up?
Super Soldier Battle has gunships, towers and a dropship. Players look up a lot. If you pushed the mouse far enough, the pitch could reach exactly straight up. At that point the camera's forward direction lines up with its up direction, and in a game that also shakes and reorients the camera, the view could flip or get into a broken state.
The fix took two lines. PointerLockControls has minPolarAngle and maxPolarAngle, and I now keep the view 0.08 radians, about 4.6 degrees, away from both poles. You can still look almost straight up at a gunship. You just can't hit the one spot where the math breaks.
Fix 2: The First Click After Play
This one was sneaky. The click on the Play button requests the pointer lock, and that same click also reached the game's mouse handler. The old handler tried to read the mouse position from a property that doesn't exist, got NaN, then compared a screen coordinate against the number 700 to decide whether to shoot. Neither line could ever be right, so the first click behaved unpredictably.
Now the handler asks one question first: is the pointer locked? If not, the click is ignored. When the lock event fires, the game also clears any trigger state and stops the gun sound, so you always enter the battle clean.
Fix 3: The Gun That Kept Firing
Hold the trigger and press Escape, or alt-tab in the middle of a burst, and the browser can swallow the mouse release. The game never hears it, so it keeps thinking the trigger is held. That means a gun that keeps firing and a sound that keeps looping.
I added a small firing watchdog that runs every frame. It tracks the real state of the mouse button separately from the game's trigger flag and makes them agree. If the pointer isn't locked, nothing fires, period. It runs right before the code that decides whether you are shooting, so a lost event is fixed within one frame.
Fix 4: How Do You Keep Momentum When Jumping in a Three.js FPS?
Movement in the game stops instantly when you let go of a key. On the ground that feels snappy and precise. In the air it felt terrible. If you sprinted, jumped and let go of W, your forward speed dropped to zero and you fell straight down instead of finishing the jump.
The fix: only zero the speed when you release a key on the ground. While you are in the air, the momentum from your take-off carries through the arc, and keys you press in the air still steer. Sprint runs at about 2.2 times walking speed, wading through water slows you down, and the jump now peaks at a little over 7 units instead of the old moon-jump of around 28.
Why These Small Fixes Matter for a Three.js FPS Game
None of these bugs crashed the game. That is what made them dangerous. They just made it feel slightly wrong, and players can't tell you why, they just stop playing. Controls are the one system every player uses every second, so a one-line fix there is worth more than a week on a new feature.
The bigger lesson is to treat input as something that can go out of sync. Pointer lock can be lost at any time. Events can be swallowed. Frames can hitch. Every fix above is really the same idea: decide what the truth is (is the pointer locked, is the button really down, is the player on the ground) and check it every frame instead of trusting a single event.
Build These Three.js FPS Controls Yourself
I turned all four fixes into a premium step-by-step tutorial with a runnable demo: a training range with crates to shoot, a tall tower to look up at, and a live HUD that shows your look angle, speed, jump height and trigger state. You can even toggle the fixes off to feel the difference. Read the Three.js FPS controls tutorial, and if you want a complete game to drop them into, try the first person shooter starter or play my Three.js zombie attack FPS game.
Then jump into Super Soldier Battle, look straight up at a gunship, sprint-jump off a ridge, and tell me how it feels. If you are building your own shooter and want help with the controls, contact me.
Frequently Asked Questions
What are Three.js FPS controls?
They are the input layer of a first-person game: mouse look, WASD movement, sprint, jump and fire. In Three.js they are usually built on PointerLockControls, an addon that hides the cursor, locks the pointer to the page and turns mouse movement into camera rotation, plus your own movement and physics code.
Is PointerLockControls better than FirstPersonControls for a game?
For a shooter, yes. PointerLockControls gives unlimited mouse movement with a hidden cursor, which is how every desktop FPS works. FirstPersonControls turns the camera based on where the cursor sits on screen, which is fine for fly-throughs but feels wrong for aiming.
Why does my Three.js camera flip when I look straight up?
At exactly straight up or straight down the camera's forward direction lines up with its up direction, and the math can flip the view. PointerLockControls has minPolarAngle and maxPolarAngle. Keeping them a small margin away from the poles, such as 0.08 radians, stops it.
Why does the first click after Play fire a shot or do nothing?
The click that requests pointer lock also reaches your mouse handler. If your handler doesn't check controls.isLocked, that click is treated as a shot. Ignore clicks while the pointer is not locked and reset the trigger when the lock event fires.
Can I play Super Soldier Battle with these controls?
Yes. All four fixes in this post are live in Super Soldier Battle, my free browser shooter at supersoldierbattle.com. Pointer lock needs a mouse, so play it on a desktop browser with mouse and keyboard.
Play Super Soldier Battle
Play Super Soldier Battle free in your browser on a desktop with mouse and keyboard, and feel all four fixes in a real fight.
Building a Three.js game and need help with controls, rendering or performance? I'm available for freelance and contract work: contact me.