번들 기능 패키지는 플레이어에게 아이템 컬렉션을 할인된 가격으로 판매할 수 있는 즉시 사용 가능한 기능을 제공합니다. 플레이어가 번들을 사용자 정의 경험 내 통화 또는 Robux로 구매할 수 있도록 허용할 지 여부, 사용하려는 번들 유형, 판매하려는 아이템 세트, 게임 플레이 중 플레이어에게 어떤 방식으로 프롬프트를 제공할 지 등을 선택할 수 있습니다.
패키지를 사용자 정의하는 옵션을 사용하여 번들을 경험의 디자인 및 수익 목표에 맞게 조정할 수 있습니다. 예를 들어:
- 신규 플레이어에게 가치를 제공하고 초기 소비를 장려하는 할인이 포함된 스타터 팩을 제공하여 낮은 전환율 지표를 목표로 설정합니다.
- 다양한 가격대의 아이템 번들을 제공하여 소비 깊이를 증가시킵니다.
- 제한된 시간 동안의 독점 아이템 번들을 제공하여 라이브 운영(LiveOps) 이벤트를 수익화합니다.

패키지 받기
제작자 상점은 프로젝트 내에서 사용할 수 있도록 Roblox 및 Roblox 커뮤니티가 만든 모든 자산을 찾는 데 사용할 수 있는 툴박스의 탭입니다. 모델, 이미지, 메시, 오디오, 플러그인, 비디오 및 글꼴 자산을 포함합니다. 제작자 상점을 사용하여 하나 이상의 자산을 직접 열려 있는 경험에 추가할 수 있습니다. 기능 패키지도 포함됩니다!
모든 기능 패키지가 제대로 작동하려면 코어 기능 패키지가 필요합니다. 코어 및 번들 기능 패키지 자산이 귀하의 인벤토리에 추가되면, 플랫폼의 모든 프로젝트에서 재사용할 수 있습니다.
인벤토리에서 패키지를 경험으로 가져오는 방법은 다음과 같습니다:
다음 구성 요소 집합에서 인벤토리에 추가 링크를 클릭하여 Studio 내에서 코어 및 번들 기능 패키지를 인벤토리로 추가합니다.
Studio의 창 메뉴 또는 홈 탭 툴바에서 툴박스를 엽니다.
툴박스 창에서 인벤토리 탭을 클릭합니다. 내 모델 정렬이 표시됩니다.

기능 패키지 코어 타일을 클릭한 다음 번들 기능 패키지 타일을 클릭합니다. 두 패키지 폴더가 탐색기 창에 표시됩니다.
패키지 폴더를 ReplicatedStorage로 끌어다 놓습니다.
패키지를 사용하여 플레이어 구매를 추적하도록 데이터 저장소 호출을 허용합니다.
- Studio의 파일 ⟩ 경험 설정 창을 엽니다.
- 보안 탭으로 이동한 후 API 서비스에 대한 스튜디오 액세스 허용을 활성화합니다.
통화 정의
경험에 자체 통화 시스템이 있는 경우, ReplicatedStorage.FeaturePackagesCore.Configs.Currencies에 정의하여 코어 기능 패키지에 등록할 수 있습니다. 이 파일에는 주석 처리된 Gems 통화 예제가 이미 포함되어 있으며, 자신의 통화로 대체하면 됩니다.
통화Gems = {displayName = "Gems",symbol = "💎",icon = nil,},
Currencies 스크립트는 코어 기능 패키지에 통화에 대한 메타데이터를 제공합니다:
- (필수) displayName - 통화의 이름입니다. 기호나 아이콘을 지정하지 않으면 이 이름이 구매 버튼에 사용됩니다 (예: "100 Gems").
- (선택 사항) symbol - 통화의 아이콘으로 사용할 텍스트 문자가 있다면, 이는 구매 버튼에서 displayName 대신 사용됩니다 (예: "💎100").
- (선택 사항) icon - 통화에 대한 AssetId 이미지 아이콘이 있다면, 이는 구매 버튼에서 displayName 대신 사용됩니다 (예: 가격 "🖼️100"의 왼쪽에 이미지를 배치).
통화 설정이 완료되면, 번들의 가격, 통화 및 아이콘을 수동으로 지정해야 하며, 번들의 관련 개발 제품에서 해당 정보가 가져와지지 않도록 해야 합니다.
번들-- dev 제품을 사용하려면, 하나의 번들에서만 사용될 고유한 devProductId를 제공해야 합니다.-- 우리는 개발 제품에서 번들 가격과 아이콘을 가져올 것입니다pricing = {priceType = CurrencyTypes.PriceType.Marketplace,devProductId = 1795621566,},-- 그렇지 않다면, dev 제품 대신 경험 내 통화를 사용하려면 다음을 사용하십시오:-- 여기의 가격은 경험 내 통화로, 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/name을 명시적으로 설정할 필요가 없으며, 제품 정보를 통해 가져옵니다:
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,-- dev 제품을 사용하려면, 하나의 번들에만 사용될 고유한 devProductId를 제공해야 합니다.-- 우리는 개발 제품에서 번들 가격과 아이콘을 가져올 것입니다pricing = {priceType = CurrencyTypes.PriceType.Marketplace,devProductId = <DEV_PRODUCT_ID>,},-- 그렇지 않으면, dev 제품 대신 경험 내 통화를 사용하려면 다음을 사용하십시오:-- 여기의 가격은 경험 내 통화로, 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.onPlayerAdded(player)-- 모든 신규 사용자에게 제공할 스타터 번들이 있었다면, 여기에서 프롬프트를 제공할 수 있습니다-- ... 번들은 플레이어가 이미 구매했는지 또는 만료되었는지 여부를 처리합니다.-- 번들Id를 사용하여 `Bundles.promptIfValidAsync(player, "StarterBundle")`를 호출합니다.-- 여기에서도 예제로 부르기 때문에 언제든지 호출할 수 있습니다.onPromptBundleXYZEvent(player)end번들을 프롬프트합니다. 이는 게임 플레이에 의존하지만, 예제는 onPlayerAdded에서 스타터 번들을 프롬프트합니다.
번들 기능 패키지 로직은 각 플레이어가 이미 번들을 구매한 경우 또는 제안이 만료되었다면 반복 제안을 하지 않도록 보장합니다.
플레이어에게 번들을 프롬프트하려면 Bundles.promptIfValidAsync(player, bundleId)를 호출하십시오.
READMElocal function onPromptBundleXYZEvent(player: Player)-- 플레이어에게 번들을 제안하도록 사용할 경험 이벤트를 연결합니다-- ... 이곳은 플레이어에게 번들을 제안하기 위한 자격 기준을 충족할 때입니다-- ... 예를 들어, 플레이어가 합류할 때 또는 플레이어가 레벨업할 때 번들을 제안할 수 있습니다.task.spawn(Bundles.promptIfValidAsync, player, <Some_Bundle_Id>)-- ... 여러 번들을 생성할 경우, 위의 함수 호출을 task.spawn()으로 래핑하여 카운트다운 간의 불일치를 최소화합니다.end
영수증 ID의 중복 기록에 대한 모범 사례 지침은 다음과 같습니다:
번들 기능 패키지는 동일한 영수증을 두 번 처리하지 않기 위해 ReceiptIds를 기록하지만, 구매 핸들러가 끝난 후 구매 흐름이 실패한 경우, 다시 시도할 때 아이템을 다시 수여하지 않도록 하려면 ReceiptIds를 테이블에 기록해야 합니다.
번들 기능 패키지는 구매가 어떤 단계에서든 실패하면 ReceiptId를 기록하지 않기 때문에, 구매Handler의 일환으로 영수증을 처리하기 전에 ReceiptId를 테이블에 기록하는 것이 중요합니다.
이 중복성은 모든 구매 로직이 적절히 처리되었는지 확인하고 데이터 저장소와 번들 기능 패키지의 데이터 저장소가 최종 일관성을 이루도록 도와줍니다. 데이터 저장소가 진실의 출처가 됩니다.
상수 구성
코어 기능 패키지의 상수는 두 곳에 있습니다:
공유 상수는 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시간 동안 표시되는 단일 사용 스타터 팩 제안입니다.
| 이름 | 유형 | 설명 |
|---|---|---|
| includeOfflineTime | bool | (선택 사항) 설정되지 않으면, 경험에 머무는 시간만이 남은 제안 지속 시간에 포함됩니다. |
| singleUse | bool | (선택 사항) 설정되지 않으면, 구매 후 다시 구매할 수 있습니다. 설정된 경우, 처음 구매되거나 만료되면, 번들을 더 이상 프롬프트할 수 없으며 Bundles.promptIfValidAsync를 번들Id로 호출해도 마찬가지입니다. |
FixedTime
FixedTime 번들이 플레이어에게 제공되면, 설정된 협정 세계시(UTC) 종료 시점까지 사용 가능합니다. 이 유형은 플레이어의 헤드업 디스플레이에 표시되며, 번들이 만료되거나 플레이어가 구매할 때까지 향후 세션에서 자동으로 프롬프트됩니다.
이 번들 유형의 일반적인 예는 특정 한 달 동안만 제공되는 휴일 제안입니다.
OneTime
OneTime 번들은 플레이어에게 제공될 때만 사용 가능하며, 플레이어의 헤드업 디스플레이에 표시되지 않습니다. 플레이어가 프롬프트를 닫으면 서버에서 다시 프롬프트하기 전까지는 재개방할 수 없습니다.
이 번들 유형의 일반적인 예는 플레이어가 소모될 때 즉시 경험 내 통화를 구매할 수 있는 제안입니다.