Esta página aún no está traducida: se muestra la versión en inglés.
Catalog & Commerce
Artículos, precios, billeteras, tiendas, compras y derechos de uso. Integraciones reales con las tiendas de las plataformas que lo permiten (Stripe, App Store, Google Play, Steam, Xbox), tiendas con horario y segmentadas por audiencia, y un flujo de compra en el que cada paso admite un hook.
Cuándo usarlo
- Vendes cosas: por dinero real a través de Stripe, App Store, Google Play, Steam o Xbox, o por moneda de la billetera.
- Cada tienda (storefront) debe resolverse para cada jugador: el servidor calcula el horario, la audiencia y el precio, y el cliente nunca calcula quién cumple los requisitos.
- Las reglas de precios viven en un solo hook que puedes probar: los descuentos, los cambios de precio y los vetos se ejecutan antes de cualquier cobro.
- Un recibo no debe poder reutilizarse, y un reembolso debe revocar el derecho de uso (entitlement) con los mismos eventos con los que se otorgó.
- No lo necesitas si nunca vendes artículos (item). Las recompensas igual llegan por el único
Grantdel módulo de comercio, con origenreward(así llegan los cofres de fin de ciclo de Leaderboards), de modo que incluso un juego sin tienda conserva un único registro auditable de todo lo otorgado.
Quién hace qué
| Actor | Qué hace en esta página |
|---|---|
player | explora las tiendas, compra, administra su billetera y canjea códigos |
seller | configura el catálogo, los precios y los horarios de las tiendas |
backend-service | valida recibos; cambia precios u otorga artículos mediante los hooks de compra |
En resumen
main, ya resuelta para este jugador, y compra con la billetera// 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"))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.
// 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.// 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"));before cambia el precio de la primera compra y after otorga el artículo[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)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.
Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unreal se suscribe a los eventos de compra y de cambio de derechos de uso. Están planeadas funciones escritas desde el motor: grafos de Blueprint y C++ (traducido a un formato de script que valida la plataforma). Ambas se analizan y se suben como cualquier declaración, y la ejecución sigue ocurriendo en la plataforma.
Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unity se suscribe a los eventos de compra y de cambio de derechos de uso.
El modelo
Qué declara un artículo del catálogo.
| Declara | Qué es |
|---|---|
key | el artículo es contenido creado por el equipo y se identifica por una clave, así que cambiarle el nombre en el código es solo eso: un cambio de nombre |
kind | consumable: se gasta; o durable: se obtiene una sola vez y se conserva |
prices | un precio es una cantidad monetaria: un entero en unidades menores más un código de moneda, nunca un número de punto flotante. Un artículo puede tener varios, por ejemplo en moneda del juego y en moneda real |
identificador externo por proveedor | un espacio declarado por cada proveedor, porque cada tienda conoce el artículo por su propio id |
a qué apunta | opcionalmente, a una entidad de cualquier tipo declarado; en ese caso, comprar el artículo otorga la propiedad de esa entidad |
Qué composiciones se permiten.
| Qué es | |
|---|---|
qué se puede comprar | un artículo del catálogo, nunca una entidad cualquiera, porque una compra necesita a alguien que otorgue el derecho y responda por el reembolso |
dos niveles, no hay un tercero | un paquete (bundle) es un artículo del catálogo formado por otros artículos; una tienda es un conjunto de ofertas, y cada oferta apunta a un artículo y puede redefinir su precio y el contenido de un paquete |
un precio regional | lo define una tienda, no el artículo |
Qué declara una tienda.
| Declara | Qué es |
|---|---|
offers | el conjunto de ofertas; cada una apunta a un artículo |
schedule | cuándo abre y cuándo cierra la ventana, en hora de calendario y siempre en UTC |
audience | un predicado, no una lista de jugadores: la audiencia es una regla que se sigue cumpliendo, no un snapshot |
Si un jugador no pertenece a la audiencia de una tienda, para él esa tienda no existe.
Los estados de un pedido.
| Desde | Hacia |
|---|---|
created | awaiting payment |
awaiting payment | paid · declined · expired |
paid | granted |
paid o granted | refunded |
| Siempre | Qué es |
|---|---|
el precio | queda fijado en el pedido en el momento de crearlo, así que un cambio de precio posterior no puede alterar lo acordado |
awaiting payment | tiene un plazo declarado, y se declara por proveedor, porque cada uno tiene el suyo |
el otorgamiento | está separado del pago: paid y granted son estados distintos. Que llegue el dinero y que aparezca lo comprado son dos hechos, y mezclarlos oculta cuál de los dos falló |
un derecho de uso | registra su origen —una compra, un código promocional, una recompensa, un regalo—, así que un año después todavía puedes responder “¿de dónde salió esto?” |
un derecho de uso consumible | se acumula: cambia mediante un incremento con clave de idempotencia, nunca sobrescribiendo lo que se leyó |
la propiedad | es un predicado de dueño: un derecho de uso pertenece a un jugador por el mismo mecanismo que cualquier fila con dueño |
el catálogo | se declara en el código y llega al panel, por defecto, en el modo de propiedad seed: el código crea lo que falta y los cambios de un diseñador sobreviven a la siguiente subida |
los secretos de los proveedores | viven en el plano de operador; nunca en la declaración y nunca en un repositorio |
las capacidades de un proveedor | se declaran: si tiene una API utilizable y qué puede hacer, para que el catálogo no prometa un flujo que la tienda no puede atender |
Un reembolso puede llegar en cualquier momento
Un reembolso es una transición externa: llega sin que el juego haya pedido nada. Qué pasa con lo otorgado se declara: hay tres respuestas posibles y ninguna por defecto.
Cada punto de extensión indica el tipo que le entrega al hook: antes de la compra, una intención de compra —jugador, oferta, proveedor, precio—, y después, la compra misma. Un hook nunca recibe un conjunto de datos sin tipo.
Errores
- Fuera de la audiencia responde not found, y repetir no sirve de nada. Fuera del horario también responde not found, pero vale la pena repetir cuando se abra la ventana.
- El proveedor no está disponible y el proveedor rechazó el pago son respuestas distintas a propósito: la primera es unavailable y se puede reintentar con backoff; la segunda es un conflicto que reintentar no resuelve.
- Un recibo inválido es un rechazo de validación; un recibo ya consumido por otro pedido o por otro jugador es un conflicto, y por eso reutilizarlo no sirve de nada.
- El precio cambió entre la lectura de la tienda y la compra: es una precondition failure. Vuelve a leer y decide de nuevo, en lugar de que te cobren el precio nuevo sin avisarte.
- Si no alcanza la moneda del juego, es un conflicto, no forbidden: el permiso para comprar existe, pero el saldo no alcanza. Vale la pena repetir después de recargar.
- Un derecho de uso durable que el jugador ya tiene es un conflicto.
- La región o la edad no permiten la compra: responde forbidden, y repetir no sirve de nada.
- Venció el plazo del pedido: es un conflicto; crea un pedido nuevo.
- Se agotó el límite de gasto: responde como conflicto o como límite de frecuencia, según el límite de que se trate, e indica cuándo se reinicia.
Límites
Las cifras llegarán con el capítulo sobre los límites de la plataforma; lo que pasa al alcanzar cada tope ya está definido.
- Tamaño del catálogo: publicar otro artículo se rechaza como conflicto.
- Tiendas por proyecto: se rechaza la creación.
- Ofertas en una tienda: se rechaza la oferta que se agrega; la tienda nunca se recorta en silencio.
- Vida útil de un pedido que espera el pago: pasa a
expiredy se emite un evento. - Frecuencia de intentos de compra: un límite de frecuencia que indica cuándo volver a intentar.
- Límite de gasto por período: un conflicto que indica cuándo se reinicia el límite.
- Retención de pedidos: pasado el período declarado, un pedido deja de poder leerse, en lugar de desaparecer sin explicación.
- Derechos de uso por jugador: se rechaza el nuevo otorgamiento, y los ya otorgados nunca se eliminan para hacer lugar.
- La precisión de un precio no es un límite, sino un tipo: un entero en unidades menores.