Aller au contenu principal
Le jeu en direct

Cette page n’est pas encore traduite : la version anglaise est affichée.

Locomotion

Vous déclarez comment une chose se déplace ; personne n’écrit d’intégrateur. Un modèle de mouvement transforme des entrées numérotées en mouvement faisant autorité : il s’appuie sur les collisions, il est enregistré pour la prédiction, et les buffs, les debuffs et le terrain le modifient.

Quand l’utiliser

  • Des entités se déplacent sous l’entrée du joueur (tanks, personnages, véhicules), et le serveur doit faire autorité sur leur déplacement (locomotion).
  • Vous préférez déclarer des limites de vitesse, d’accélération et de rotation plutôt qu’écrire un intégrateur.
  • Le gameplay bouscule les corps : recul (knockback) avec Impulse, Teleport, et des modificateurs temporaires du genre boue.
  • Le mouvement doit paraître instantané : le même modèle déclaré avance sur le serveur et dans la boucle de prédiction.
  • Inutile quand les positions ne changent que par sauts discrets. Un champ synchronisé sur l’entité suffit alors.

Qui fait quoi

ActeurCe qu’il fait sur cette page
schema-authordéclare les modèles de mouvement, les contraintes et les bindings
room-ownerapplique impulsions, téléportations et modificateurs depuis l’hôte
playersoumet des entrées numérotées ; lit l’état du mouvement

Les salles concernées

Ce module fonctionne là où la plateforme fait avancer la simulation : dans les salles (room) déclarées Host = "Backend". Si c’est votre propre serveur de jeu qui possède la simulation (PlayServ en métaserveur), le mouvement, les collisions et la prédiction restent dans le moteur, et cette page décrit l’alternative hébergée par la plateforme, pas une obligation.

En bref

Déclarer le modèle de mouvement du Tank : Locomotion.Tank avec des limites de vitesse, d’accélération et de rotation
[Entity("tank")]
public class Tank
{
    [Sync] public Vector3 Position;
    [Motion(Model.Tank, MaxSpeed = 8f, Acceleration = 14f, TurnRateDeg = 120f)]
    public Motion Motion;
}
@Entity('tank')
export class Tank {
  @Sync() position!: Vector3;
  @Motion({ model: 'tank', maxSpeed: 8, acceleration: 14, turnRateDeg: 120 }) motion: Motion;
}
@entity("tank")
class Tank:
    position: Vector3 = sync()
    motion = locomotion.motion(model="tank", max_speed=8.0, acceleration=14.0, turn_rate_deg=120.0)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

UCLASS(PSEntity = "tank")
class UTank : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSSync) FVector3f Position;

    UPROPERTY(PSMotion = (Model = "Tank", MaxSpeed = "8.0", Acceleration = "14.0", TurnRateDeg = 120))
    FPSMotion Motion;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
Unity C# is the same C# API here — les mêmes attributs compilent dans Unity, sur la base 2021.3 — aucune syntaxe C# 12 ici
[Entity("tank")]
public class Tank
{
    [Sync] public Vector3 Position;
    [Motion(Model.Tank, MaxSpeed = 8f, Acceleration = 14f, TurnRateDeg = 120f)]
    public Motion Motion;
}

L’entrée du client est une intention numérotée. C’est la plateforme qui fait avancer le mouvement :

L’entrée du client comme intention numérotée : Motion.Drive envoyé au rythme des entrées, calculé côté serveur
room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f);   // cl — sent at input rate
room.my<Tank>().motion.drive({ throttle: 1, steer: -0.4 });   // cl — sent at input rate
room.my(Tank).motion.drive(throttle=1.0, steer=-0.4)   # cl — a bot brain drives the same way
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

Room->Entities->Of<UTank>()->Select().GetMine().Then(
    TPSOnResult<UTank*>::CreateWeakLambda(this, [this](const TPSResult<UTank*>& Result)
    {
        if (!Result.HasValue()) { return; }
        // client — sent at input rate, numbered so the platform can acknowledge
        Result.Value()->Motion->SubmitInput(FPSMoveInput{ .Throttle = 1.f, .Steer = -0.4f }, InputSequence);
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — fonctionne tel quel dans Unity avec les types générés
room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f);   // cl — sent at input rate

Les verbes côté serveur :

Verbes serveur : une impulsion de recul, un modificateur de boue de 3 secondes, une téléportation propre
tank.Motion.Impulse(knockback);
tank.Motion.Modify("mud", speedMultiplier: 0.6f, duration: 3.Seconds());
tank.Motion.Teleport(spawn);
tank.motion.impulse(knockback);
tank.motion.modify('mud', { speedMultiplier: 0.6, duration: seconds(3) });
tank.motion.teleport(spawn);
tank.motion.impulse(knockback)
tank.motion.modify("mud", speed_multiplier=0.6, duration=seconds(3))
tank.motion.teleport(spawn)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

// on a dedicated server / master-client host
Tank->Motion->Impulse(EPSImpulseKind::Impulse, KnockbackVelocity);
Tank->Motion->Modify({ .Modifier = TEXT("mud"), .SpeedMultiplier = 0.6f, .For = FPSDuration::Seconds(3.f) });
Tank->Motion->Teleport(SpawnPosition, SpawnFacing);
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — un build client maître dispose des mêmes verbes d’hôte ; un client ordinaire voit leurs résultats sous forme de mouvement prédit puis réconcilié
tank.Motion.Impulse(knockback);
tank.Motion.Modify("mud", speedMultiplier: 0.6f, duration: 3.Seconds());
tank.Motion.Teleport(spawn);

Le modèle

Ce qu’une entité déclare pour se déplacer.

DéclareCe que c’est
modèle de mouvementl’un des modèles fournis — steering, tank, character, vehicle, flying — sous forme de plusieurs implémentations d’un même pas, avec des conditions de choix déclarées et un modèle par défaut. Le pas est une fonction pure (pose, input, dt) → pose
paramètresdes valeurs déclarées que le client peut lire ; sans elles, la prédiction diverge systématiquement. Ce sont des seed — un game designer les ajuste, et un déploiement ne doit pas perdre ses modifications en silence — tandis que les limites sur lesquelles repose l’anti-triche peuvent être managed : une modification depuis la console d’administration est alors refusée
limitesvitesse, accélération et freinage maximaux, rotation maximale par entrée, multiplicateur de marche arrière, et une vitesse de rotation distincte pour les pièces. La rotation par entrée se déclare à part de la vitesse de rotation, exprès : l’une borne un saut instantané, l’autre une vitesse continue, et ce sont deux défenses différentes
comportement sur entrée périméestop, continue until a declared deadline ou continue indefinitely. Il n’y a pas de valeur par défaut « continuer comme avant » — un joueur dont le réseau a lâché continuerait de rouler
tolérance de posel’écart admis entre la pose annoncée et celle du serveur ; elle peut varier selon l’état : à l’arrêt, en mouvement et juste après une réapparition, ce sont trois tolérances différentes
fréquence du pas et plafond de rattrapageà quelle fréquence le pas s’exécute, et combien de pas peuvent s’enchaîner d’un coup quand le serveur est en retard
types d'impulsionchacun avec son intensité et sa façon de s’amortir

Ce qui vaut pour chaque pas.

ToujoursCe que c’est
ce que possède le modulela position et l’orientation d’une entité au fil du temps, et rien d’autre. L’historique de ces positions est conservé par l’entité, pas ici : « où était le joueur il y a 300 ms » a donc exactement une réponse, et non deux tampons aux périodes différentes
autoritéelle appartient au serveur : en mode simulation de la plateforme, un client envoie une intention, jamais un résultat
collisionselles ne sont pas résolues ici : le module demande aux collisions si une position est admissible et quelle est la réponse, et ne tient aucune table de réponses à lui
entréec’est une intention : « avance », « à droite », « tourne la tourelle par là » — acceptée telle quelle, puisqu’elle n’affirme rien sur le monde
une pose annoncéec’est une affirmation, jamais un fait. Hors de la tolérance déclarée, elle est ramenée à la pose admissible la plus proche, ce qui produit un pose_clamped observable
numérotation des entréeselle est obligatoire : un même numéro de séquence n’est jamais appliqué deux fois, et un numéro inférieur est écarté
une limiteelle écrête, elle ne refuse pas : « dix mètres en avant sur ce tick » devient ce qui est admissible, pas une erreur. Le serveur ne peut donc pas produire de pose illégale, et le client n’est pas noyé sous les refus à chaque frame
une impulsion obéit aux mêmes contraintesle recul d’une arme, une poussée, une explosion ou un knockback viennent d’ailleurs que de l’entrée, et aucun ne contourne les collisions : le recul n’enfonce pas un tank dans un rocher
mêmes règles, pas mêmes bitsaucun résultat identique au bit près n’est promis entre plateformes. Ce qui est promis, ce sont les mêmes règles, et la reproductibilité au sein d’une autorité
le pasil est pur et piloté par le tick : le même code fait avancer le mouvement sur le serveur et dans la boucle de prédiction du client, et c’est ce qui rend la réconciliation exacte

Erreurs

  • Une entité sans modèle de mouvement, un preset partiellement rempli, une impulsion sans amortissement déclaré : autant d’échecs de validation au déploiement, pas au runtime.
  • La génération attendue ne correspond pas : c’est un échec de précondition, et il vaut la peine de réessayer après relecture — une entrée envoyée avant une réapparition ne doit pas s’appliquer après.
  • La fréquence des entrées est dépassée : la réponse relève de la catégorie rate limit, avec une échéance.
  • L’entité n’est pas contrôlable : c’est un conflit, et réessayer n’a de sens qu’après un changement d’état.
  • Trois situations ne sont des refus ni dans un sens ni dans l’autre, et toutes trois sont observables. Une entrée périmée est écartée, une intention qui dépasse une limite est écrêtée, et une pose hors tolérance est ramenée avec pose_clamped. Le faire en silence laisserait le client croire que son action a été appliquée, et il divergerait du serveur pour de bon.

Limites

Les chiffres arriveront avec le chapitre sur les limites de la plateforme ; ce qui se passe à chaque plafond est déjà fixé.

  • Vitesse et accélération maximales : écrêtées, jamais refusées.
  • Rotation maximale par entrée : écrêtée.
  • Fréquence des entrées par acteur : une limitation de débit, avec une échéance.
  • Pas de rattrapage : au-delà du plafond, des pas sont écartés avec une conséquence déclarée. Le temps de simulation prend du retard, de façon observable, au lieu de rattraper d’un bond qui donnerait l’impression que tout le monde se téléporte en même temps.
  • Intensité d’une impulsion : écrêtée au maximum déclaré.
  • Impulsions simultanées par entité : une nouvelle évince la plus ancienne, et l’éviction est observable ; aucune sommation illimitée en silence.
  • Durée de vie d’une pose annoncée : une pose plus ancienne que la période déclarée n’est pas prise en compte.