Saltar al contenido principal
El juego en vivo

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, Teleport y 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é

ActorQué hace en esta página
schema-authordeclara modelos de movimiento, restricciones y bindings
room-owneraplica impulsos, teletransportes y modificadores desde el host
playerenví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

Declaración del modelo de movimiento 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)
Próximamente — Go

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
Unity C# is the same C# API here — los mismos atributos compilan en Unity con la versión base 2021.3 — aquí no hay sintaxis de C# 12
[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:

La entrada del cliente como intención numerada: Motion.Drive se envía a la frecuencia de entrada y avanza del lado del servidor
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
Próximamente — Go

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.

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 — se ejecuta tal cual en Unity con los tipos generados
room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f);   // cl — sent at input rate

Verbos del lado del servidor:

Verbos del servidor: un impulso de empuje, un modificador de barro de 3 segundos y un teletransporte limpio
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)
Próximamente — Go

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.
Unity C# is the same C# API here — un build de cliente maestro tiene los mismos verbos de host; un cliente común ve sus resultados como movimiento predicho y reconciliado
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 declaraQué es
modelo de movimientouno 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ámetrosvalores 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ímitesvelocidad, 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 obsoletastop, 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 posecuá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óncon qué frecuencia se ejecuta el paso y cuántos pasos se pueden dar de una vez cuando el servidor va atrasado
tipos de impulsocada uno con su magnitud y su forma de decaer

Qué se cumple en cada paso.

SiempreQué es
el módulo es dueño dela 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
autoridades del servidor: en el modo simulación de la plataforma, el cliente envía una intención, nunca un resultado
colisionesno 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
entradaes una intención: “adelante”, “derecha”, “gira la torreta hacia allá”. Se acepta tal como llega, porque no afirma nada sobre el mundo
una pose informadaes 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 entradaes obligatoria: el mismo número de secuencia nunca se aplica dos veces, y uno menor se descarta
un límiterecorta, 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 restriccionesel 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énticosno 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 pasoes 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.