Saltar al contenido principal
Empieza aquí

Esta página aún no está traducida: se muestra la versión en inglés.

Primeros pasos

Una arena jugable (mapa, tanques, disparos y botín), declarada de principio a fin. Nada de lo que sigue es un bucle de juego: la simulación se ejecuta dentro de la plataforma.

Antes de empezar

Necesitas un proyecto con un entorno (environment) dev (se crea en el plano de operador, que gestiona ese ciclo de vida), la CLI playserv con la sesión iniciada en ese proyecto y el paquete del SDK para tu binding. En tu juego no se instala nada más.

Cada llamada que haces tú aparece en los ejemplos de abajo; entre una y otra, la plataforma actúa según lo que dice cada declaración. La habilidad, la estadística y la tabla de botín que se usan abajo son presets de entidad: declaraciones sobre entidades, no módulos independientes.

Declara el mundo

Una entidad es tu esquema más sus aspectos en vivo. Cada comportamiento es un atributo, junto al campo (field) que describe:

Declaración de la entidad Tank: tres políticas de sincronización y tres aspectos de juego, una línea para cada uno
[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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

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 — los mismos atributos compilan en Unity, y nada de esto necesita C# 12: se compila con la versión base 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;
}

Modificar un campo [Sync] ya es la operación de red. No hay que armar ningún snapshot ni hacer ninguna llamada de envío.

Ninguno de esos tipos lo defines tú, y cada uno pertenece a una página:

En el bloqueDe dónde viene
Vector3, Statel paquete base de tu binding
Body y las formas de cuerpoCollision
Motion y los cinco modelos de movimientoLocomotion
ObstacleSet, Drop, Flight, Ammo, Effectlos presets de entidad que los usan
EntryRequest, Verdict, StatEventargumentos de los hooks, que te pasa el módulo donde conectas el hook
SeatMatchmaking
Scope y los alcances de sincronizaciónVisibility
Tick y las frecuencias de tickRooms

Los enums son cerrados. Una regla que ningún miembro cubre se escribe como predicado, no como un miembro nuevo: con [Aspect("loadout", Visible = "owner == caller.player")] se expresa la visibilidad por campo cuando Scope.Owner no es exactamente la regla que querías (Data & Subscriptions).

Declara la sala

Una plantilla de sala (room template) define qué es una sesión y nombra las declaraciones en las que se apoya. No hay ninguna clase de sala (room) que heredar ni un método de tick que completar, porque el funcionamiento interno de la sala es cosa de la plataforma:

La plantilla battle y las tres declaraciones que nombra: una arena, una tabla de botín y un arma
[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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

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 — las mismas cuatro declaraciones compilan en Unity; un build de Unity lee la plantilla subida y entra a salas creadas a partir de ella
[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;
}

Los ids entre comillas son claves de contenido, no texto libre:

ClaveQué nombra
rockun objeto del conjunto de obstáculos del mapa (Map)
ammo.shell, railgunartículos del catálogo (Catalog & Commerce); gracias a eso, el disparo descuenta munición y lo que recoges termina en el inventario
battle, arena, crate-loot, shelllas claves que registran estas cuatro declaraciones

playserv push rechaza una declaración cuya clave no existe en el entorno de destino, así que una clave mal escrita falla al desplegar y no la primera vez que alguien usa la habilidad. Esté donde esté la plantilla, puedes reajustarla sin volver a desplegar el build del motor: el modelo subido es lo que live-ops edita en el panel.

Escribe tus reglas como hooks

Los hooks son funciones en la nube que la plataforma llama en pasos con nombre. Reciben y devuelven tipos concretos: la firma no lleva un objeto de contexto ni un logger.

Tres reglas como hooks: rechazar en la entrada a los jugadores baneados, darle 20 proyectiles a cada jugador nuevo y sortear botín al morir
[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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unreal se suscribe a los eventos que produce. Están previstas funciones escritas desde el motor: grafos de Blueprint y C++ (traducido a un formato de script que la plataforma valida). Ambos se analizan y se suben como cualquier declaración, y la ejecución sigue en la plataforma.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unity se suscribe a los eventos que produce.

Un filtro Before puede rechazar el paso; un observador After se ejecuta cuando el paso ya se confirmó y no puede rechazarlo. Por eso, si el paquete inicial no llega, se pierden 20 proyectiles, no el inicio de sesión. El resto está en Extensibility.

Qué hay detrás de cada línea de ese bloque:

La líneaQué la ejecuta en realidad
se dispara Stats.Depletedla estadística (stat) llega a su mínimo: 0 para Hp, porque la declaración solo fija Max
la transición a la muerteAtMin = "death" en el paso 1; el hook agrega la consecuencia, no la transición
el daño[Effect(Damage = 35)] en el proyectil, que la plataforma aplica cuando impacta
RollAtse genera sobre la declaración [DropTable]; por eso la clase es partial y por eso en la pestaña de Go aparece drops.RollAtCrateLoot
la recogidaal pasar por encima del botín, los artículos se transfieren de forma atómica al inventario del jugador; esa transferencia es el evento changed que dibuja el HUD

Tipos generados para el motor

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

Ejecútalo (o deja que lo ejecute la CI) cada vez que subas el esquema. Los tipos se regeneran y nunca se editan a mano: el Tank generado es el Tank subido. playserv push lee un proyecto de motor igual que un proyecto de servidor: los especificadores de UHT y los atributos de C# ya son la declaración, así que para exportar basta con apuntar la CLI al proyecto de UE o de Unity. En qué hilo llega un callback y cuándo termina una suscripción lo fija el modelo de runtime: Hilos, ciclo de vida y pruebas.

Conecta un cliente

La API del cliente es simétrica: los módulos son los mismos, y lo que un build puede llamar lo decide la clave con la que se ejecuta. Un build del motor lleva una clave de jugador (player key): aquí es projectKey, la credencial de un proyecto y un entorno, que se emite en el panel y viaja dentro del build. No nombra ningún rol: los roles se resuelven en el servidor en cada solicitud, y el jugador al que se aplican llega con SignIn. Aquí los bindings del motor son los protagonistas; los bindings de servidor usan la misma interfaz en modo headless (el cerebro de un bot, una prueba de carga, una herramienta de operaciones):

La sesión del cliente: iniciar sesión, buscar partida, entrar, reaccionar a los cambios, conducir y disparar
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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

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 — funciona tal cual en Unity con los tipos generados
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);

Cuatro llamadas, y cada una responde algo distinto:

LlamadaCon qué responde
Findun ticket colocado y un lugar (seat) reservado en una sala. La reserva dura el plazo que declara la plantilla; si vence, pierdes el lugar, no el derecho a jugar
Joinse resuelve cuando llega el estado actual de la sala. El Tank que la plantilla genera para cada miembro que entra forma parte de ese estado, así que My<Tank>() responde en la línea siguiente y todo lo que viene después de Join es tráfico en vivo
Castdisparar es el verbo de la habilidad (ability), no una segunda interfaz: una declaración [Projectile] es una habilidad con balística encima, así que Cast revisa el tiempo de recarga, la munición y el objetivo igual que con un dash o una curación (Entity Presets)
Abilitiesse genera: la generación de código reúne las habilidades y los proyectiles declarados en un solo tipo por binding

Los rechazos llegan como el Problem tipado de la plataforma: un código y un motivo legible para una persona. C#, TypeScript, Python y Unreal lo lanzan como excepción; Go lo devuelve como valor de error, y por eso en esa pestaña se revisa cada llamada. Un servidor dedicado de Unreal o un cliente maestro ejecutan el mismo binario con una clave de host (host key), y sus roles incluyen las filas mc: en Rooms, Alojar una sala recorre esa interfaz de punta a punta, y en Access & Roles se declaran las claves y los roles que hay detrás.

Sube y juega

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

playserv push escanea el proyecto donde se ejecuta (hooks con atributos y declaraciones) y despliega en el entorno que elijas (--env dev por defecto). playserv schema push por sí solo sube únicamente el modelo, y playserv schema diff muestra la referencia contra la que se compara un push (Esquema como código).

Un push se aplica completo o no se aplica, y se rechaza en lugar de fusionarse si el esquema desplegado cambió desde tu diff. Pasar un modelo de dev a prod es una acción del plano de operador (operator plane), no una llamada del SDK (el plano de operador).

Los cambios incompatibles se convierten en migraciones

Un cambio que rompería los datos existentes no viaja con el push. Se convierte en una migración que primero lees y después ejecutas o cancelas (Esquema como código).

playserv open battle crea una sala a partir de la plantilla battle subida y la abre en el panel, donde puedes inspeccionar el estado de la sala y sus miembros mientras juegas en ella. El panel ya muestra la plantilla, el mapa, la tabla de botín (drop table) y los hooks: el mismo modelo que escribiste en código, que también puedes editar ahí.

Los números que no elegiste

Capacity = 8, Hz30, Hz = 10 y Cooldown = 1.5f son los ajustes de este juego, no techos. Los límites de la plataforma están por encima, y para cada uno está definido lo que ve quien llama cuando lo alcanza:

En el límiteQué recibe quien llama
entrar a una sala llena o cerradaconflict: vale la pena reintentar cuando se libere un lugar
crear salas por encima del límite por proyecto o por actorun rechazo; no se elimina nada de lo que ya estaba creado
crear salas o iniciar sesión demasiado rápidoun rechazo por límite de frecuencia que indica cuánto esperar
un evento con datos por encima del máximo de la salaun rechazo antes del envío; los datos nunca se truncan
una lectura que supera el máximo de filas de un roltantas filas como permite el máximo, más una marca que indica que se recortó

Los números en sí dependen del entorno y llegarán con los límites de la plataforma; el comportamiento en el límite no espera a que lleguen (Rooms, Access & Roles, Auth & Players).

Siguientes pasos

  1. Ejemplos, la sección que viene justo después de esta: una tabla de clasificación en Tanks, botiquines en Tanks o la receta del torneo diario para el bucle meta. Cada uno construye una funcionalidad real, y cada paso enlaza a la página del módulo responsable de lo que acabas de usar.
  2. Cómo funciona el SDK, cuando su forma empiece a importarte más que la próxima funcionalidad: Conceptos básicos es el diccionario, y cuatro artículos responden quién llama (Quién toma las decisiones), cómo se escriben los permisos otorgados (grant) (Access & Roles), de qué está hecho el SDK (Cómo está construido el SDK) y cómo se ejecuta (Hilos, ciclo de vida y pruebas).
  3. Después, los módulos. Todas las páginas de módulo siguen la misma estructura, así que cada una se lee más rápido que la anterior. Entity y Data & Subscriptions son las dos en las que se apoya todo lo demás.

Rutas de lectura por rol

Sea cual sea tu rol, lee primero Quién toma las decisiones, porque un solo SDK con permisos otorgados para cada actor es la base común, y ten Conceptos básicos abierto al lado.

Si eresQué leer, en orden
Desarrollador de cliente (Unity · cliente de Unreal · TS)Auth & Players → Matchmaking → Rooms → Entity → Data & Subscriptions, y luego según la funcionalidad: Inventory · Leaderboards · Messaging · Profile
Desarrollador de servidor (C# · TS · Python · Go)Esquema como código → los bloques de construcción → Entity → Extensibility → Access & Roles, y luego los módulos cuyas declaraciones te tocan: Rooms · Matchmaking · Leaderboards · Catalog & Commerce
Desarrollador de servidor dedicado en UnrealRooms (Alojar una sala) → Bots → Locomotion · objetos del mundo → Map → Qué sobrevive a la pérdida de un host