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ê
| Ator | O que faz nesta página |
|---|---|
schema-author | declara RPCs, os modos deles e quem pode chamá-los |
qualquer ator | invoca uma chamada com resposta ou unidirecional, onde a declaração permite |
membro do grupo | responde a uma chamada distribuída; volta uma resposta por membro |
Resumo
[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)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.
| Declara | O que é |
|---|---|
name | tirado do vocabulário de verbos |
input | os argumentos que quem chama precisa escolher |
output | exatamente 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 resposta | with 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ção | immediate: 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 |
streaming | se a entrada e a saída chegam em partes e são tratadas à medida que chegam, em vez de inteiras |
idempotency | um RPC unidirecional também leva uma chave de idempotência: não ter resposta não significa não ter reentrega |
overridability | declarada no próprio método. Sem declaração, um método não pode ser sobrescrito |
context | onde 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 |
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.
| Carrega | O que é |
|---|---|
arguments | só o que quem chama precisa escolher |
contexto implícito | o 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 |
references | um objeto do SDK passado como argumento viaja como um Ref tipado: um identificador ou um cursor, não uma cópia do conteúdo |
outcome | um 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.
| Guarda | O que é |
|---|---|
state | accepted → running → completed ou failed, os dois últimos terminais |
lifetime | declarado; depois dele o resultado fica indisponível, e pedi-lo gera uma recusa, não uma resposta vazia |
cancel | idempotente. É um pedido: o estado terminal que você observa é aquele, entre completed e failed, a que o trabalho chegou |
Vale para todo RPC.
| Sempre | O que é |
|---|---|
um único handler | exatamente 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 estados | nem 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 fluxo | nã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 escrita | nenhuma 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 |
[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 ranexport 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 ranAinda 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 ranErros
- 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.