start / blog / gameplay
Veröffentlicht: 2. Dezember 2025 10 Min. Lesezeit gameplay
Warum sich Ihr Charakter-Controller anfühlt, als würde er rutschen

Warum sich Ihr Charakter-Controller anfühlt, als würde er rutschen

Wenn Spieler sagen, ein Charakter-Controller fühlt sich rutschig an, beschreiben sie vier separate Bugs: Bremsen, Bodenerkennung, Hangbehandlung und Interpolation.

Was „es fühlt sich rutschig an“ eigentlich bedeutet

Letztes Jahr führte ich einen Sechs‑Personen‑Playtest mit einem Third‑Person‑Prototyp durch. Alle sechs sagten dasselbe: Der Charakter-Controller fühlt sich an, als würde er rutschen. Niemand konnte Details nennen, und ich verbrachte die ersten zwei Tage am falschen Ort, justierte Kameraglatten und Animations‑Blend‑Times.

Das Problem zeigte sich, als ich einen 240 FPS‑Capture Frame für Frame durchging. Nachdem der Spieler den Stick losgelassen hatte, bewegte sich der Charakter noch 11 Frames weiter, ungefähr 180 ms bei 60 Hz. Auf einer Rampe hielt die horizontale Geschwindigkeit, aber die sich ändernde Bodennormale schob den Körper vorwärts. Auf einer 20 cm‑Stufe fing die capsule an und stockte einen Moment.

Also ist „Rutschen“ kein einzelner Bug. Die Verzögerungskurve, die Bodenerkennung, die Hangbehandlung und die Physik‑zu‑Render‑Synchronisation waren jeweils fehlerhaft, und alle vier traten als dieselbe Beschwerde auf. Dieser Beitrag erklärt, wie ich sie nacheinander behoben habe.

Rigidbody oder kinematische Bewegung?

Die erste Version war das klassische Rigidbody + AddForce-Paar. Auf dem Papier sieht das richtig aus: Die Engine simuliert, du wendest Kraft an. In der Praxis lässt das Nullsetzen der Reibung im Physics‑Material den Charakter jede Rampe hinunterrutschen, und das Einschalten der Reibung tötet deine Geschwindigkeit, sobald du eine Wand streifst. Es gibt keinen guten Mittelwert, weil ein Koeffizient sowohl für Böden als auch für Wände gilt.

Ich landete bei kinematischer Bewegung. Das Rigidbody ist noch da, aber isKinematic ist aktiviert; ich integriere die Geschwindigkeit selbst und sweep mit einem Capsule‑Cast jeden Physik‑Step. Ich probierte auch Unitys CharacterController: Er bietet nur Step Offset und Slope Limit, man sieht sein Depenetration‑Verhalten nicht und er trifft eigene Entscheidungen, wenn man eine Rampe hinuntergeht.

Die Regel wurde einfach. Kollisionsabfragen gehören zur Physics‑Engine, Geschwindigkeitsentscheidungen zu mir. Die CharacterMotor-Klasse umfasst etwa 300 Zeilen und jede Bewegungszahl lebt dort, an einem Ort. Der Preis ist, Knockback und das Tragen von beweglichen Plattformen von Hand zu schreiben. Die Zeit, die du beim Feintuning sparst, ist weitaus größer.

Das Sichtbarste, was du aufgibst, ist die Interaktion mit dynamischen Bodies. Der Charakter schiebt Kisten nicht mehr selbst, weil die Engine ihn nicht mehr als Masse sieht. Wenn ein Sweep einen dynamischen Rigidbody trifft, wende ich einen Impuls an, skaliert mit der relativen Geschwindigkeit und dem Massenverhältnis; diese fünfzehn Zeilen brachten das verlorene Verhalten zurück, das Spieler tatsächlich sehen.

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

Bodenerkennung und Hänge mit Capsule‑Casts

Ein einzelner nach unten gerichteter Ray zerfällt an Kanten. Wenn das Zentrum des Charakters die Plattformkante um 3‑4 cm überschreitet, verfehlt der Ray, der Body gilt für einen Frame als in der Luft und ist im nächsten wieder am Boden. Stattdessen benutze ich einen CapsuleCast mit demselben Radius wie der Body: 0,15 m nach unten, mit einem 0,02 m Skin‑Allowance.

Der begehbare Test liest die vertikale Komponente der Treffer‑Normalen: hit.normal.y >= 0.5736f, der Kosinus von 55 Grad. Um das Zucken genau an der Grenze zu stoppen, fügte ich Hysterese hinzu, trat bei 55 Grad in den grounded‑Zustand ein und blieb bis 58 Grad grounded. Dieser einzelne Vergleich beendete das grounded/airborne‑Oszillieren an steilen Rampenkanten.

Auf Hängen besteht die eigentliche Arbeit darin, die Geschwindigkeit auf die richtige Ebene zu projizieren. Rohes horizontales Input anwenden lässt dich bergab beschleunigen und bergauf kriechen. Das Flachlegen des Input‑Vektors mit ProjectOnPlane gegen die Bodennormale macht die Gehgeschwindigkeit unabhängig vom Gefälle. Ich snappe außerdem bis zu 0,3 m zum Boden bei absteigenden Rampen; ohne das wird jeder Abstieg zu einem kleinen Sprung.

Ein einzelner Cast liefert einen einzelnen Treffer, und wo zwei Oberflächen aufeinandertreffen, flippt die Normale von Frame zu Frame. Der Wechsel zu CapsuleCastNonAlloc mit einem acht‑Ergebnis‑Puffer und das Auswählen der steilsten begehbaren Normale entfernte diese Instabilität. Die Queries auf eine eigene Layer‑Mask zu legen half ebenfalls: Trigger und Damage‑Volumes auszuschließen reduzierte die Cast‑Kosten um etwa ein Drittel.

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.

← Alle Beiträge