home / blog / gameplay
Published: 2 December 2025 10 min read gameplay
Why Your Character Controller Feels Like It Is Sliding

Why Your Character Controller Feels Like It Is Sliding

When players say a character controller feels slippery, they are describing four separate bugs: braking, ground detection, slope handling and interpolation.

What "it feels slippery" actually means

Last year I ran a six-person playtest on a third-person prototype. All six said the same thing: the character controller feels like it is sliding. Nobody could add any detail, and I spent the first two days looking in the wrong place, tweaking camera smoothing and animation blend times.

The problem showed up once I stepped through a 240 FPS capture frame by frame. After the player released the stick, the character kept moving for 11 more frames, roughly 180 ms at 60 Hz. On a ramp the horizontal speed held, but the changing ground normal pushed the body forward. On a 20 cm step the capsule caught and stalled for a moment.

So "sliding" is not one bug. The deceleration curve, the ground check, slope handling and the physics-to-render sync were each broken, and all four surfaced as the same complaint. This post walks through how I fixed them one at a time.

Rigidbody or kinematic movement?

The first version was the classic Rigidbody plus AddForce pairing. On paper it looks right: the engine simulates, you apply force. In practice, zeroing the physics material friction makes the character slide down every slope, and turning friction on kills your speed whenever you brush a wall. There is no good value in between, because one coefficient serves both floors and walls.

I ended up with kinematic movement. The Rigidbody is still there but isKinematic is on; I integrate velocity myself and sweep with a capsule cast every physics step. I also tried Unity's CharacterController: it exposes nothing beyond step offset and slope limit, you cannot see its depenetration behaviour, and it makes its own decisions when you walk down a ramp.

The rule became simple. Collision queries belong to the physics engine, velocity decisions belong to me. The CharacterMotor class is around 300 lines and every movement number lives there, in one place. The cost is writing knockback and moving-platform carry by hand. The time you save while tuning feel is far larger than that.

The most visible thing you give up is interaction with dynamic bodies. The character no longer pushes crates on its own, because the engine does not see it as a mass any more. When a sweep hits a dynamic Rigidbody I apply an impulse scaled by the relative velocity and the mass ratio; those fifteen lines brought back the part of the lost behaviour that players actually see.

CharacterMotor.cs
public sealed class CharacterMotor : MonoBehaviour { private const int MaxBounces = 4; private const float Skin = 0.02f; private const float WallFriction = 0.85f; private const float MaxSlopeCos = 0.5736f; // cos(55 degrees) private Vector3 CollideAndSlide(Vector3 motion, Vector3 origin, int depth) { if (depth >= MaxBounces || motion.sqrMagnitude < 1e-6f) return Vector3.zero; Vector3 dir = motion.normalized; GetCapsulePoints(origin, out Vector3 p0, out Vector3 p1); if (!Physics.CapsuleCast(p0, p1, _radius, dir, out RaycastHit hit, motion.magnitude + Skin, _collisionMask)) return motion; // Stop just short of the surface, then slide with whatever is left. Vector3 travelled = dir * Mathf.Max(0f, hit.distance - Skin); Vector3 leftover = Vector3.ProjectOnPlane(motion - travelled, hit.normal); // Walls bleed speed; walkable ground keeps it, so slopes do not slow you down. if (hit.normal.y < MaxSlopeCos) leftover *= WallFriction; return travelled + CollideAndSlide(leftover, origin + travelled, depth + 1); } }
csharpCharacterMotor.cs

Ground detection and slopes with capsule casts

A single downward ray falls apart at edges. When the character's centre passes the platform edge by 3-4 cm the ray misses, the body counts as airborne for one frame and is grounded again on the next. Instead I use a CapsuleCast with the same radius as the body: 0.15 m downward, with a 0.02 m skin allowance.

The walkable test reads the vertical component of the hit normal: hit.normal.y >= 0.5736f, the cosine of 55 degrees. To stop chattering right at the limit I added hysteresis, entering the grounded state at 55 degrees and staying grounded up to 58. That single comparison ended the grounded/airborne oscillation on steep ramp edges.

On slopes the real work is projecting velocity onto the right plane. Apply raw horizontal input and you accelerate downhill and crawl uphill. Flattening the input vector with ProjectOnPlane against the ground normal makes walking speed independent of the incline. I also snap to ground for up to 0.3 m on descending ramps; without it, every downhill transition turns into a small hop.

A single cast returns a single hit, and where two surfaces meet the normal you get flips from frame to frame. Switching to CapsuleCastNonAlloc with an eight-result buffer and picking the steepest walkable normal removed that instability. Putting the queries on their own layer mask helped as well: excluding triggers and damage volumes cut the cast cost by about a third.

Step offset and sliding along walls

Step climbing takes three sweeps: up by the step height (0.35 m), forward by the movement, then back down. If all three are clear and the downward sweep lands on a walkable normal, I move the body there; otherwise I cancel and treat the surface as a wall. Those three extra casts account for about 0.06 ms of the 0.28 ms per-step motor cost I measured.

Wall sliding is the collide-and-slide loop itself. On a hit I project the remaining motion onto the contact plane and sweep again. One projection is not enough in interior corners, so I allow at most four iterations and discard the leftover motion if the fourth still hits. In the build where iterations were unbounded, frame time spiked to 4 ms in tight corners.

No matter how careful the sweep is, the body eventually drifts into geometry, especially when a moving platform pushes it. At the end of every physics step I measure overlap with Physics.ComputePenetration and push out, capped at 0.2 m per step. Without that cap the character once teleported through a wall: the measured overlap exceeded 3 m because a mesh collider in the scene had a negative scale.

The step sweep has two guards on it. The first is that it only runs while grounded; when I left it enabled in the air, the character climbed itself up any wall it ran into. The second is that the upward sweep cancels the whole step attempt if it hits a ceiling, otherwise the body briefly pushes into the ceiling in low corridors.

Acceleration, braking and air control

Braking is the most direct cause of the sliding feeling. Instead of pulling velocity toward a target with Lerp, I keep separate rates: 45 m/s² ground acceleration, 60 m/s² braking, 12 m/s² in air. At a 6.2 m/s walk speed that is a full stop roughly 100 ms after release, close to half the 180 ms testers complained about.

Braking being higher than acceleration is deliberate. Make them symmetric and the character feels unresponsive; push braking much higher and it feels robotic. A ratio between 1.3 and 1.5 has been consistently good across my projects. I tried exposing this as an AnimationCurve for designers; it produced nothing better than two numbers and made debugging harder.

In the air the rules change. I preserve momentum, apply no braking, add input at the low acceleration rate and clamp horizontal speed to the ground maximum. The player can steer mid-air but cannot gain speed by jumping. In the build where I forgot the clamp, testers reached 1.4 times the intended speed with a jump-sprint combo and pushed straight past the level bounds.

Two settings remain on the input side. I cut the analogue dead zone at 0.15 and remap the rest of the range back to zero-to-one, otherwise slow walking is unreachable. I also split facing from movement direction: the body turns at 720 degrees per second while the velocity vector changes immediately, because putting rotation ahead of velocity made sharp direction changes feel delayed.

Fixed timestep, interpolation and an order

The motor runs at a 50 Hz fixed step while the display refreshes at 144 Hz. Write the position directly in the physics step and some render frames show the same pose twice. The resulting micro-jitter comes back as "it stutters", and most people assume it is a frame drop and go looking in the GPU profile.

The fix is to keep the previous and current physics poses and interpolate between them in Update by the leftover time ratio. The visual root, meaning the mesh and the camera target, rides the interpolated pose while the collision capsule stays on the physics pose. Binding the camera to that same interpolated pose in LateUpdate matters too: a one-frame-late read makes the character appear to swim in front of the camera.

The second benefit of a fixed step is repeatability. Because movement always passes through the same number of steps, the same input produces the same path even when frame time swings; without it, jump distance varied by about 20 cm between 60 Hz and 144 Hz. When a long frame arrives I cap the accumulated time at four steps, otherwise the simulation never catches up after a loading hitch.

If you are hearing the same complaint, work in this order: measure and fix the stop time first, then add ground snapping, then the step sweep, then interpolation last. Draw the capsule casts and the ground normal on screen at every stage; that visualisation took me half a day to write and showed every bug at a glance for the next three weeks. "It feels like it is sliding" is not a vague sentence, it is the sum of four numbers you have not measured yet.

← All posts