Эта страница ещё не переведена — показана английская версия.
Locomotion
Вы объявляете, как объект движется, и никто не пишет интегратор. Модель движения превращает пронумер ованный ввод в авторитетное движение. Она работает вместе со столкновениями, записывает историю для предсказания и учитывает баффы, дебаффы и рельеф.
Когда это нужно
- Сущности (entity) двигаются по вводу игрока — танки, персонажи, техника, — и решать, где они находятся, должен сервер.
- Вместо того чтобы писать интегратор, вы хотите объявить ограничения скорости, ускорения и поворота.
- Геймплей толкает тела: отбрасывание через
Impulse,Teleportи модификаторы вроде грязи, действующие заданное время. - Движение должно ощущаться мгновенным: по одной и той же объявленной модели шаги считаются и на сервере, и в цикле предсказания.
- Не нужен, если позиции меняются только дискретными шагами. Для этого хватит синхронизируемого поля на сущности.
Кто что делает
| Актор | Что делает на этой странице |
|---|---|
schema-author | объявляет модели движения, ограничения и биндинги |
room-owner | применяет импульсы, телепорт и модификаторы с хоста |
player | отправляет пронумерованный ввод; читает состояние движения |
К каким комнатам это относится
Модуль работает там, где шаги симуляции считает платформа, — в комнатах (room), объявленных с Host = "Backend". Если симуляцией владеет ваш собственный игровой сервер (а PlayServ служит метасервером), перемещение, столкновения и предсказание остаются на стороне движка. Тогда эта страница описывает вариант с хостингом на платформе, а не обязательное требование.
Коротко
Tank: Locomotion.Tank с ограничениями скорости, ускорения и поворота[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)Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
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;
}Клиент присылает не результат, а пронумерованное намерение. Само движение шаг за шагом считает платформа:
Motion.Drive уходит с частотой ввода, а шаги считаются на сервереroom.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 wayКак объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
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 rateКоманды на стороне сервера:
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)Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
// 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);Модель
Что объявляет сущность, чтобы двигаться.
| Что объявляется | Что это |
|---|---|
модель движения | одна из готовых моделей — steering, tank, character, vehicle, flying. Каждая устроена как несколько реализаций одного шага с объявленными условиями выбора и реализацией по умолчанию. Шаг — чистая функция (pose, input, dt) → pose |
параметры | объявленные значения, которые может прочитать клиент; без них предсказание (prediction) систематически расходится с сервером. Они помечены seed: их настраивает геймдизайнер, и деплой не должен молча терять его правки. Ограничения, на которые опирается антич ит, можно пометить managed — тогда правка из админ-панели отклоняется |
ограничения | максимальная скорость, ускорение и торможение, максимальный поворот за один ввод, множитель заднего хода и отдельная скорость вращения частей. Поворот за ввод намеренно объявлен отдельно от скорости вращения: первый ограничивает мгновенный скачок, вторая — непрерывную скорость, и это разные линии защиты |
поведение при устаревшем вводе | stop (остановиться), continue until a declared deadline (продолжать до объявленного срока) или continue indefinitely (продолжать без ограничений). Варианта «продолжать как раньше» по умолчанию нет: иначе игрок (player), у которого пропала сеть, так и ехал бы дальше |
допуск позы | насколько заявленная клиентом поза может отличаться от серверной. Допуск может зависеть от состояния: на месте, в движении и сразу после респауна — три разных допуска |
частота шага и предел догона | как часто выполняется шаг и сколько шагов можно сделать за раз, если сервер отстаёт |
виды импульсов | у каждого своя величина и свой способ затухания |
Что верно для любого шага.
| Всегда | Что это |
|---|---|
модулю принадлежат | позиция и ориентация сущности во времени, и больше ничего. Историю этих позиций хранит сущность, а не этот модуль. Поэтому на вопрос «где был игрок 300 мс назад» есть ровно один ответ, а не два буфера с разными периодами |
авторитетность | у сервера: в режиме симуляция платформы клиент отправляет намерение, а не результат |
столкновения | здесь не разрешаются. Допустима ли позиция и какой будет реакция, модуль узнаёт через столкновения, а собственной таблицы реакций не держит |
ввод | это намерение: «вперёд», «вправо», «поверни башню туда». Его принимают как есть, потому что о мире он ничего не утверждает |
заявленная поза | это утверждение, а не факт. Если она выходит за объявленный допуск, её прижимают к ближайшей допустимой позе, и это порождает наблюдаемое событие pose_clamped |
нумерация ввода | обязательна: один и тот же номер никогда не применяется дважды, а ввод с меньшим номером отбрасывается |
ограничение | не отклоняет, а прижимает. «Десять метров вперёд за этот тик» превращается в то, что допустимо, а не в ошибку. Так сервер не может получить недопустимую позу, а клиента не заваливают отказами каждый кадр |
импульс подчиняется тем же ограничениям | отдача, толчок, взрыв и отбрасывание приходят не из ввода, но ни одно из них не обходит столкновения: отдача не загонит танк в скалу |
одинаковые правила, а не одинаковые биты | побитово одинакового результата на разных платформах не обещается. Обещаются одинаковые правила и воспроизводимость в пределах одной авторитетной стороны |
шаг | чистая функция, которая выполняется по тикам. Один и тот же код считает движение на сервере и в цикле предсказания на клиенте, и именно поэтому реконсиляция получается точной |
Ошибки
- Отсутствие модели движения на сущности, частично заполненный пресет и импульс без объявленного затухания — всё это ошибки валидации при деплое, а не во время игры.
- Не совпало ожидаемое поколение — это нарушение предусловия. Повторять имеет смысл, только перечитав состояние: ввод, отправленный до респауна, не должен примениться после него.
- Превышена частота ввода — ответ приходит в категории ограничения частоты со сроком.
- Сущность не управляема — это конфликт, и повторять запрос имеет смысл только после того, как состояние изменится.
- Три случая — не отказ ни в одну сторону, и все три наблюдаемы. Устаревший ввод отбрасывается, намерение сверх ограничения прижимается к нему, а поза за пределами допуска прижимается с событием
pose_clamped. Если бы это происходило молча, клиент считал бы, что всё применилось, и навсегда разошёлся бы с сервером.
Ограничения
Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждом пределе зафиксировано уже сейчас.
- Максимальная скорость и ускорение: значение прижимается, отказа не бывает.
- Максимальный поворот за ввод: значение прижимается.
- Частота ввода на актора: ограничение частоты со сроком.
- Шаги догона: сверх предела шаги отбрасываются с объявленными последствиями. Время симуляции заметно отстаёт — зато не догоняет рывком, при котором кажется, будто все разом телепортировались.
- Величина импульса: прижимается к объявленному максимуму.
- Одновременные импульсы на сущность: новый вытесняет самый старый, и это вытеснение наблюдаемо. Молчаливого неограниченного сложения нет.
- Время жизни заявленной позы: поза старше объявленного периода не рассматривается.