Перейти до основного вмісту
Будівельні блоки

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

Data

Змінили поле — і синхронізація відбулася сама. Data — це механіка під кожним синхронізованим полем: дельти відносно останнього підтвердженого стану, аспект як одиниця політики, пріоритет і частота надсилання, підписки, які можна відновити, вікно зберігання і хуки до та після зміни.

Ви звертаєтеся до сутностей (entity), а не до таблиць. Як читати й змінювати дані (шукати, фільтрувати, сортувати, гортати сторінки, підписуватися на вибірку), описано на сторінці Entity, а тут — механіка під цим. Шляху до таблиці в споживача немає, як немає й другого способу запису: зміна — це операція Entity, а дельта — її наслідок.

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

  • Потрібно реплікувати стан на клієнти без коду для снапшотів: змінити поле — це і є вся синхронізація (sync).
  • Поля (field) відрізняються терміновістю чи аудиторією: для кожного аспекту задають пріоритет і верхню межу частоти надсилання, а для туману війни — предикат видимості.
  • Клієнт, що перепідключається, не має мовчки розійтися зі станом: пропуск виявляють і повідомляють про нього, а на пропуск, довший за вікно зберігання (retained window), відповідають повним станом.
  • Потрібне недавнє минуле: саме вікно зберігання дельт з індексом за sim_time читають передбачення (prediction) і компенсація лагів.
  • Правило валідації має жити в одному місці: хук до зміни обмежує значення або забороняє зміну, ще до того як її застосують.
  • Не чіпайте налаштувань, якщо вам треба лише читати чи робити запити. Інтерфейс Entity працює поверх цієї механіки, нічого в ній не змінюючи.

Хто що робить

АкторЩо робить на цій сторінці
schema-authorоголошує аспекти, їхню політику синхронізації і предикат видимості
будь-який акторпідписується на ціль; продовжує з позиції; запитує повний стан
backend-serviceпише хуки до і після зміни
operatorстежить за вартістю персональних пакетів; бачить, коли доставка деградує або пакет обрізають

Коротко

Два аспекти на tank: рух — 30 надсилань на секунду, спорядження — лише для власника
public class Motion
{
    public Vector3 Position;
    [Sync(Hz = 4)] public float Fuel;                    // one field overrides the aspect
}

public class Loadout { public int Ammo; }

[Entity("tank")]
public class Tank
{
    [Aspect("motion", Priority = 10, Hz = 30)]           // policy lives on the aspect
    public Motion Motion = new();

    [Aspect("loadout", Visible = "owner == caller.player")]
    public Loadout Loadout = new();

    public float InternalHeat;                           // in no aspect — never leaves the server
}

tank.Motion.Position = next;   // ← the change; the delta is its consequence
export class Motion {
  position!: Vector3;
  @Sync({ hz: 4 }) fuel = 0;                             // one field overrides the aspect
}

export class Loadout { ammo = 0; }

@Entity('tank')
export class Tank {
  @Aspect('motion', { priority: 10, hz: 30 })            // policy lives on the aspect
  motion = new Motion();

  @Aspect('loadout', { visible: 'owner == caller.player' })
  loadout = new Loadout();

  internalHeat = 0;                                      // in no aspect — never leaves the server
}

tank.motion.position = next;   // ← the change; the delta is its consequence
class Motion:
    position: Vector3
    fuel: float = sync(hz=4)                             # one field overrides the aspect

class Loadout:
    ammo: int = 0

@entity("tank")
class Tank:
    motion: Motion = aspect("motion", priority=10, hz=30)          # policy lives on the aspect
    loadout: Loadout = aspect("loadout", visible="owner == caller.player")
    internal_heat: float = 0.0                           # in no aspect — never leaves the server

tank.motion.position = next_pos   # ← the change; the delta is its consequence
Незабаром — Go

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

// declarations compile into the same pushed model — playserv push from the UE project or CI
USTRUCT()
struct FMotion
{
    GENERATED_BODY()

    UPROPERTY() FVector3f Position;
    UPROPERTY(PSSync = (Hz = 4)) float Fuel;             // one field overrides the aspect
};

USTRUCT()
struct FLoadout
{
    GENERATED_BODY()

    UPROPERTY() int32 Ammo;
};

UCLASS(PSEntity = "tank")
class UTank : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSAspect = (Name = "motion", Priority = 10, Hz = 30))   // policy lives on the aspect
    FMotion Motion;

    UPROPERTY(PSAspect = (Name = "loadout", Visible = "owner == caller.player"))
    FLoadout Loadout;

    float InternalHeat = 0.f;                            // no UPROPERTY, in no aspect — never leaves the server
};

Tank->Motion.Position = Next;   // ← the change; the delta is its consequence
Unity C# is the same C# API here — ті самі атрибути C# компілюються в Unity (базова версія 2021.3): оголошення потрапляють у ту саму модель, і змінити поле — це так само вся синхронізація
public class Motion
{
    public Vector3 Position;
    [Sync(Hz = 4)] public float Fuel;                    // one field overrides the aspect
}

public class Loadout { public int Ammo; }

[Entity("tank")]
public class Tank
{
    [Aspect("motion", Priority = 10, Hz = 30)]           // policy lives on the aspect
    public Motion Motion = new();

    [Aspect("loadout", Visible = "owner == caller.player")]
    public Loadout Loadout = new();

    public float InternalHeat;                           // in no aspect — never leaves the server
}

tank.Motion.Position = next;   // ← the change; the delta is its consequence

Модель

Що несе дельта.

ПолеЩо це
змінені полялише вони, ніколи не весь об’єкт
pairпара екземпляр × аспект, до якої належить дельта
numberпорядковий номер у межах цієї пари; саме завдяки йому можна виявити пропуск

Що оголошує аспект.

Усе це задають атрибутом — на аспекті чи на окремому полі, — і ніколи викликом під час роботи. Аспект задає значення за замовчуванням, а поле може його перевизначити. Одиницею політики лишається аспект, бо інакше не було б із чого збирати пресети.

ОголошуєЗначення і чим це не є
priorityвизначає, що надсилати першим, коли каналу не вистачає. Не гарантія затримки: пріоритет відносний, він упорядковує надсилання між полями, а не гарантує строк доставки
максимальна частота оновленняверхня межа для надсилання. Не обіцянка, що дані надходитимуть із такою частотою: отримання залежить від каналу
лише дельтине надсилати те, що не змінилося
режим доставкиshared packet: усім те саме, це дешево для CPU. per-actor packet: кожен одержувач отримує власний пакет за своєю зоною видимості; це дорого для CPU, але необхідно, коли гравців багато
правило видимостіпредикат, який вирішує, хто взагалі отримує дані; докладно — на сторінці Visibility
a field changes — revision 15a receiver that acknowledged 12a receiver that acknowledged 14a receiver past the retained window delta 12 → 15delta 14 → 15full state, marked as such
Кожен одержувач отримує різницю відносно підтвердженого ним стану; той, хто вийшов за вікно зберігання, отримує повний стан (номери ревізій умовні)

Що містить підписка.

МіститьЩо це
targetекземпляр, вибірка чи аспект; підписка отримує дельти цієї цілі. Ціль — не потік даних: одна ціль може охоплювати багато пар, а порядок гарантовано всередині пари, а не в межах цілі
positionзвідки продовжити; її передає споживач. Якщо пропуск довший за вікно зберігання, замість потоку дельт приходить повний стан, тож навіть після довгого розриву клієнт не лишиться мовчки з хибним станом
stateactive → gap detected → resynchronised | closed; closed — кінцевий стан

Що справджується для будь-якого потоку.

ЗавждиЩо це
злиттядельти його допускають (див. нижче). Саме цим дельта відрізняється від події: якщо загубити подію, інформація зникне назавжди
виявлення пропусківмовчки губити дельту заборонено; споживач стежить за порядковим номером у парі
порядокдіє в межах однієї пари екземпляр × аспект; між парами його не гарантовано в жодній формі
обхідіде лише оголошеними елементами: фільтром, сортуванням чи включенням може бути лише оголошене поле й оголошене посилання. Вибірки (selection) в сутності — це проєкція тієї самої моделі, а власного обходу цей примітив споживачеві не дає: другої мови запитів немає
історіяскладається з дельт: миттєве вікно сутності — це вікно зберігання дельт з індексом за sim_time. Його глибина — обмеження цього примітива, і відтворюваності для полів із рухомою комою він не гарантує
бюджет пакетівдеградує так, як оголошено: коли бюджет на персональні пакети вичерпано, платформа переходить на shared packet за оголошенням, а не починає довільно губити одержувачів

Проміжні значення можуть пропасти

Між надсиланнями дельти зливаються: 100 → 90 → 80 може прийти як 100 → 80, бо кінцевий стан однаково правильний. Список доставлених значень — не історія того, що сталося.

Що і коли може робити хук.

Хук до зміни на аспекті motion: від’ємне пальне відхиляють ще до того, як зміну застосують
[Before(Data.Change, aspect: "tank.motion")]
public static Verdict ClampFuel(Change<Motion> change) =>
    change.Next.Fuel < 0 ? Hook.Reject("negative fuel") : Hook.Continue(change);
export const clampFuel = before(Data.change, { aspect: 'tank.motion' }, (change: Change<Motion>) =>
  change.next.fuel < 0 ? Hook.reject('negative fuel') : Hook.continue(change));
@before(data.change, aspect="tank.motion")
def clamp_fuel(change: Change[Motion]) -> Verdict:
    return hook.reject("negative fuel") if change.next.fuel < 0 else hook.proceed(change)
Незабаром — Go

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

Виконується поза рушієм

Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а ваш код на Unreal підписується на події, які він породжує. Згодом можна буде писати функції і в рушії: графами Blueprint і на C++ (з трансляцією в скриптовий формат, який перевіряє платформа). І те, й інше аналізують і публікують, як будь-яке оголошення, а виконується код однаково на платформі.

Виконується поза рушієм

Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а ваш код на Unity підписується на події, які він породжує.

ХукЩо може робити
до змінизмінити її або заборонити. Заборонена зміна не породжує жодної дельти: підписники не бачать нічого, а не значення з подальшим виправленням
після змінидодати побічні ефекти; зірвати зміну він не може ніколи

Хуки на видалення вішають на Entity, де й живе видалення; цей примітив перехоплює зміну.

Помилки

  • Неоголошена ціль підписки — це відмова валідації.
  • Немає дозволу підписатися — відповідь forbidden або not found, залежно від того, чи саме існування цілі є таємницею: відмова не має ставати оракулом.
  • Позиція відновлення, яку не вдалося розібрати, — це хибний запит, а не мовчазний перезапуск із поточної миті.
  • Підписка, яку закрила платформа, — це конфлікт, і його видно: closed у машині станів кінцевий, і клієнтові не треба здогадуватися, що підписку закрито.
  • Вичерпано кількість підписок — це конфлікт: дозвіл є, але місця не лишилося.

Обмеження

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

  • Вікно зберігання дельт: якщо продовжити з позиції, старшої за вікно, прийде повний стан, а не відмова.
  • Підписки на одного актора: нову відхиляють, а наявні працюють далі.
  • Розмір дельти: дельту ділять, а не обрізають, і поділ видно.
  • Частота надсилання: верхня межа, а не гарантія.
  • Вартість персонального пакета: коли бюджет вичерпано, відбувається оголошена деградація до shared packet.