Перейти к основному содержимому
Живая игра

Эта страница ещё не переведена — показана английская версия.

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

Как объявлять через атрибуты в 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
Unity C# is the same C# API here — те же атрибуты компилируются в Unity на базовой версии 2021.3 — синтаксиса C# 12 здесь нет
[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 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
Скоро — Go

Как объявлять через атрибуты в 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.
Unity C# is the same C# API here — работает в Unity без изменений, со сгенерированными типами
room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f);   // cl — sent at input rate

Команды на стороне сервера:

Команды сервера: импульс отбрасывания, модификатор грязи на 3 секунды, мгновенный телепорт
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

Как объявлять через атрибуты в 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.
Unity C# is the same C# API here — в сборке мастер-клиента есть те же команды хоста; обычный клиент видит их результат как предсказанное движение после реконсиляции
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. Если бы это происходило молча, клиент считал бы, что всё применилось, и навсегда разошёлся бы с сервером.

Ограничения

Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждом пределе зафиксировано уже сейчас.

  • Максимальная скорость и ускорение: значение прижимается, отказа не бывает.
  • Максимальный поворот за ввод: значение прижимается.
  • Частота ввода на актора: ограничение частоты со сроком.
  • Шаги догона: сверх предела шаги отбрасываются с объявленными последствиями. Время симуляции заметно отстаёт — зато не догоняет рывком, при котором кажется, будто все разом телепортировались.
  • Величина импульса: прижимается к объявленному максимуму.
  • Одновременные импульсы на сущность: новый вытесняет самый старый, и это вытеснение наблюдаемо. Молчаливого неограниченного сложения нет.
  • Время жизни заявленной позы: поза старше объявленного периода не рассматривается.