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

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

機能パッケージコアタイルをクリックし、次にバンドル機能パッケージタイルをクリックします。両方のパッケージフォルダーがエクスプローラーウィンドウに表示されます。
パッケージフォルダーをReplicatedStorageにドラッグします。
パッケージを使用してプレイヤーの購入を追跡するためにデータストア呼び出しを許可します。
- スタジオのファイル ⟩ エクスペリエンス設定ウィンドウを開きます。
- セキュリティタブに移動し、次にAPIサービスへのスタジオアクセスを有効にするを有効にします。
通貨を定義する
ゲームに独自の通貨システムがある場合は、ReplicatedStorage.FeaturePackagesCore.Configs.Currenciesでそれらをコア機能パッケージに登録できます。このファイルには、すでにコメントアウトされたGems通貨の例がありますので、それを自分のものに置き換えてください。
Gems = {
displayName = "宝石",
symbol = "💎",
icon = nil,
},Currenciesスクリプトは、コア機能パッケージに通貨に関するメタデータを伝えます:
- (必須) displayName - 通貨の名前。シンボルやアイコンを指定しない場合、この名前が購入ボタンに使用されます(例:"100 宝石")。
- (オプション) symbol - 通貨のアイコンとして使用するテキスト文字がある場合、これは購入ボタンでdisplayNameの代わりに使用されます(例:"💎100")。
- (オプション) icon - 通貨のためのAssetId画像アイコンがある場合、これは購入ボタンでdisplayNameの代わりに使用されます(例:価格の左に画像が配置されます "🖼️100")。
通貨が設定されたら、バンドルの価格、通貨、アイコンを手動で指定する必要があります。これは、バンドルに関連付けられた開発者製品から情報が取得されるのではなく、ヘッドアップディスプレイのためです。
-- 開発製品を使用する場合は、一意のdevProductIdを提供する必要があります。これは1つのバンドルでのみ使用されます。
-- バンドルの価格とアイコンは開発者製品から取得します
pricing = {
priceType = CurrencyTypes.PriceType.Marketplace,
devProductId = 1795621566,
},
-- それ以外の場合、開発製品の代わりにゲーム内通貨を使用したい場合は、次のようにします:
-- ここでの価格はゲーム内通貨であり、ロブックスではありません
pricing = {
priceType = CurrencyTypes.PriceType.InExperience,
price = 79,
currencyId = "Gems",
icon = 18712203759,
},BundlesExampleスクリプトを参照して、setInExperiencePurchaseHandlerを呼び出す必要があります。
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特に、awardInExperiencePurchaseを記入する必要があります。これは、例のinitializePurchaseHandlers内のCurrenciesをループして呼び出されます(すなわち、各currencyIdはBundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)を通じてハンドラーに接続されています)。
バンドルを定義する
ゲームで提供可能なすべてのバンドルは、ReplicatedStorage.Bundles.Configs.Bundles内で定義できます。タイプは同じフォルダー内のTypesスクリプトからエクスポートされます。
devProductIdを使用している場合は、バンドルのメインdevProductIdをゲーム内のものと一致させる必要があります。これは、MarketplaceServiceを通じてバンドル自体を購入するために促されます。バンドルの販売を別々に追跡しやすくするために、新しい開発者製品を使用することを強くお勧めします。
複数のアイテムを含むバンドルを希望する場合、これらがすでにゲーム内の開発者製品で表されている場合は、アイテムの価格/assetId/名前を明示的に設定する必要はありません。これらは製品情報を通じて取得されます:
{
itemType = ItemTypes.ItemType.DevProduct,
devProductId = <DEV_PRODUCT_ID>,
metadata = {
caption = {
text = "x1",
color = Color3.fromRGB(236, 201, 74),
} -- キャプションはオプションです!このフィールドを省略することもできます
}
},それ以外の場合は、これらのアイテムの詳細を手動で構成できます:
{
itemType = ItemTypes.ItemType.Robux,
priceInRobux = 49,
icon = <IMAGE_ASSET_ID>,
metadata = {
caption = {
text = "x1",
color = Color3.fromRGB(236, 201, 74),
} -- キャプションはオプションです!このフィールドを省略することもできます
}
},例えば、バンドル全体は次のようになるでしょう:
local starterBundle: Types.RelativeTimeBundle = {
bundleType = Types.BundleType.RelativeTime,
-- 開発製品を使用する場合は、一意のdevProductIdを提供する必要があります。これは1つのバンドルでのみ使用されます。
-- バンドルの価格とアイコンは開発者製品から取得します
pricing = {
priceType = CurrencyTypes.PriceType.Marketplace,
devProductId = <DEV_PRODUCT_ID>,
},
-- それ以外の場合、開発製品の代わりにゲーム内通貨を使用したい場合は、次のようにします:
-- ここでの価格はゲーム内通貨であり、ロブックスではありません
-- pricing = {
-- priceType = CurrencyTypes.PriceType.InExperience,
-- price = 79,
-- currencyId = <CURRENCY_ID>,
-- icon = <IMAGE_ASSET_ID>,
-- },
includedItems = {
[1] = {
-- アイテム自体は開発者製品を通じて販売されていないため、ロブックスでの価値を示し、アイコンを与えます
-- 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, -- 購入または期限切れになった場合、プレイヤーが促すことを試みても無効になります(onPlayerAdded)。スタジオでテスト中はこれをfalseにすることができます。
durationInSeconds = 900, -- 15分
includesOfflineTime = false, -- ゲーム内で経過した時間のみをカウントします
metadata = {
displayName = "スターターバンドル",
description = "75%節約して、先行スタートを切りましょう!",
},
}サーバーロジックを統合する
ReplicatedStorage.Bundles.Server.Examples.BundlesExampleを見てみましょう。これは、サーバーがバンドル機能パッケージと上記のModuleScriptのメソッドとどのように相互作用するかを示しています。以下のスニペットはそのスクリプトからのものです。
バンドル機能パッケージをゲームにドラッグした後、主に4つのことを接続する必要があります:
購入ハンドラーを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.promptIfValidAsync(player, "StarterBundle")-- これは例としてここで呼び出していますが、いつでもどこでも呼び出すことができますonPromptBundleXYZEvent(player)endバンドルを促します。これはゲームプレイに依存しますが、例としてプレイヤーが参加したときにスターターバンドルを促します。
バンドル機能パッケージのロジックは、各プレイヤーがすでにバンドルを購入している場合や、オファーがすでに期限切れになっている場合に再度オファーを受けないようにします。
プレイヤーにバンドルを促したいときは、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を記録していることを確認してください。
この冗長性は、すべての購入ロジックが適切に処理され、データストアとバンドル機能パッケージのデータストアが最終的に一貫性を持つことを保証します。データストアが真実の源となります。
定数を構成する
コア機能パッケージの定数は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を呼び出しても再度促すことはできません。 |
FixedTime
FixedTimeバンドルがプレイヤーに提供されると、設定された協定世界時(UTC)の終了まで利用可能です。このタイプはプレイヤーのヘッドアップディスプレイに表示され、バンドルが期限切れになるかプレイヤーが購入するまで、将来のセッションで自動的に促されます。
このバンドルタイプの一般的な例は、特定の月にのみ利用可能なホリデーオファーです。
OneTime
OneTimeバンドルは、プレイヤーに提供された瞬間にのみ利用可能です。プレイヤーのヘッドアップディスプレイには表示されず、プレイヤーがプロンプトを閉じると、サーバーによって再度促されるまで再オープンできません。
このバンドルタイプの一般的な例は、プレイヤーがゲーム内通貨を使い果たした瞬間に購入するオファーです。