Le pack de fonctionnalités Bundles offre une fonctionnalité prête à l'emploi pour vendre des collections d'objets aux joueurs à un prix réduit. Vous pouvez choisir de permettre aux joueurs d'acheter des bundles en utilisant une monnaie personnalisée dans l'expérience ou des Robux, quel type de bundle vous souhaitez utiliser, quel ensemble d'objets vous souhaitez vendre et comment vous souhaitez inviter les joueurs pendant leur gameplay.
En utilisant les options de personnalisation du pack, vous pouvez adapter vos bundles pour atteindre les objectifs de conception et de monétisation de vos expériences, tels que :
- Cibler un faible taux de conversion en offrant des packs de démarrage à prix réduit qui apportent une valeur aux nouveaux joueurs et encouragent les dépenses précoces.
- Augmenter la profondeur de dépense en regroupant des objets à différents prix pour séduire un éventail de joueurs.
- Monétiser les opérations en direct (LiveOps) événements en proposant des bundles à durée limitée d'objets exclusifs.

Obtenir le pack
La Boutique Créateur est un onglet de la Boîte à outils que vous pouvez utiliser pour trouver tous les actifs créés par Roblox et la communauté Roblox pour utilisation dans vos projets, y compris des modèles, des images, des maillages, de l'audio, des plugins, des vidéos et des actifs de police. Vous pouvez utiliser la Boutique Créateur pour ajouter un ou plusieurs actifs directement dans une expérience ouverte, y compris les packs de fonctionnalités !
Chaque pack de fonctionnalités nécessite le pack de fonctionnalités Core pour fonctionner correctement. Une fois que les actifs des packs de fonctionnalités Core et Bundles sont dans votre inventaire, vous pouvez les réutiliser dans n'importe quel projet sur la plateforme.
Pour obtenir les packages de votre inventaire dans votre expérience :
Ajoutez le pack de fonctionnalités Core et le pack de fonctionnalités Bundles à votre inventaire dans Studio en cliquant sur le lien Ajouter à l'inventaire dans le jeu d'éléments suivant.
Depuis le menu Fenêtre de Studio ou la barre d'outils de l'onglet Accueil, ouvrez la Boîte à outils.
Dans la fenêtre Boîte à outils, cliquez sur l'onglet Inventaire. Le tri Mes modèles s'affiche.

Cliquez sur le carreau Pack de fonctionnalités Core, puis sur le carreau Pack de fonctionnalités Bundles. Les deux dossiers de packs s'affichent dans la fenêtre Explorateur.
Faites glisser les dossiers de packs dans ReplicatedStorage.
Autorisez les appels de stockage de données à suivre les achats des joueurs avec les packs.
- Ouvrez la fenêtre Fichier ⟩ Paramètres de l'expérience de Studio.
- Allez à l'onglet Sécurité, puis activez Activer l'accès Studio aux services API.
Définir les monnaies
Si votre expérience a son propre système de monnaies, vous pouvez les enregistrer avec le pack de fonctionnalités Core en les définissant dans ReplicatedStorage.FeaturePackagesCore.Configs.Currencies. Il y a un exemple commenté d'une monnaie Gems déjà présent dans ce fichier ; remplacez-le par le vôtre.
MonnaiesGems = {displayName = "Gems",symbol = "💎",icon = nil,},
Le script Currencies informe le pack de fonctionnalités Core de certaines métadonnées concernant votre monnaie :
- (obligatoire) displayName - Le nom de votre monnaie. Si vous ne spécifiez pas de symbole ou d'icône, ce nom est utilisé dans les boutons d'achat (c'est-à-dire "100 Gems").
- (optionnel) symbol - Si vous avez un caractère de texte à utiliser comme icône pour votre monnaie, cela sera utilisé à la place du displayName dans les boutons d'achat (c'est-à-dire "💎100").
- (optionnel) icon - Si vous avez un icône image AssetId pour votre monnaie, cela sera utilisé à la place du displayName dans les boutons d'achat (c'est-à-dire que l'image sera placée à gauche du prix "🖼️100").
Une fois votre monnaie configurée, vous devez spécifier manuellement le prix du bundle, la monnaie et l'icône pour l'affichage tête haute au lieu que cette information soit récupérée à partir du produit développeur associé au bundle.
Bundles-- Si vous souhaitez utiliser un produit développeur, vous devez fournir un devProductId unique, utilisé uniquement par un bundle.-- Nous récupérerons le prix du bundle et l'icône à partir du produit développeurpricing = {priceType = CurrencyTypes.PriceType.Marketplace,devProductId = 1795621566,},-- Sinon, si vous souhaitez utiliser une monnaie en expérience au lieu d'un produit développeur, vous pouvez utiliser cela à la place :-- Le prix ici est dans la monnaie en expérience, pas en Robuxpricing = {priceType = CurrencyTypes.PriceType.InExperience,price = 79,currencyId = "Gems",icon = 18712203759,},
Vous devez également référencer le script BundlesExample pour appeler setInExperiencePurchaseHandler.
BundlesExample
local function awardInExperiencePurchase(
_player: Player,
_bundleId: Types.BundleId,
_currencyId: CurrencyTypes.CurrencyId,
_price: number
)
-- Vérifiez si le joueur a suffisamment de monnaie pour acheter le bundle
-- Mettez à jour les données du joueur, donnez des objets, etc.
-- Déduisez la monnaie du joueur
task.wait(2)
return true
end
local function initializePurchaseHandlers()
local bundles = Bundles.getBundles()
for bundleId, bundle in bundles do
-- Le bundle n'est pas associé à un produit développeur s'il n'a pas de type de prix de marché
if not bundle or bundle.pricing.priceType ~= "Marketplace" then
continue
end
Bundles.setPurchaseHandler(bundleId, awardMarketplacePurchase)
receiptHandlers[bundle.pricing.devProductId] = receiptHandler
end
-- Si vous avez des monnaies en expérience que vous utilisez pour les bundles, définissez le gestionnaire ici
for currencyId, _ in Currencies do
Bundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)
end
end
Spécifiquement, vous devez remplir awardInExperiencePurchase, qui est appelé par une boucle à travers Currencies dans la fonction d'exemple initializePurchaseHandlers (c'est-à-dire que chaque currencyId est connecté au gestionnaire via Bundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)).
Définir les bundles
Tous les bundles offerts dans votre expérience peuvent être définis dans ReplicatedStorage.Bundles.Configs.Bundles, avec des types exportés depuis le script Types dans le même dossier.
Si vous utilisez un devProductId, vous devez mettre à jour le devProductId principal du bundle pour qu'il corresponde à celui de votre expérience. C'est ce qui sera proposé via MarketplaceService pour acheter le bundle lui-même. Il est fortement recommandé d'utiliser un nouveau produit développeur pour le bundle afin de faciliter le suivi des ventes séparées.
Si vous souhaitez un bundle avec plusieurs objets, et si ceux-ci sont déjà représentés par des produits développeurs dans votre expérience, vous n'avez pas besoin de spécifier explicitement le prix de l'objet/assetId/nom, qui sera récupéré via les informations du produit :
README{itemType = ItemTypes.ItemType.DevProduct,devProductId = <DEV_PRODUCT_ID>,metadata = {caption = {text = "x1",color = Color3.fromRGB(236, 201, 74),} -- La légende est optionnelle ! Vous pouvez également omettre ce champ}},
Sinon, vous pouvez configurer manuellement ces détails d'objets :
README{itemType = ItemTypes.ItemType.Robux,priceInRobux = 49,icon = <IMAGE_ASSET_ID>,metadata = {caption = {text = "x1",color = Color3.fromRGB(236, 201, 74),} -- La légende est optionnelle ! Vous pouvez également omettre ce champ}},
Par exemple, votre bundle entier ressemblera probablement à ceci :
READMElocal starterBundle: Types.RelativeTimeBundle = {bundleType = Types.BundleType.RelativeTime,-- Si vous souhaitez utiliser un produit développeur, vous devez fournir un devProductId unique, utilisé uniquement par un bundle.-- Nous récupérerons le prix du bundle et l'icône à partir du produit développeurpricing = {priceType = CurrencyTypes.PriceType.Marketplace,devProductId = <DEV_PRODUCT_ID>,},-- Sinon, si vous souhaitez utiliser une monnaie en expérience au lieu d'un produit développeur, vous pouvez utiliser cela à la place :-- Le prix ici est dans la monnaie en expérience, pas en Robux-- pricing = {-- priceType = CurrencyTypes.PriceType.InExperience,-- price = 79,-- currencyId = <CURRENCY_ID>,-- icon = <IMAGE_ASSET_ID>,-- },includedItems = {[1] = {-- L'objet lui-même n'est pas vendu via un produit développeur, donc indiquez combien il vaut en Robux et donnez une icône-- Le priceInRobux aide Bundles à montrer la valeur relative du prix du bundle par rapport à la somme de son contenuitemType = ItemTypes.ItemType.Robux,priceInRobux = 49,icon = <IMAGE_ASSET_ID>,-- Alternativement, si cela a un produit développeur, laissez de côté le prix et l'icône ci-dessus et définissez simplement le devProductId-- Le prix et l'icône seront récupérés à partir du produit développeur-- devProductId = <ITEM_DEV_PRODUCT_ID>-- Il existe d'autres champs de métadonnées optionnels qui sont spécifiques à l'interface utilisateur si nécessairemetadata = {caption = {text = "x1",color = Color3.fromRGB(236, 201, 74),},},},[2] = {itemType = ItemTypes.ItemType.Robux,priceInRobux = 99,icon = <IMAGE_ASSET_ID>,metadata = {caption = {text = "x1",color = Color3.fromRGB(236, 201, 74),},},},[3] = {itemType = ItemTypes.ItemType.Robux,priceInRobux = 149,icon = <IMAGE_ASSET_ID>,metadata = {caption = {text = "x1",color = Color3.fromRGB(236, 201, 74),},},},},singleUse = true, -- Une fois acheté ou expiré, ne sera plus valide même si votre expérience essaie de proposer (onPlayerAdded). Vous pouvez mettre cela à false pendant les tests dans studio.durationInSeconds = 900, -- 15 minutesincludesOfflineTime = false, -- Ne compte que le temps écoulé dans l'expériencemetadata = {displayName = "BUNDLE DE DÉMARRAGE",description = "Économisez 75 % et obtenez un bon départ !",},}
Intégrer la logique serveur
Jetez un œil à ReplicatedStorage.Bundles.Server.Examples.BundlesExample, qui montre comment votre serveur interagira avec le pack de fonctionnalités Bundles et les méthodes ci-dessus sur ModuleScript. Les extraits ci-dessous proviennent de ce script.
Vous devez principalement relier quatre éléments après avoir glissé le pack de fonctionnalités Bundles dans votre expérience :
Connectez les gestionnaires d'achats via Bundles.setPurchaseHandler pour spécifier les fonctions à appeler pour récompenser des objets lorsque l'achat est en cours de traitement.
BundlesExamplelocal function awardMarketplacePurchase(_player: Player, _bundleId: Types.BundleId, _receiptInfo: { [string]: any })-- Mettez à jour les données du joueur, donnez des objets, etc.-- ... ET enregistrez receiptInfo.PurchaseId afin que nous puissions vérifier si l'utilisateur possède déjà ce bundletask.wait(2)return Enum.ProductPurchaseDecision.PurchaseGrantedendlocal function awardInExperiencePurchase(_player: Player,_bundleId: Types.BundleId,_currencyId: CurrencyTypes.CurrencyId,_price: number)-- Vérifiez si le joueur a suffisamment de monnaie pour acheter le bundle-- Mettez à jour les données du joueur, donnez des objets, etc.-- Déduisez la monnaie du joueurtask.wait(2)return trueendlocal function initializePurchaseHandlers()local bundles = Bundles.getBundles()for bundleId, bundle in bundles do-- Le bundle n'est pas associé à un produit développeur s'il n'a pas de type de prix de marchéif not bundle or bundle.pricing.priceType ~= "Marketplace" thencontinueendBundles.setPurchaseHandler(bundleId, awardMarketplacePurchase)receiptHandlers[bundle.pricing.devProductId] = receiptHandlerend-- Si vous avez des monnaies en expérience que vous utilisez pour les bundles, définissez le gestionnaire icifor currencyId, _ in Currencies doBundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)endendConnectez votre logique pour MarketplaceService.ProcessReceipt, mais cela pourrait être fait ailleurs si votre expérience a déjà des produits développeurs à vendre. Essentiellement, lorsqu'un reçu de produit développeur est en cours de traitement, ils appeleront maintenant Bundles.getBundleByDevProduct pour vérifier si le produit appartient à un bundle. Si c'est le cas, le script appelle alors Bundles.processReceipt.
BundlesExample-- Traitez le reçu du marché pour déterminer si le joueur doit être facturé ou nonlocal function processReceipt(receiptInfo): Enum.ProductPurchaseDecisionlocal userId, productId = receiptInfo.PlayerId, receiptInfo.ProductIdlocal player = Players:GetPlayerByUserId(userId)if not player thenreturn Enum.ProductPurchaseDecision.NotProcessedYetendlocal handler = receiptHandlers[productId] -- Obtenez le gestionnaire pour le produitlocal success, result = pcall(handler, receiptInfo, player) -- Appelez le gestionnaire pour vérifier si la logique d'achat a réussiif not success or not result thenwarn("Échec du traitement du reçu :", receiptInfo, result)return Enum.ProductPurchaseDecision.NotProcessedYetendreturn Enum.ProductPurchaseDecision.PurchaseGrantedendlocal function receiptHandler(receiptInfo: { [string]: any }, player: Player)local bundleId, _bundle = Bundles.getBundleByProductId(receiptInfo.ProductId)if bundleId then-- Cet achat appartient à un bundle, laissez Bundles s'en occuperlocal purchaseDecision = Bundles.processReceiptAsync(player, bundleId, receiptInfo)return purchaseDecision == Enum.ProductPurchaseDecision.PurchaseGrantedend-- Cet achat n'appartient pas à un bundle,-- ... Gérez toute votre logique existante ici si vous en avezreturn falseendConnectez Players.PlayerAdded:Connect(Bundles.OnPlayerAdded) afin que le pack de fonctionnalités Bundles redémarre les générations de bundles actifs qui n'ont pas encore expiré pour un joueur.
READMElocal function onPlayerAdded(player: Player)-- Informez Bundles lorsque le joueur rejoint afin qu'il puisse recharger ses donnéesBundles.onPlayerAdded(player)-- Si vous aviez un bundle de démarrage que vous souhaitiez offrir à tous les nouveaux utilisateurs, vous pourriez le proposer ici-- ... Bundles gérera si le joueur l'a déjà acheté ou s'il a expiré puisqu'il n'est pas répétable-- Bundles.promptIfValidAsync(player, "StarterBundle")-- Appel de ceci juste à titre d'exemple, vous pouvez appeler cela à tout moment ou n'importe oùonPromptBundleXYZEvent(player)endProposez des bundles. Bien que cela dépende du gameplay, l'exemple propose aux joueurs un StarterBundle onPlayerAdded.
La logique du pack de fonctionnalités Bundles garantit que chaque joueur ne reçoive pas une offre répétée s'il a déjà acheté le bundle ou si l'offre a déjà expiré (en fonction de la configuration du bundle).
Chaque fois que vous souhaitez proposer un bundle à un joueur, appelez Bundles.promptIfValidAsync(player, bundleId).
READMElocal function onPromptBundleXYZEvent(player: Player)-- Connectez l'événement d'expérience que vous souhaitez utiliser pour déterminer quand un joueur reçoit une proposition de bundle-- ... Cela sera chaque fois que vous avez rempli vos critères d'éligibilité pour proposer le bundle à un joueur-- ... Par exemple, si vous souhaitez proposer un bundle lorsqu'un joueur rejoint, ou lorsqu'un joueur monte de niveautask.spawn(Bundles.promptIfValidAsync, player, <Some_Bundle_Id>)-- ... Si vous créez plusieurs bundles, utiliser task.spawn() pour envelopper l'appel de la fonction ci-dessus minimisera les écarts entre les compteursend
Considérez les meilleures pratiques suivantes concernant les enregistrements redondants de ReceiptIds :
Bien que le pack de fonctionnalités Bundles enregistre les ReceiptIds pour éviter le traitement du même reçu deux fois, vous devriez également enregistrer les ReceiptIds dans vos tables pour que si le flux d'achat échoue après que son gestionnaire d'achats a déjà terminé, vous sachiez lors de la tentative subséquente de ne pas attribuer à nouveau des objets.
Le pack de fonctionnalités Bundles ne déduira pas le ReceiptId si l'achat échoue à n'importe quelle étape, donc vous devez vous assurer que vous enregistrez le ReceiptId dans vos tables avant de traiter le reçu en tant que partie de votre purchaseHandler.
Cette redondance aide à garantir que toute la logique d'achat a été correctement traitée et que votre magasin de données et le magasin de données du pack de fonctionnalités Bundles atteignent une cohérence éventuelle, votre magasin de données étant la source de vérité.
Configurer les constantes
Les constantes pour le pack de fonctionnalités Core résident à deux endroits :
Les constantes partagées résident dans ReplicatedStorage.FeaturePackagesCore.Configs.SharedConstants.
Les constantes spécifiques au pack, dans ce cas le pack de fonctionnalités Bundles, se trouvent dans ReplicatedStorage.Bundles.Configs.Constants.
Les principales choses que vous pourriez vouloir ajuster pour répondre aux exigences de conception de votre expérience :
- Identifiants d'actifs sonores
- Durée de l'effet d'achat et couleurs des particules
- Collapsibilité de l'affichage tête haute
De plus, vous pouvez trouver des chaînes pour traduction regroupées à un endroit : ReplicatedStorage.FeaturePackagesCore.Configs.TranslationStrings.
Personnaliser les composants UI
En modifiant les objets du pack, tels que les couleurs, la police et la transparence, vous pouvez ajuster la présentation visuelle de vos invites de bundle. Cependant, gardez à l'esprit que si vous déplacez des objets dans la hiérarchie, le code ne pourra pas les trouver, et vous devrez apporter des ajustements à votre code.
Une invite est composée de deux composants de haut niveau :
- PromptItem – Le composant individuel répété pour chaque objet d'un bundle (image de l'objet, légende, nom, prix).
- Prompt – La fenêtre d'invite elle-même.
L'affichage tête haute est également composé de deux composants :
- HudItem – Un composant individuel qui représente chaque option de menu dans l'affichage tête haute.
- Hud – À remplir programmatique avec HudItems.
Si vous voulez avoir un meilleur contrôle sur l'affichage tête haute, au lieu d'utiliser simplement l'interface utilisateur HUD existante dans ReplicatedStorage.Bundles.Objects.BundlesGui, vous pouvez déplacer des éléments pour répondre à vos propres exigences de conception. Assurez-vous simplement de mettre à jour le comportement du script client dans le script ReplicatedStorage.Bundles.Client.UIController.
Référence API
Types
RelativeTime
Une fois que le bundle RelativeTime est proposé à un joueur, il reste disponible jusqu'à ce que la durée s'écoule. Ce type s'affiche sur l'affichage tête haute du joueur et est automatiquement proposé lors des sessions futures jusqu'à ce que le bundle expire ou que le joueur l'achète.
Un exemple courant de ce type de bundle est une offre de pack de démarrage à usage unique qui s'affiche à tous les nouveaux joueurs pendant 24 heures.
| Nom | Type | Description |
|---|---|---|
| includeOfflineTime | bool | (Optionnel) Si ce n'est pas défini, seul le temps passé dans l'expérience comptera pour la durée restante de l'offre. |
| singleUse | bool | (Optionnel) Si ce n'est pas défini, l'achat peut être réactivé après son achat ou son expiration. Si défini, une fois acheté ou expiré pour la première fois, il ne sera plus jamais proposable, même si vous appelez Bundles.promptIfValidAsync avec le bundleId. |
FixedTime
Une fois que le bundle FixedTime est proposé à un joueur, il reste disponible jusqu'à la fin de l'heure universelle coordonnée (UTC) définie. Ce type s'affiche sur l'affichage tête haute du joueur et est automatiquement proposé lors des sessions futures jusqu'à ce que le bundle expire ou que le joueur l'achète.
Un exemple courant de ce type de bundle est une offre de vacances qui n'est disponible que pour un mois donné.
OneTime
Un bundle OneTime n'est disponible que au moment où il est proposé à un joueur. Il ne s'affiche pas sur l'affichage tête haute du joueur, et une fois qu'un joueur ferme l'invite, elle ne peut pas être rouverte jusqu'à ce qu'elle soit à nouveau proposée par le serveur.
Un exemple courant de ce type de bundle est une offre pour acheter plus de monnaie en expérience au moment où un joueur en manque.