accueil / blog / gameplay
Publié: 2 décembre 2025 10 min de lecture gameplay
Pourquoi votre contrôleur de personnage semble glisser

Pourquoi votre contrôleur de personnage semble glisser

Lorsque les joueurs disent qu’un contrôleur de personnage glisse, ils décrivent quatre bugs : freinage, détection du sol, gestion des pentes et interpolation.

Ce que signifie réellement « glissant »

L’an dernier, j’ai organisé un test de jeu à six personnes sur un prototype à la troisième personne. Les six ont dit la même chose : le contrôleur de personnage semble glisser. Personne n’a pu apporter de détail, et j’ai passé les deux premiers jours à regarder au mauvais endroit, à ajuster le lissage de la caméra et les temps de transition des animations.

Le problème est apparu quand j’ai parcouru image par image une capture à 240 FPS. Après que le joueur a relâché le stick, le personnage a continué à se déplacer pendant 11 images supplémentaires, soit environ 180 ms à 60 Hz. Sur une rampe, la vitesse horizontale était maintenue, mais la normale du sol changeante poussait le corps vers l’avant. Sur une marche de 20 cm, la capsule s’est accrochée et s’est arrêtée un instant.

Donc « glissant » n’est pas un seul bug. La courbe de décélération, la vérification du sol, la gestion des pentes et la synchronisation physique‑rendu étaient chacune défectueuses, et les quatre se sont manifestées sous la même plainte. Cet article explique comment je les ai corrigées une à une.

Mouvement avec Rigidbody ou cinématique ?

La première version utilisait le couple classique Rigidbody + AddForce. En théorie, cela paraît correct : le moteur simule, vous appliquez une force. En pratique, annuler le frottement du matériau physique fait glisser le personnage sur chaque pente, et activer le frottement tue votre vitesse dès que vous effleurez un mur. Il n’existe pas de valeur intermédiaire satisfaisante, car un même coefficient sert à la fois aux sols et aux murs.

J’ai fini par un mouvement cinématique. Le Rigidbody reste présent mais isKinematic est activé ; j’intègre la vélocité moi‑même et je réalise un balayage avec un capsule cast à chaque pas de physique. J’ai aussi testé le CharacterController d’Unity : il n’expose rien au‑delà du décalage de marche et de la limite de pente, vous ne voyez pas son comportement de dépénétration, et il prend ses propres décisions quand vous descendez une rampe.

La règle est devenue simple. Les requêtes de collision appartiennent au moteur physique, les décisions de vélocité me reviennent. La classe CharacterMotor fait environ 300 lignes et chaque paramètre de mouvement y vit, en un seul endroit. Le coût est d’écrire à la main le recul et le transport sur plateformes mobiles. Le temps gagné lors du réglage du feeling dépasse largement cet effort.

Ce que vous perdez le plus visiblement, c’est l’interaction avec les corps dynamiques. Le personnage ne pousse plus les caisses de lui‑même, car le moteur ne le considère plus comme une masse. Lorsqu’un sweep touche un Rigidbody dynamique, j’applique une impulsion proportionnelle à la vélocité relative et au rapport de masses ; ces quinze lignes ont restauré le comportement perdu que les joueurs remarquent réellement.

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

Détection du sol et pentes avec des capsule casts

Un simple rayon descendant se désintègre aux bords. Quand le centre du personnage dépasse le bord de la plateforme de 3‑4 cm, le rayon manque, le corps est considéré aérien pendant une image puis à nouveau au sol à la suivante. À la place, j’utilise un CapsuleCast avec le même rayon que le corps : 0,15 m vers le bas, avec une marge de peau de 0,02 m.

Le test de marche lit la composante verticale de la normale de l’impact : hit.normal.y >= 0.5736f, le cosinus de 55 degrés. Pour éviter le chatter à la limite, j’ai ajouté de l’hystérésis, entrant en état au sol à 55° et restant au sol jusqu’à 58°. Cette unique comparaison a éliminé l’oscillation sol/air sur les bords de rampes raides.

Sur les pentes, le vrai travail consiste à projeter la vélocité sur le bon plan. Appliquer l’entrée horizontale brute vous fait accélérer en descente et ramper en montée. Aplatir le vecteur d’entrée avec ProjectOnPlane contre la normale du sol rend la vitesse de marche indépendante de l’inclinaison. J’ai aussi ancré le personnage au sol jusqu’à 0,3 m sur les rampes descendantes ; sans cela, chaque transition en descente se transforme en petit saut.

Un seul cast renvoie un seul impact, et là où deux surfaces se rencontrent la normale bascule d’une image à l’autre. Passer à CapsuleCastNonAlloc avec un tampon de huit résultats et choisir la normale marchable la plus raide a supprimé cette instabilité. Placer les requêtes sur leur propre masque de couche a également aidé : exclure les déclencheurs et les volumes de dégâts a réduit le coût du cast d’environ un tiers.

Décalage de marche et glissement le long des murs

L'escalade de marche utilise trois balayages : monter de la hauteur de marche (0,35 m), avancer selon le mouvement, puis redescendre. Si les trois sont libres et que le balayage descendant atterrit sur une normale franchissable, je déplace le corps à cet endroit ; sinon j'annule et considère la surface comme un mur. Ces trois casts supplémentaires représentent environ 0,06 ms du coût moteur de 0,28 ms par pas que j'ai mesuré.

Le glissement contre les murs est la boucle de collision‑et‑glissement elle‑même. Lors d'un impact, je projette le mouvement restant sur le plan de contact et je refais un balayage. Une projection ne suffit pas dans les coins intérieurs, donc je permets au maximum quatre itérations et j'abandonne le mouvement restant si la quatrième touche encore. Dans la version où les itérations n'étaient pas limitées, le temps d'image a bondi à 4 ms dans les coins serrés.

Aussi précis que soit le balayage, le corps finit par dériver dans la géométrie, surtout lorsqu'une plateforme mobile le pousse. À la fin de chaque pas de physique je mesure le chevauchement avec Physics.ComputePenetration et je le repousse, limité à 0,2 m par pas. Sans cette limite le personnage a une fois traversé un mur : le chevauchement mesuré dépassait 3 m parce qu'un collider maillé dans la scène avait une échelle négative.

Le balayage de marche possède deux garde‑fous. Le premier est qu'il ne s'exécute que lorsqu'il est au sol ; quand je l'ai laissé activé en l'air, le personnage s'est escaladé tout mur qu'il a heurté. Le second est que le balayage ascendant annule toute tentative de marche s'il touche un plafond, sinon le corps pousse brièvement contre le plafond dans les couloirs étroits.

Accélération, freinage et contrôle aérien

Le freinage est la cause la plus directe de la sensation de glissement. Au lieu de rapprocher la vitesse d'une cible avec Lerp, je conserve des taux séparés : 45 m/s² d'accélération au sol, 60 m/s² de freinage, 12 m/s² en l'air. À une vitesse de marche de 6,2 m/s, cela permet un arrêt complet environ 100 ms après le relâchement, proche de la moitié des 180 ms dont se plaignaient les testeurs.

Le fait que le freinage soit supérieur à l'accélération est délibéré. Les rendre symétriques rend le personnage insensible ; augmenter le freinage de façon excessive le rend robotique. Un ratio entre 1,3 et 1,5 a été constamment satisfaisant dans mes projets. J'ai essayé d'exposer cela comme une AnimationCurve pour les designers ; cela n'a produit rien de plus que deux nombres et a compliqué le débogage.

En l'air les règles changent. Je conserve l'élan, n'applique aucun freinage, ajoute l'entrée au taux d'accélération faible et limite la vitesse horizontale au maximum au sol. Le joueur peut diriger en l'air mais ne peut pas gagner de vitesse en sautant. Dans la version où j'avais oublié la limitation, les testeurs ont atteint 1,4 fois la vitesse prévue avec une combinaison saut‑sprint et ont dépassé les limites du niveau.

Deux réglages restent du côté de l'entrée. Je coupe la zone morte analogique à 0,15 et remappe le reste de la plage de zéro à un, sinon la marche lente est inatteignable. Je sépare également l'orientation de la direction de déplacement : le corps tourne à 720 degrés par seconde tandis que le vecteur de vitesse change immédiatement, car placer la rotation avant la vitesse rendait les changements de direction brusques retardés.

Pas fixe, interpolation et un ordre

Le moteur fonctionne à un pas fixe de 50 Hz tandis que l'affichage se rafraîchit à 144 Hz. J'écris la position directement dans le pas de physique et certaines images de rendu affichent la même pose deux fois. Le micro‑tremblement qui en résulte revient sous la forme de « ça saccade », et la plupart des gens supposent qu'il s'agit d'une perte d'image et vont chercher dans le profil GPU.

La solution consiste à conserver les poses physiques précédente et actuelle et à interpoler entre elles dans Update selon le ratio du temps restant. La racine visuelle, c'est‑à‑dire le maillage et la cible de la caméra, suit la pose interpolée tandis que la capsule de collision reste sur la pose physique. Lier la caméra à cette même pose interpolée dans LateUpdate compte aussi : une lecture retardée d'une image fait apparaître le personnage comme nageant devant la caméra.

Le deuxième avantage d'un pas fixe est la répétabilité. Parce que le mouvement passe toujours par le même nombre de pas, la même entrée produit le même trajet même lorsque le temps d'image varie ; sans cela, la distance de saut variait d'environ 20 cm entre 60 Hz et 144 Hz. Lorsqu'une longue image arrive, je limite le temps accumulé à quatre pas, sinon la simulation ne rattrape jamais le retard après un accroc de chargement.

Si vous entendez la même plainte, travaillez dans cet ordre : mesurez et corrigez d'abord le temps d'arrêt, puis ajoutez le snapping au sol, ensuite le balayage de marche, enfin l'interpolation. Dessinez les casts de capsule et la normale du sol à l'écran à chaque étape ; cette visualisation m'a pris une demi‑journée à écrire et a montré chaque bug d'un coup d'œil pendant les trois semaines suivantes. « On a l'impression que ça glisse » n'est pas une phrase vague, c'est la somme de quatre nombres que vous n'avez pas encore mesurés.

← Tous les articles