包装 功能包提供了出盒功能,可以向玩家出售物品集合以折扣价格。您可以选择是否允许玩家使用自定义体验货币或 Robux 购买包装,使用哪种包装类型,出售哪些物品,以及如何在游戏期间提示玩家。
使用包裹的自定义选项,您可以将包裹调整为满足体验的设计和货币化目标,例如:
- 通过提供折扣 起动包 来瞄准低转化率,这些包提供价值给新玩家并鼓励早期支出。
- 通过将物品在各个价格点绑定到一起来增加支出深度,以吸引一系列玩家。
- 通过提供限时包装的独家物品来货币化实时操作(LiveOps)事件事件。

获取包装
创建者商店是您可以使用的工具箱选项卡,可用于查找 Roblox 和 Roblox 社区制作的所有资产,包括模型、图像、网格、音频、插件、视频和字体资产。您可以使用创作者商店直接将一个或多个资产添加到开放体验中,包括功能包!
每个功能包都需要 核心 功能包正常运行。一旦 核心 和 包 功能包资产位于您的道具,您可以在平台上的任何项目中重复使用它们。
要将库存的包从你的体验中获取到你的体验:
在工具栏中,选择视图标签。
点击 工具箱 。显示 工具箱 窗口。
在 工具箱 窗口中,单击 库存 选项卡。显示 我的模型 排序。
单击 功能包核心 瓦片,然后单击 包装功能包 瓦片。两个包文件夹都显示在 资源管理器 窗口。
将包裹文件夹拖入 复制存储 。
允许数据存储调用跟踪玩家购买包装产品的购买。
- 在工具栏的 主页 选项卡中,选择 游戏设置 。
- 导航到 安全 选项卡,然后启用 启用工作室访问API服务 。
定义货币
如果您的体验拥有自己的货币系统,您可以通过定义它们在 核心 功能包中注册它们来注册它们。文件中已包含评论的宝石货币示例;请替换为自己的货币示例。
货币
Gems = {displayName = "Gems",symbol = "💎",icon = nil,},
Currencies 脚本告诉 核心 功能包有关您的货币的一些元数据:
- (需要) displayName - 您的货币名称。如果您未指定符号或标志,此名称将用于购买按钮(即“100颗宝石”)。
- (可选) symbol - 如果您有一个文本角色用作货币的图标,这将被用作购买按钮中的 displayName (即“💎100”)的替代。
- (可选) icon - 如果你有一个AssetId图标图像为你的货币,这将被用作购买按钮中的displayName图标(即图像将放置在价格的左侧“🖼️100”)
一旦您的货币已设置,您需要手动指定套装的价格、货币和图标,用于头部显示,而不是从套装关联的开发产品中获取该信息。
包装物
-- 如果您想使用开发产品,您必须提供独特的 devProductId,仅由一个套装使用。-- 我们将从开发者产品中获取包价和图标pricing = {priceType = CurrencyTypes.PriceType.Marketplace,devProductId = 1795621566,},-- 否则,如果你想使用体验货币而不是开发产品,你可以使用以下内容进行替换:-- 价格在这里是在体验中的货币,不是 Robuxpricing = {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
具体地,你需要填写 ,这是通过循环在例子 内调用的 (即每个货币ID通过Bundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)连接到处理器。
定义包
所有在您的体验中可用的包裹都可以在 ReplicatedStorage.Bundles.Configs.Bundles 内定义,其类型从 Types 脚本中导出到同一文件夹。
如果你使用了 devProductId ,你需要更新套装的主 devProductId 以匹配你体验中的一个。这是需要通过 MarketplaceService 提示购买包的内容。 强烈建议使用新开发者产品为包裹进行跟踪,以便更容易跟踪分开的销售。 如果你想要一个包含多个项目的包,如果这些已经由开发者产品在你的体验中代表,你不需要明确设置项目价格/资产ID/名称,它将通过产品信息获取:
阅读说明
{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,仅由一个套装使用。-- 我们将从开发者产品中获取包价和图标pricing = {priceType = CurrencyTypes.PriceType.Marketplace,devProductId = <DEV_PRODUCT_ID>,},-- 否则,如果你想使用体验货币而不是开发产品,你可以使用以下内容进行替换:-- 价格在这里是在体验中的货币,不是 Robux-- 价格 = {-- 价格类型 = 货币类型.价格类型.在体验中,-- 价格 = 79,-- 货币ID=<CURRENCY_ID>,-- 图标 = <IMAGE_ASSET_ID>,-- },includedItems = {[1] = {-- 物品本身不通过开发者产品出售,因此表示它在 Robux 中的价值以及提供一个标志-- 价格InRobux帮助包裹显示包裹价格与其内容总和的相对价值itemType = ItemTypes.ItemType.Robux,priceInRobux = 49,icon = <IMAGE_ASSET_ID>,-- 或者,如果这有一个开发产品离开价格和上面的图标,只需设置 devProductId-- 价格和图标将从开发者产品中获取-- devProductId = <ITEM_DEV_PRODUCT_ID>-- 如果需要,还有更多可选的元数据字段是用户界面特定的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),也不再有效。你可以在工作室进行测试时将其设置为虚假。durationInSeconds = 900, -- 15 分钟includesOfflineTime = false, -- 仅计算体验中滞后时间metadata = {displayName = "STARTER BUNDLE",description = "Save 75% and get a head start!",},}
整合服务器逻辑
看看 ReplicatedStorage.Bundles.Server.Examples.BundlesExample , 它显示了您的服务器如何与 包装 功能包和上面的方法互动在 ModuleScript 上。以下片段来自该脚本。
你主要需要将四个东西连接起来一次拖动 包装 功能包到你的体验中:
通过 Bundles.setPurchaseHandler 连接购买处理程序来指定在购买处理时调用奖励物品的函数。
包裹示例local function awardMarketplacePurchase(_player: Player, _bundleId: Types.BundleId, _receiptInfo: { [string]: any })-- 更新玩家数据、提供物品等-- ... 并记录收据信息.购买Id 以便我们检查用户是否已拥有此套装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)endend连接你的逻辑为 MarketplaceService.ProcessReceipt ,但如果你的体验已经有待售开发产品,这可能会在其他地方完成。实际上,当开发者产品收据正在处理时,他们现在会调用 Bundles.getBundleByDevProduct 来检查产品是否属于套装。如果它这样做,那么脚本将调用 Bundles.processReceipt .
包裹示例-- 从市场接收过程收据以确定玩家是否需要被收费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("Failed to process receipt:", 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 falseend连接 Players.PlayerAdded:Connect(Bundles.OnPlayerAdded) 以便 包装组 功能包重新提示任何尚未过期的活动包给玩家。
阅读说明local function onPlayerAdded(player: Player)-- 告诉包裹当玩家加入时,可以重新加载他们的数据Bundles.onPlayerAdded(player)-- 如果你有一些新手包想向所有新用户提供,你可以在这里提示它-- ... 包将处理如果玩家已经购买了它或者已过期,因为它不可重复-- 包装.提示如果有效异玩家(player, "StarterBundle")-- 在这里调用这个仅仅是个例子,你可以随时随地调用这个onPromptBundleXYZEvent(player)end提示包。虽然这取决于游戏玩法,但示例提示玩家使用新手包 onPlayerAdded 。
包装 功能包逻辑确保每个玩家不会收到重复优惠,如果他们已经购买了套装,或者已经让优惠过期(基于包装配置)。
每当你想向玩家提示包时,请调用 Bundles.promptIfValidAsync(player, bundleId) 。
阅读说明local function onPromptBundleXYZEvent(player: Player)-- 连接任何你想使用的体验事件来确定玩家何时收到包时提示-- ... 这将在你满足资格条件时触发,以提示玩家包套装-- ... 例如,如果你想在玩家加入时提示包裹,或当玩家升级时提示包裹task.spawn(Bundles.promptIfValidAsync, player, <Some_Bundle_Id>)-- ... 如果创建多个包,使用 task.spaw重生点() 来包装上述函数调用,将最大限度地减少倒计时之间的差异end
考虑以下关于收据ID重复录制的最佳实践指南:
虽然 包装 功能包记录了收据ID以避免两次处理相同的收据,但你也应该在你的表中记录收据ID,以便如果购买流程在购买处理器已经完成后失败,你就知道在后续重试时不要再授予物品。
包装 功能包不会记录收据ID,如果在任何步骤购买失败,那么你应该确保在处理收据作为你的购买处理器的一部分之前记录收据ID。
这种重复可以确保所有购买逻辑都得到了适当处理,使您的数据存储和 包装 功能包的数据存储最终达到一致,您的数据存储是真实来源。
配置常量
核心 功能包的常量在两个地方生活:
共享常量生活在 ReplicatedStorage.FeaturePackagesCore.Configs.SharedConstants 。
包特定常量,在这里是 包装 功能包,生活在 ReplicatedStorage.Bundles.Configs.Constants 。
您可能需要调整的主要事情,以满足体验的设计要求:
- 音效资产ID
- 购买效果时间和粒子颜色
- 头部显示可折叠性
此外,您可以找到分布在一个位置的翻译字符串:ReplicatedStorage.FeaturePackagesCore.Configs.TranslationStrings。
自定义用户界面组件
通过修改包裹对象,例如颜色、字体和透明度,您可以调整包裹提示的视觉呈现。但是,请记住,如果你移动任何对象在层次上,代码将无法找到它们,你需要对你的验证码进行调整。
提示由两个高级组成部分组成:
- PromptItem – 包装内的每个物品重复的个体组件(物品图像、说明、名称、价格)。
- Prompt – 提示窗口本身。
头部显示也由两个组成部分:
- HudItem – 每个菜单选项在头部显示中的代表的个人组件。
- Hud – 用程序填充 HudItems 。
如果您想对头部显示拥有更大的控制权,而不仅仅使用现有的 HUD 用户界面内的 ReplicatedStorage.Bundles.Objects.BundlesGui,您可以移动事物以满足自己的设计要求。只要确保在 ReplicatedStorage.Bundles.Client.UIController 脚本中更新客户端脚本行为。
API 参考
类型
相对时间
一旦 RelativeTime 包裹被提供给玩家,时间限制到期之前就仍然可用。该类型显示在玩家的头部显示上,并在包裹过期或玩家购买它之前自动提示未来的会话。
该包类型的常见示例是向所有新玩家显示 24 小时的一次性启动包优惠。有关如何实现新手包束的行业最佳实践,请参阅新手包设计。
名称 | 类型 | 描述 |
---|---|---|
includeOfflineTime | bool | (可选) 如果未设置,只有在体验中花费的时间才会计入剩余的报价期。 |
singleUse | bool | (可选) 如果未设置,购买可以在购买或过期后重新启用。如果已设置,一次购买或过期后,即使您调用 Bundles.promptIfValidAsync 与包含 bundleId 的包,也不会再次提示。 |
固定时间
一旦 FixedTime 包裹被提供给玩家,它将保持到设置协调的普通时间(UTC)结束为止。该类型显示在玩家的头部显示上,并在包裹过期或玩家购买它之前自动提示未来的会话。
这类包裹类型的常见例子是仅在特定月份可用的节日优惠。
一次性
一个 OneTime 包只在向玩家提供时才可用。它不会显示在玩家的头部显示上,一旦玩家关闭提示,再次提示服务器时才能重新打开。
这类包裹类型的常见例子是玩家在经验结束时购买更多经验货币的提议。