Перейти к основному содержимому
Строительные блоки

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

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 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экземпляр, выборка или аспект; подписка (subscription) получает дельты этой цели. Цель — не поток данных: одна цель может охватывать много пар, а порядок гарантируется внутри пары, а не по всей цели
positionоткуда продолжить; её предъявляет потребитель. Если пропуск больше окна хранения, вместо потока дельт приходит полное состояние, поэтому после долгого разрыва клиент не останется незаметно в неверном состоянии
stateactive → 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

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

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

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

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

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

ХукЧто может
до измененияизменить его или запретить. Запрещённое изменение вообще не создаёт дельты: подписчики ничего не видят, а не получают значение и следом исправление
после изменениядобавить побочные эффекты; сорвать само изменение он не может

Хуки на удаление ставятся в Entity, где удаление и происходит, а этот примитив даёт хуки на изменение.

Ошибки

  • Необъявленная цель подписки — это отказ при валидации.
  • Нет разрешения (permission) на подписку — ответ будет forbidden или not found, смотря считается ли секретом само существование цели. По отказу нельзя догадаться о том, что скрыто.
  • Позиция возобновления, которую не удалось разобрать, — это некорректный запрос, а не молчаливый перезапуск с текущего момента.
  • Подписка, закрытая со стороны платформы, — это конфликт, и он виден явно: состояние closed конечное, и клиенту не нужно догадываться, что подписка в него перешла.
  • Исчерпано допустимое число подписок — это конфликт: разрешение есть, но места больше нет.

Ограничения

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

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