Aller au contenu principal
Par où commencer

Cette page n’est pas encore traduite : la version anglaise est affichée.

Premiers pas

Une arène jouable (carte, chars, tirs, butin), déclarée de bout en bout. Rien de ce qui suit n’est une boucle de jeu : la simulation tourne dans la plateforme.

Avant de commencer

Il vous faut un projet doté d’un environnement dev (créé sur le plan opérateur, qui gère ce cycle de vie), la CLI playserv connectée à ce projet et le package SDK de votre binding. Rien d’autre ne s’installe dans votre jeu.

Chaque appel que vous faites figure dans les exemples ci-dessous ; entre deux appels, c’est la plateforme qui agit selon ce que dit une déclaration. Trois éléments utilisés plus bas sont des presets d’entité, c’est-à-dire des déclarations posées sur des entités et non des modules à part entière : la capacité (ability), la statistique et la table de butin (drop table).

Déclarer le monde

Une entité, c’est votre schéma plus ses aspects dynamiques. Un attribut par comportement, placé à côté du champ (field) qu’il décrit :

Déclaration de l’entité Tank : trois politiques de synchronisation et trois aspects de gameplay, une ligne chacun
[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)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

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 — les mêmes attributs compilent dans Unity, et rien ici n’exige C# 12 — le code compile dès la version de référence 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;
}

Modifier un champ [Sync], c’est déjà l’opération réseau. Pas de snapshot à assembler, pas d’appel d’envoi à écrire.

Aucun de ces types n’est à définir de votre côté, et chacun relève d’une seule page :

Dans le blocVient de
Vector3, Statle package de base de votre binding
Body et les formes de corpsCollision
Motion et les cinq modèles de mouvementLocomotion
ObstacleSet, Drop, Flight, Ammo, Effectles presets d’entité qui les utilisent
EntryRequest, Verdict, StatEventles données reçues par les hooks, fournies par le module auquel le hook se rattache
SeatMatchmaking
Scope et les portées de synchronisationVisibility
Tick et les fréquences de tickRooms

Les énumérations sont fermées. Une règle qu’aucun membre ne couvre s’écrit comme un prédicat, pas comme un nouveau membre : [Aspect("loadout", Visible = "owner == caller.player")] exprime une visibilité par champ quand Scope.Owner ne correspond pas tout à fait à la règle voulue (Data & Subscriptions).

Déclarer la salle

Un modèle de salle (room template) décrit ce qu’est une session et nomme les déclarations sur lesquelles il s’appuie. Pas de classe de salle dont hériter, pas de méthode de tick à remplir, car le fonctionnement interne d’une salle appartient à la plateforme :

Le modèle battle et les trois déclarations qu’il nomme : une arène, une table de butin, une arme
[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)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

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 — les quatre mêmes déclarations compilent dans Unity ; un build Unity lit le modèle publié et rejoint les salles créées à partir de lui
[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;
}

Les identifiants entre guillemets sont des clés de contenu, pas du texte libre :

CléCe qu’elle désigne
rockun élément de décor du jeu d’obstacles de la carte (Map)
ammo.shell, railgundes objets du catalogue (Catalog & Commerce) — c’est aussi grâce à eux que le tir débite les munitions et que le butin ramassé atterrit dans l’inventaire
battle, arena, crate-loot, shellles clés qu’enregistrent ces quatre déclarations

playserv push refuse une déclaration dont la clé n’existe pas dans l’environnement ciblé : une clé mal saisie échoue donc au déploiement, et non au premier lancement de la capacité. Où que vive le modèle, il reste réglable sans redéployer le build du jeu : c’est le modèle publié que les live-ops modifient dans le panneau d’administration.

Écrire vos règles sous forme de hooks

Les hooks sont des fonctions cloud que la plateforme appelle à des étapes nommées. Entrée typée, sortie typée : ni objet de contexte fourre-tout ni logger dans la signature.

Trois règles sous forme de hooks : refouler les joueurs bannis à l’entrée, donner 20 obus à un nouveau joueur, tirer le butin à la mort
[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)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

S’exécute hors du moteur

Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — Unreal s’abonne aux événements qui en résultent. Des fonctions écrites côté moteur sont prévues : des graphes Blueprint, et du C++ (traduit vers une cible de script validée par la plateforme). Les deux seront analysés et publiés comme n’importe quelle déclaration ; l’exécution reste sur la plateforme.

S’exécute hors du moteur

Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — Unity s’abonne aux événements qui en résultent.

Un filtre Before peut refuser l’étape ; un observateur After s’exécute une fois l’étape validée et ne le peut plus. Un kit de départ qui n’arrive pas coûte donc 20 obus, pas la connexion. La suite se trouve dans Extensibility.

Ce qui exécute réellement chaque ligne de ce bloc :

La ligneCe qui l’exécute réellement
Stats.Depleted se déclenchela statistique atteint son plancher — 0 pour Hp, puisque la déclaration ne fixe que Max
la transition vers la mortAtMin = "death" à l’étape 1 ; le hook ajoute la conséquence, pas la transition
les dégâts[Effect(Damage = 35)] sur l’obus, que la plateforme applique à l’impact
RollAtgénéré sur la déclaration [DropTable] — d’où le partial, et d’où le drops.RollAtCrateLoot de l’onglet Go
le ramassagerouler sur le butin transfère les objets dans l’inventaire du joueur, de façon atomique ; ce transfert est l’événement changed qu’affiche le HUD

Types générés pour le moteur

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

Lancez cette commande (ou laissez la CI le faire) après chaque publication du schéma. Les types sont régénérés, jamais modifiés à la main, et le Tank généré est exactement le Tank publié. playserv push lit un projet moteur exactement comme un projet serveur : les spécificateurs UHT et les attributs C# sont la déclaration, si bien que pour exporter, il suffit de pointer la CLI sur le projet UE ou Unity. Le thread sur lequel arrive un callback et le moment où prend fin un abonnement (subscription) sont fixés par le modèle d’exécution — Threads, cycle de vie et tests.

Connecter un client

L’API client est symétrique : ce sont les mêmes modules, et c’est la clé sous laquelle tourne un build qui décide de ce qu’il peut appeler. Un build moteur embarque une clé joueur (player key) — ici projectKey, l’identifiant d’un projet et d’un environnement, émis dans le panneau et livré dans le build. Elle ne nomme aucun rôle : les rôles se résolvent côté serveur à chaque requête, et le joueur qui les porte arrive avec SignIn. Les bindings moteur sont ici au premier plan ; les bindings serveur pilotent la même interface sans interface graphique (un cerveau de bot, un test de charge, un outil d’exploitation) :

La session client : se connecter, trouver une partie, rejoindre la salle, réagir aux changements, piloter et tirer
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)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

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 — tourne tel quel dans Unity avec les types générés
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);

Quatre appels, et chacun répond à une question différente :

AppelCe qu’il renvoie
Findun ticket placé et une place (seat) réservée dans une salle. La réservation tient pendant la durée que déclare le modèle ; la laisser expirer coûte la place, pas le droit de jouer
Joinse résout une fois arrivé l’état courant de la salle. Le Tank que le modèle fait apparaître pour chaque membre entrant fait partie de cet état : My<Tank>() répond donc dès la ligne suivante, et tout ce qui suit Join est du trafic en direct
Casttirer passe par le verbe des capacités, pas par une seconde interface : une déclaration [Projectile] est une capacité avec de la balistique en plus, donc Cast vérifie le temps de recharge, les munitions et la cible exactement comme pour un dash ou un soin (Entity Presets)
Abilitiesgénéré — la génération de code rassemble les capacités et les projectiles déclarés en un seul type par binding

Les refus arrivent sous la forme du Problem typé de la plateforme : un code et un motif lisible par un humain. C#, TypeScript, Python et Unreal le lèvent comme exception ; Go le renvoie comme valeur d’erreur, d’où la vérification de chaque appel dans cet onglet. Un serveur dédié Unreal ou un client maître (master-client) exécute le même binaire. Seule la clé change : c’est une clé hôte (host key), et ses rôles portent les lignes mc. Dans Rooms, Héberger une salle décrit cette interface de bout en bout, et Access & Roles montre où se déclarent les clés et les rôles qui la sous-tendent.

Publier et jouer

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

playserv push analyse le projet dans lequel il tourne — hooks à attributs et déclarations — et déploie dans l’environnement ciblé (--env dev par défaut). playserv schema push seul ne publie que le modèle, et playserv schema diff montre ce à quoi une publication sera comparée (Le schéma dans le code).

Une publication s’applique en entier ou pas du tout, et elle est refusée, pas fusionnée, si le schéma déployé a changé depuis votre diff. Faire passer un modèle de dev à prod relève du plan opérateur (operator plane), pas d’un appel SDK (le plan opérateur).

Les changements cassants deviennent des migrations

Un changement qui casserait les données existantes ne part pas avec la publication. Il devient une migration que vous relisez d’abord, puis que vous exécutez ou annulez (Le schéma dans le code).

playserv open battle crée une salle à partir du modèle battle publié et l’ouvre dans le panneau, où vous pouvez inspecter son état et ses membres pendant que vous y jouez. Le panneau affiche désormais le modèle de salle, la carte, la table de butin et les hooks : ce que vous avez écrit dans le code, modifiable là aussi.

Les chiffres que vous n’avez pas choisis

Capacity = 8, Hz30, Hz = 10 et Cooldown = 1.5f sont les réglages de ce jeu, pas des plafonds. Les limites propres à la plateforme se situent au-dessus, et chacune est déclarée avec ce que l’appelant observe lorsqu’il l’atteint :

À la limiteCe que reçoit l’appelant
rejoindre une salle pleine ou ferméeconflict — à retenter quand une place se libère
créer des salles au-delà de la limite par projet ou par acteurun refus ; rien de ce qui existe déjà n’est supprimé
créer des salles ou se connecter trop viteun refus pour dépassement de débit, qui indique le délai à attendre
un événement dont les données dépassent le plafond de la salleun refus avant l’envoi ; rien n’est jamais tronqué
une lecture au-delà du plafond de lignes d’un rôleautant de lignes que le plafond en autorise, plus un marqueur signalant la coupe

Les valeurs elles-mêmes dépendent de l’environnement et arriveront avec les limites de la plateforme ; le comportement à la limite, lui, n’attend pas (Rooms, Access & Roles, Auth & Players).

Pour aller plus loin

  1. Exemples, la section qui suit immédiatement : un classement dans Tanks, des trousses de soin dans Tanks ou la recette du tournoi quotidien pour la boucle méta. Chaque fois une vraie fonctionnalité, et chaque étape renvoie à la page du module dont relève ce que vous venez d’utiliser.
  2. Fonctionnement du SDK, quand sa forme commence à compter davantage que la prochaine fonctionnalité : Concepts clés sert de dictionnaire, et quatre articles répondent à qui appelle (Qui prend les décisions), comment s’écrivent les droits accordés (Access & Roles), de quoi est fait le SDK (Comment le SDK est construit) et comment il s’exécute (Threads, cycle de vie et tests).
  3. Ensuite, les modules. Toutes les pages de module suivent la même structure : chacune se lit donc plus vite que la précédente. Entity et Data & Subscriptions sont les deux sur lesquelles tout le reste s’appuie.

Parcours de lecture par rôle

Quel que soit votre rôle, commencez par Qui prend les décisions : un seul SDK, avec des droits accordés (grant) propres à chaque acteur, c’est le prérequis commun. Gardez Concepts clés ouvert à côté.

Vous êtesÀ lire, dans l’ordre
Développeur client (Unity · client Unreal · TS)Auth & Players → Matchmaking → Rooms → Entity → Data & Subscriptions, puis selon la fonctionnalité : Inventory · Leaderboards · Messaging · Profile
Développeur serveur (C# · TS · Python · Go)Le schéma dans le code → les briques de base → Entity → Extensibility → Access & Roles, puis les modules dont vous possédez les déclarations : Rooms · Matchmaking · Leaderboards · Catalog & Commerce
Développeur de serveur dédié UnrealRooms (Héberger une salle) → Bots → Locomotion · objets du monde → Map → Ce qui survit à la perte d’un hôte