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

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

Prediction & Lag Compensation

Предсказание вперёд и компенсация назад — поверх данных, которые помнят, когда событие произошло на самом деле. Клиент откликается мгновенно, сервер остаётся прав, а попадания оцениваются по тому, что видел стрелок.

Когда это нужно

  • Ввод должен ощущаться мгновенным даже при задержке, а решать по-прежнему должен сервер. Клиентское предсказание (prediction) забегает вперёд, а при расхождении клиент проводит реконсиляцию по состоянию сервера.
  • Попадания нужно оценивать по тому, что видел стрелок: ResolveAt отматывает хитбоксы к тику, который был у него на экране.
  • Дуги прицеливания и маркеры приземления должны совпадать с исходом: клиент и сервер строят один и тот же прогноз Trajectory.
  • Критичные для игры поля (field) не должны откатываться никогда: вы объявляете, что предсказывается, а что ждёт ответа сервера.
  • Нужно настроить эффект резинки (rubber-banding): окна и допуски для каждой сущности (entity), телеметрия ошибок предсказания.
  • Не нужен, если задержка не мешает. Пошаговым и неторопливым играм хватает обычных дельт Data & Subscriptions.

Кто что делает

АкторЧто делает на этой странице
schema-authorобъявляет, какие поля предсказываются, а какие меняет только авторитетная сторона; задаёт окно предсказания
room-ownerпроверяет попадания по историческому состоянию; отматывает мир назад
playerпредсказывает движение и проводит реконсиляцию; подписывается на коррекции

К каким комнатам это относится

Модуль работает там, где шаги симуляции считает платформа, — в комнатах (room), объявленных с Host = "Backend". Если симуляцией владеет ваш собственный игровой сервер (а PlayServ служит метасервером), перемещение, столкновения и предсказание остаются на стороне движка. Тогда эта страница описывает вариант с хостингом на платформе, а не обязательное требование. См. Кто выполняет тик.

Коротко

Под одним словом скрываются три разных механизма. Решения в них принимают разные стороны, и ломаются они по-разному, поэтому у каждого своя статья:

МеханизмЧто делаетГде работаетЦена ошибки
Предсказание своего движенияприменяет объявленную модель к вашему собственному вводу, не дожидаясь сервераклиенткоррекция: ввод переигрывается, результат сглаживается
Как показывать других игроковрисует чужие сущности в промежутках между пришедшими состояниямиклиентзаметный рывок
Lag Compensationотматывает цели к моменту, который видел стрелоксерверкто-то погибает несправедливо

Четыре пресета, и отказ от предсказания — тоже один из них.

ПресетПредсказывает своёКомпенсируетСглаживает чужих
shooterдав окне примерно полторы секундыда
arcadeданетда
observerнетнетда
no predictionнетнетнет — состояние приходит от авторитетной стороны с объявленным окном интерполяции

Пошаговым играм, стратегиям и большинству мобильных проектов предсказание не нужно вовсе. Если объявить пресет no prediction, клиент будет показывать состояние таким, какое оно есть, и не станет угадывать.

У модуля нет своей модели движения, таблицы реакций, геометрии и окна истории. Всё это принадлежит Locomotion, Collision, Map и Entity соответственно. Предсказание применяет их раньше или читает в обратную сторону, но никогда не объявляет вторую копию.

Предсказание на Tank: предсказываемые поля, Hp меняет только авторитетная сторона, окно — 8 тиков вперёд и 64 назад
[Entity("tank")]
[Prediction(ForwardTicks = 8, MaxRewindTicks = 64)]
public class Tank
{
    [Sync, Predicted] public Vector3 Position;                        // rolls back and replays
    [Sync, Predicted] public Vector3 Velocity;

    [Stat(Max = 100), AuthoritativeOnly] public Stat Hp;   // never predicted
}
@Entity('tank')
@Prediction({ forwardTicks: 8, maxRewindTicks: 64 })
export class Tank {
  @Sync() @Predicted() position!: Vector3;                    // rolls back and replays
  @Sync() @Predicted() velocity!: Vector3;

  @Stat({ max: 100 }) @AuthoritativeOnly() hp: Stat;   // never predicted
}
@entity("tank")
@prediction(forward_ticks=8, max_rewind_ticks=64)
class Tank:
    position: Vector3 = sync(predicted=True)                  # rolls back and replays
    velocity: Vector3 = sync(predicted=True)

    hp = stat(max=100, authoritative_only=True)   # never predicted
Скоро — Go

Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.

UCLASS(PSEntity = "tank", PSPrediction = (ForwardTicks = 8, MaxRewindTicks = 64))
class UTank : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSSync = (Predicted = "true")) FVector3f Position;   // rolls back and replays
    UPROPERTY(PSSync = (Predicted = "true")) FVector3f Velocity;

    UPROPERTY(PSStat = (Max = 100, AuthoritativeOnly = "true")) FPSStat Hp;   // never predicted
};
// 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")]
[Prediction(ForwardTicks = 8, MaxRewindTicks = 64)]
public class Tank
{
    [Sync, Predicted] public Vector3 Position;                        // rolls back and replays
    [Sync, Predicted] public Vector3 Velocity;

    [Stat(Max = 100), AuthoritativeOnly] public Stat Hp;   // never predicted
}

Проверка с компенсацией лагов (lag compensation) отвечает на вопрос «где были все в момент выстрела»:

Проверка попадания в одном хуке: ResolveAt(shooterViewTick) отматывает хитбоксы к тому, что видел стрелок
[After(Projectiles.HitReported)]
public static void Validate(HitReport hit) =>
    hit.ResolveAt(hit.ShooterViewTick);    // rewinds hitboxes, sub-tick interpolated
export const validate = after(Projectiles.hitReported, (hit: HitReport) =>
  hit.resolveAt(hit.shooterViewTick));    // rewinds hitboxes, sub-tick interpolated
@after(projectiles.hit_reported)
def validate(hit: HitReport):
    hit.resolve_at(hit.shooter_view_tick)    # rewinds hitboxes, sub-tick interpolated
Скоро — Go

Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.

Выполняется вне движка

Хук — это облачная функция: он выполняется на платформе, а не в движке. Напишите его на C#, TypeScript или Python, а Unreal подпишется на события, которые он порождает. В планах — функции, написанные прямо в движке: графы Blueprint и C++, который будет транслироваться в скрипт, проверяемый платформой. Их будут анализировать и загружать так же, как любые объявления, а выполняться они по-прежнему будут на платформе.

Выполняется вне движка

Хук — это облачная функция: он выполняется на платформе, а не в движке. Напишите его на C#, TypeScript или Python, а Unity подпишется на события, которые он порождает.

Прогноз траектории общий для сервера и клиента — по нему рисуют дуги прицеливания и маркеры приземления. Это операция самого модуля: траектория экстраполируется с учётом набора препятствий карты, поэтому по одним и тем же входным данным обе стороны рисуют одну и ту же дугу:

Один вызов Trajectory: прогноз с учётом столкновений, общий для сервера и превью прицела
var arc = room.Prediction.Trajectory(from, velocity, steps: 30);   // collision-aware
const arc = room.prediction.trajectory(from, velocity, { steps: 30 });   // collision-aware
arc = room.prediction.trajectory(origin, velocity, steps=30)   # collision-aware
Скоро — Go

Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.

// collision-aware: the arc the platform itself would walk
Room->Prediction->Trajectories->Get(LaunchPosition, LaunchVelocity, /*Steps*/ 30,
    TPSOnResult<FPSTrajectory>::CreateWeakLambda(this, [this](const TPSResult<FPSTrajectory>& Result)
    {
        if (!Result.HasValue()) { return; }
        DrawArc(Result.Value());
    }));
// 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 без изменений, со сгенерированными типами
var arc = room.Prediction.Trajectory(from, velocity, steps: 30);   // collision-aware

Модель

Предсказуемость объявляется на аспекте. Аспект, который меняет только авторитетная сторона по правилам, неизвестным клиенту, нельзя пометить предсказуемым. Такая попытка — ошибка валидации при деплое, а не сюрприз во время игры.

Что объявляется.

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

Что отматывается, а что намеренно нет.

Что это
отматываетсяпозиции и ориентации целей, а также геометрия динамических препятствий, если тип объявляет её исторической
не отматываетсясостояние жизни — мёртвые не оживают, чтобы в них можно было попасть, — а также владение, счёт и инвентарь
правило разделениярешение принимается в прошлом, а последствия применяются в настоящем
ВсегдаЧто это
авторитетное состояние называет учтённый вводоно несёт номер последнего применённого ввода. Благодаря этому реконсиляция точная, а не приблизительная
расхождениенаблюдаемо: клиент знает, что его предсказание поправили, и не уплывает незаметно
время обзораэто утверждение, а не факт: актор сообщает, какой момент он видел. За пределами окна платформа отказывает, а не экстраполирует. Молчаливая экстраполяция — подарок читеру: ему достаточно прислать время постарше
откат не обещает воспроизводимости вычислений с плавающей точкойэто то же ограничение, что и везде в контракте
одно кольцо истории, два потребителяреконсиляция и запросы с компенсацией лагов читают один и тот же трек мгновенной истории сущности. Откат живёт здесь, а не в модулях, чьё состояние отматывается. Кольцо восстанавливает позы спорного тика, а затем столкновениям задают обычный вопрос о пересечениях этих поз. Своей истории у них нет, и о «тике обзора» они ничего не знают
client tick:  apply input locally (predict) → buffer it → send, tick-stamped
server tick:  step the same movement model → authoritative state → delta out
client recv:  authoritative state for tick T → if divergence beyond tolerance:
              rewind to T → replay buffered inputs T+1..now → smooth

Откатываются только поля, объявленные предсказываемыми. Мелкий дрейф сглаживается, а настоящее расхождение приводит к откату и переигрыванию ввода.

Ошибки

  • Время обзора за пределами окна — это конфликт: пришлите актуальное.
  • Запрос истории за пределами окна — тоже конфликт.
  • Две ошибки объявления ловятся при деплое: окно компенсации больше буфера истории и аспект, помеченный предсказуемым, хотя у клиента нет правил, чтобы его предсказать. Ни одна из них не доберётся до живого матча.
  • Два случая — не ошибки, и оба наблюдаемы. Если буфер ввода переполнен, предсказание приостанавливается до подтверждения, а ввод молча не отбрасывается. А расхождение выше порога означает, что состояние авторитетной стороны принимается как есть, — это объявленная коррекция, а не сбой.

Ограничения

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

  • Окно компенсации: за его пределами — отказ, никогда не экстраполяция.
  • Окно экстраполяции для чужих сущностей: сущность помечается устаревшей, экстраполяция прекращается.
  • Буфер неподтверждённого ввода: предсказание приостанавливается до подтверждения; ввод никогда не отбрасывается молча.
  • Глубина истории для отката: не меньше окна компенсации, это проверяется при деплое.
  • Частота действий со временем обзора: ограничение частоты со сроком.
  • Сущности, которые клиент предсказывает одновременно: сверх предела предсказание не выполняется. Это объявленная деградация, а не отказ.