Pular para o conteúdo principal
Serviços em torno do jogo

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

Catalog & Commerce

Itens, preços, carteiras, lojas, compras e direitos de uso. Integração real com as lojas das plataformas que permitem isso (Stripe, App Store, Google Play, Steam, Xbox), lojas com agenda e público-alvo, e um fluxo de compra em que cada etapa aceita um hook.

Quando usar

  • Você vende coisas: por dinheiro real, via Stripe, App Store, Google Play, Steam ou Xbox, ou por moeda da carteira (wallet).
  • Cada loja (storefront) precisa ser resolvida por jogador: agenda, público e preço são calculados no servidor, e o cliente nunca faz a conta de quem tem direito.
  • As regras de preço ficam num único hook que dá para testar: descontos, mudanças de preço e vetos rodam antes de qualquer cobrança.
  • Um recibo não pode ser reaproveitado, e um reembolso (refund) precisa revogar o direito de uso (entitlement) pelos mesmos eventos que o concederam.
  • Dispensável quando nenhum item é vendido. Mesmo assim, as recompensas chegam pelo único Grant do comércio, com origem reward (é assim que chegam os baús de fim de ciclo de Leaderboards), e até um jogo sem loja mantém um único registro auditável de tudo o que foi concedido.

Quem faz o quê

AtorO que faz nesta página
playernavega pelas lojas, compra, gerencia a carteira e resgata códigos
sellerconfigura o catálogo, os preços e a agenda das lojas
backend-servicevalida recibos; muda preços ou concede itens pelos hooks de compra

Resumo

Obter a loja main, já resolvida para este jogador, e comprar com a carteira
// client — the storefront arrives already resolved for this player
var front = await playserv.Commerce.Storefront("main");
var order = await playserv.Commerce.Purchase(front.Items.First(), pay: Pay.Wallet("gems"));
// client — the storefront arrives already resolved for this player
const front = await playserv.commerce.storefront('main');
const order = await playserv.commerce.purchase(front.items[0], { pay: Pay.wallet('gems') });
# client — the storefront arrives already resolved for this player
front = await playserv.commerce.storefront("main")
order = await playserv.commerce.purchase(front.items[0], pay=Pay.wallet("gems"))
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.

// client — the storefront arrives already resolved for this player
Client->Commerce->Storefronts->Select().Then(
    TPSOnResult<TPSPage<FPSStorefront>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<FPSStorefront>>& Result)
    {
        if (!Result.HasValue()) { return; }
        const FPSStorefront& Front = Result.Value().Rows[0];
        Client->Commerce->Orders->Create(FPSIdempotencyKey(CartId), Front.Items[0], FPSPay::Wallet(TEXT("gems")));
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — roda sem mudanças no Unity com os tipos gerados
// client — the storefront arrives already resolved for this player
var front = await playserv.Commerce.Storefront("main");
var order = await playserv.Commerce.Purchase(front.Items.First(), pay: Pay.Wallet("gems"));
Dois hooks de compra: before muda o preço da primeira compra, after concede o item
[Before(Commerce.Purchase)]                    // veto or reprice
public static Verdict FirstBuyDiscount(PurchaseIntent p) =>
    p.Player.Purchases == 0 ? Hook.Continue(p.WithPrice(p.Price * 0.5m)) : Hook.Continue(p);

[After(Commerce.Purchase)]                     // grant — side effects only
public static Task Grant(Purchase done) =>
    done.Player.Inventory.Grant(done.Item, done.Count);
// veto or reprice
export const firstBuyDiscount = before(Commerce.purchase, (p: PurchaseIntent) =>
  p.player.purchases === 0 ? Hook.continue(p.withPrice(p.price * 0.5)) : Hook.continue(p));

// grant — side effects only
export const grant = after(Commerce.purchase, (done: Purchase) =>
  done.player.inventory.grant(done.item, done.count));
@before(commerce.purchase)                     # veto or reprice
def first_buy_discount(p: PurchaseIntent) -> Verdict:
    return Hook.continue_(p.with_price(p.price * 0.5)) if p.player.purchases == 0 else Hook.continue_(p)

@after(commerce.purchase)                      # grant — side effects only
async def grant(done: Purchase):
    await done.player.inventory.grant(done.item, done.count)
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

Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva em C#, TypeScript ou Python; o Unreal assina os eventos de compra e de mudança de direito de uso. Estão planejadas funções escritas na engine: grafos de Blueprint e C++ (traduzido para um formato de script validado pela plataforma). As duas são analisadas e publicadas como qualquer declaração, e a execução continua na plataforma.

Executa fora da engine

Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva em C#, TypeScript ou Python; o Unity assina os eventos de compra e de mudança de direito de uso.

O modelo

O que um item do catálogo declara.

DeclaraO que é
keyo item é conteúdo criado pela equipe, identificado por uma chave: renomear no código é só renomear
kindconsumable, que se gasta; ou durable, que se possui uma única vez
pricesum preço é uma quantia monetária: um inteiro em unidades menores mais um código de moeda, nunca um float. Um item pode ter vários, em moeda do jogo e em dinheiro real
identificador externo por provedorum campo declarado para cada provedor, porque cada loja conhece o item pelo próprio id
para onde apontaopcionalmente, uma entidade de qualquer tipo declarado; nesse caso, comprar o item concede a posse dessa entidade

Que composições são permitidas.

O que é
o que pode ser compradoum item do catálogo, nunca uma entidade qualquer, porque uma compra precisa de alguém que conceda o direito e responda pelo reembolso
dois níveis, sem terceiroum pacote (bundle) é um item do catálogo formado por itens; uma loja é um conjunto de ofertas, e cada oferta aponta para um item e pode sobrescrever o preço dele e o conteúdo de um pacote
um preço regionalé definido pela loja, não no item

O que uma loja declara.

DeclaraO que é
offerso conjunto de ofertas, cada uma apontando para um item
schedulequando a janela abre e fecha, em horário de relógio, sempre em UTC
audienceum predicado, não uma lista de jogadores: o público é uma regra que continua valendo, não um snapshot

Para um jogador fora do público, a loja não existe.

Os estados de um pedido.

DePara
createdawaiting payment
awaiting paymentpaid · declined · expired
paidgranted
paid ou grantedrefunded
SempreO que é
o preçofica fixado no pedido (order) no momento em que ele é criado, então uma mudança de preço depois não altera o que foi combinado
awaiting paymenttem um prazo declarado, e declarado por provedor, porque cada um tem o seu
a concessãoé separada do pagamento: paid e granted são estados diferentes. O dinheiro chegar e a coisa aparecer são dois fatos, e misturar os dois esconde qual deles falhou
um direito de usoguarda a origem (uma compra, um código promocional, uma recompensa, um presente), então dá para responder “de onde veio isso?” um ano depois
um direito de uso consumívelse acumula: muda por incremento com chave de idempotência, nunca sobrescrevendo o que foi lido
a posseé um predicado de dono: um direito de uso pertence a um jogador pelo mesmo mecanismo que qualquer linha com dono
o catálogoé declarado no código e chega ao painel, por padrão, no modo de posse seed: o código cria o que falta, e as edições de um designer sobrevivem à próxima publicação
os segredos dos provedoresficam no plano do operador, nunca na declaração e nunca num repositório
as capacidades de um provedorsão declaradas: se ele tem uma API utilizável e o que consegue fazer, para que o catálogo não prometa um fluxo que a loja não atende

Um reembolso pode chegar a qualquer momento

Um reembolso é uma transição externa: chega sem que o jogo tenha pedido nada. O que acontece com o que foi concedido é declarado: existem três respostas e nenhuma é padrão.

Cada ponto de extensão diz o tipo que entrega ao hook: antes da compra, a intenção de compra (jogador, oferta, provedor, preço); depois, a própria compra. Um hook nunca recebe um saco de dados sem tipo.

Erros

  • Fora do público responde not found, e não adianta repetir. Fora da agenda também responde not found, mas vale repetir quando a janela abrir.
  • O provedor está indisponível e o provedor recusou o pagamento são respostas diferentes de propósito: a primeira é unavailable e pode ser repetida com backoff; a segunda é um conflito que repetir não resolve.
  • Um recibo inválido é uma recusa de validação; um recibo já consumido por outro pedido ou outro jogador é um conflito, e é isso que torna inútil reaproveitá-lo.
  • O preço mudou entre a leitura da loja e a compra é uma precondition failure: leia de novo e decida outra vez, em vez de pagar o preço novo sem perceber.
  • Moeda do jogo insuficiente é um conflito, não forbidden: a permissão para comprar existe, o saldo é que não basta. Vale repetir depois de recarregar.
  • Um direito de uso durável que o jogador já tem é um conflito.
  • A região ou a idade não permite a compra responde forbidden, e não adianta repetir.
  • O prazo do pedido venceu é um conflito: crie um pedido novo.
  • O limite de gastos se esgotou responde como conflito ou como limite de frequência, conforme o limite em questão, e informa quando ele reinicia.

Limites

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

  • Tamanho do catálogo: publicar mais um item é recusado como conflito.
  • Lojas por projeto: a criação é recusada.
  • Ofertas numa loja: a inclusão é recusada; a loja nunca é cortada em silêncio.
  • Tempo de vida de um pedido aguardando pagamento: transição para expired, com um evento.
  • Frequência de tentativas de compra: limite de frequência com prazo para tentar de novo.
  • Limite de gastos por período: um conflito que informa quando o limite reinicia.
  • Retenção de pedidos: passado o período declarado, o pedido deixa de poder ser lido, em vez de sumir sem explicação.
  • Direitos de uso por jogador: a concessão é recusada, e os já concedidos nunca são removidos.
  • A precisão de um preço não é um limite, é um tipo: um inteiro em unidades menores.