Эта страница ещё не переведена — показана английская версия.
Data
Изменили поле — синхронизация произойдёт сама. Data — это механика под каждым синхронизируемым полем: дельты от последне го подтверждённого состояния, аспект как единица политики, приоритет и частота отправки, возобновляемые подписки, окно хранения и хуки до и после изменения.
Вы обращаетесь к сущностям (entity), а не к таблицам. Как их читать и менять — искать, фильтровать, сортировать, листать страницами, подписываться на выборку, — описано на странице Entity, а здесь речь о механике под этим. Прямого доступа к таблицам у потребителя нет, как нет и второго способа записи: изменение — это операция Entity, а дельта — её следствие.
Когда это нужно
- Нужно реплицировать состояние на клиенты, не собирая снапшоты вручную. Изменить поле — это и есть вся синхронизация (sync).
- У полей разная срочность или разные получатели. Для каждого аспекта задаются приоритет и верхняя граница частоты отправки, а для тумана войны — предикат видимости (visibility).
- Клиент после переподключения не должен незаметно разойтись с сервером. Пропуск обнаруживается и явно сообщается, а если он вышел за окно хранения, клиент получает полное состояние.
- Нужно недавнее прошлое. Предсказание и компенсация лагов читают окно хранения дельт, проиндексированное по
sim_time. - Правило валидации должно жить в одном месте. Хук до изменения ограничивает значение или запрещает изменение, прежде чем оно будет применено.
- Настройки не трогайте, если нужно только читать или делать запросы. Интерфейс 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 | экземпляр, выборка или аспект; подписка (subscription) получает дельты этой цели. Цель — не поток данных: одна цель может охватывать много пар, а порядок гарантируется внутри пары, а не по всей цели |
position | откуда продолжить; её предъявляет потребитель. Если пропуск больше окна хранения, вместо потока дельт приходит полное состояние, поэтому после долгого разрыва клиент не останется незаметно в неверном состоянии |
state | active → gap detected → resynchronised | closed; closed — конечное |
Что верно для любого потока данных.
| Всегда | Что это |
|---|---|
слияние | дельты его допускают (см. ниже). Этим дельта и отличается от события: потерянное событие уносит информацию навсегда |
обнаружение пропусков | молча терять дельту запрещено; потребитель следит за порядковым номером в паре |
порядок | соблюдается внутри одной пары экземпляр × аспект; между парами не гарантируется ни в каком виде |
обход | идёт только по объявленному: фильтром, сортировкой или включением может быть только объявленное поле и объявленная ссылка. Выборки сущности — проекция этой модели, а своего обхода этот примитив потребителю не даёт: второго языка запросов нет |
история | строится из дельт: мгновенное окно сущности — это окно хранения дельт, проиндексированное по sim_time. Его глубина — ограничение этого примитива, и воспроизводимость для полей с плавающей точкой не гарантируется |
бюджет пакетов | деградация объявлена заранее: когда бюджет персональных пакетов исчерпан, платформа переходит на общий пакет так, как объявлено, а не начинает терять случайных получателей |
Промежуточные значения могут пропускаться
Между отправками дельты сливаются: 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, где удаление и происходит, а этот примитив даёт хуки на изменение.
Ошибки
- Необъявленная цель подписки — это отказ при валидации.
- Нет разрешения (permission) на подписку — ответ будет forbidden или not found, смотря считается ли секретом само существование цели. По отказу нельзя догадаться о том, что скрыто.
- Позиция возобновления, которую не удалось разобрать, — это некорректный запрос, а не молчаливый перезапуск с текущего момента.
- Подписка, закрытая со стороны платформы, — это конфликт, и он виден явно: состояние
closedконечное, и клиенту не нужно догадываться, что подписка в него перешла. - Исчерпано допустимое число подписок — это кон фликт: разрешение есть, но места больше нет.
Ограничения
Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждой границе уже определено.
- Окно хранения дельт: если продолжить с позиции старше окна, придёт полное состояние, а не отказ.
- Число подписок на актора: новую подписку отклоняют, существующие продолжают работать.
- Размер дельты: дельта делится на части, а не обрезается, и это деление видно.
- Частота отправки: верхняя граница, а не гарантия.
- Стоимость персонального пакета: когда бюджет исчерпан, происходит объявленный переход на общий пакет.