Aller au contenu principal
Briques de base

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

RPC

Un appel typé dont le corps vit ailleurs. Déclarez la procédure là où elle a sa place, sur un module ou dans une entité, et chaque binding reçoit une méthode générée que l’on peut attendre (await). Le verbe est invoke ; le sens unique est un mode de la déclaration, pas un verbe à part.

Quand l’utiliser

  • L’appelant a besoin d’une réponse : requête/réponse avec un retour typé.
  • L’appelant signale quelque chose et passe à la suite : un RPC déclaré à sens unique, rien ne revient.
  • Le travail dure plus longtemps que l’appel : un RPC déclaré différé renvoie un descripteur de tâche au lieu d’expirer.
  • Une question, plusieurs répondants : un appel de groupe représente N appels, et chaque réponse arrive rattachée au membre qui l’a envoyée.
  • Le verbe appartient à un objet : déclarez-le dans l’entité, là où il vit et nulle part ailleurs (Entity montre la déclaration).
  • Inutile quand personne n’est chargé d’agir. Un fait auquel les autres se contentent de réagir est un événement.

Qui fait quoi

ActeurCe qu’il fait sur cette page
schema-authordéclare les RPC, leurs modes et qui a le droit de les appeler
tout acteurinvoque un appel avec réponse ou à sens unique, là où la déclaration le permet
membre du grouperépond à un appel diffusé au groupe ; chaque membre renvoie une réponse

En bref

Déclarez un RPC avec réponse et un RPC à sens unique ; invoquez les deux, puis diffusez l’appel à un groupe
[Rpc]                                        // answering, immediate, not overridable — the bare defaults
public static ScoreVerdict SubmitScore(ScoreReport report) => Scores.Judge(report);

[Rpc(OneWay = true)]                         // declared one-way: nothing travels back
public static void ReportPing(PingSample sample) => Metrics.Add(sample);

// invoking — generated, typed, awaitable
var verdict = await playserv.Rpc.Invoke.SubmitScore(report);
playserv.Rpc.Invoke.ReportPing(sample);      // one-way by declaration, not by call site

// group fan-out: N calls, one answer bound to each member
await foreach (var answer in squad.Invoke.ReadyCheck())
    Hud.Mark(answer.Member, answer.Ready);
export class MatchRpcs {
  @Rpc()                                     // answering, immediate, not overridable — the bare defaults
  static submitScore(report: ScoreReport): ScoreVerdict { return Scores.judge(report); }

  @Rpc({ oneWay: true })                     // declared one-way: nothing travels back
  static reportPing(sample: PingSample): void { Metrics.add(sample); }
}

// invoking — generated, typed, awaitable
const verdict = await playserv.rpc.invoke.submitScore(report);
playserv.rpc.invoke.reportPing(sample);      // one-way by declaration, not by call site

// group fan-out: N calls, one answer bound to each member
for await (const answer of squad.invoke.readyCheck())
  hud.mark(answer.member, answer.ready);
@rpc()                                       # answering, immediate, not overridable — the bare defaults
def submit_score(report: ScoreReport) -> ScoreVerdict:
    return scores.judge(report)

@rpc(one_way=True)                           # declared one-way: nothing travels back
def report_ping(sample: PingSample):
    metrics.add(sample)

# invoking — generated, typed, awaitable
verdict = await playserv.rpc.invoke.submit_score(report)
playserv.rpc.invoke.report_ping(sample)      # one-way by declaration, not by call site

# group fan-out: N calls, one answer bound to each member
async for answer in squad.invoke.ready_check():
    hud.mark(answer.member, answer.ready)
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.

// invoking — generated, typed (the client surface; bodies live where routing sends them)
Client->Rpc->Call->SubmitScore(Report,
    TPSOnResult<FScoreVerdict>::CreateWeakLambda(this, [this](const TPSResult<FScoreVerdict>& Result)
    {
        if (!Result.HasValue()) { return; }
        Hud->ShowVerdict(Result.Value());
    }));
Client->Rpc->CallOneWay->ReportPing(Sample);   // one-way by declaration, not by call site

// group fan-out: one call, one answer bound to each member
Squad->Call->ReadyCheck(TPSOnResult<FReadyAnswer>::CreateWeakLambda(this,
    [this](const TPSResult<FReadyAnswer>& Answer)
    {
        if (!Answer.HasValue()) { return; }
        Hud->Mark(Answer.Value().Member, Answer.Value().Ready);   // the delegate fires once per member
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
// Unity invokes; RPC bodies execute on the platform or a host — engines are not a handler runtime
var verdict = await playserv.Rpc.Invoke.SubmitScore(report);
playserv.Rpc.Invoke.ReportPing(sample);      // one-way by declaration, not by call site

await foreach (var answer in squad.Invoke.ReadyCheck())
    Hud.Mark(answer.Member, answer.Ready);

L’endroit où le corps s’exécute — fonction cloud, client, client maître ou serveur de jeu — relève du routage, déclaré méthode par méthode ; Extensibility traite des surcharges et du middleware. Un appel qui échoue lève un Problem typé (Core).

Le modèle

Ce que déclare un RPC.

DéclareCe que c’est
nametiré du vocabulaire des verbes
inputles arguments que l’appelant doit choisir
outputexactement un type déclaré. Une réponse plus courte est un type déclaré à part entière, pas le même type avec des champs en moins ; sinon « non demandé », « absent » et « masqué par le masque d’accès » se ressembleraient
mode de réponsewith a reply : une valeur du type de sortie déclaré ou un refus typé. one-way : pas de réponse ; l’appelant n’apprend qu’un échec d’envoi local
mode d'exécutionimmediate : le résultat revient dans l’appel. deferred : l’appel renvoie un descripteur de tâche, et le résultat se lit ou arrive par abonnement. Déclaré, et non choisi à l’exécution selon la charge, car les appelants dépendent de la forme de la réponse
streamingindique si l’entrée et la sortie arrivent par morceaux et sont traitées au fil de l’eau, plutôt qu’en un bloc
idempotencyun RPC à sens unique porte lui aussi une clé d’idempotence : pas de réponse ne veut pas dire pas de relivraison
overridabilitydéclarée sur la méthode elle-même. Sans déclaration, une méthode ne peut pas être surchargée
contextl’endroit où il est déclaré. Un RPC déclaré dans une entité fait partie de cette entité et n’existe pas en dehors. Le déclarer dans le serveur de jeu, c’est l’enregistrer dans le routeur — il n’existe pas d’autre moyen d’en ajouter un
with a reply · immediatewith a reply · deferredone-wayinvokethe outcome, or a typed refusalinvokea work descriptor, at oncethe outcome, laterinvokeno reply within the callread it, or subscribeonly a local send failure
Ce que l’appelant récupère : le résultat dans l’appel, un descripteur de tâche tout de suite et le résultat plus tard, ou aucune réponse

N’utilisez pas le sens unique si le résultat vous importe

Un appel à sens unique ne signale à l’appelant qu’un échec d’envoi local. Si vous devez savoir ce qui s’est passé, déclarez une réponse : un résultat inconnu coûte plus cher qu’un refus connu.

Ce que transporte une invocation.

TransporteCe que c’est
argumentsuniquement ce que l’appelant doit choisir
contexte implicitele destinataire, l’appelant et le contexte ambiant, liés avant votre premier paramètre écrit — on ne demande jamais à la méthode d’une entité l’identifiant de cette entité
referencesun objet du SDK passé en argument voyage sous la forme d’une Ref typée : un identifiant ou un curseur, pas une copie de son contenu
outcomeune valeur du type de sortie déclaré, ou un Problem typé

Une référence est une adresse, pas une permission

Le destinataire résout une Ref pour son propre compte, avec ses propres permissions et prédicats. Passer une référence ne donne pas accès à ce qu’elle désigne.

Ce que contient le descripteur d’un appel différé.

ContientCe que c’est
stateaccepted → running → completed ou failed, les deux derniers étant terminaux
lifetimedéclarée ; passé ce délai, le résultat n’est plus disponible et le demander provoque un refus, pas une réponse vide
cancelidempotent. C’est une demande : l’état terminal que vous observez est celui des deux, completed ou failed, que la tâche a atteint

Vrai pour tout RPC.

ToujoursCe que c’est
un seul handlerexactement un handler logique. Un RPC demande une action, et son handler doit exister ; un événement énonce un fait et peut n’avoir aucun destinataire. Un RPC à sens unique n’est donc pas un événement, et s’adresser à un groupe représente N appels, pas un seul : Groups fournit les adresses, et les réponses reviennent sous forme de flux, chacune rattachée au membre qui l’a envoyée
pas de machine à étatsni une déclaration ni un appel immédiat n’en ont : l’appel a renvoyé un résultat ou non, et les règles de timeout s’appliquent ensuite. Seul un appel différé a des états observables
un fluxn’est pas atomique. Une sortie en streaming ne garantit rien sur l’ensemble : le destinataire doit gérer une interruption et distinguer « terminé » de « interrompu »
pas de prédicat en écritureaucune écriture n’accepte de prédicat en entrée : « faire ceci pour tous ceux qui remplissent cette condition » n’est pas une opération. Une action de masse s’exprime par énumération : lisez l’ensemble, puis passez la liste à une opération par lot dont la sémantique d’échec partiel est déclarée
Un RPC différé : l’appel renvoie un descripteur de tâche, le résultat arrive en s’y rattachant
[Rpc(Execution = Execution.Deferred)]        // minutes of work — an answer inside the call would be a timeout
public static MatchReport BuildMatchReport(MatchId match) => Reports.Build(match);

var work = await playserv.Rpc.Invoke.BuildMatchReport(matchId);   // the descriptor, not the report
work.OnOutcome(report => Hud.ShowReport(report));                 // or read it later, by descriptor
await work.Cancel();                                              // a request, not a promise nothing ran
export class ReportRpcs {
  @Rpc({ execution: Execution.Deferred })    // minutes of work — an answer inside the call would be a timeout
  static buildMatchReport(match: MatchId): MatchReport { return Reports.build(match); }
}

const work = await playserv.rpc.invoke.buildMatchReport(matchId);  // the descriptor, not the report
work.onOutcome((report) => hud.showReport(report));                // or read it later, by descriptor
await work.cancel();                                               // a request, not a promise nothing ran
@rpc(execution=Execution.DEFERRED)           # minutes of work — an answer inside the call would be a timeout
def build_match_report(match: MatchId) -> MatchReport:
    return reports.build(match)

work = await playserv.rpc.invoke.build_match_report(match_id)   # the descriptor, not the report
work.on_outcome(lambda report: hud.show_report(report))         # or read it later, by descriptor
await work.cancel()                                             # a request, not a promise nothing ran
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.

// the client side of a deferred call: a descriptor now, the outcome against it later
Client->Rpc->Call->BuildMatchReport(MatchId,
    TPSOnResult<FPSDeferredHandle>::CreateWeakLambda(this, [this](const TPSResult<FPSDeferredHandle>& Result)
    {
        if (!Result.HasValue()) { return; }
        const FPSDeferredHandle Work = Result.Value();
        TPSSubscription ReportWatch = Client->Rpc->Deferred->Subscribe(Work,
            [this](const FMatchReport& Report) { Hud->ShowReport(Report); });
        Client->Rpc->Deferred->Cancel(Work);   // a request, not a promise nothing ran
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
// the client side of a deferred call: a descriptor now, the outcome against it later
var work = await playserv.Rpc.Invoke.BuildMatchReport(matchId);
work.OnOutcome(report => Hud.ShowReport(report));
await work.Cancel();                         // a request, not a promise nothing ran

Erreurs

  • Le droit porte sur le RPC, jamais sur la primitive. Il n’existe pas de « peut invoquer » global : chaque déclaration nomme la permission atomique que doit détenir son appelant. Un appelant qui ne l’a pas reçoit un refus typé forbidden, pas un abandon silencieux.
  • Une instance masquée se lit comme not found. Un RPC d’entité invoqué sur une instance que le prédicat de lignes de l’appelant masque répond exactement comme la lecture de cette instance : le refus n’apprend rien à l’appelant sur ce qui existe.
  • L’absence de handler, c’est unavailable, pas not found. Le RPC est déclaré, il existe donc ; ce qui manque, c’est une route. Réessayer vaut la peine, car un serveur de jeu peut revenir ; not found dirait à l’appelant d’arrêter.
  • Un refus de hook porte le code et le motif propres au hook : « rejeté par une règle du jeu » n’arrive donc jamais sous l’apparence de « le transport a lâché ».
  • Un timeout n’est pas un résultat. Pour un appel différé, vous lisez le descripteur ; pour un appel immédiat, la clé d’idempotence déclarée permet de réessayer sans risque.

Limites

Les valeurs chiffrées arriveront avec le chapitre sur les limites de la plateforme ; ce qui se passe à chaque plafond est déjà fixé.

  • Taille de l’entrée : l’appel est refusé avant l’exécution.
  • Taille de la sortie : refusée plutôt que tronquée, car on ne distingue pas une réponse rognée d’une réponse complète.
  • Débit d’appels par acteur : un refus pour limite de débit, qui indique quand réessayer.
  • Appels différés simultanés par acteur : le nouveau est refusé, ceux en cours se terminent.
  • Durée de vie du descripteur : passé ce délai, le résultat n’est plus disponible, et le demander provoque un refus.
  • Profondeur de la chaîne d’appels : un refus déclaré en cas de dépassement, pas un épuisement des ressources ni une rupture silencieuse.