Aller au contenu principal
Services autour du jeu

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 Grant du commerce, avec l’origine reward (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

ActeurCe qu’il fait sur cette page
playerparcourt les boutiques, achète, gère son portefeuille, utilise des codes
sellerconfigure le catalogue, les prix et le calendrier des boutiques
backend-servicevalide les reçus ; modifie les prix ou attribue des objets via les hooks d’achat

En bref

Récupérer la boutique 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"))
Bientôt — Go

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.
Unity C# is the same C# API here — s’exécute tel quel dans Unity avec les types générés
// 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"));
Deux hooks d’achat : 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)
Bientôt — Go

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.

S’exécute hors du moteur

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.

S’exécute hors du moteur

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éclareCe que c’est
keyl’objet (item) est du contenu créé par l’équipe, désigné par une clé : le renommer dans le code, c’est simplement le renommer
kindconsumable : il se dépense ; ou durable : on le possède une fois pour toutes
pricesun 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 fournisseurun 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'acheterun 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èmeun 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égionals’exprime dans une boutique, pas sur l’objet

Ce que déclare une boutique.

DéclareCe que c’est
offersl’ensemble des offres, chacune désignant un objet
schedulel’heure d’ouverture et de fermeture de la fenêtre, en heure réelle, toujours en UTC
audienceun 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.

DeVers
createdawaiting payment
awaiting paymentpaid · declined · expired
paidgranted
paid ou grantedrefunded
ToujoursCe que c’est
le prixest 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 paymenta une échéance déclarée, et déclarée par fournisseur, car chacun a la sienne
l'attributionest 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'usagegarde 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 consommables’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 catalogueest 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 fournisseursvivent dans le plan opérateur, jamais dans la déclaration ni dans un dépôt
les capacités d'un fournisseursont 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.