After making a simple character controller in JS for my 7DFPS 2020 entry, I wanted to make a more refined version. The one I made for the jam simply directly mapped input to movement velocity, which is not too bad for at slow speeds but feels very 'arcadey' for anything faster. Of course the style of your character controller will depend on the type of game you're making, the slow moving direct control was fine for a puzzle game, but as I wasn't planning to make a full game I needed to pick a game to emulate.
Inspired by recently re-playing quake 3 arena I thought I'd try to create something with a similar feel, the fast movement speed, the relatively low air control, and something which could handle external forces such as jump pads and rocket jumping, whilst still restricting myself to using axis-aligned bounding boxes for physics for simplicity.
Play the Character Controller Demo
If you're interested in the precise implementation the code is available on github.
A extremely high-level summary of how this controller works would be:
- Track Current Velocity.
- Update horizontal velocity based on smoothed input, capped by a maximum speed.
- Update vertical velocity by applying gravity and track if touching the ground.
- Use velocity deltas to impart significant changes, such as jumping, launch pads or knockback.
- Track "lastGrounded" and "lastJumpAttempt" times for niceties such as allowing jump just after running off a ledge, and allowing the jump key to be pressed just before landing.
To get the right feel there were a few nuances:
- Air movement has reduced acceleration and lower maximum speed.
- To support external forces and faster grounded speed than air speed, only allow deceleration via input when travelling faster than maximum speed.
- A drag force is always applied in the air, compared to a slowing force only being applied when there is no input if grounded.
- To support sliding whilst grounded a different slowing formula was needed above maximum movement speed.
I tried a few approaches to dealing with external launch forces, including a smash bros style tracking the launch separately and allowing air movement on top of that, which if a game had knockback as a keep mechanic might be appropriate, however in the end simply allowing you to slow yourself when above "maximum air speed" and allowing normal movement with in that resulted in the best feel.
Fun with Physics
There were a few interesting issues when re-implementing the physics from the jam game. Making sure you can slide along the face of a wall which might be made up of an arbitrary number of boxes was not trivial, as was making sure you attempt to slide in direction desired by the player when hitting the corner of a box, rather than using what is physically most accurate, which turns out is irritating. Who'd have thought ignoring player intent might frustrate your player.
Another challenge was getting the player to fit through gaps precisely the same size as them, I had to set the final position of the player after a collision based on the outer bounds of the box they collided with added to the extents of the player to avoid floating point precision issues which occurred when naively scaling the movement amount according to the calculated collision time.
Finally both for the stability of physics and the character movement code I implemented a cap on amount of frame time to consider, some of the formulae used in the movement code will have different behaviour for different frame rates (you'll come to a stop quicker with a lower frame rate) and because the collision detection is discrete if you travel considerably faster than your bounding box size per frame you are in danger of skipping through walls. To robustly solve this I would need to implement a proper fixed time step and multi-sampling in my physics (or an alternative) for high speed objects, however for the speeds in this demo a simple cap on the frame time was sufficient.
Using a Level Editor
The other interesting part of this demo was in order to easily test different scenarios I really needed a better way of creating test levels than laying out collision boxes in code. In order to do this I made some levels using TrenchBroom, and then implemented a very basic .map loader (restricting myself to brushes that were AABB - given that's all I supported with my physics) using this Quake MAP specs page to guide me.
It was extremely tempting to go down the rabbit hole of supporting all the functionality of this level editor, and that might be something I try in future, but that's outside the scope of this demo, and it would probably lead me into trying to write support for more complex collision shapes and object orientated bounding boxes, which would also be fun I think I should try to make some games with what I've got before I do that!