Cette page n’est pas encore traduite : la version anglaise est affichée.
Catalog & Commerce
Objets, prix, portefeuilles, boutiques, achats et droits d’usage. De vraies intégrations avec les stores, là où les plateformes les permettent (Stripe, App Store, Google Play, Steam, Xbox) ; des boutiques programmées et ciblées par audience ; un parcours d’achat dont chaque étape accepte un hook.
Quand l’utiliser
- Vous vendez des choses : contre de l’argent réel via Stripe, App Store, Google Play, Steam ou Xbox, ou contre la monnaie du portefeuille (wallet).
- Chaque boutique (storefront) doit être résolue pour chaque joueur : le serveur calcule le calendrier, l’audience et le prix, et le client ne calcule jamais qui y a droit.
- Les règles de prix tiennent dans un seul hook que vous pouvez tester : remises, changements de prix et vetos s’exécutent avant tout débit.
- Un reçu (receipt) ne doit pas pouvoir être rejoué, et un remboursement doit révoquer le droit d’usage (entitlement) par les mêmes événements que ceux qui l’ont accordé.
- Inutile si vous ne vendez jamais rien. Les récompenses arrivent tout de même par l’unique
Grantdu commerce, avec l’originereward(c’est ainsi que les coffres de fin de cycle de Leaderboards sont remis) : même un jeu sans boutique garde un seul registre d’attributions, auditable.
Qui fait quoi
| Acteur | Ce qu’il fait sur cette page |
|---|---|
player | parcourt les boutiques, achète, gère son portefeuille, utilise des codes |
seller | configure le catalogue, les prix et le calendrier des boutiques |
backend-service | valide les reçus ; modifie les prix ou attribue des objets via les hooks d’achat |
En bref
main, déjà résolue pour ce joueur, et acheter avec le portefeuille// 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"))La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.
// 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 modifie le prix du premier achat, after attribue l’objet[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)La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.
Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python ; Unreal s’abonne aux événements d’achat et de changement de droit d’usage. Les fonctions écrites dans le moteur sont prévues : graphes Blueprint et C++ (traduit vers une cible de script validée par la plateforme). Les deux sont analysées et publiées comme n’importe quelle déclaration ; l’exécution reste sur la plateforme.
Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python ; Unity s’abonne aux événements d’achat et de changement de droit d’usage.
Le modèle
Ce que déclare un objet du catalogue.
| Déclare | Ce que c’est |
|---|---|
key | l’objet (item) est du contenu créé par l’équipe, désigné par une clé : le renommer dans le code, c’est simplement le renommer |
kind | consumable : il se dépense ; ou durable : on le possède une fois pour toutes |
prices | un prix est une quantité monétaire : un entier en unités mineures accompagné d’un code de devise, jamais un nombre à virgule flottante. Un objet peut en porter plusieurs, en monnaie du jeu comme en monnaie réelle |
identifiant externe par fournisseur | un emplacement déclaré par fournisseur, car chaque store connaît l’objet sous son propre id |
ce qu'il désigne | éventuellement une entité de n’importe quel type déclaré ; acheter l’objet donne alors la propriété de cette entité |
Ce qu’une composition a le droit d’être.
| Ce que c’est | |
|---|---|
ce qui peut s'acheter | un objet du catalogue, jamais une entité quelconque : un achat a besoin de quelqu’un qui accorde le droit et répond du remboursement |
deux niveaux, pas de troisième | un pack (bundle) est un objet du catalogue composé d’objets ; une boutique est un ensemble d’offres, et chaque offre désigne un objet et peut surcharger son prix et le contenu d’un pack |
un prix régional | s’exprime dans une boutique, pas sur l’objet |
Ce que déclare une boutique.
| Déclare | Ce que c’est |
|---|---|
offers | l’ensemble des offres, chacune désignant un objet |
schedule | l’heure d’ouverture et de fermeture de la fenêtre, en heure réelle, toujours en UTC |
audience | un prédicat, pas une liste de joueurs : l’audience est une règle qui reste vraie, pas un snapshot |
Une boutique dont un joueur ne fait pas partie de l’audience n’existe pas pour ce joueur.
Les états d’une commande.
| De | Vers |
|---|---|
created | awaiting payment |
awaiting payment | paid · declined · expired |
paid | granted |
paid ou granted | refunded |
| Toujours | Ce que c’est |
|---|---|
le prix | est figé dans la commande (order) au moment de sa création : un changement de prix ultérieur ne peut pas modifier ce qui a été convenu |
awaiting payment | a une échéance déclarée, et déclarée par fournisseur, car chacun a la sienne |
l'attribution | est distincte du paiement : paid et granted sont deux états différents. L’arrivée de l’argent et l’apparition de ce qui a été acheté sont deux faits ; les confondre masque celui qui a échoué |
un droit d'usage | garde son origine (un achat, un code promo, une récompense, un cadeau) : un an plus tard, vous savez encore répondre à « d’où vient ceci ? » |
un droit d'usage consommable | s’accumule : il évolue par incrément avec une clé d’idempotence, jamais en écrasant ce qui a été lu |
la propriété | est un prédicat de propriétaire : un droit d’usage appartient à un joueur par le même mécanisme que n’importe quelle ligne possédée |
le catalogue | est déclaré dans le code et arrive par défaut dans le panneau en mode de propriété seed : le code crée ce qui manque, et les modifications d’un game designer survivent à la publication suivante |
les secrets des fournisseurs | vivent dans le plan opérateur, jamais dans la déclaration ni dans un dépôt |
les capacités d'un fournisseur | sont déclarées : dispose-t-il d’une API utilisable, et que sait-il faire ? Ainsi, le catalogue ne promet pas un parcours que le store ne peut pas assurer |
Un remboursement peut arriver à tout moment
Un remboursement est une transition externe : il arrive sans que le jeu ait rien demandé. Le sort de ce qui a été attribué est déclaré : il existe trois réponses, et aucune par défaut.
Chaque point d’extension nomme le type qu’il transmet au hook : avant l’achat, une intention d’achat (joueur, offre, fournisseur, prix) ; après, l’achat lui-même. Un hook ne reçoit jamais un sac de valeurs non typé.
Erreurs
- Hors de l’audience répond not found, et réessayer ne sert à rien. Hors du calendrier répond aussi not found, mais vaut la peine d’être retenté une fois la fenêtre ouverte.
- Le fournisseur est indisponible et le fournisseur a refusé le paiement sont volontairement deux réponses différentes : la première est unavailable et se retente avec un délai croissant ; la seconde est un conflit qu’un nouvel essai ne résoudra pas.
- Un reçu invalide est un refus de validation ; un reçu déjà consommé par une autre commande ou un autre joueur est un conflit, et c’est ce qui rend le rejeu inutile.
- Le prix a changé entre la lecture de la boutique et l’achat : c’est une precondition failure. Relisez et décidez à nouveau, plutôt que de payer le nouveau prix sans le savoir.
- Une monnaie de jeu insuffisante est un conflit, pas forbidden : la permission d’acheter est acquise, c’est le solde qui ne suffit pas. Cela vaut la peine de réessayer après avoir rechargé.
- Un droit d’usage durable déjà détenu est un conflit.
- La région ou l’âge n’autorise pas l’achat répond forbidden, et réessayer ne sert à rien.
- L’échéance de la commande est dépassée : c’est un conflit, créez une nouvelle commande.
- Le plafond de dépenses est atteint : selon le plafond en cause, la réponse est un conflit ou une limite de débit, et elle indique quand le plafond se réinitialise.
Limites
Les chiffres arriveront avec le chapitre sur les limites de la plateforme ; ce qui se passe à chaque plafond est déjà fixé.
- La taille du catalogue : la publication d’un objet supplémentaire est refusée comme conflit.
- Les boutiques par projet : la création est refusée.
- Les offres d’une boutique : l’ajout est refusé ; la boutique n’est jamais tronquée en silence.
- La durée de vie d’une commande en attente de paiement : passage à
expired, avec un événement. - La fréquence des tentatives d’achat : une limite de débit, avec un délai.
- Le plafond de dépenses par période : un conflit qui indique quand le plafond se réinitialise.
- La rétention des commandes : au-delà de la durée déclarée, une commande n’est plus lisible, au lieu de disparaître sans explication.
- Les droits d’usage par joueur : l’attribution est refusée, et ceux déjà attribués ne sont jamais évincés.
- La précision d’un prix n’est pas une limite mais un type : un entier en unités mineures.