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

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

Перші кроки

Зберемо арену, у яку вже можна грати: мапу, танки, стрільбу й предмети, що випадають, — усе з оголошень (declaration), від початку до кінця. Ігрового циклу нижче не буде: симуляція працює всередині платформи.

Що знадобиться

Проєкт із середовищем (environment) dev — його створюють на операторському рівні, який керує життєвим циклом середовищ. CLI playserv, авторизований у цьому проєкті. Пакет SDK для вашого біндингу. Більше нічого встановлювати в гру не треба.

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

Оголосіть світ

Сутність (entity) — це ваша схема разом із її живими аспектами. Кожну поведінку задає один атрибут поруч із полем (field), яке він описує:

Оголошення сутності Tank: три правила синхронізації і три ігрові аспекти, по рядку на кожен
[Entity("tank")]
public class Tank
{
    [Sync] public Vector3 Position;                     // synced every tick
    [Sync(Hz = 10)] public float Fuel;               // ~10 times a second
    [Sync(To = Scope.Owner)] public int Ammo;        // owner's eyes only

    [Stat(Max = 100, AtMin = "death")] public Stat Hp;
    [Body(Shape.Capsule, Radius = 0.6f)] public Body Body;
    [Motion(Model.Tank, MaxSpeed = 8f, TurnRateDeg = 120f)] public Motion Motion;
}
@Entity('tank')
export class Tank {
  @Sync() position!: Vector3;                     // synced every tick
  @Sync({ hz: 10 }) fuel = 0;                  // ~10 times a second
  @Sync({ to: Scope.Owner }) ammo = 0;         // owner's eyes only

  @Stat({ max: 100, atMin: 'death' }) hp: Stat;
  @Body({ shape: 'capsule', radius: 0.6 }) body: Body;
  @Motion({ model: 'tank', maxSpeed: 8, turnRateDeg: 120 }) motion: Motion;
}
@entity("tank")
class Tank:
    position: Vector3 = sync()                      # synced every tick
    fuel: float = sync(hz=10)                    # ~10 times a second
    ammo: int = sync(to=Scope.OWNER)             # owner's eyes only

    hp = stat(max=100, at_min="death")
    body = collision.body(shape="capsule", radius=0.6)
    motion = locomotion.motion(model="tank", max_speed=8.0, turn_rate_deg=120.0)
Незабаром — Go

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

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

    UPROPERTY(PSSync)                        FVector3f Position;   // synced every tick
    UPROPERTY(PSSync = (Hz = 10))            float     Fuel;       // ~10 times a second
    UPROPERTY(PSSync = (To = "Owner"))       int32     Ammo;       // owner's eyes only

    UPROPERTY(PSStat = (Max = 100, AtMin = "death"))                                FPSStat Hp;
    UPROPERTY(PSBody = (Shape = "Capsule", Radius = "0.6"))                         FPSBody Body;
    UPROPERTY(PSMotion = (Model = "Tank", MaxSpeed = "8.0", TurnRateDeg = 120))     FPSMotion Motion;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
Unity C# is the same C# API here — ті самі атрибути компілюються в Unity, і C# 12 тут не потрібен — усе збирається на базовій версії 2021.3
[Entity("tank")]
public class Tank
{
    [Sync] public Vector3 Position;                     // synced every tick
    [Sync(Hz = 10)] public float Fuel;               // ~10 times a second
    [Sync(To = Scope.Owner)] public int Ammo;        // owner's eyes only

    [Stat(Max = 100, AtMin = "death")] public Stat Hp;
    [Body(Shape.Capsule, Radius = 0.6f)] public Body Body;
    [Motion(Model.Tank, MaxSpeed = 8f, TurnRateDeg = 120f)] public Motion Motion;
}

Змінити поле з [Sync] — це і є мережева операція. Не треба ні збирати снапшот, ні викликати відправлення.

Жоден із цих типів вам не треба визначати, і кожен описано на своїй сторінці:

Тип у прикладіЗвідки він
Vector3, Statбазовий пакет вашого біндингу
Body і форми тілCollision
Motion і п’ять моделей рухуLocomotion
ObstacleSet, Drop, Flight, Ammo, Effectпресети сутностей, які їх використовують
EntryRequest, Verdict, StatEventаргументи хуків — їх передає модуль, до якого ви підключаєте хук
SeatMatchmaking
Scope і області синхронізаціїVisibility
Tick і частоти тікуRooms

Переліки закриті. Правило, якого не покриває жодне значення, записують предикатом, а не новим значенням. Наприклад, якщо Scope.Owner — не зовсім те правило, яке ви мали на увазі, видимість окремого поля задають так: [Aspect("loadout", Visible = "owner == caller.player")] (Data & Subscriptions).

Оголосіть кімнату

Шаблон кімнати (room template) описує сесію і перелічує оголошення, на які вона спирається. Успадковувати клас кімнати (room) і писати метод тіку не треба: усе, що відбувається всередині кімнати, — справа платформи:

Шаблон battle і три оголошення, на які він посилається: арена, таблиця випадання, зброя
[RoomTemplate("battle", Map = "arena")]
public static class Battle
{
    public static int Capacity = 8;
    public static Tick Tick = Tick.Hz30;
}
[Map("arena", Seed = 42, Bounds = "160x160")]
public static class Arena
{
    [Scatter("rock", Count = 40, MinSpacing = 6)] public static ObstacleSet Rocks;
}
[DropTable("crate-loot")]
public static partial class CrateLoot
{
    [Entry("ammo.shell", Weight = 60, Count = "2..4")] public static Drop AmmoShell;
    [Entry("railgun", Weight = 1)]                     public static Drop Railgun;  // the jackpot
}
[Projectile("shell", Cooldown = 1.5f)]
public static class Shell
{
    [Ballistics(Speed = 24, Gravity = 9.8f)] public static Flight Arc;
    [Ammo("ammo.shell", PerShot = 1)]        public static Ammo Load;
    [Effect(Damage = 35)]                    public static Effect OnHit;
}
@RoomTemplate('battle', { map: 'arena' })
export class Battle {
  static capacity = 8;
  static tick = Tick.hz30;
}
@Map('arena', { seed: 42, bounds: '160x160' })
export class Arena {
  @Scatter('rock', { count: 40, minSpacing: 6 }) rocks: ObstacleSet;
}
@DropTable('crate-loot')
export class CrateLoot {
  @Entry('ammo.shell', { weight: 60, count: [2, 4] }) ammoShell: Drop;
  @Entry('railgun', { weight: 1 }) railgun: Drop;              // the jackpot
}
@Projectile('shell', { cooldown: 1.5 })
export class Shell {
  @Ballistics({ speed: 24, gravity: 9.8 }) arc: Flight;
  @Ammo('ammo.shell', { perShot: 1 }) load: Ammo;
  @Effect({ damage: 35 }) onHit: Effect;
}
@room_template("battle", map="arena")
class Battle:
    capacity = 8
    tick = Tick.HZ30

@Map("arena", seed=42, bounds="160x160")
class Arena:
    rocks = scatter("rock", count=40, min_spacing=6)

@drop_table("crate-loot")
class CrateLoot:
    ammo_shell = entry("ammo.shell", weight=60, count=(2, 4))
    railgun = entry("railgun", weight=1)                       # the jackpot

@projectile("shell", cooldown=1.5)
class Shell:
    arc = ballistics(speed=24, gravity=9.8)
    load = ammo("ammo.shell", per_shot=1)
    on_hit = effect(damage=35)
Незабаром — Go

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

USTRUCT(PSRoomTemplate = (Name = "battle", Map = "arena", Capacity = 8, Tick = 30))
struct FBattle { GENERATED_BODY() };

USTRUCT(PSMap = (Name = "arena", Seed = 42, Bounds = "160x160"))
struct FArena
{
    GENERATED_BODY()

    UPROPERTY(PSScatter = (Obstacle = "rock", Count = 40, MinSpacing = 6)) FPSObstacles Rocks;
};

USTRUCT(PSDropTable = "crate-loot")
struct FCrateLoot
{
    GENERATED_BODY()

    UPROPERTY(PSEntry = (Item = "ammo.shell", Weight = 60, Count = "2..4")) FPSDrop AmmoShell;
    UPROPERTY(PSEntry = (Item = "railgun", Weight = 1))                     FPSDrop Railgun;  // the jackpot
};

USTRUCT(PSProjectile = (Name = "shell", Cooldown = "1.5"))
struct FShell
{
    GENERATED_BODY()

    UPROPERTY(PSBallistics = (Speed = "24.0", Gravity = "9.8")) FPSFlight Arc;
    UPROPERTY(PSAmmo = (Item = "ammo.shell", PerShot = 1))      FPSAmmo Load;
    UPROPERTY(PSEffect = (Damage = 35))                         FPSEffect OnHit;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
Unity C# is the same C# API here — ті самі чотири оголошення компілюються в Unity; збірка Unity читає завантажений шаблон і приєднується за ним до кімнат
[RoomTemplate("battle", Map = "arena")]
public static class Battle
{
    public static int Capacity = 8;
    public static Tick Tick = Tick.Hz30;
}
[Map("arena", Seed = 42, Bounds = "160x160")]
public static class Arena
{
    [Scatter("rock", Count = 40, MinSpacing = 6)] public static ObstacleSet Rocks;
}
[DropTable("crate-loot")]
public static partial class CrateLoot
{
    [Entry("ammo.shell", Weight = 60, Count = "2..4")] public static Drop AmmoShell;
    [Entry("railgun", Weight = 1)]                     public static Drop Railgun;  // the jackpot
}
[Projectile("shell", Cooldown = 1.5f)]
public static class Shell
{
    [Ballistics(Speed = 24, Gravity = 9.8f)] public static Flight Arc;
    [Ammo("ammo.shell", PerShot = 1)]        public static Ammo Load;
    [Effect(Damage = 35)]                    public static Effect OnHit;
}

Рядки в лапках — це ключі контенту, а не довільний текст:

КлючЩо він позначає
rockоб’єкт із набору перешкод мапи (Map)
ammo.shell, railgunпредмети каталогу (Catalog & Commerce) — саме тому постріл списує снаряд, а підібране потрапляє в інвентар
battle, arena, crate-loot, shellключі, які реєструють ці чотири оголошення

playserv push відхиляє оголошення, ключа якого немає в цільовому середовищі, тож помилку в ключі ви побачите під час деплою, а не на першому пострілі. Де б не лежав шаблон, його можна переналаштувати без перезбирання гри: live-ops редагує в панелі саме завантажену модель.

Запишіть правила як хуки

Хуки — це хмарні функції, які платформа викликає на іменованих кроках. На вході й на виході — конкретні типи, а в сигнатурі немає ні універсального об’єкта контексту, ні логера.

Три правила як хуки: не пустити забанених гравців, видати новачкові 20 снарядів, розіграти лут після смерті
[Before(Rooms.Entry, room: "battle")]
public static Verdict ValidateEntry(EntryRequest entry) =>
    entry.Player.IsBanned
        ? entry.Reject(Problem.Banned, "banned from this project")
        : entry.Accept();

[After(Auth.SignIn, created: true)]
public static async Task GrantStarterPack(Player player)
{
    await player.Inventory.Grant("ammo.shell", count: 20);
}

[After(Stats.Depleted, stat: "hp")]
public static void OnDeath(StatEvent e) => CrateLoot.RollAt(e.Entity.Position);
export const validateEntry = before(Rooms.entry, { room: 'battle' },
  (entry: EntryRequest) =>
    entry.player.isBanned
      ? entry.reject(Problem.banned, 'banned from this project')
      : entry.accept());

export const grantStarterPack = after(Auth.signIn, { created: true },
  async (player: Player) => {
    await player.inventory.grant('ammo.shell', { count: 20 });
  });

export const onDeath = after(Stats.depleted, { stat: 'hp' }, (e: StatEvent) => {
  CrateLoot.rollAt(e.entity.position);
});
@before(rooms.entry, room="battle")
def validate_entry(entry: EntryRequest) -> Verdict:
    if entry.player.is_banned:
        return entry.reject(Problem.BANNED, "banned from this project")
    return entry.accept()

@after(auth.sign_in, created=True)
async def grant_starter_pack(player: Player):
    await player.inventory.grant("ammo.shell", count=20)

@after(stats.depleted, stat="hp")
def on_death(e: StatEvent):
    CrateLoot.roll_at(e.entity.position)
Незабаром — Go

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

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

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

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

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

Хук Before може відхилити крок. Хук After спрацьовує, коли крок уже зафіксовано, і скасувати його не може. Тож якщо стартовий набір видати не вдалося, гравець (player) недоотримає 20 снарядів, але в гру ввійде. Решта — на сторінці Extensibility.

Що насправді стоїть за кожним рядком цього прикладу:

РядокХто або що його насправді виконує
спрацьовує Stats.Depletedхарактеристика (stat) дійшла до мінімуму — для Hp це 0, бо оголошення задає лише Max
перехід у стан смертіAtMin = "death" із кроку 1; хук додає наслідок, а не сам перехід
шкода[Effect(Damage = 35)] на снаряді; платформа застосовує її, коли снаряд влучає
RollAtгенерується для оголошення [DropTable] — тому клас позначено partial, а у вкладці Go виклик називається drops.RollAtCrateLoot
підбиранняколи танк проїжджає по луту, предмети атомарно переходять в інвентар гравця; цей перехід і є подія (event) changed, яку відображає HUD

Згенеровані типи для рушія

playserv schema codegen   # Unreal C++ → Plugins/PlayServ/Generated · Unity C# → Packages/com.playserv.sdk/Generated

Запускайте генерацію (або доручіть її CI) після кожного завантаження схеми. Типи не редагують вручну — їх щоразу генерують заново, тож згенерований Tank і є завантаженим Tank. playserv push читає проєкт рушія так само, як серверний: специфікатори UHT і атрибути C# самі й становлять оголошення, тож щоб експортувати модель, досить указати CLI на проєкт UE чи Unity. У якому потоці прийде колбек і коли завершиться підписка (subscription), визначає модель виконання — Потоки, час життя і тести.

Підключіть клієнт

Клієнтський API симетричний: модулі ті самі, а що може викликати збірка, визначає ключ, з яким вона працює. Збірка гри має ключ гравця (player key) — тут це projectKey, облікові дані для одного проєкту й одного середовища. Його видають у панелі й постачають усередині збірки. Ролей у ключі немає: сервер визначає їх під час кожного запиту, а гравець, від імені якого йде запит, з’являється після SignIn. Головні тут — біндинги для рушіїв, а серверні біндинги працюють із тим самим інтерфейсом у headless-режимі (мозок бота, навантажувальний тест, службовий інструмент):

Клієнтська сесія: увійти, знайти матч, приєднатися, стежити за змінами, їхати й стріляти
var playserv = await PlayServ.Connect(projectKey);
var session = await playserv.Auth.SignIn(Provider.Device, create: true);

var seat = await playserv.Matchmaking.Find("battle");
var room = await playserv.Rooms.Join(seat);

room.Entities<Tank>().OnChange(tank => Render(tank));

var aim = new Vector3(24f, 0f, 12f);              // the world point under the crosshair
room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f);
await room.My<Tank>().Cast(Abilities.Shell, aim);
const playserv = await PlayServ.connect(projectKey);
const session = await playserv.auth.signIn(Provider.Device, { create: true });

const seat = await playserv.matchmaking.find('battle');
const room = await playserv.rooms.join(seat);

room.entities<Tank>().onChange((tank) => render(tank));

const aim: Vector3 = { x: 24, y: 0, z: 12 };      // the world point under the crosshair
room.my<Tank>().motion.drive({ throttle: 1, steer: -0.4 });
await room.my<Tank>().cast(Shell, aim);
playserv = await PlayServ.connect(project_key)
session = await playserv.auth.sign_in(Provider.DEVICE, create=True)

seat = await playserv.matchmaking.find("battle")
room = await playserv.rooms.join(seat)

room.entities(Tank).on_change(lambda tank: render(tank))

aim = Vector3(24, 0, 12)                          # the world point under the crosshair
room.my(Tank).motion.drive(throttle=1.0, steer=-0.4)
await room.my(Tank).cast(Shell, aim)
Незабаром — Go

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

FPlayServClient::Connect(ProjectKey,
    TPSOnResult<FPlayServClient*>::CreateWeakLambda(this, [this](const TPSResult<FPlayServClient*>& ConnectResult)
    {
        if (!ConnectResult.HasValue()) { return; }
        FPlayServClient* Client = ConnectResult.Value();
        Client->Auth->SignInAnonymous(FPSIdempotencyKey(DeviceId),
            TPSOnResult<FPSSession>::CreateWeakLambda(this, [this, Client](const TPSResult<FPSSession>& SignedIn)
            {
                if (!SignedIn.HasValue()) { return; }
                FindBattle(Client);
            }));
    }));

// in FindBattle(FPlayServClient* Client): a ticket, the seat it wins, the room it opens
Client->Matchmaking->Of<FBattleQueue>()->Tickets->Create(FPSTicketClaim{ .Mode = TEXT("battle") },
    TPSOnResult<FPSTicket*>::CreateWeakLambda(this, [this, Client](const TPSResult<FPSTicket*>& TicketResult)
    {
        if (!TicketResult.HasValue()) { return; }
        TPSSubscription Placement = TicketResult.Value()->Subscribe([this, Client](const FPSSeat& Seat)
        {
            Client->Rooms->Join(Seat,
                TPSOnResult<FPSRoom*>::CreateWeakLambda(this, [this](const TPSResult<FPSRoom*>& JoinResult)
                {
                    if (!JoinResult.HasValue()) { return; }
                    EnterBattle(JoinResult.Value());
                }));
        });
    }));

// in EnterBattle(FPSRoom* Room): render what you see, drive what is yours
TPSSubscription TankView = Room->Entities->Of<UTank>()->Select()
    .Subscribe([this](const TArray<UTank*>& Tanks) { Render(Tanks); });

const FVector3f Aim(24.f, 0.f, 12.f);            // the world point under the crosshair
Room->Entities->Of<UTank>()->Select().GetMine().Then(
    TPSOnResult<UTank*>::CreateWeakLambda(this, [this, Aim](const TPSResult<UTank*>& MineResult)
    {
        if (!MineResult.HasValue()) { return; }
        UTank* MyTank = MineResult.Value();
        MyTank->Motion->SubmitInput(FPSMoveInput{ .Throttle = 1.f, .Steer = -0.4f }, InputSequence);
        MyTank->Call->Cast(PSKeys::Ability::Shell, Aim);
    }));
// 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 playserv = await PlayServ.Connect(projectKey);
var session = await playserv.Auth.SignIn(Provider.Device, create: true);

var seat = await playserv.Matchmaking.Find("battle");
var room = await playserv.Rooms.Join(seat);

room.Entities<Tank>().OnChange(tank => Render(tank));

var aim = new Vector3(24f, 0f, 12f);              // the world point under the crosshair
room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f);
await room.My<Tank>().Cast(Abilities.Shell, aim);

Чотири виклики — і кожен повертає своє:

ВикликЩо повертає
Findстворює один тікет і резервує місце (seat) у кімнаті. Резерв тримається стільки, скільки задає шаблон; якщо час минув, гравець утрачає місце, але не право грати
Joinзавершується, коли надійде поточний стан кімнати. Танк, якого шаблон створює для кожного нового учасника (member), входить у цей стан, тож My<Tank>() повертає його вже в наступному рядку, а все після Join — це живий трафік
Castпостріл — це дія здібності (ability), а не окремий інтерфейс: оголошення [Projectile] і є здібністю, до якої додано балістику. Тому Cast перевіряє перезаряджання, боєзапас і ціль так само, як для ривка чи лікування (Entity Presets)
Abilitiesзгенерований тип: кодогенерація збирає оголошені здібності й снаряди в один тип для кожного біндингу

Відмову платформа повертає як типізований Problem — код і пояснення для людини. C#, TypeScript, Python і Unreal викидають його як виняток, а Go повертає як значення помилки — тому у вкладці Go код перевіряє кожен виклик. Виділений сервер Unreal або майстер-клієнт запускає той самий бінарник, але з ключем хоста (host key), і його ролі містять рядки mc. Увесь цей сценарій описано в розділі Хостинг кімнати на сторінці Rooms, а ключі й ролі, що за ним стоять, оголошують на сторінці Access & Roles.

Завантажте й грайте

playserv push          # schema + declarations + hooks, one deploy
playserv open battle   # a dev-env room, live in the panel

playserv push сканує проєкт, у якому його запустили, — хуки й оголошення на атрибутах — і деплоїть їх у вибране середовище (типово --env dev). playserv schema push завантажує лише модель, а playserv schema diff показує, з чим порівнюватимуть завантаження (Схема в коді).

Платформа застосовує завантаження цілком або не застосовує зовсім. Якщо розгорнута схема змінилася після вашого diff, платформа відхилить завантаження, а не зіллє зміни. Перенести модель із dev у prod можна на операторському рівні (operator plane), а не викликом SDK (The Operator Plane).

Несумісні зміни стають міграціями

Зміна, яка зламала б наявні дані, не проходить разом із завантаженням. Вона стає міграцією: ви спершу її переглядаєте, а потім запускаєте або скасовуєте (Схема в коді).

playserv open battle створює одну кімнату із завантаженого шаблону battle і відкриває її в панелі: там видно стан кімнати та її учасників, поки ви в ній граєте. Тепер у панелі є шаблон, мапа, таблиця випадання (drop table) і хуки — та сама модель, яку ви написали в коді, і там її теж можна редагувати.

Числа, які обирали не ви

Capacity = 8, Hz30, Hz = 10 і Cooldown = 1.5f — це налаштування саме цієї гри, а не стеля. Власні ліміти платформи вищі, і для кожного описано, що отримає код, який викликає, коли впреться в межу:

На межіЩо отримує код, який викликає
вхід у заповнену або закриту кімнатуconflict — варто повторити, коли звільниться місце
створення кімнати понад ліміт на проєкт чи на акторавідмова; уже створені кімнати нікуди не зникають
надто часте створення кімнат або вхідвідмова через ліміт частоти із зазначенням, скільки чекати
корисне навантаження події, більше за ліміт кімнативідмова ще до відправлення, без обрізання
читання понад стелю рядків, дозволену ролістільки рядків, скільки дозволяє стеля, і позначка, що результат обрізано

Самі числа задають для кожного середовища окремо, і їх опублікують разом із лімітами платформи. Поведінка на межі на них не чекає (Rooms, Access & Roles, Auth & Players).

Що далі

  1. Приклади — наступний розділ: таблиця лідерів у Tanks, аптечки в Tanks або рецепт щоденного турніру для мета-циклу. Кожен приклад — одна справжня фіча, і кожен крок веде на сторінку модуля, яким ви щойно скористалися.
  2. Як працює SDK — коли будова SDK почне важити більше за наступну фічу. Основні поняття — це словник, а ще чотири статті відповідають на запитання хто викликає (Хто ухвалює рішення), як записують доступ (Access & Roles), із чого складається SDK (Як побудовано SDK) і як його виконують (Потоки, час життя і тести).
  3. Далі — модулі. Усі сторінки модулів мають однакову структуру, тож кожну наступну читати легше. На дві з них спирається все інше: Entity і Data & Subscriptions.

Що читати залежно від ролі

Хоч би яка у вас роль, почніть зі сторінки Хто ухвалює рішення: один SDK, у якому кожен актор має свої видані права (grant), — спільна основа для всіх. А Основні поняття тримайте відкритими поруч.

Хто виЩо читати, по черзі
Розробник клієнта гри (Unity · клієнт Unreal · TS)Auth & Players → Matchmaking → Rooms → Entity → Data & Subscriptions, далі — за потрібними фічами: Inventory · Leaderboards · Messaging · Profile
Серверний розробник (C# · TS · Python · Go)Схема в коді → будівельні блоки → Entity → Extensibility → Access & Roles, далі — модулі, оголошення яких ведете ви: Rooms · Matchmaking · Leaderboards · Catalog & Commerce
Розробник виділеного сервера на UnrealRooms (розділ «Хостинг кімнати») → Bots → Locomotion · об’єкти світу → Map → Що лишається після втрати хоста