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
| Acteur | Ce qu’il fait sur cette page |
|---|---|
schema-author | déclare les modèles de mouvement, les contraintes et les bindings |
room-owner | applique impulsions, téléportations et modificateurs depuis l’hôte |
player | soumet 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
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)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[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 :
Motion.Drive envoyé au rythme des entrées, calculé côté serveurroom.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f); // cl — sent at input rateroom.my<Tank>().motion.drive({ throttle: 1, steer: -0.4 }); // cl — sent at input rateroom.my(Tank).motion.drive(throttle=1.0, steer=-0.4) # cl — a bot brain drives the same wayLa 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.room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f); // cl — sent at input rateLes verbes côté serveur :
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)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.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éclare | Ce que c’est |
|---|---|
modèle de mouvement | l’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ètres | des 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 |
limites | vitesse, 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ée | stop, 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 pose | l’é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'impulsion | chacun avec son intensité et sa façon de s’amortir |
Ce qui vaut pour chaque pas.
| Toujours | Ce que c’est |
|---|---|
ce que possède le module | la 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 |
collisions | elles 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ée | c’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ée | c’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ées | elle 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 limite | elle é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 contraintes | le 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 bits | aucun 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 pas | il 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.