Lesson 7
The camera and juice
The camera decides what a player sees of their own movement. Following with a lerp or a critically damped spring, dead zones, look-ahead and following height only on landing; smoothing that behaves the same at any frame rate; and juice: squash, stretch and screen shake driven by trauma.
The player never sees the character’s position. They see where it is on screen, which is the character’s motion minus the camera’s. A camera that copies every step and every jump turns a good controller into a seasick one; a camera that lags too far hides what’s ahead. Itay Keren’s GDC 2015 talk Scroll Back catalogues how side-scrollers have solved this, game by game. This lesson builds the common pieces on the feel lab’s camera, then adds the feedback that makes movement read as weight.
The widgets show what the camera sees: a view 24 tiles wide, the previous lessons’ box and controller, and a plot of the last few seconds underneath.
Following: locked, lerp or spring
Loading…
The box runs right for 2 s, stops, runs back, stops, and takes a few steps. The plot shows where it is on screen, in tiles from the centre.
Locked on puts the camera on the box every frame. The box never leaves the centre (0.00 tiles), so every start and stop is the whole world lurching.
Lerp (Keren’s lerp-smoothing) closes a fraction of the gap each tick. It lags while the box runs, up to 2.92 tiles here, and settles gently when it stops. It is an ease-out: the camera’s speed jumps the moment the target starts moving, which shows as a jolt whenever the target moves suddenly.
Spring (physics-smoothing) treats the camera as a mass on a spring pulled towards the target. Critically damped (just enough damping not to overshoot), it gathers speed and settles smoothly: ease in and ease out. At the lab’s 3 Hz it stays within 0.88 tiles of the box. The lerp’s rate and the spring’s frequency aren’t the same unit, so tune each by eye, not by number.
// A critically damped spring, stepped with its exact solution for one tick (target held fixed):
// no overshoot and stable at any stiffness or tick.
void spring_step(float& x, float& v, float target, float omega /* 2*pi*frequency */, float dt) {
float k = expf(-omega * dt), e = x - target, j = v + omega * e;
x = target + (e + j * dt) * k;
v = (v - omega * j * dt) * k;
}
Dead zones and look-ahead
Tick Dead zone (Keren’s camera-window). The box can move ±1 tile from the centre without the camera following at all; once it pushes past the edge, the camera follows the edge. Small adjustments, standing jumps and shuffles stop moving the view: with the spring, the view moves 23.7 tiles over the 6 seconds instead of 28.7. Tick Show the dead zone to see the box. Many 2D games use a window wider than it is tall, or one-sided windows that only scroll forwards.
Tick Look-ahead (close to Keren’s dual forward focus): the camera’s target leads the box by up to 3 tiles in the direction it’s running, so the player sees more of what they’re running into. The box sits behind the centre while it runs, and the view swings across when it turns. Swinging the lead in gradually matters; snap it and every turn is a jolt. Super Mario World switches its focus once Mario passes a threshold; Cave Story shifts it with walking speed.
Height: follow on landing
Vertically the problem is different. A jump goes up and comes back down, and a camera that follows it bobs the whole world with every jump.
Loading…
The box hops up three 2-tile steps and drops back to the floor. Followed all the time, the view moves up and down 16.5 tiles over the five seconds, though the climb and the drop add up to only 12.0: the rest is bobbing. Tick Follow height only on landing (Keren’s platform-snapping, introduced by Super Mario World): the camera follows the height of the last ground the box stood on, and moves to a new height only on landing, or when the box falls more than 3 tiles below it. The view then moves exactly the 12.0 tiles of the climb and drop.
Smoothing with time, not frames
The most common camera bug isn’t in any of these choices but in how they’re stepped. The classic lerp closes a fixed fraction of the gap per frame, x += (target - x) * 0.05f, which is tuned at one frame rate and wrong at every other. After half a second it has closed this much of a gap:
| 30 fps | 60 fps | 144 fps | |
|---|---|---|---|
| 5% per frame | 54% | 79% | 98% |
| 1 − e^(−3·dt) per frame | 78% | 78% | 78% |
The fix is to work out the fraction from the time step, a = 1 - expf(-rate * dt), which is exact exponential decay at any frame rate. Springs have a version of the same trap: stepped the simple way (velocity, then position), even at a fixed 60 Hz, a critically damped spring starts to overshoot at 7.1 Hz and grows without bound above 7.9 Hz. The exact update above never overshoots at any stiffness, which is why the lab uses it.
Juice: squash, stretch and shake
Juice is feedback that doesn’t change the rules: the character stretching as it leaves the ground and squashing as it lands, a screen shake on a hard landing, dust, a few frames of freeze on a heavy hit. None of it changes where anything goes, but players read it as weight and impact, and miss it when it’s gone.
Loading…
The box walks off ledges 2, 5 and 9 tiles high. The rule that keeps juice from becoming noise is to scale it with the event, and make it decay fast. The lab drives shake with trauma, an idea from Squirrel Eiserloh’s GDC 2016 talk Juicing Your Cameras With Math: events add to a value between 0 and 1, time drains it, and the shake offset is proportional to trauma squared. A landing here adds trauma for the distance fallen beyond an ordinary jump: 0 for the 2-tile drop, 0.10 for the 5-tile drop, 0.60 for the 9-tile drop. Squared, the middle one’s shake is a thirty-sixth of the last one’s, so small events barely register and big ones really do.
void add_trauma(float amount) { trauma = std::min(1.0f, trauma + amount); }
void update(float dt) { trauma = std::max(0.0f, trauma - decay * dt); }
Vec2 shake_offset(float time) { // smooth noise, not a new random number each frame
float k = trauma * trauma * max_offset;
return { k * noise(seed_x, time * frequency), k * noise(seed_y, time * frequency) };
}
Squash and stretch work the same way: stretch on take-off, squash on landing by an amount that grows with the landing speed, and ease back over a few frames. Untick Squash and stretch and the box seems to land on nothing.
Takeaways
- A lerp eases out; a critically damped spring eases in and out without overshooting. Step either with time, not per frame.
- A dead zone stops small movements moving the view; look-ahead shows what’s coming; both should ease in.
- Follow height on landing, not every jump, and follow falls once they’re clearly falls.
- Juice scales with the event and decays fast: drive shake with trauma and square it.
- Keep juice and the camera’s offset out of the simulation.
In the lab
The lab’s course has a long run for look-ahead, a tower and a deep shaft for vertical following and a hard landing, and every camera setting live.
Run the course with a locked camera in the feel lab →
Further reading
- Itay Keren, Scroll Back: The Theory and Practice of Cameras in Side-Scrollers (Game Developer, 2015, from his GDC 2015 talk): camera-window, platform-snapping, forward focus, lerp- and physics-smoothing, with the games that introduced them.
- Squirrel Eiserloh, Math for Game Programmers: Juicing Your Cameras With Math (GDC 2016): smoothed camera motion, framing, and trauma-driven shake with smooth noise.
- Steve Swink, Game Feel: A Game Designer’s Guide to Virtual Sensation (Morgan Kaufmann, 2008): polish as one of the three parts of game feel.