Esta página aún no está traducida: se muestra la versión en inglés.
Locomotion
Declaras cómo se mueve algo; nadie escribe un integrador. Un modelo de movimiento convierte la entrada numerada en movimiento autoritativo, integrado con las colisiones, registrado para la predicción y modificado por buffs, debuffs y el terreno.
Cuándo usarlo
- Las entidades se mueven según la entrada del jugador (tanques, personajes, vehículos), y el servidor tiene que tener la autoridad sobre el movimiento.
- Prefieres declarar límites de velocidad, aceleración y giro antes que escribir un integrador.
- El gameplay empuja los cuerpos: empujes (knockback) con
Impulse,Teleporty modificadores tipo barro con duración. - El movimiento tiene que sentirse instantáneo: el mismo modelo declarado avanza en el servidor y en el bucle de la predicción.
- No lo uses si las posiciones solo cambian en pasos discretos. Para eso ya alcanza un campo sincronizado en la entidad.
Quién hace qué
| Actor | Qué hace en esta página |
|---|---|
schema-author | declara modelos de movimiento, restricciones y bindings |
room-owner | aplica impulsos, teletransportes y modificadores desde el host |
player | envía entrada numerada; lee el estado del movimiento |
A qué salas se aplica
Este módulo se ejecuta donde la plataforma avanza la simulación: en las salas (room) declaradas con Host = "Backend". Si tu propio servidor de juego es dueño de la simulación (PlayServ como metaservidor), el movimiento, las colisiones y la predicción se quedan del lado del motor, y esta página describe la alternativa alojada en la plataforma, no un requisito.
En resumen
Tank: Locomotion.Tank con límites de velocidad, aceleración y giro[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)Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.
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;
}La entrada del cliente es una intención numerada. La plataforma avanza el movimiento:
Motion.Drive se envía a la frecuencia de entrada y avanza del lado del servidorroom.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 wayTodavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.
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 rateVerbos del lado del servidor:
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)Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.
// 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);El modelo
Qué declara una entidad para moverse.
| Qué se declara | Qué es |
|---|---|
modelo de movimiento | uno del conjunto incluido — steering, tank, character, vehicle, flying — como varias implementaciones de un mismo paso, con condiciones de elección declaradas y uno por defecto. El paso es una función pura (pose, input, dt) → pose |
parámetros | valores declarados que el cliente puede leer; sin ellos, la predicción diverge de forma sistemática. Son seed — un diseñador los ajusta y un deploy no debe perder esos cambios en silencio —, mientras que los límites en los que se apoya el antitrampas pueden ser managed, y entonces se rechaza cualquier edición desde la consola de administración |
límites | velocidad, aceleración y frenado máximos, giro máximo por entrada, el multiplicador de marcha atrás y una velocidad de rotación aparte para las partes. El giro por entrada se declara separado de la velocidad de rotación a propósito: uno acota un salto instantáneo y la otra, una tasa continua; son defensas distintas |
comportamiento ante entrada obsoleta | stop, continue until a declared deadline o continue indefinitely. No hay un valor por defecto de “seguir como antes”: un jugador que se quedó sin red seguiría manejando |
tolerancia de pose | cuánto puede alejarse de la del servidor la pose que informa el cliente; puede variar según el estado: quieto, en movimiento y justo después de reaparecer son tres tolerancias distintas |
frecuencia del paso y tope de recuperación | con qué frecuencia se ejecuta el paso y cuántos pasos se pueden dar de una vez cuando el servidor va atrasado |
tipos de impulso | cada uno con su magnitud y su forma de decaer |
Qué se cumple en cada paso.
| Siempre | Qué es |
|---|---|
el módulo es dueño de | la posición y orientación de una entidad en el tiempo, y de nada más. El historial de esas posiciones lo guarda la entidad, no este módulo, así que “¿dónde estaba el jugador hace 300 ms?” tiene exactamente una respuesta, y no dos buffers con períodos distintos |
autoridad | es del servidor: en el modo simulación de la plataforma, el cliente envía una intención, nunca un resultado |
colisiones | no se resuelven aquí: el módulo le pregunta al sistema de colisiones si una posición es admisible y cuál es la respuesta, y no tiene una tabla de respuestas propia |
entrada | es una intención: “adelante”, “derecha”, “gira la torreta hacia allá”. Se acepta tal como llega, porque no afirma nada sobre el mundo |
una pose informada | es una afirmación, nunca un hecho. Fuera de la tolerancia declarada se ajusta a la pose admisible más cercana, y eso produce un pose_clamped observable |
numeración de la entrada | es obligatoria: el mismo número de secuencia nunca se aplica dos veces, y uno menor se descarta |
un límite | recorta, no rechaza: “diez metros hacia adelante en este tick” se convierte en lo que sea admisible, no en un error. Así, el servidor no puede producir una pose ilegal y el cliente no recibe una avalancha de rechazos en cada frame |
un impulso obedece las mismas restricciones | el retroceso, un empujón, una explosión y el empuje llegan desde fuera de la entrada, y ninguno se salta las colisiones: el retroceso no mete a un tanque dentro de una roca |
reglas idénticas, no bits idénticos | no se promete un resultado idéntico bit a bit entre plataformas. Lo que se promete son las mismas reglas y la reproducibilidad dentro de una misma autoridad |
el paso | es puro y avanza por ticks: el mismo código calcula el movimiento en el servidor y dentro del bucle de predicción del cliente, y eso es lo que hace exacta la reconciliación |
Errores
- Un modelo de movimiento que falta en la entidad, un preset a medio llenar y un impulso sin decaimiento declarado son fallas de validación en el deploy, no en runtime.
- La generación esperada no coincide: es una falla de precondición, y conviene repetir después de volver a leer. La entrada enviada antes de una reaparición no debe aplicarse después de ella.
- Si se supera la frecuencia de entrada, la respuesta cae en la categoría de rate limit, con un plazo.
- La entidad no es controlable: es un conflicto, y repetir solo tiene sentido después de que cambie el estado.
- Hay tres casos que no son un rechazo en ninguna dirección, y los tres son observables. La entrada obsoleta se descarta, una intención que supera un límite se recorta y una pose fuera de la tolerancia se ajusta con
pose_clamped. Si cualquiera de ellos pasara en silencio, el cliente creería que se aplicó y quedaría desincronizado del servidor para siempre.
Límites
Los números llegarán con el capítulo de límites de la plataforma; lo que pasa en cada tope ya está definido.
- Velocidad y aceleración máximas: se recortan, nunca se rechazan.
- Giro máximo por entrada: se recorta.
- Frecuencia de entrada por actor: un rate limit con plazo.
- Pasos de recuperación: más allá del tope, los pasos se descartan con una consecuencia declarada. El tiempo de simulación se atrasa, de forma observable, en lugar de ponerse al día de un salto que parece un teletransporte de todos a la vez.
- Magnitud del impulso: se recorta al máximo declarado.
- Impulsos simultáneos por entidad: uno nuevo desplaza al más antiguo, y ese desplazamiento es observable; no hay una suma silenciosa sin límite.
- Vida útil de una pose informada: no se tiene en cuenta una pose más antigua que el período declarado.