Pular para o conteúdo principal
Blocos de construção

Esta página ainda não foi traduzida: mostrando a versão em inglês.

RPC

Uma chamada tipada cujo corpo roda em outro lugar. Declare o procedimento onde ele faz sentido, num módulo ou dentro de uma entidade, e cada binding ganha um método gerado que pode ser aguardado com await. O verbo é invoke; a chamada sem resposta é um modo da declaração, não um verbo à parte.

Quando usar

  • Quem chama precisa de uma resposta: requisição/resposta com retorno tipado.
  • Quem chama avisa e segue em frente: um RPC declarado como unidirecional, sem nada voltando.
  • O trabalho dura mais que a chamada: um RPC declarado como diferido devolve um descritor de trabalho em vez de estourar o timeout.
  • Uma pergunta, muitos respondendo: uma chamada a um grupo são N chamadas, e cada resposta chega vinculada ao membro que a enviou.
  • O verbo pertence a uma coisa: declare-o dentro da entidade, onde ele vive e em nenhum outro lugar (Entity mostra a declaração).
  • Dispensável quando ninguém precisa agir. Um fato ao qual os outros só reagem é um evento.

Quem faz o quê

AtorO que faz nesta página
schema-authordeclara RPCs, os modos deles e quem pode chamá-los
qualquer atorinvoca uma chamada com resposta ou unidirecional, onde a declaração permite
membro do gruporesponde a uma chamada distribuída; volta uma resposta por membro

Resumo

Declare um RPC com resposta e um unidirecional; invoque os dois e depois distribua a chamada para um grupo
[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)
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

// 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);

Onde o corpo roda — cloud function, cliente, cliente mestre ou servidor do jogo — é questão de roteamento, declarada por método; Extensibility trata de sobrescritas e middleware. Uma chamada que falha lança um Problem tipado (Core).

O modelo

O que um RPC declara.

DeclaraO que é
nametirado do vocabulário de verbos
inputos argumentos que quem chama precisa escolher
outputexatamente um tipo declarado. Uma resposta mais curta é um tipo declarado próprio, não o mesmo tipo com campos omitidos; do contrário, “não foi pedido”, “ausente” e “oculto pela máscara de acesso” pareceriam iguais
modo de respostawith a reply: um valor do tipo de saída declarado ou uma recusa tipada. one-way: sem resposta; quem chama só fica sabendo de uma falha local de envio
modo de execuçãoimmediate: o resultado volta dentro da chamada. deferred: a chamada devolve um descritor de trabalho, e o resultado é lido ou chega por assinatura (subscription). Declarado, e não escolhido em tempo de execução conforme a carga, porque quem chama depende do formato da resposta
streamingse a entrada e a saída chegam em partes e são tratadas à medida que chegam, em vez de inteiras
idempotencyum RPC unidirecional também leva uma chave de idempotência: não ter resposta não significa não ter reentrega
overridabilitydeclarada no próprio método. Sem declaração, um método não pode ser sobrescrito
contextonde ele é declarado. Um RPC declarado dentro de uma entidade faz parte dessa entidade e não existe fora dela. Declarar um RPC no servidor do jogo já é registrá-lo no roteador — não existe uma segunda forma de adicionar um
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
O que quem chama recebe de volta: o resultado dentro da chamada, um descritor de trabalho agora e o resultado depois, ou nenhuma resposta

Não use unidirecional quando precisar do resultado

Uma chamada unidirecional só informa a quem chama uma falha local de envio. Se você precisa saber o que aconteceu, declare uma resposta: um resultado desconhecido custa mais do que uma recusa conhecida.

O que uma invocação carrega.

CarregaO que é
argumentssó o que quem chama precisa escolher
contexto implícitoo receptor, quem chama e o contexto do ambiente, vinculados antes do primeiro parâmetro que você escreve — o método de uma entidade nunca pede o identificador dessa entidade
referencesum objeto do SDK passado como argumento viaja como um Ref tipado: um identificador ou um cursor, não uma cópia do conteúdo
outcomeum valor do tipo de saída declarado ou um Problem tipado

Uma referência é um endereço, não uma permissão

Quem recebe resolve um Ref em nome próprio, com as próprias permissões e predicados. Passar uma referência não dá acesso ao que ela aponta.

O que o descritor de uma chamada diferida guarda.

GuardaO que é
stateaccepted → running → completed ou failed, os dois últimos terminais
lifetimedeclarado; depois dele o resultado fica indisponível, e pedi-lo gera uma recusa, não uma resposta vazia
cancelidempotente. É um pedido: o estado terminal que você observa é aquele, entre completed e failed, a que o trabalho chegou

Vale para todo RPC.

SempreO que é
um único handlerexatamente um handler lógico. Um RPC pede uma ação, e o handler dele precisa existir; um evento afirma um fato e pode não ter destinatário nenhum. Por isso um RPC unidirecional não é um evento, e endereçar um grupo são N chamadas, não uma: Groups fornece os endereços, e as respostas voltam como um fluxo (stream), cada uma vinculada ao membro que a enviou
sem máquina de estadosnem a declaração nem uma chamada imediata têm uma: a chamada devolveu um resultado ou não, e aí valem as regras de timeout. Só uma chamada diferida tem estados observáveis
um fluxonão é atômico. Uma saída em streaming não promete nada sobre o todo, então quem recebe precisa tratar uma interrupção e distinguir “concluído” de “interrompido”
sem predicado na escritanenhuma escrita aceita um predicado como entrada: “faça isto para todos que atendem a esta condição” não é uma operação. Uma ação em massa se expressa por enumeração: leia o conjunto e passe a lista para uma operação em lote com semântica de falha parcial declarada
Um RPC diferido: a chamada devolve um descritor de trabalho, e o resultado chega vinculado a ele
[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
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

// 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

Erros

  • O direito fica no RPC, nunca na primitiva. Não existe um “pode invocar” genérico: cada declaração nomeia a permissão atômica que quem chama precisa ter. Quem chama sem ela recebe uma recusa tipada forbidden, não um descarte silencioso.
  • Uma instância oculta aparece como not found. Um RPC de entidade invocado numa instância que o predicado de linhas de quem chama esconde responde exatamente como a leitura dessa instância responderia, então a recusa não revela nada sobre o que existe.
  • Sem handler é unavailable, não not found. O RPC está declarado, então existe; o que falta é uma rota. Vale tentar de novo, já que um servidor do jogo pode voltar; not found diria a quem chama para parar.
  • Uma recusa de hook traz o código e o motivo do próprio hook, então “rejeitado por uma regra do jogo” nunca chega parecendo “o transporte quebrou”.
  • Um timeout não é um resultado. Numa chamada diferida, você lê o descritor; numa imediata, a chave de idempotência declarada torna a nova tentativa segura.

Limites

Os números chegam com o capítulo de limites da plataforma; o que acontece em cada teto já está definido.

  • Tamanho da entrada: a chamada é recusada antes de executar.
  • Tamanho da saída: recusada em vez de truncada, porque uma resposta cortada não se distingue de uma completa.
  • Taxa de chamadas por ator: uma recusa por limite de taxa, dizendo quando tentar de novo.
  • Chamadas diferidas simultâneas por ator: a nova é recusada; as que já estão em andamento terminam.
  • Tempo de vida do descritor: depois dele o resultado fica indisponível, e pedir gera uma recusa.
  • Profundidade da cadeia de chamadas: uma recusa declarada quando excedida, não recursos esgotados nem uma quebra silenciosa.