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.

Extensibility

Todo cenário da plataforma é uma cadeia de funções registradas: substitua um elo ou envolva-o. Você muda o comportamento da plataforma substituindo os passos dela, e não fazendo um fork do código-fonte.

Quando usar

  • Um passo da plataforma precisa rodar a sua lógica: declare a substituição de um elo nomeado com [Override(…)].
  • Você precisa de verificações ou efeitos colaterais em volta de um passo: middleware Before/After em ordem, que pode vetar ou notificar.
  • O código precisa rodar por agendamento, por evento ou por webhook: os gatilhos (trigger) entregam a você um contexto já interpretado e tipado.
  • Você precisa saber o que vai rodar de fato antes do deploy: simule a cadeia (dry-run) e leia a ordem resolvida.
  • Dispensável quando a regra diz respeito às escritas de uma única entidade. Um hook de Data & Subscriptions é a forma mais leve.

Quem faz o quê

AtorO que faz nesta página
backend-servicesobrescreve elos, envolve passos com middleware, escreve handlers de gatilhos
operatorinspeciona cadeias, define a ordem, lê segredos, simula a resolução

Resumo

Três formas de extensão: barrar o SignIn, envolver o grant com middleware, rodar código num cron
// gate one named step of the auth scenario — a before hook may refuse, fail-closed
[Before(Auth.SignIn)]
public static Task<Verdict> GateRegion(SignInAttempt a) =>
    a.Region == "sanctioned"
        ? Hook.Reject(Problem.Forbidden, "region not served")
        : Hook.Continue(a);

// wrap a step with ordered middleware
PlayServ.Extend.Scenario("commerce.purchase")
    .Before("grant", LogPurchaseIntent)
    .After("grant", NotifySquad, order: 10);

// customer code on a trigger
[OnSchedule("0 4 * * *")]
public static async Task NightlyCleanup() { ... }
// gate one named step of the auth scenario — a before hook may refuse, fail-closed
export const gateRegion = before(Auth.signIn, (a: SignInAttempt) =>
  a.region === 'sanctioned'
    ? Hook.reject(Problem.forbidden, 'region not served')
    : Hook.continue(a));

// wrap a step with ordered middleware
playserv.extend.scenario('commerce.purchase')
  .before('grant', logPurchaseIntent)
  .after('grant', notifySquad, { order: 10 });

// customer code on a trigger
export const nightlyCleanup = onSchedule('0 4 * * *', async () => { /* ... */ });
# gate one named step of the auth scenario — a before hook may refuse, fail-closed
@before(auth.sign_in)
async def gate_region(a):
    if a.region == "sanctioned":
        return hook.reject(problem.FORBIDDEN, "region not served")
    return hook.cont(a)

# wrap a step with ordered middleware
playserv.extend.scenario("commerce.purchase") \
    .before("grant", log_purchase_intent) \
    .after("grant", notify_squad, order=10)

# customer code on a trigger
@on_schedule("0 4 * * *")
async def nightly_cleanup(): ...
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.

Executa fora da engine

Sobrescritas, middleware e gatilhos rodam todos como cloud functions na plataforma, não na engine. Escreva-os em C#, TypeScript ou Python — o Unreal assina os eventos resultantes. Funções escritas na engine estão planejadas: grafos de Blueprint e C++ (traduzido para um formato de script validado pela plataforma). Os dois são analisados e publicados como qualquer declaração; a execução continua na plataforma.

Executa fora da engine

Sobrescritas, middleware e gatilhos rodam todos como cloud functions na plataforma, não na engine. Escreva-os em C#, TypeScript ou Python — o Unity assina os eventos resultantes.

O modelo

Onde a plataforma deixa você se conectar.

TermoO que é
função registradaum passo da plataforma que pode ser sobrescrito — “criar perfil”, “calcular preço”
scenarioa cadeia ordenada que um fluxo da plataforma executa: autenticação, entrada, compra
overridabilityse um elo pode ser substituído, só envolvido, ou se é fixo
middlewareum handler ordenado de antes/depois em volta de um elo
triggero que dispara o seu código: um evento, um agendamento, um webhook
secretum valor que o seu handler pode ler
invocationuma execução, com o trace dela
a platform scenario — a purchasevalidateresolve price — your [Override], chosen by condition; ours is the defaultyour Before hook — a gatekeeper: may reject; if it breaks, grant does not rungrantyour After hook — an observer: side effects only; if it breaks, grant still standsnotify
O seu código dentro de um cenário da plataforma: um elo substituído, outro envolvido; os elos dos lados não sabem qual implementação respondeu

O que um hook declara.

DeclaraO que é
positiono passo nomeado ao qual ele se conecta
kindgatekeeper — uma verificação de admissão ou uma validação, e ele bloqueia em caso de falha: o passo não roda quando o próprio hook quebra; ou observer — um log, uma notificação, um contador, e ele deixa passar em caso de falha: o passo roda, e a falha ainda é reportada em vez de engolida. Não existe padrão
momentbefore — antes da validação, recebe o payload tipado e pode alterá-lo ou rejeitá-lo; ou after — depois que o passo foi confirmado, recebe a requisição e o resultado, só para efeitos colaterais, e nunca pode fazer a operação falhar nem alterar a resposta
effecto que o hook faz, não só onde ele fica. Assim a ordem é descrita em termos do trabalho, e não de números, e continua valendo quando outra pessoa adiciona um hook ao lado do seu
versão e condiçãoum handler que vale para um ambiente ou público é uma versão declarada daquele hook, e não um desvio dentro do corpo dele; é isso que o interruptor do painel liga e desliga

Três formas de conectar código.

FormaPara que usar
atributo [Before(Step)] / [After(Step)]uma regra num passo nomeado — a maioria dos hooks
atributo [Override(Link)]substituir por completo a implementação de um elo
Extend.Scenario("…").Before("link", fn, order: n)envolver um elo dentro de uma cadeia, quando a ordem em relação a outros middlewares importa
SempreO que é
as trêsfazem o deploy com playserv push
as duas formas com atributosão o que o painel mostra, porque a declaração leva o nome do passo ou do elo para o modelo publicado
a forma de middlewareleva uma ordem, que é o que uma cadeia precisa
a atribuição na inicialização(Scenario.OnX = fn) continua disponível para um handler que não precisa aparecer na árvore do painel de administração
substituir um elodeixa os elos dos lados intactos, e nenhum deles sabe qual implementação respondeu — o passo da própria plataforma ou o seu

Para onde vai uma chamada, e o que vale para toda rota.

SempreO que é
quatro direçõesuma cloud function · o backend externo do estúdio · o servidor do jogo · outra direção declarada
o roteadoré orientado a mensagens e sinais; request-response é um adaptador sobre ele, e não a natureza dele
correspondênciaé feita pelo nome declarado da operação ou do sinal e por nada mais: nem o formato do payload, nem quem chama, nem a carga
a direçãonão faz parte do contrato da operação: mover um handler de uma direção para outra não é uma mudança que quebra compatibilidade
“o servidor do jogo”é definido pelo que ele é, não por quem o hospeda — a nossa frota e a hospedagem própria de um estúdio são uma direção só, e a declaração não traz nenhuma marca de quem é dono da infraestrutura
RPCs do servidor do jogosão registrados no mesmo roteador: declarar um já é registrá-lo, e não existe uma segunda forma
ordemo middleware roda de cima para baixo e, onde um passo tem mais de uma implementação, o roteador escolhe da esquerda para a direita pela condição; quando nada corresponde, responde a versão marcada como padrão

Uma cadeia com sobrescritas e middleware pode ser resolvida e lida antes de qualquer execução. A ordem resolvida pode ser inspecionada no painel e pelo código.

O que é uma restrição entre hooks, e quando ela é verificada.

O que é
uma restrição nomeadaum ponto de extensão pode nomear os efeitos que ele restringe — uma verificação anti-cheat precisa vir antes de uma alocação, um recibo exige uma cobrança neste ponto — e não restringir mais nada
um efeito não nomeadofica sem restrição, e não é recusado
uma violaçãoé um defeito da configuração, e a recusa nomeia os dois hooks e a restrição que eles quebraram — não é um aviso nem uma reordenação silenciosa
quando é verificadaem todo ato que pode mudar o que roda num ponto: registrar, fazer o deploy, mudar o arranjo. Por isso um arranjo que chega à execução já foi admitido
nunca reverificada em runtimeum arranjo admitido não é verificado de novo quando executa
SempreO que é
handlerssão tipados na entrada e na saída: nada de dynamic, nada de sacolas de contexto. O handle da plataforma está no ambiente, e o contexto da invocação — quem chama, gatilho, trace — chega já interpretado
o que um build da engine vêos eventos que o cenário emite depois, porque uma sobrescrita ou um middleware roda na plataforma, e o runtime de uma engine não é lugar para hospedar isso. É o que mostram as abas de engine nos exemplos desta página
Duas implementações de uma mesma função, escolhidas por condição e com um padrão; uma versão de hook controlada do mesmo jeito
[Rpc("resolve_price", Default = true)]
public static Price ResolvePrice(Sku sku) => Pricing.Base(sku);

[Rpc("resolve_price", When = "env == 'staging'")]
public static Price ResolvePriceStaging(Sku sku) => Pricing.WithDiscount(sku, 0.5f);

// a hook can carry a version too, gated by its own condition
[After("grant", When = "audience == 'beta'")]
public static void NotifySquadBeta(GrantResult r) => Messaging.PingBeta(r.Squad);
export const resolvePrice = rpc('resolve_price', { default: true },
  (sku: Sku) => Pricing.base(sku));

export const resolvePriceStaging = rpc('resolve_price', { when: "env == 'staging'" },
  (sku: Sku) => Pricing.withDiscount(sku, 0.5));

// a hook can carry a version too, gated by its own condition
export const notifySquadBeta = after('grant', { when: "audience == 'beta'" },
  (r: GrantResult) => Messaging.pingBeta(r.squad));
@rpc("resolve_price", default=True)
def resolve_price(sku: Sku) -> Price:
    return pricing.base(sku)

@rpc("resolve_price", when="env == 'staging'")
def resolve_price_staging(sku: Sku) -> Price:
    return pricing.with_discount(sku, 0.5)

# a hook can carry a version too, gated by its own condition
@after("grant", when="audience == 'beta'")
def notify_squad_beta(r: GrantResult):
    messaging.ping_beta(r.squad)
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.

Executa fora da engine

Sobrescritas, middleware e gatilhos rodam todos como cloud functions na plataforma, não na engine. Escreva-os em C#, TypeScript ou Python — o Unreal assina os eventos resultantes. Funções escritas na engine estão planejadas: grafos de Blueprint e C++ (traduzido para um formato de script validado pela plataforma). Os dois são analisados e publicados como qualquer declaração; a execução continua na plataforma.

Executa fora da engine

Sobrescritas, middleware e gatilhos rodam todos como cloud functions na plataforma, não na engine. Escreva-os em C#, TypeScript ou Python — o Unity assina os eventos resultantes.

A lógica da própria plataforma pode ser baixada, editada e publicada de novo. São funções, como as suas:

playserv functions pull matchmaking.match     # the platform's implementation, as source
# edit: widen the skill window for the weekend event
playserv push                                 # your version registers; the default stays as fallback

O que você personaliza aqui é lógica: as sobrescritas, os middlewares e as versões de que esta página trata.

Não dá para adicionar campos às entidades da plataforma

Um jogador, uma sala, um registro de ranking e um pedido são estado do sistema, com as próprias máquinas de estados, e não o ponto de partida do seu modelo de dados. Os seus dados são uma entidade sua, declarada em Esquema como código e ligada ao estado da plataforma por um predicado: o dono é este jogador, o escopo é esta sala. Isso também garante que eles continuem seus quando o modelo da própria plataforma mudar.

Erros

  • Um nome que não corresponde a nenhum handler registrado responde not found — uma chamada nunca é descartada em silêncio só porque ninguém estava ouvindo.
  • Um nome registrado duas vezes, e um conjunto com duas implementações para a mesma condição, são recusados quando o conjunto é declarado: no deploy, não na hora da chamada.
  • A recusa de um hook traz o código e o motivo do próprio hook, então “uma regra do jogo disse não” nunca chega parecendo uma falha de transporte.
  • Um hook que falha se comporta conforme o tipo declarado — fail-open ou fail-closed —, e qual dos dois ele é foi declarado, não deduzido do que aconteceu.
  • Um hook não pode mudar o que já foi definido: nem o dono, nem o alvo, nem o ranking ou a conversa a que a chamada se dirigia. Ele corrige entradas e devolve um veredito.

Limites

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

  • O prazo de execução de um hook: passado o prazo, vale o comportamento em caso de falha que o tipo dele declara.
  • Hooks numa mesma posição e implementações de um mesmo método: registrar mais um é recusado.
  • A profundidade de aninhamento de “um hook invoca uma operação que tem hooks”: uma recusa declarada, nunca esgotamento de recursos.
  • O tamanho do contexto passado a um hook: truncar é proibido, então o registro é recusado em vez de um handler receber metade de um contexto.

Lições e receitas: um ranking no Tanks usa os hooks deste módulo; um torneio diário passa por este módulo.