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

Цю сторінку ще не перекладено — показано англійську версію.

Prediction & Lag Compensation

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

Коли це потрібно

  • Введення має відчуватися миттєвим навіть із затримкою, а вирішувати й далі має сервер. Передбачення (prediction) на клієнті забігає вперед. Коли стан розходиться із сервером, клієнт проводить реконсиляцію (reconciliation).
  • Влучання треба оцінювати за тим, що бачив стрілець: 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

Що відмотують назад, а що навмисно ні.

Що це
відмотуютьпозиції й орієнтації цілей, а також геометрію динамічних перешкод, якщо тип оголошує її історичною
не відмотуютьстан життя (мертві не оживають, щоб у них можна було влучити), а також власника, рахунок та інвентар
правило поділурішення ухвалюють у минулому, а наслідки застосовують у теперішньому
ЗавждиЩо це
авторитетний стан називає введення, яке врахуваввін містить номер останнього застосованого введення. Завдяки цьому реконсиляція точна, а не приблизна
розбіжністьвидно: клієнт знає, що його передбачення виправили, і не дрейфує непомітно
момент оглядуце твердження, а не факт: актор повідомляє, який момент бачив. За межами вікна платформа відмовляє, а не екстраполює: мовчазна екстраполяція — подарунок для чітера, якому досить надіслати старіший час
відкат не обіцяє відтворюваності обчислень із рухомою комоюце те саме обмеження, що й усюди в контракті
одне кільце історії, два споживачіреконсиляція і запити з компенсацією лагів читають ту саму миттєву історію сутності. Відкат належить цьому модулю, а не тим, які відмотують: кільце відновлює пози спірного тіку, а потім модулю Collision ставлять звичайне питання про перетин цих поз. Власної історії він не зберігає і про «тік огляду» нічого не знає
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

Відкочуються лише поля, які оголошено передбачуваними. Дрібний дрейф клієнт згладжує, а справжня розбіжність змушує його відмотати стан і програти введення заново.

Помилки

  • Момент огляду за межами вікна — це конфлікт: надішліть актуальний.
  • Запит історії за межами вікна — теж конфлікт.
  • Два дефекти оголошення деплой ловить одразу: вікно компенсації, більше за буфер історії, і аспект, позначений передбачуваним, хоча клієнт не має правил, щоб його передбачати. Жоден із них не потрапить у живий матч.
  • Два випадки не є помилками, і обидва видно. Коли буфер введення заповнений, передбачення призупиняється до підтвердження, а не відкидає введення мовчки. А розбіжність понад поріг означає, що клієнт приймає стан авторитетної сторони як є: це оголошена корекція, а не збій.

Обмеження

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

  • Вікно компенсації: за його межами — відмова, ніколи не екстраполяція.
  • Вікно екстраполяції для чужих сутностей: сутність позначають застарілою, екстраполяція припиняється.
  • Буфер непідтвердженого введення: передбачення призупиняється до підтвердження; введення ніколи не відкидають мовчки.
  • Глибина історії для відкату: не менша за вікно компенсації, це перевіряє деплой.
  • Частота дій із моментом огляду: обмеження частоти з терміном.
  • Сутності, які клієнт передбачає одночасно: понад межу передбачення не виконується, і це оголошена деградація, а не відмова.