Gói tính năng Bundles cung cấp chức năng sẵn có để bán các bộ sưu tập vật phẩm cho người chơi với mức giá giảm. Bạn có thể chọn cho phép người chơi mua bundles bằng cách sử dụng một loại tiền tệ trong trò chơi tùy chỉnh hoặc Robux, loại bundle mà bạn muốn sử dụng, bộ vật phẩm mà bạn muốn bán, và cách bạn muốn nhắc nhở người chơi trong quá trình chơi của họ.
Sử dụng các tùy chọn tùy chỉnh của gói, bạn có thể điều chỉnh các bundles của mình để đáp ứng các mục tiêu thiết kế và kiếm tiền của trò chơi của bạn, chẳng hạn như:
- Nhắm đến một tỷ lệ chuyển đổi thấp bằng cách cung cấp các gói khởi đầu giảm giá mang lại giá trị cho người chơi mới và khuyến khích chi tiêu sớm.
- Tăng cường độ sâu chi tiêu bằng cách gộp các vật phẩm ở nhiều mức giá khác nhau để thu hút một loạt người chơi.
- Kiếm tiền từ các hoạt động trực tiếp (LiveOps) sự kiện bằng cách cung cấp các gói vật phẩm độc quyền có thời gian giới hạn.

Nhận gói
Creator Store là một tab của Toolbox mà bạn có thể sử dụng để tìm tất cả các tài sản được tạo ra bởi Roblox và cộng đồng Roblox để sử dụng trong các dự án của bạn, bao gồm mô hình, hình ảnh, mesh, âm thanh, plugin, video và tài sản phông chữ. Bạn có thể sử dụng Creator Store để thêm một hoặc nhiều tài sản trực tiếp vào một trò chơi đang mở, bao gồm cả các gói tính năng!
Mỗi gói tính năng đều yêu cầu gói tính năng Core để hoạt động đúng cách. Khi các tài sản của gói Core và Bundles có trong kho của bạn, bạn có thể tái sử dụng chúng trong bất kỳ dự án nào trên nền tảng.
Để đưa các gói từ kho của bạn vào trò chơi:
Thêm gói Core và Bundles vào kho của bạn trong Studio bằng cách nhấp vào liên kết Add to Inventory trong bộ thành phần sau.
Từ menu Window hoặc thanh công cụ tab Home của Studio, mở Toolbox.
Trong cửa sổ Toolbox, nhấp vào tab Inventory. Phân loại My Models sẽ hiển thị.

Nhấp vào ô Feature Package Core, sau đó nhấp vào ô Bundle Feature Package. Cả hai thư mục gói sẽ hiển thị trong cửa sổ Explorer.
Kéo các thư mục gói vào ReplicatedStorage.
Cho phép các cuộc gọi lưu trữ dữ liệu theo dõi các giao dịch mua của người chơi với các gói.
- Mở cửa sổ File ⟩ Experience Settings của Studio.
- Điều hướng đến tab Security, sau đó bật Enable Studio Access to API Services.
Định nghĩa tiền tệ
Nếu trò chơi của bạn có hệ thống tiền tệ riêng, bạn có thể đăng ký chúng với gói tính năng Core bằng cách định nghĩa chúng trong ReplicatedStorage.FeaturePackagesCore.Configs.Currencies. Có một ví dụ đã được chú thích về một loại tiền tệ Gems trong tệp này; hãy thay thế nó bằng của riêng bạn.
Gems = {
displayName = "Gems",
symbol = "💎",
icon = nil,
},Kịch bản Currencies cho gói tính năng Core biết một số siêu dữ liệu về tiền tệ của bạn:
- (bắt buộc) displayName - Tên của tiền tệ của bạn. Nếu bạn không chỉ định một biểu tượng hoặc hình ảnh, tên này sẽ được sử dụng trong các nút mua (tức là "100 Gems").
- (tùy chọn) symbol - Nếu bạn có một ký tự văn bản để sử dụng làm biểu tượng cho tiền tệ của bạn, điều này sẽ được sử dụng thay vì displayName trong các nút mua (tức là "💎100").
- (tùy chọn) icon - Nếu bạn có một hình ảnh biểu tượng AssetId cho tiền tệ của bạn, điều này sẽ được sử dụng thay vì displayName trong các nút mua (tức là hình ảnh sẽ được đặt bên trái giá "🖼️100")
Khi tiền tệ của bạn đã được thiết lập, bạn cần chỉ định thủ công giá của gói, tiền tệ và biểu tượng cho hiển thị thông tin thay vì thông tin đó được lấy từ sản phẩm phát triển liên kết với gói.
-- Nếu bạn muốn sử dụng một sản phẩm phát triển, bạn phải cung cấp một devProductId duy nhất, chỉ được sử dụng bởi một gói.
-- Chúng tôi sẽ lấy giá gói và biểu tượng từ sản phẩm phát triển
pricing = {
priceType = CurrencyTypes.PriceType.Marketplace,
devProductId = 1795621566,
},
-- Nếu không, nếu bạn muốn sử dụng tiền tệ trong trò chơi thay vì một sản phẩm phát triển, bạn có thể sử dụng cái sau:
-- Giá ở đây là bằng tiền tệ trong trò chơi, không phải Robux
pricing = {
priceType = CurrencyTypes.PriceType.InExperience,
price = 79,
currencyId = "Gems",
icon = 18712203759,
},Bạn cũng cần tham chiếu đến kịch bản BundlesExample để gọi setInExperiencePurchaseHandler.
local function awardInExperiencePurchase(
_player: Player,
_bundleId: Types.BundleId,
_currencyId: CurrencyTypes.CurrencyId,
_price: number
)
-- Kiểm tra xem người chơi có đủ tiền tệ để mua gói không
-- Cập nhật dữ liệu người chơi, tặng vật phẩm, v.v.
-- Trừ tiền tệ từ người chơi
task.wait(2)
return true
end
local function initializePurchaseHandlers()
local bundles = Bundles.getBundles()
for bundleId, bundle in bundles do
-- Gói không liên kết với một sản phẩm phát triển nếu nó không có loại giá thị trường
if not bundle or bundle.pricing.priceType ~= "Marketplace" then
continue
end
Bundles.setPurchaseHandler(bundleId, awardMarketplacePurchase)
receiptHandlers[bundle.pricing.devProductId] = receiptHandler
end
-- Nếu bạn có bất kỳ tiền tệ trong trò chơi nào mà bạn đang sử dụng cho các gói, hãy đặt trình xử lý ở đây
for currencyId, _ in Currencies do
Bundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)
end
endCụ thể, bạn cần điền vào awardInExperiencePurchase, được gọi bởi một vòng lặp qua Currencies bên trong ví dụ initializePurchaseHandlers (tức là mỗi currencyId được kết nối với trình xử lý thông qua Bundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)).
Định nghĩa các gói
Tất cả các gói có thể cung cấp trong trò chơi của bạn có thể được định nghĩa trong ReplicatedStorage.Bundles.Configs.Bundles, với các loại được xuất từ kịch bản Types trong cùng thư mục.
Nếu bạn đang sử dụng một devProductId, bạn cần cập nhật devProductId chính của gói để khớp với cái trong trò chơi của bạn. Đây là cái sẽ được nhắc qua MarketplaceService để mua gói đó. Rất khuyến nghị sử dụng một sản phẩm phát triển mới cho gói để dễ dàng theo dõi doanh số riêng biệt.
Nếu bạn muốn một gói với nhiều vật phẩm, và nếu những vật phẩm này đã được đại diện bởi các sản phẩm phát triển trong trò chơi của bạn, bạn không cần phải thiết lập giá/assetId/tên vật phẩm một cách rõ ràng, mà sẽ được lấy qua thông tin sản phẩm:
{
itemType = ItemTypes.ItemType.DevProduct,
devProductId = <DEV_PRODUCT_ID>,
metadata = {
caption = {
text = "x1",
color = Color3.fromRGB(236, 201, 74),
} -- Chú thích là tùy chọn! Bạn cũng có thể bỏ qua trường này
}
},Nếu không, bạn có thể cấu hình chi tiết vật phẩm đó một cách thủ công:
{
itemType = ItemTypes.ItemType.Robux,
priceInRobux = 49,
icon = <IMAGE_ASSET_ID>,
metadata = {
caption = {
text = "x1",
color = Color3.fromRGB(236, 201, 74),
} -- Chú thích là tùy chọn! Bạn cũng có thể bỏ qua trường này
}
},Ví dụ, toàn bộ gói của bạn có thể trông như thế này:
local starterBundle: Types.RelativeTimeBundle = {
bundleType = Types.BundleType.RelativeTime,
-- Nếu bạn muốn sử dụng một sản phẩm phát triển, bạn phải cung cấp một devProductId duy nhất, chỉ được sử dụng bởi một gói.
-- Chúng tôi sẽ lấy giá gói và biểu tượng từ sản phẩm phát triển
pricing = {
priceType = CurrencyTypes.PriceType.Marketplace,
devProductId = <DEV_PRODUCT_ID>,
},
-- Nếu không, nếu bạn muốn sử dụng tiền tệ trong trò chơi thay vì một sản phẩm phát triển, bạn có thể sử dụng cái sau:
-- Giá ở đây là bằng tiền tệ trong trò chơi, không phải Robux
-- pricing = {
-- priceType = CurrencyTypes.PriceType.InExperience,
-- price = 79,
-- currencyId = <CURRENCY_ID>,
-- icon = <IMAGE_ASSET_ID>,
-- },
includedItems = {
[1] = {
-- Vật phẩm không được bán qua một sản phẩm phát triển, vì vậy hãy chỉ ra giá trị của nó bằng Robux và cung cấp một biểu tượng
-- Giá priceInRobux giúp Bundles hiển thị giá trị tương đối của giá gói so với tổng giá trị của các nội dung của nó
itemType = ItemTypes.ItemType.Robux,
priceInRobux = 49,
icon = <IMAGE_ASSET_ID>,
-- Ngoài ra, nếu điều này có một sản phẩm phát triển, hãy bỏ qua giá và biểu tượng ở trên và chỉ cần đặt devProductId
-- Giá và biểu tượng sẽ được lấy từ sản phẩm phát triển
-- devProductId = <ITEM_DEV_PRODUCT_ID>
-- Có nhiều trường siêu dữ liệu tùy chọn hơn mà UI có thể cần nếu cần
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, -- Khi đã mua hoặc hết hạn, không còn hợp lệ ngay cả khi trò chơi của bạn cố gắng nhắc nhở (onPlayerAdded). Bạn có thể đặt điều này thành false trong khi thử nghiệm trong studio.
durationInSeconds = 900, -- 15 phút
includesOfflineTime = false, -- Chỉ tính thời gian trôi qua trong trò chơi
metadata = {
displayName = "GÓI KHỞI ĐẦU",
description = "Tiết kiệm 75% và bắt đầu ngay!",
},
}Tích hợp logic máy chủ
Hãy xem ReplicatedStorage.Bundles.Server.Examples.BundlesExample, cho thấy cách máy chủ của bạn sẽ tương tác với gói tính năng Bundles và các phương thức trên ModuleScript. Các đoạn mã dưới đây là từ kịch bản đó.
Bạn chủ yếu cần kết nối bốn thứ khi kéo gói tính năng Bundles vào trò chơi của bạn:
Kết nối các trình xử lý mua sắm thông qua Bundles.setPurchaseHandler để chỉ định các hàm sẽ được gọi để trao tặng vật phẩm khi một giao dịch mua đang được xử lý.
BundlesExamplelocal function awardMarketplacePurchase(_player: Player, _bundleId: Types.BundleId, _receiptInfo: { [string]: any })-- Cập nhật dữ liệu người chơi, tặng vật phẩm, v.v.-- ... VÀ ghi lại receiptInfo.PurchaseId để chúng tôi có thể kiểm tra xem người dùng đã có gói này chưatask.wait(2)return Enum.ProductPurchaseDecision.PurchaseGrantedendlocal function awardInExperiencePurchase(_player: Player,_bundleId: Types.BundleId,_currencyId: CurrencyTypes.CurrencyId,_price: number)-- Kiểm tra xem người chơi có đủ tiền tệ để mua gói không-- Cập nhật dữ liệu người chơi, tặng vật phẩm, v.v.-- Trừ tiền tệ từ người chơitask.wait(2)return trueendlocal function initializePurchaseHandlers()local bundles = Bundles.getBundles()for bundleId, bundle in bundles do-- Gói không liên kết với một sản phẩm phát triển nếu nó không có loại giá thị trườngif not bundle or bundle.pricing.priceType ~= "Marketplace" thencontinueendBundles.setPurchaseHandler(bundleId, awardMarketplacePurchase)receiptHandlers[bundle.pricing.devProductId] = receiptHandlerend-- Nếu bạn có bất kỳ tiền tệ trong trò chơi nào mà bạn đang sử dụng cho các gói, hãy đặt trình xử lý ở đâyfor currencyId, _ in Currencies doBundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)endendKết nối logic của bạn cho MarketplaceService.ProcessReceipt, nhưng điều này có thể được thực hiện ở nơi khác nếu trò chơi của bạn đã có các sản phẩm phát triển để bán. Về cơ bản, khi một biên nhận sản phẩm phát triển đang được xử lý, họ sẽ gọi Bundles.getBundleByDevProduct để kiểm tra xem sản phẩm có thuộc về một gói không. Nếu có, kịch bản sẽ gọi Bundles.processReceipt.
BundlesExample-- Xử lý biên nhận từ thị trường để xác định xem người chơi có cần bị tính phí hay khônglocal 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] -- Lấy trình xử lý cho sản phẩmlocal success, result = pcall(handler, receiptInfo, player) -- Gọi trình xử lý để kiểm tra xem logic mua sắm có thành công khôngif not success or not result thenwarn("Không thể xử lý biên nhận:", 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-- Giao dịch mua này thuộc về một gói, để Bundles xử lýlocal purchaseDecision = Bundles.processReceiptAsync(player, bundleId, receiptInfo)return purchaseDecision == Enum.ProductPurchaseDecision.PurchaseGrantedend-- Giao dịch mua này không thuộc về một gói,-- ... Xử lý tất cả logic hiện có của bạn ở đây nếu bạn córeturn falseendKết nối Players.PlayerAdded:Connect(Bundles.OnPlayerAdded) để gói tính năng Bundles nhắc lại bất kỳ gói nào đang hoạt động mà chưa hết hạn cho một người chơi.
READMElocal function onPlayerAdded(player: Player)-- Thông báo cho Bundles khi người chơi tham gia để nó có thể tải lại dữ liệu của họBundles.onPlayerAdded(player)-- Nếu bạn có một gói khởi đầu nào đó mà bạn muốn cung cấp cho tất cả người dùng mới, bạn có thể nhắc nhở điều đó ở đây-- ... Bundles sẽ xử lý nếu người chơi đã mua nó hoặc nếu nó đã hết hạn vì nó không thể lặp lại-- Bundles.promptIfValidAsync(player, "StarterBundle")-- Gọi điều này ở đây chỉ để ví dụ, bạn có thể gọi điều này bất cứ khi nào hoặc ở đâu bạn muốnonPromptBundleXYZEvent(player)endNhắc nhở các gói. Trong khi điều này phụ thuộc vào lối chơi, ví dụ nhắc nhở người chơi với một StarterBundle onPlayerAdded.
Logic của gói tính năng Bundles đảm bảo mỗi người chơi không nhận được một đề nghị lặp lại nếu họ đã mua gói, hoặc nếu họ để cho đề nghị đã hết hạn (dựa trên cấu hình gói).
Bất cứ khi nào bạn muốn nhắc nhở một gói cho một người chơi, hãy gọi Bundles.promptIfValidAsync(player, bundleId).
READMElocal function onPromptBundleXYZEvent(player: Player)-- Kết nối bất kỳ sự kiện trò chơi nào mà bạn muốn sử dụng để xác định khi nào một người chơi được nhắc nhở gói-- ... Điều này sẽ là bất cứ khi nào bạn đã đáp ứng tiêu chí đủ điều kiện của mình để nhắc nhở một người chơi gói-- ... Ví dụ, nếu bạn muốn nhắc nhở một gói khi một người chơi tham gia, hoặc khi một người chơi lên cấptask.spawn(Bundles.promptIfValidAsync, player, <Some_Bundle_Id>)-- ... Nếu tạo nhiều gói, việc sử dụng task.spawn() để bọc cuộc gọi hàm ở trên sẽ giảm thiểu sự khác biệt giữa các đếm ngượcend
Hãy xem xét các hướng dẫn thực hành tốt sau đây về việc ghi lại ReceiptIds dư thừa:
Trong khi gói tính năng Bundles ghi lại ReceiptIds để tránh xử lý cùng một biên nhận hai lần, bạn cũng nên ghi lại ReceiptIds bên trong các bảng của bạn để nếu quy trình mua sắm thất bại sau khi trình xử lý mua của họ đã hoàn thành, bạn biết trong lần thử lại tiếp theo không nên trao tặng vật phẩm một lần nữa.
Gói tính năng Bundles sẽ không ghi lại ReceiptId nếu giao dịch mua thất bại ở bất kỳ bước nào, vì vậy bạn nên đảm bảo rằng bạn đang ghi lại ReceiptId trong các bảng của bạn trước khi xử lý biên nhận như một phần của purchaseHandler của bạn.
Sự dư thừa này giúp đảm bảo rằng tất cả logic mua sắm đã được xử lý một cách thích hợp và rằng kho dữ liệu của bạn và kho dữ liệu của gói tính năng Bundles đạt được tính nhất quán cuối cùng, với kho dữ liệu của bạn là nguồn thông tin chính xác.
Cấu hình hằng số
Các hằng số cho gói tính năng Core sống trong hai vị trí:
Các hằng số chia sẻ sống trong ReplicatedStorage.FeaturePackagesCore.Configs.SharedConstants.
Các hằng số cụ thể cho gói, trong trường hợp này là gói tính năng Bundles, sống trong ReplicatedStorage.Bundles.Configs.Constants.
Những điều chính mà bạn có thể muốn điều chỉnh để đáp ứng các yêu cầu thiết kế của trò chơi của bạn:
- ID tài sản âm thanh
- Thời gian hiệu ứng mua sắm và màu sắc hạt
- Khả năng thu gọn hiển thị thông tin
Ngoài ra, bạn có thể tìm thấy các chuỗi để dịch được phân tách thành một vị trí: ReplicatedStorage.FeaturePackagesCore.Configs.TranslationStrings.
Tùy chỉnh các thành phần UI
Bằng cách sửa đổi các đối tượng gói, chẳng hạn như màu sắc, phông chữ và độ trong suốt, bạn có thể điều chỉnh cách trình bày hình ảnh của các nhắc nhở gói của bạn. Tuy nhiên, hãy nhớ rằng nếu bạn di chuyển bất kỳ đối tượng nào xung quanh theo cấu trúc phân cấp, mã sẽ không thể tìm thấy chúng, và bạn sẽ cần phải điều chỉnh mã của mình.
Một nhắc nhở được tạo thành từ hai thành phần cấp cao:
- PromptItem – Thành phần cá nhân được lặp lại cho mỗi vật phẩm trong một gói (hình ảnh vật phẩm, chú thích, tên, giá).
- Prompt – Cửa sổ nhắc nhở chính nó.
Hiển thị thông tin cũng được tạo thành từ hai thành phần:
- HudItem – Một thành phần cá nhân đại diện cho mỗi tùy chọn menu trong hiển thị thông tin.
- Hud – Để được điền một cách lập trình với HudItems.
Nếu bạn muốn có quyền kiểm soát lớn hơn đối với hiển thị thông tin, thay vì chỉ sử dụng UI HUD hiện có trong ReplicatedStorage.Bundles.Objects.BundlesGui, bạn có thể di chuyển mọi thứ xung quanh để đáp ứng các yêu cầu thiết kế của riêng bạn. Chỉ cần đảm bảo cập nhật hành vi kịch bản của khách hàng trong kịch bản ReplicatedStorage.Bundles.Client.UIController.
Tham chiếu API
Các loại
RelativeTime
Khi gói RelativeTime được cung cấp cho một người chơi, nó sẽ vẫn có sẵn cho đến khi thời gian hết hạn. Loại này hiển thị trên hiển thị thông tin của người chơi và tự động nhắc nhở trong các phiên tương lai cho đến khi gói hết hạn hoặc người chơi mua nó.
Một ví dụ phổ biến về loại gói này là một đề nghị gói khởi đầu một lần sử dụng hiển thị cho tất cả người chơi mới trong 24 giờ.
| Tên | Loại | Mô tả |
|---|---|---|
| includeOfflineTime | bool | (Tùy chọn) Nếu không được thiết lập, chỉ thời gian dành cho trò chơi sẽ được tính vào thời gian còn lại của đề nghị. |
| singleUse | bool | (Tùy chọn) Nếu không được thiết lập, giao dịch mua có thể được kích hoạt lại sau khi đã mua hoặc hết hạn. Nếu được thiết lập, một khi đã mua hoặc hết hạn lần đầu tiên, nó sẽ không bao giờ có thể được nhắc nhở lại, ngay cả khi bạn gọi Bundles.promptIfValidAsync với bundleId. |
FixedTime
Khi gói FixedTime được cung cấp cho một người chơi, nó sẽ vẫn có sẵn cho đến cuối thời gian phối hợp toàn cầu (UTC) đã đặt. Loại này hiển thị trên hiển thị thông tin của người chơi và tự động nhắc nhở trong các phiên tương lai cho đến khi gói hết hạn hoặc người chơi mua nó.
Một ví dụ phổ biến về loại gói này là một đề nghị lễ hội chỉ có sẵn trong một tháng nhất định.
OneTime
Một gói OneTime chỉ có sẵn vào thời điểm nó được cung cấp cho một người chơi. Nó không hiển thị trên hiển thị thông tin của người chơi, và một khi người chơi đóng nhắc nhở, nó không thể được mở lại cho đến khi nó được nhắc nhở bởi máy chủ một lần nữa.
Một ví dụ phổ biến về loại gói này là một đề nghị mua thêm tiền tệ trong trò chơi ngay khi người chơi hết tiền.