バンドル機能パッケージは、プレイヤーにアイテムのコレクションを割引価格で販売するための機能を提供します。プレイヤーがバンドルを購入する際に、カスタムな体験内通貨やRobuxを使用するかどうか、使用したいバンドルの種類、販売したいアイテムのセット、およびプレイヤーにゲームプレイ中にどのようにプロンプトを表示するかを選択できます。
パッケージのカスタマイズオプションを使用することで、あなたの体験のデザインや収益化の目標に応じてバンドルを調整できます。たとえば:
- 新規プレイヤーに価値を提供し、早期の支出を促す割引版スタートパックを提供することで、低いコンバージョン率の指標をターゲットにすること。
- 様々な価格帯のアイテムをバンドルすることで、幅広いプレイヤーにアピールし支出の深さを増加させること。
- 限定アイテムのバンドルを提供することで、ライブオペレーション(LiveOps)イベントを収益化すること。

パッケージを取得する
クリエイターストアは、あなたのプロジェクト内で使用するためにRobloxおよびRobloxコミュニティによって作成されたすべてのアセットを見つけるために使用できるツールボックスのタブです。モデル、画像、メッシュ、オーディオ、プラグイン、ビデオ、フォントアセットを含むアセットを追加できます。クリエイターストアを使用して、オープンな体験に1つ以上のアセットを直接追加することもできます(機能パッケージを含む!)。
すべての機能パッケージは、正しく機能するためにCore機能パッケージを必要とします。Coreとバンドル機能パッケージアセットがあなたのインベントリに入ったら、プラットフォーム上の任意のプロジェクトで再利用できます。
インベントリから体験にパッケージを取得するには:
次のコンポーネントセットのインベントリに追加リンクをクリックして、Coreおよびバンドル機能パッケージをStudio内のインベントリに追加します。
Studioのウィンドウメニューまたはホームタブツールバーからツールボックスを開きます。
ツールボックスウィンドウで、インベントリタブをクリックします。マイモデルのソートが表示されます。

機能パッケージCoreタイルをクリックし、次にバンドル機能パッケージタイルをクリックします。両方のパッケージフォルダーがエクスプローラーウィンドウに表示されます。
パッケージフォルダーをReplicatedStorageにドラッグします。
データストアコールを許可して、プレイヤーの購入を追跡します。
- Studioのファイル ⟩ 体験設定ウィンドウを開きます。
- セキュリティタブに移動し、次にAPIサービスへのStudioアクセスを有効にするを有効にします。
通貨を定義する
あなたの体験に独自の通貨システムがある場合、ReplicatedStorage.FeaturePackagesCore.Configs.Currenciesにそれを定義することでCore機能パッケージに登録できます。このファイルにはすでにコメント付きのGems通貨の例がありますので、それをあなた自身のものに置き換えてください。
通貨Gems = {displayName = "ジェム",symbol = "💎",icon = nil,},
Currenciesスクリプトは、Core機能パッケージにあなたの通貨に関するいくつかのメタデータを提供します。
- (必須) displayName - あなたの通貨の名前。この名前は、シンボルやアイコンを指定しない場合、購入ボタンで使用されます(例:「100 ジェム」)。
- (オプション) symbol - 通貨のアイコンとして使用するテキスト文字がある場合、購入ボタンでこの名称を代わりに使用されます(例:「💎100」)。
- (オプション) icon - 通貨のためのAssetId画像アイコンがある場合、購入ボタンでこのアイコンが使用されます(例:価格の左に画像「🖼️100」が置かれます)。
通貨が設定されたら、バンドルの価格、通貨、アイコンを手動で指定する必要があります。この情報は、バンドルに関連する開発者製品から取得されるのではありません。
バンドル-- 開発商品を使用したい場合は、ユニークなdevProductIdを提供する必要があります。他のバンドルでは使用されないものです。-- 我々は開発者製品からバンドルの価格とアイコンを取得しますpricing = {priceType = CurrencyTypes.PriceType.Marketplace,devProductId = 1795621566,},-- そうでない場合、開発製品の代わりに体験内通貨を使用する場合は、次のようにします:-- ここでの価格は体験内通貨であり、Robuxではありませんpricing = {priceType = CurrencyTypes.PriceType.InExperience,price = 79,currencyId = "Gems",icon = 18712203759,},
BundlesExampleスクリプトを参照してsetInExperiencePurchaseHandlerを呼ぶ必要があります。
BundlesExample
local function awardInExperiencePurchase(
_player: Player,
_bundleId: Types.BundleId,
_currencyId: CurrencyTypes.CurrencyId,
_price: number
)
-- プレイヤーがバンドルを購入するのに十分な通貨を持っているか確認する
-- プレイヤーデータを更新し、アイテムを提供するなど
-- プレイヤーから通貨を差し引く
task.wait(2)
return true
end
local function initializePurchaseHandlers()
local bundles = Bundles.getBundles()
for bundleId, bundle in bundles do
-- バンドルは市場価格タイプを持っていない場合、開発者製品に関連付けられていません
if not bundle or bundle.pricing.priceType ~= "Marketplace" then
continue
end
Bundles.setPurchaseHandler(bundleId, awardMarketplacePurchase)
receiptHandlers[bundle.pricing.devProductId] = receiptHandler
end
-- バンドルのために使用している体験内通貨がある場合は、ここでハンドラーを設定します
for currencyId, _ in Currencies do
Bundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)
end
end
特に、initializePurchaseHandlers内の通貨をループすることで呼ばれるawardInExperiencePurchaseを記入する必要があります(すなわち、各currencyIdはBundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)を通じてハンドラーに接続されています)。
バンドルを定義する
体験で提供されるすべてのバンドルは、ReplicatedStorage.Bundles.Configs.Bundles内に定義できます。これは、同じフォルダー内のTypesスクリプトからエクスポートされた型を使用します。
devProductIdを使用する場合は、バンドルの主要なdevProductIdを体験内のものと一致させる必要があります。これは、MarketplaceServiceを通じてバンドル自体を購入するために提示されるものです。個別の販売を追跡しやすくするために、新しい開発者製品をバンドルに使用することを強く推奨します。
複数のアイテムを含むバンドルが必要な場合、それらがすでに体験内の開発者製品で表現されている場合、アイテムの価格/assetId/名前を明示的に設定する必要はなく、製品情報から取得されます:
README{itemType = ItemTypes.ItemType.DevProduct,devProductId = <DEV_PRODUCT_ID>,metadata = {caption = {text = "x1",color = Color3.fromRGB(236, 201, 74),} -- キャプションはオプション!このフィールドを省略することもできます}},
そうでない場合、これらのアイテムの詳細を手動で設定できます:
README{itemType = ItemTypes.ItemType.Robux,priceInRobux = 49,icon = <IMAGE_ASSET_ID>,metadata = {caption = {text = "x1",color = Color3.fromRGB(236, 201, 74),} -- キャプションはオプション!このフィールドを省略することもできます}},
たとえば、あなたのバンドル全体は以下のようになるでしょう:
READMElocal starterBundle: Types.RelativeTimeBundle = {bundleType = Types.BundleType.RelativeTime,-- 開発商品を使用したい場合は、ユニークなdevProductIdを提供する必要があります。他のバンドルでは使用されていません。-- 我々は開発者製品からバンドルの価格とアイコンを取得しますpricing = {priceType = CurrencyTypes.PriceType.Marketplace,devProductId = <DEV_PRODUCT_ID>,},-- そうでない場合、開発製品の代わりに体験内通貨を使用する場合は、次のようにします:-- ここでの価格は体験内通貨であり、Robuxではありません-- pricing = {-- priceType = CurrencyTypes.PriceType.InExperience,-- price = 79,-- currencyId = <CURRENCY_ID>,-- icon = <IMAGE_ASSET_ID>,-- },includedItems = {[1] = {-- アイテム自体は開発者製品を介して販売されていないため、Robuxでの価値を示し、アイコンを提供します-- priceInRobuxは、バンドルの価格とその内容物の合計の相対的な価値を示しますitemType = ItemTypes.ItemType.Robux,priceInRobux = 49,icon = <IMAGE_ASSET_ID>,-- または、これに開発者製品がある場合は、上記の価格とアイコンを省略し、ただdevProductIdを設定します-- 価格とアイコンは開発者製品から取得されます-- devProductId = <ITEM_DEV_PRODUCT_ID>-- 必要に応じてUI固有の追加オプションのメタデータフィールドがありますmetadata = {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, -- 一度購入または期限切れになると、再度有効にはならなくなります。テスト中はfalseに設定することができます。durationInSeconds = 900, -- 15分includesOfflineTime = false, -- 体験で経過した時間のみをカウントmetadata = {displayName = "スターターバンドル",description = "75%節約して、スムーズに始めましょう!",},}
サーバーロジックを統合する
ReplicatedStorage.Bundles.Server.Examples.BundlesExampleを見て、あなたのサーバーがバンドル機能パッケージと上記のModuleScriptのメソッドとどのように相互動作するかを示します。以下のスニペットはそのスクリプトからのものです。
バンドル機能パッケージを体験にドラッグした後にフックアップすべき四つのことがあります。
購入処理中にアイテムを授与するための関数を指定するために、Bundles.setPurchaseHandlerで購入ハンドラーを接続します。
BundlesExamplelocal function awardMarketplacePurchase(_player: Player, _bundleId: Types.BundleId, _receiptInfo: { [string]: any })-- プレイヤーデータを更新し、アイテムを授与します-- ... およびreceiptInfo.PurchaseIdを記録し、ユーザーがこのバンドルをすでに持っているかどうか確認しますtask.wait(2)return Enum.ProductPurchaseDecision.PurchaseGrantedendlocal function awardInExperiencePurchase(_player: Player,_bundleId: Types.BundleId,_currencyId: CurrencyTypes.CurrencyId,_price: number)-- プレイヤーがバンドルを購入するのに十分な通貨を持っているか確認する-- プレイヤーデータを更新し、アイテムを授与します-- プレイヤーから通貨を差し引くtask.wait(2)return trueendlocal function initializePurchaseHandlers()local bundles = Bundles.getBundles()for bundleId, bundle in bundles do-- バンドルは市場価格タイプを持っていない場合、開発者製品に関連付けられていませんif not bundle or bundle.pricing.priceType ~= "Marketplace" thencontinueendBundles.setPurchaseHandler(bundleId, awardMarketplacePurchase)receiptHandlers[bundle.pricing.devProductId] = receiptHandlerend-- バンドルのために使用している体験内通貨がある場合は、ここでハンドラーを設定しますfor currencyId, _ in Currencies doBundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)endendMarketplaceService.ProcessReceiptにあなたのロジックを接続しますが、もしすでに体験で開発者製品が販売されている場合は、他の場所で行われるかもしれません。本質的に、開発者製品のレシートが処理されるときに、Bundles.getBundleByDevProductを呼び出して、その製品がバンドルに属するかどうか確認します。そうであれば、スクリプトは次にBundles.processReceiptを呼び出します。
BundlesExample-- マーケットプレイスからのレシートを処理して、プレイヤーに対して請求する必要があるかどうかを判断しますlocal 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] -- 製品のハンドラーを取得local success, result = pcall(handler, receiptInfo, player) -- ハンドラーを呼び出して購入ロジックが成功したか確認しますif not success or not result thenwarn("レシートの処理に失敗しました:", 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-- この購入はバンドルに属しています。バンドルに処理させますlocal purchaseDecision = Bundles.processReceiptAsync(player, bundleId, receiptInfo)return purchaseDecision == Enum.ProductPurchaseDecision.PurchaseGrantedend-- この購入はバンドルに属していないため、-- ... 既存のロジックをここで処理しますreturn falseendPlayers.PlayerAdded:Connect(Bundles.OnPlayerAdded)を接続して、バンドル機能パッケージがまだ期限切れでないアクティブなバンドルをプレイヤーに再プロンプトできるようにします。
READMElocal function onPlayerAdded(player: Player)-- プレイヤーが参加したときにBundlesに知らせて、データを再読み込みできるようにしますBundles.onPlayerAdded(player)-- すべての新規ユーザーに提供したいスターターバンドルがある場合は、ここでプロンプトを表示できます-- ... バンドルが既に購入済みか、期限が切れている場合はBundlesが処理します-- Bundles.promptIfValidAsync(player, "StarterBundle")-- ここで呼び出すのは単なる例です。どこでも好きなときに呼び出すことができますonPromptBundleXYZEvent(player)endバンドルをプロンプトします。これはゲームプレイに依存しますが、例として、プレイヤーが参加したときにStarterBundleをプロンプトします。
バンドル機能パッケージのロジックは、各プレイヤーがすでにバンドルを購入しているか、オファーがすでに期限切れである場合、同じオファーが繰り返されないようにします。
プレイヤーにバンドルをプロンプトしたいときは、Bundles.promptIfValidAsync(player, bundleId)を呼び出します。
READMElocal function onPromptBundleXYZEvent(player: Player)-- プレイヤーがバンドルのプロンプトを受け取る時期を判断するために、任意の体験イベントを接続します-- ... プレイヤーにバンドルをプロンプトするための適格基準を満たすときにこの処理を行います-- ... たとえば、プレイヤーが参加したときやレベルアップしたときにバンドルをプロンプトする場合ですtask.spawn(Bundles.promptIfValidAsync, player, <Some_Bundle_Id>)-- ... 複数のバンドルを作成する場合、上記の関数呼び出しをtask.spawn()でラップすることで、カウントダウン間の矛盾を最小限に抑えますend
ReceiptIdsの冗長な記録に関する以下のベストプラクティスのガイダンスを考慮してください:
バンドル機能パッケージは、同じレシートを二度処理しないようにReceiptIdsを記録しますが、購入フローが購入ハンドラーが終了した後に失敗した場合、アイテムを再度授与しないようにするために、あなたのテーブル内でもReceiptIdsを記録することを推奨します。
バンドル機能パッケージは、購入がどのステップで失敗した場合もReceiptIdを記録しませんので、購入ハンドラーの一部としてレシートを処理する前にReceiptIdをテーブルに記録することを確認してください。
この冗長性は、すべての購入ロジックが適切に処理され、あなたのデータストアとバンドル機能パッケージのデータストアが最終的な整合性に達することを保証します。あなたのデータストアが真実の源となります。
定数を設定する
Core機能パッケージの定数は、2つの場所に存在します:
共有定数はReplicatedStorage.FeaturePackagesCore.Configs.SharedConstantsにあります。
パッケージ固有の定数は、今回のバンドル機能パッケージに関してReplicatedStorage.Bundles.Configs.Constantsにあります。
あなたの体験のデザイン要件を満たすために調整したい主要な項目:
- サウンドアセットID
- 購入効果の持続時間とパーティクルの色
- ヘッドアップディスプレイの折りたたみ機能
さらに、翻訳のための文字列が1つの場所に分けて見つかるか、ReplicatedStorage.FeaturePackagesCore.Configs.TranslationStringsにあります。
UIコンポーネントをカスタマイズする
パッケージオブジェクトを修正することで(色、フォント、透明度など)、バンドルプロンプトの視覚的なプレゼンテーションを調整できます。ただし、オブジェクトを階層的に移動させた場合、コードがそれらを見つけることができなくなり、コードを調整する必要があることに注意してください。
プロンプトは2つの高レベルコンポーネントで構成されています。
- PromptItem – バンドル内の各アイテムに対して繰り返される個々のコンポーネント(アイテム画像、キャプション、名前、価格)。
- Prompt – プロンプトウィンドウそのもの。
ヘッドアップディスプレイも2つのコンポーネントで構成されています。
- HudItem – ヘッドアップディスプレイ内の各メニューオプションを表す個々のコンポーネント。
- Hud – プログラム的にHudItemsで埋め込まれます。
ヘッドアップディスプレイの制御をより強化したい場合は、ReplicatedStorage.Bundles.Objects.BundlesGui内の既存のHUD UIを単に使用するのではなく、自身のデザイン要件に合わせてものを移動できます。クライアントスクリプトの動作をReplicatedStorage.Bundles.Client.UIControllerスクリプトで更新することを忘れないでください。
APIリファレンス
タイプ
RelativeTime
RelativeTimeバンドルがプレイヤーに提供されると、それは期間が終了するまで利用可能です。このタイプは、プレイヤーのヘッドアップディスプレイに表示され、バンドルが期限切れになったりプレイヤーが購入するまで、将来のセッションで自動的にプロンプトされます。
このバンドルタイプの一般的な例は、すべての新しいプレイヤーに24時間提供される単一使用のスターターパックオファーです。
| 名前 | タイプ | 説明 |
|---|---|---|
| includeOfflineTime | bool | (オプション) 設定しない場合、体験内で過ごした時間のみが残りのオファーの有効期間にカウントされます。 |
| singleUse | bool | (オプション) 設定しない場合、購入後または期限が切れた後に再度アクティブにすることができます。 設定された場合、最初に購入または期限切れになった後は、バンドルは再度プロンプトできなくなります。Bundles.promptIfValidAsyncをバンドルIdで呼び出しても同様です。 |
FixedTime
FixedTimeバンドルがプレイヤーに提供されると、それは設定された協定世界時(UTC)の終了まで利用可能です。このタイプは、プレイヤーのヘッドアップディスプレイに表示され、バンドルが期限切れになるかプレイヤーが購入するまで、将来のセッションで自動的にプロンプトされます。
このバンドルタイプの一般的な例は、特定の月だけ利用できるホリデーオファーです。
OneTime
OneTimeバンドルは、プレイヤーに提供される瞬間にしか利用できません。プレイヤーのヘッドアップディスプレイには表示されず、プレイヤーがプロンプトを閉じると、サーバーから再度プロンプトされるまで再オープンできません。
このバンドルタイプの一般的な例は、プレイヤーが体験内通貨が切れた瞬間に追加の体験内通貨を購入するオファーです。