套餐包

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

套餐功能包提供开箱即用的功能,以折扣价格向玩家销售物品集合。您可以选择是否允许玩家使用自定义的游戏内货币或Robux购买套餐,选择要使用的套餐类型、要销售的物品集合,以及在游戏过程中如何提示玩家。

通过使用该包的自定义选项,您可以根据游戏的设计和货币化目标来调整您的套餐,例如:

  • 通过提供折扣的入门包来针对低转化率指标,为新玩家提供价值并鼓励早期消费。
  • 通过将物品捆绑在不同的价格点上来增加消费深度,以吸引不同的玩家。
  • 通过提供限时的独家物品套餐来货币化实时操作(LiveOps)事件

获取包

创作者商店工具箱的一个选项卡,您可以在其中找到所有由Roblox和Roblox社区制作的资产,以供在您的项目中使用,包括模型、图像、网格、音频、插件、视频和字体资产。您可以使用创作者商店将一个或多个资产直接添加到打开的游戏中,包括功能包!

每个功能包都需要核心功能包才能正常运行。一旦核心套餐功能包资产在您的库存中,您可以在平台上的任何项目中重复使用它们。

要将包从您的库存中导入到游戏中:

  1. 通过单击以下组件集中的添加到库存链接,将核心套餐功能包添加到Studio中的库存中。

  2. 从Studio的窗口菜单或主页选项卡工具栏中,打开工具箱

  3. 工具箱窗口中,单击库存选项卡。我的模型排序显示。

    Studio的工具箱窗口,库存选项卡突出显示。
  4. 单击功能包核心图块,然后单击套餐功能包图块。两个包文件夹在资源管理器窗口中显示。

  5. 将包文件夹拖入ReplicatedStorage

  6. 允许数据存储调用跟踪玩家的购买情况。

    1. 打开Studio的文件体验设置窗口。
    2. 导航到安全性选项卡,然后启用启用Studio访问API服务

定义货币

如果您的游戏有自己的货币系统,您可以通过在ReplicatedStorage.FeaturePackagesCore.Configs.Currencies中定义它们来使用核心功能包进行注册。该文件中已经有一个注释掉的Gems货币示例;请用您自己的货币替换它。

货币
Gems = {
displayName = "宝石",
symbol = "💎",
icon = nil,
},

Currencies脚本告诉核心功能包有关您的货币的一些元数据:

  • (必需)displayName - 您的货币名称。如果您未指定符号或图标,则在购买按钮中使用此名称(即“100宝石”)。
  • (可选)symbol - 如果您有一个文本字符作为货币的图标,则在购买按钮中使用此字符代替displayName(即“💎100”)。
  • (可选)icon - 如果您有一个AssetId图像图标作为您的货币,则在购买按钮中使用此图标代替displayName(即图像将放置在价格“🖼️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

具体来说,您需要填写awardInExperiencePurchase,该函数由initializePurchaseHandlers中的Currencies循环调用(即每个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),
} -- 标题是可选的!您也可以省略此字段
}
},

例如,您的整个套餐可能看起来像这样:

README
local 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, -- 一旦购买或过期,即使您的游戏尝试提示也不再有效。您可以在Studio中测试时将其设置为false。
durationInSeconds = 900, -- 15分钟
includesOfflineTime = false, -- 仅计算在游戏中经过的时间
metadata = {
displayName = "入门套餐",
description = "节省75%,获得开局优势!",
},
}

集成服务器逻辑

查看ReplicatedStorage.Bundles.Server.Examples.BundlesExample,该示例展示了您的服务器如何与套餐功能包及上述ModuleScript中的方法进行交互。以下代码片段来自该脚本。

您主要需要在将套餐功能包拖入游戏后连接四个内容:

  1. 通过Bundles.setPurchaseHandler连接购买处理程序,以指定在处理购买时调用的函数以奖励物品。

    BundlesExample
    local function awardMarketplacePurchase(_player: Player, _bundleId: Types.BundleId, _receiptInfo: { [string]: any })
    -- 更新玩家数据,给予物品等。
    -- ... 并记录receiptInfo.PurchaseId,以便我们可以检查用户是否已经拥有此套餐
    task.wait(2)
    return Enum.ProductPurchaseDecision.PurchaseGranted
    end
    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
  2. 连接您的逻辑到MarketplaceService.ProcessReceipt,但如果您的游戏已经有开发产品在售,这可能在其他地方完成。基本上,当处理开发产品收据时,它们现在将调用Bundles.getBundleByDevProduct来检查该产品是否属于一个套餐。如果是,则脚本将调用Bundles.processReceipt

    BundlesExample
    -- 从市场处理收据以确定玩家是否需要被收费
    local function processReceipt(receiptInfo): Enum.ProductPurchaseDecision
    local userId, productId = receiptInfo.PlayerId, receiptInfo.ProductId
    local player = Players:GetPlayerByUserId(userId)
    if not player then
    return Enum.ProductPurchaseDecision.NotProcessedYet
    end
    local handler = receiptHandlers[productId] -- 获取产品的处理程序
    local success, result = pcall(handler, receiptInfo, player) -- 调用处理程序以检查购买逻辑是否成功
    if not success or not result then
    warn("处理收据失败:", receiptInfo, result)
    return Enum.ProductPurchaseDecision.NotProcessedYet
    end
    return Enum.ProductPurchaseDecision.PurchaseGranted
    end
    local function receiptHandler(receiptInfo: { [string]: any }, player: Player)
    local bundleId, _bundle = Bundles.getBundleByProductId(receiptInfo.ProductId)
    if bundleId then
    -- 此购买属于一个套餐,让Bundles处理它
    local purchaseDecision = Bundles.processReceiptAsync(player, bundleId, receiptInfo)
    return purchaseDecision == Enum.ProductPurchaseDecision.PurchaseGranted
    end
    -- 此购买不属于套餐,
    -- ... 如果您有任何现有逻辑,请在此处处理
    return false
    end
  3. 连接Players.PlayerAdded:Connect(Bundles.OnPlayerAdded),以便套餐功能包重新提示任何尚未过期的活动套餐给玩家。

    README
    local function onPlayerAdded(player: Player)
    -- 告诉Bundles玩家加入时,以便它可以重新加载他们的数据
    Bundles.onPlayerAdded(player)
    -- 如果您有一些入门套餐想要提供给所有新用户,您可以在此处提示
    -- ... Bundles将处理玩家是否已经购买了它或是否过期,因为它不可重复
    -- Bundles.promptIfValidAsync(player, "StarterBundle")
    -- 仅为示例在此调用,您可以在任何时候或任何地方调用此函数
    onPromptBundleXYZEvent(player)
    end
  4. 提示套餐。虽然这取决于游戏玩法,但示例提示玩家在onPlayerAdded时使用StarterBundle。

    • 套餐功能包逻辑确保每个玩家不会在已经购买套餐或让优惠过期后再次收到重复优惠(基于套餐配置)。

    • 每当您想要向玩家提示套餐时,调用Bundles.promptIfValidAsync(player, bundleId)

    README
    local function onPromptBundleXYZEvent(player: Player)
    -- 连接您想要使用的任何游戏事件,以确定何时提示玩家套餐
    -- ... 这将是每当您满足提示玩家套餐的资格标准时
    -- ... 例如,如果您想在玩家加入时提示套餐,或者在玩家升级时
    task.spawn(Bundles.promptIfValidAsync, player, <Some_Bundle_Id>)
    -- ... 如果创建多个套餐,使用task.spawn()包装上述函数调用将最小化倒计时之间的差异
    end

考虑以下关于冗余记录ReceiptIds的最佳实践指导:

  • 虽然套餐功能包确实记录ReceiptIds以避免处理相同的收据两次,但您还应该在您的表中记录ReceiptIds,以便如果购买流程在其购买处理程序已经完成后失败,您知道在后续重试时不要再次奖励物品。

  • 如果购买在任何步骤失败,套餐功能包将不会记录ReceiptId,因此您应该确保在处理收据之前在您的表中记录ReceiptId,作为您的purchaseHandler的一部分。

  • 这种冗余有助于确保所有购买逻辑已得到适当处理,并且您的数据存储和套餐功能包的数据存储达到最终一致性,您的数据存储是事实的来源。

配置常量

核心功能包的常量位于两个位置:

  • 共享常量位于ReplicatedStorage.FeaturePackagesCore.Configs.SharedConstants中。

  • 特定于包的常量,在这种情况下是套餐功能包,位于ReplicatedStorage.Bundles.Configs.Constants中。

您可能想要调整的主要内容以满足游戏的设计要求:

  • 声音资产ID
  • 购买效果持续时间和粒子颜色
  • 头部显示的可折叠性

此外,您可以在一个位置找到用于翻译的字符串:ReplicatedStorage.FeaturePackagesCore.Configs.TranslationStrings

自定义UI组件

通过修改包对象,例如颜色、字体和透明度,您可以调整套餐提示的视觉呈现。然而,请记住,如果您在层次结构中移动任何对象,代码将无法找到它们,您需要对代码进行调整。

提示由两个高级组件组成:

  • PromptItem – 为套餐中的每个物品重复的单个组件(物品图像、标题、名称、价格)。
  • Prompt – 提示窗口本身。

头部显示也由两个组件组成:

  • HudItem – 表示头部显示中每个菜单选项的单个组件。
  • Hud – 用HudItems程序填充。

如果您想对头部显示有更大的控制权,而不仅仅是使用ReplicatedStorage.Bundles.Objects.BundlesGui中的现有HUD UI,您可以移动内容以满足自己的设计要求。只需确保在ReplicatedStorage.Bundles.Client.UIController脚本中更新客户端脚本行为。

API参考

类型

RelativeTime

一旦将RelativeTime套餐提供给玩家,它将保持可用,直到时间持续时间耗尽。此类型在玩家的头部显示上显示,并在未来的会话中自动提示,直到套餐过期或玩家购买它。

此套餐类型的一个常见示例是向所有新玩家显示的单次使用入门包优惠,持续24小时。

名称类型描述
includeOfflineTimebool(可选) 如果未设置,则仅在游戏中花费的时间将计入剩余优惠持续时间。
singleUsebool(可选) 如果未设置,则购买可以在购买或过期后重新激活。

如果设置,则一旦第一次购买或过期,将永远无法再次提示,即使您调用Bundles.promptIfValidAsync与bundleId。

FixedTime

一旦将FixedTime套餐提供给玩家,它将保持可用,直到设定的协调世界时(UTC)结束。此类型在玩家的头部显示上显示,并在未来的会话中自动提示,直到套餐过期或玩家购买它。

此套餐类型的一个常见示例是仅在给定月份可用的假日优惠。

OneTime

OneTime套餐仅在提供给玩家的时刻可用。它不会在玩家的头部显示上显示,一旦玩家关闭提示,直到服务器再次提示时无法重新打开。

此套餐类型的一个常见示例是当玩家用完时购买更多游戏内货币的优惠。

©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。