Devlog: Intricacies of a good physics-based character controller

This post is mostly done, but is still under construction.

Like many systems, the systems by which a player moves have facets and layers of complexity, built on a layer of physics such as gravity and collision response.

For the most basic implementation, the collision response facet is matched by a movement facet that turns input directly into in-world horizontal motion, after scaling by some constant factor. The gravity facet is matched by a jump facet that sets an upward velocity if grounded and the jump button is pressed. This method is good because it’s simple and responsive, but bad because it’s too responsive. In the real world, nothing is able to start or stop instantly, so players often perceive its stops and direction changes as too snappy.

This can be somewhat mitigated through camera smoothing, but it isn’t a full solution. A facet between the player controller and the input system to introduce artificial input lag can sometimes make the player feel somewhat weighted, but it’s unreliable, hard to tune, still offers very no precision control, and can still feel too sharp. A true solution would be curving movement speed by how long the input has been pressed. This eliminates the start and stop sharpness, and gives good control for fine-tuning, but turning is still too sharp.

Fixed acceleration vs percent drag

Up until recently, my preferred method made movement fully governed by kinematics and used acceleration by a constant instead of setting velocity to a constant: This fixes startup and slowdown, but this requires a careful balance between friction and acceleration that makes top speed hard to determine and fine-tuning difficult. Furthermore, it can make turning around feel too heavy instead.

Tuning friction too low results in good startups but sliding stops (shown). Tuning friction too high results in good stops but slow startups.
Either way, calculating topspeed is impossible without calculus.

Friction-based

My current preference uses exponential decay to mimic dynamic friction: the player’s speed ratio (current/target) will approach the intended direction by a fixed ratio in fixed time. This allows startups to immediately show results but still take a moment to reach top speed, and turnarounds feel much better since the higher difference in velocity means higher acceleration. However, stopping feels like sliding on ice because the function is asymptotic. To address this, we can introduce a static friction facet: whenever speed drops below a certain threshold, and we aren’t trying to move in that direction, stop immediately. Conversely, when we are trying to move in a direction but our speed is below that threshold, immediately boost to that threshold. Combined, this ensures that the player’s character immediately responds to new inputs, but doesn’t sacrifice a sense of weight since it takes a moment to reach the new intended input.



Simulating player input left, then right, then stop. Left: Without thresholding. Right: With thresholding.
Tuned to lose 98.75% current velocity every sec or 35.48% every 1/10 sec, with a topspeed of 6.
Graphs generated by my platforming analysis toolset.

The angled surface problem

Making horizontal movement kinematic introduces multiple problems when traversing sloped surfaces. When moving up a slope quickly, the player retains upward velocity and fly up, which may be intended behaviour. When standing still, the player will begin to slide down. When moving down a slope, the player’s object will cycle between falling and having falling velocity cancelled by collision with ground, creating a zig-zag path.

Moving down slopes can be approached like climbing, by creating the player’s intended movement relative the slope instead of in global space. This requires listening for collisions to retrieve their normals so input can be projected onto the tangent. However, if wall and ceiling climbing is not an intentional feature, there would have to be a limit to the maximum traversible angle. Furthermore, it is important to store contact points so that the connection isn’t accidentally broken by one frame of not colliding.

To approach sliding down slopes, I originally tried suspending gravity, but that only intensified the zig-zagging. My current solution is once again not to treat it as global: if we’re colliding with a surface, instead either cancel it out the unbalanced force by projecting gravity onto the tangent, or simulate gravity manually and project it onto the collision normal.

Free-body diagram of the character, relative to the slope they stand on. W is weight, T is tangent, N is normal, R is normal force. Here, Wx is unbalanced, causing the character to slide down the slope.

Two variants of these movement systems available in both projects in this GitHub repo, but in need of clean up.

Leave a Reply