To start this off, I would like to make the disclaimer that I am not a professional Quake player. In fact, I have only played Quake 1 and Quake Live, and my combined playtime is in the magnitude of hours, not tens or thousands.
That said, every now and then, I get the feeling to play Quake. One should play the classics, right? The feeling never seems to last, but I do always enjoy Quake’s movement. Delightful to zoom around that starting chamber. Since I am currently working on a first-person puzzle game, I figure I should find out what makes Quake feel so good, to replicate that feeling in my game.
The most visually distinct “effect” in Quake is the way the camera leans when strafing. This might also be happening when you move forward and turn—which could imply that this lean is actually based on the player’s velocity on the right/left axis, and that turning has a bit of inertia—but the effect is hard to detect by just eyeballing. It should also be noted, that this lean does not apply to the gun in the middle: it actually accentuates the lean by staying upright.
The camera bobs slightly on the up/down axis, while the gun has a more pronounced bob, down and towards the player. The gun doesn’t seem to bob in a sine wave either, like the camera: it seems to stay relatively still, until it dips noticeably whenever the camera dips. Modern gun bobs are more elaborate, but I do think there is a certain charm to Quake’s bob.
When releasing the movement keys, the player will slow to a stop over a short period of time. I can not tell if there is acceleration when you start moving, but if there is, it is very sharp. Still, moving forward does feel very smooth, so maybe it does accelerate over a few frames. This effect is even more pronounced in Quake Live: the movement feels really smooth. I hope the source will enlighten me further on this.
If you would like to check out the source for yourself, it is on id’s GitHub.
Lucky me, the leaning code is the first function of view.c. The lean is based on the player’s velocity on the right axis, as I suspected, and it seems linear. Simple to implement, for a nice effect.
Head bobbing is implemented in the very next function of view.c. The code is pretty much what you’d expect, with a bobbing period of 0.6 seconds. The bobbing intensity is based on the player’s velocity on the XY plane. This is good for fading out the effect, taking into account how quickly the player accelerates / brakes.
Turns out, the gun does not actually bob up and down with varying speed: it bobs in sync with the camera, forwards and backwards. Because of how perspective projection works, the effect is more pronounced as it gets closer to the camera, which led me to believe the bob is not a sine wave, assuming the motion was up/down. This is why it is great to have the source available!
Next up, from sv_user.c: SV_UserFriction.
The friction function is probably the spiciest code I have reviewed for this post, it’s easiest to demonstrate by just showing the code (edited for readability):
float speed = length(velocity.xz); float control = speed < sv_stopspeed.value ? sv_stopspeed.value : speed; float newspeed = (speed - host_frametime * control * friction) / speed; if (newspeed < 0) newspeed = 0; velocity *= newspeed;
Is that not wild? Maybe not. It is basically just like, friction, but
what gets me is the
control variable. The amount of friction applied
goes down as your velocity decreases, until you pass the “stop speed”
threshold, after which the friction stays constant. This causes this
sort of braking effect in game, which I really enjoy.
The acceleration function is what you would expect, like the friction was, but has no extra spices: SV_Accelerate calculates an acceleration value based on the target speed, then applies that acceleration to the player’s velocity. The acceleration is 10 * frametime * wishspeed, so wishspeed is reached in about a tenth of a second, according to my napkin calculations. 100 milliseconds is many frames! I am not sure how I was fooled into thinking the movement was so sharp I could not tell if there was acceleration, but testing it out now, the acceleration is quite clear. Never trust eyeballed observations!
What did I learn? That I should never trust my own judgement on movement. Also, that a good acceleration function combined with a good friction function make for some good movement! With a little bobbing and leaning sprinkled on top, you get some excellent movement out of a few lines of code.
I hope you learned something, or were entertained. Nevertheless, if you have comments, feel free to aim them at my Fediverse inbox or my email.