Цю сторінку ще не перекладено — показано англійську версію.
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 consequenceexport 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 consequenceclass 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, поки не вирішено (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 consequencepublic 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 |
Що містить підписка.
| Містить | Що це |
|---|---|
target | екземпляр, вибірка чи аспект; підписка отримує дельти цієї цілі. Ціль — не потік даних: одна ціль може охоплювати багато пар, а порядок гарантовано всередині пари, а не в межах цілі |
position | звідки продовжити; її передає споживач. Якщо пропуск довший за вікно зберігання, замість потоку дельт приходить повний стан, тож навіть після довгого розриву клієнт не лишиться мовчки з хибним станом |
state | active → 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, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а ваш код на Unreal підписується на події, які він породжує. Згодом можна буде писати функції і в рушії: графами Blueprint і на C++ (з трансляцією в скриптовий формат, який перевіряє платформа). І те, й інше аналізують і публікують, як будь-яке оголошення, а виконується код однаково на платформі.
Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а ваш код на Unity підписується на події, які він породжує.
| Хук | Що може робити |
|---|---|
| до зміни | змінити її або заборонити. Заборонена зміна не породжує жодної дельти: підписники не бачать нічого, а не значення з подальшим виправленням |
| після зміни | додати побічні ефекти; зірвати зміну він не може ніколи |
Хуки на видалення вішають на Entity, де й живе видалення; цей примітив перехоплює зміну.
Помилки
- Неоголошена ціль підписки — це відмова валідації.
- Немає дозволу підписатися — відповідь forbidden або not found, залежно від того, чи саме існування цілі є таємницею: відмова не має ставати оракулом.
- Позиція відновлення, яку не вдалося розібрати, — це хибний запит, а не мовчазний перезапуск із поточної миті.
- Підписка, яку закрила платформа, — це конфлікт, і його видно:
closedу машині станів кінцевий, і клієнтові не треба здогадуватися, що підписку закрито. - Вичерпано кількість підписок — це конфлікт: дозвіл є, але місця не лишилося.
Обмеження
Конкретні числа з’являться разом із розділом про обмеження платформи, а що стається на кожній межі, визначено вже зараз.
- Вікно зберігання дельт: якщо продовжити з позиції, старшої за вікно, прийде повний стан, а не відмова.
- Підписки на одного актора: нову відхиляють, а наявні працюють далі.
- Розмір дельти: дельту ділять, а не обрізають, і поділ видно.
- Частота надсилання: верхня межа, а не гарантія.
- Вартість персонального пакета: коли бюджет вичерпано, відбувається оголошена деградація до
shared packet.