Saltar al contenido principal
Bloques de construcción

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

RPC

Una llamada tipada cuyo cuerpo vive en otro lugar. Declara el procedimiento donde corresponde, en un módulo o dentro de una entidad, y cada binding obtiene un método generado al que puedes hacerle await. El verbo es invoke; la llamada unidireccional es un modo de la declaración, no un verbo aparte.

Cuándo usarlo

  • Quien llama necesita una respuesta: petición y respuesta con un resultado tipado.
  • Quien llama avisa y sigue adelante: un RPC unidireccional declarado, del que no vuelve nada.
  • El trabajo dura más que la llamada: un RPC diferido declarado devuelve un descriptor de trabajo (work descriptor) en lugar de agotar el tiempo de espera.
  • Una pregunta, muchos que responden: una llamada a un grupo son N llamadas, y cada respuesta llega vinculada al miembro que la envió.
  • La acción pertenece a una cosa: declárala dentro de la entidad, donde vive y en ningún otro lugar (Entity muestra la declaración).
  • No lo uses cuando no se le pide a nadie que actúe. Un hecho al que otros solo reaccionan es un evento.

Quién hace qué

ActorQué hace en esta página
schema-authordeclara los RPC, sus modos y quién puede llamarlos
cualquier actorinvoca una llamada con respuesta o unidireccional, donde la declaración lo permite
miembro del gruporesponde a una llamada en abanico; de cada miembro vuelve una respuesta

En resumen

Declara un RPC con respuesta y uno unidireccional; invoca ambos y luego llama en abanico a un 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)
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.

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

Dónde se ejecuta el cuerpo —función en la nube, cliente, cliente maestro o servidor de juego— es una cuestión de enrutamiento que se declara por método; Extensibility cubre las sobrescrituras y el middleware. Una llamada fallida lanza un Problem tipado (Core).

El modelo

Qué declara un RPC.

DeclaraQué es
nametomado del vocabulario de verbos
inputlos argumentos que debe elegir quien llama
outputexactamente un tipo declarado. Una respuesta más corta es su propio tipo declarado, no el mismo tipo con campos omitidos; de lo contrario, “no se pidió”, “no existe” y “oculto por la máscara de acceso” se verían igual
modo de respuestawith a reply: un valor del tipo de salida declarado o un rechazo tipado. one-way: sin respuesta; quien llama solo se entera de un fallo de envío local
modo de ejecuciónimmediate: el resultado vuelve dentro de la llamada. deferred: la llamada devuelve un descriptor de trabajo, y el resultado se lee o llega por suscripción. Se declara, no se elige en tiempo de ejecución según la carga, porque quienes llaman dependen de la forma de la respuesta
streamingsi la entrada y la salida llegan por partes y se procesan a medida que llegan, en lugar de completas
idempotencyun RPC unidireccional también lleva una clave de idempotencia (idempotency key): que no haya respuesta no significa que no haya reentrega
overridabilityse declara en el propio método. Sin declaración, un método no se puede sobrescribir
contextdónde se declara. Un RPC declarado dentro de una entidad es parte de esa entidad y no existe fuera de ella. Declararlo en el servidor de juego ya es registrarlo en el enrutador: no hay otra forma de agregarlo
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
Lo que recibe quien llama: el resultado dentro de la llamada, un descriptor de trabajo ahora y el resultado después, o ninguna respuesta

No uses una llamada unidireccional si necesitas el resultado

Una llamada unidireccional solo le avisa a quien llama de un fallo de envío local. Si necesitas saber qué pasó, declara una respuesta: un resultado desconocido cuesta más que un rechazo conocido.

Qué lleva una invocación.

LlevaQué es
argumentssolo lo que debe elegir quien llama
contexto implícitoel receptor, quien llama y el contexto del entorno, vinculados antes del primer parámetro que escribes tú: al método de una entidad nunca se le pide el identificador de esa entidad
referencesun objeto del SDK que se pasa como argumento viaja como un Ref tipado: un identificador o un cursor, no una copia de su contenido
outcomeun valor del tipo de salida declarado o un Problem tipado

Una referencia es una dirección, no un permiso

El destinatario resuelve un Ref por su cuenta, con sus propios permisos y predicados. Pasar una referencia no da acceso a lo que apunta.

Qué contiene el descriptor de una llamada diferida.

ContieneQué es
stateaccepted → running → completed o failed; los dos últimos son finales
lifetimese declara; pasado ese tiempo el resultado ya no está disponible y pedirlo devuelve un rechazo, no una respuesta vacía
cancelidempotente. Es una solicitud: el estado final que observes será el que haya alcanzado el trabajo, completed o failed

Lo que vale para cualquier RPC.

SiempreQué es
un solo handlerexactamente un handler lógico. Un RPC pide una acción y su handler tiene que existir; un evento enuncia un hecho y puede no tener ningún destinatario. Por eso un RPC unidireccional no es un evento, y dirigirse a un grupo son N llamadas, no una: Groups aporta las direcciones, y las respuestas vuelven como un flujo (stream), cada una vinculada al miembro que la envió
sin máquina de estadosno la tiene ni una declaración ni una llamada inmediata: la llamada devolvió un resultado o no, y a partir de ahí rigen las reglas de timeout. Solo una llamada diferida tiene estados observables
un flujono es atómico. Una salida en streaming no promete nada sobre el conjunto, así que el receptor debe manejar una interrupción y distinguir “completado” de “interrumpido”
escritura sin predicadoninguna escritura acepta un predicado como entrada: “haz esto para todos los que cumplan esta condición” no es una operación. Una acción masiva se expresa enumerando: lee el conjunto y luego pasa la lista a una operación por lotes con una semántica declarada para los fallos parciales
Un RPC diferido: la llamada devuelve un descriptor de trabajo y el resultado llega asociado a él
[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
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.

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

Errores

  • El derecho está en el RPC, nunca en la primitiva. No existe un “puede invocar” general: cada declaración nombra el permiso atómico que debe tener quien llama. Quien no lo tiene recibe un rechazo tipado forbidden, no un descarte silencioso.
  • Una instancia oculta se ve como not found. Un RPC de entidad invocado sobre una instancia que el predicado de filas de quien llama oculta responde exactamente igual que si se leyera esa instancia, así que el rechazo no le revela nada sobre lo que existe.
  • Sin handler es unavailable, no not found. El RPC está declarado, así que existe; lo que falta es una ruta. Vale la pena reintentar, porque un servidor de juego puede volver; not found le diría a quien llama que se detenga.
  • Un rechazo de un hook lleva el código y el motivo del propio hook, así que “rechazado por una regla del juego” nunca llega con aspecto de “se rompió el transporte”.
  • Un timeout no es un resultado. En una llamada diferida lees el descriptor; en una inmediata, la clave de idempotencia declarada hace que el reintento sea seguro.

Límites

Los números llegarán con el capítulo sobre los límites de la plataforma; lo que pasa en cada tope ya está definido.

  • Tamaño de la entrada: la llamada se rechaza antes de ejecutarse.
  • Tamaño de la salida: se rechaza en lugar de truncarse, porque una respuesta recortada no se distingue de una completa.
  • Frecuencia de llamadas por actor: un rechazo por límite de frecuencia que indica cuándo reintentar.
  • Llamadas diferidas simultáneas por actor: se rechaza la nueva, y las que están en curso terminan.
  • Vida útil del descriptor: pasado ese tiempo el resultado no está disponible, y pedirlo devuelve un rechazo.
  • Profundidad de la cadena de llamadas: al superarla, un rechazo declarado, no recursos agotados ni un corte silencioso.