Roblox Studioにゲームパス購入ボタンを置く方法
ゲーム内UIへPass名と現在価格を表示し、未所有者だけ購入画面を開き、購入後は所有済み表示へ切り替えよう。

ゲーム画面の下にVIP Passの名前、現在価格、購入ボタンを表示する。次の画像が、公式APIから現在価格を読み込んだ購入前の完成状態だ。完成までの目安は10分になる。

手順5では、ボタンからRobloxの購入画面が開くことを確認する。Studioだけの安全な完了テストを行うと所有済みへ変わり、そのPlay中はVIPドアを通れる。変化の前後は手順5の画像とGIFで確かめられる。
この記事は、ゲームパスでVIPドアを作る記事の続きだ。ServerScriptService > VipAccessがあり、サーバーがplayer.HasVipPassというBoolValueを作っている状態から始めよう。Passをまだ作成していない場合は、先に前の記事を完了する。
この記事では実際の購入を完了しない。購入ボタンから開く画面の確認までと、Studioでだけ作られるサーバー側のテストイベントを使う。本番の購入履歴やPass所有権は作られず、StopしてPlayし直すとテスト状態も消える。Roblox公式のMock Purchase案内でも、Studio内の購入シミュレーションはセッションをまたいで残らないと説明されている。
重要 — テスト表示を見てから押そう
購入画面にテスト購入であることと、Robuxを消費しないことが表示された場合だけ購入操作を進めよう。その表示がなければ画面を閉じる。この記事では、表示の有無に左右されないStudio限定のテスト手順も用意する。
1. Passを10 Robuxで販売状態にする
Creator Dashboardで対象ゲームを開き、収益化 > パスへ進む。前の記事で作ったPassを選び、販売を開こう。販売中のアイテムをオンにし、価格へ10を入力して変更を保存を押す。

保存できると、Pass一覧の価格に10が表示される。販売設定の保存ではRobuxを使わない。ただし、本番でプレイヤーが購入を確定すると、その画面に表示されたRobuxを使う。この記事の完成確認では購入を確定しない。
Tip — 設定した10を画面へ直接書かない
価格は地域などによって変わる場合がある。Dashboardの
10をTextLabelへ固定せず、次の手順でそのプレイヤー向けの現在価格を読み込む。
2. VIP Passのカードを作る
ExplorerでStarterGuiへScreenGuiを追加してVipPassGuiにする。その中へFrameのVipPassCardを追加し、さらに次の4つを入れよう。
VipPassGui
└─ VipPassCard
├─ PassName (TextLabel)
├─ PriceLabel (TextLabel)
├─ PurchaseButton (TextButton)
└─ PassStore (LocalScript)

VipPassCardは画面下へ置き、PassNameをVIP Pass、PriceLabelを価格を確認中...、PurchaseButtonを読み込み中...にする。ボタンはマウス、タッチ、ゲームパッドで使えるActivatedイベントへつなぐ。公式のボタン解説でも、複数の入力方法に対応する操作としてActivatedを使っている。
カードが画面外へ出ないよう、VipPassGui.ResetOnSpawnをオフにする。VipPassCardは幅360、高さ180ほどにし、下中央へ寄せよう。上からPass名、価格、購入ボタンの順に並べ、3つの文字が重ならないことをPlay前に確認する。細かな色や角丸は自由だが、価格と購入ボタンは背景から読める色にしよう。
Tip — LocalScriptは自分の画面を動かすScript
LocalScriptは各プレイヤーの端末で動く。今回は、その人向けの価格を取得して、その人の購入画面を開く。VIPドアを通れるかは決めず、serverが作ったHasVipPassを表示に使うだけだ。
3. Pass名と現在価格を読み込む
PassStoreを開き、すべてを次のコードへ置き換えよう。PASS_ID = 0の0だけを、前の記事でコピーしたPass IDへ置き換える。
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local PASS_ID = 0 -- 作成したPass IDへ置き換える
local player = Players.LocalPlayer
local hasVipPass = player:WaitForChild("HasVipPass")
local card = script.Parent
local passName = card:WaitForChild("PassName")
local priceLabel = card:WaitForChild("PriceLabel")
local purchaseButton = card:WaitForChild("PurchaseButton")
local productReady = false
local canRetry = false
local promptOpen = false
local currentPrice = 0
local function updateDisplay()
if hasVipPass.Value then
priceLabel.Text = "このPassを所有しています"
purchaseButton.Text = "所有済み"
purchaseButton.Active = false
purchaseButton.AutoButtonColor = false
return
end
purchaseButton.AutoButtonColor = true
if productReady then
priceLabel.Text = tostring(currentPrice) .. " Robux"
purchaseButton.Text = tostring(currentPrice) .. " Robuxで購入"
purchaseButton.Active = not promptOpen
else
purchaseButton.Active = canRetry
end
end
local function loadProduct()
productReady = false
canRetry = false
priceLabel.Text = "価格を確認中..."
purchaseButton.Text = "読み込み中..."
purchaseButton.Active = false
updateDisplay()
local success, productInfo = pcall(function()
return MarketplaceService:GetProductInfoAsync(
PASS_ID,
Enum.InfoType.GamePass
)
end)
if not success then
warn("Pass information failed:", productInfo)
priceLabel.Text = "価格を取得できない"
purchaseButton.Text = "価格を再読み込み"
canRetry = true
updateDisplay()
return
end
passName.Text = productInfo.Name
if productInfo.IsForSale and typeof(productInfo.PriceInRobux) == "number" then
currentPrice = productInfo.PriceInRobux
productReady = true
else
priceLabel.Text = "現在は販売していない"
purchaseButton.Text = "購入できない"
end
updateDisplay()
end
purchaseButton.Activated:Connect(function()
if hasVipPass.Value or promptOpen then
return
end
if not productReady then
if canRetry then
loadProduct()
end
return
end
promptOpen = true
updateDisplay()
MarketplaceService:PromptGamePassPurchase(player, PASS_ID)
end)
MarketplaceService.PromptGamePassPurchaseFinished:Connect(
function(finishedPlayer, purchasedPassId, wasPurchased)
if finishedPlayer ~= player or purchasedPassId ~= PASS_ID then
return
end
promptOpen = false
if wasPurchased and not hasVipPass.Value then
purchaseButton.Text = "所有状態を反映中..."
purchaseButton.Active = false
purchaseButton.AutoButtonColor = false
return
end
updateDisplay()
end
)
hasVipPass.Changed:Connect(updateDisplay)
updateDisplay()
loadProduct()
Regional Pricingの公式説明によると、Passの価格はユーザーによって変わる場合がある。そのため数字をコードへ直接書かず、GetProductInfoAsync()をLocalScriptから呼び、PriceInRobuxを表示する。Nameも同じ結果から取得する。

productReadyは、販売中のPassから数字の価格を受け取れたときだけtrueになる。取得中や失敗中にボタンを押しても購入画面を開かないための安全札だ。promptOpenは購入画面を開いている間の連打を止め、canRetryは通信に失敗した場合だけ同じボタンを再読み込みへ使う。
Dashboardで設定した基本価格と表示価格が違っても、すぐにコードを書き換えない。Roblox Plusの公式説明にある割引や地域価格が反映される場合がある。PriceInRobuxと実際のpromptが同じ金額なら、APIから現在価格を読めている。
4. 購入完了をサーバーで受け取る
画面のLocalScriptだけでHasVipPassを変えてはいけない。ServerScriptService > VipAccessを開き、サービスを取得している上部へ次の1行を追加する。
local ServerStorage = game:GetService("ServerStorage")
次に、setVipAccess()より下へ次のコードを追加しよう。PASS_IDは前の記事と同じ値を使う。
local function onPromptGamePassPurchaseFinished(
player,
purchasedPassId,
wasPurchased
)
if not wasPurchased or purchasedPassId ~= PASS_ID then
return
end
setVipAccess(player, true)
end
MarketplaceService.PromptGamePassPurchaseFinished:Connect(
onPromptGamePassPurchaseFinished
)
if RunService:IsStudio() then
local studioMockPurchase = Instance.new("BindableEvent")
studioMockPurchase.Name = "StudioMockVipPurchase"
studioMockPurchase.Parent = ServerStorage
studioMockPurchase.Event:Connect(function(player)
onPromptGamePassPurchaseFinished(player, PASS_ID, true)
end)
end
Passesの公式実装例も、PromptGamePassPurchaseFinishedをサーバー側で処理して特典を反映する形だ。ここではsetVipAccess()がサーバーのBoolValueとCollision Groupを同時に更新する。LocalScriptが表示を書き換えても、サーバーのVIP権限は作れない。
onPromptGamePassPurchaseFinished()は、閉じただけの結果と別商品のIDを無視する。正しいPassの成功だけをsetVipAccess()へ渡すため、別の商品画面が閉じたときにVIPになることはない。
StudioMockVipPurchaseは、現在のStudio Play中にだけServerStorageへ作るテスト用イベントだ。クライアントからは見えず、本番サーバーでは作られない。実際のRobux購入をせず、同じ完了処理へ安全にテスト結果を渡せる。
5. 購入画面とStudio限定の完了テストを確かめる
Playを押す。前の記事のSTUDIO_TEST_OWNED = falseなら、カードにPass名と現在価格が表示される。今回のテストPassでは、Dashboardへ保存した販売状態をAPIから読み込み、VIP Access、10 Robux、10 Robuxで購入が表示された。
この表示はチュートリアル用のHasVipPass = falseを見ており、Roblox上のインベントリが本当に未所有だと証明するものではない。今回の制作者アカウントはPassをすでに所有していたため、購入画面では未所有者向けの購入確定やキャンセル分岐を検証していない。
購入ボタンを押し、購入画面が開くことを確認する。制作アカウントなど、すでにPassを所有しているアカウントでは「すでに所有しているため請求されない」という画面になる。この場合も購入は行われないので、画面を閉じよう。

本当に未所有のアカウントを使う場合も、画面にStudioのテスト購入であることとRobuxを消費しないことが明記された場合だけ完了処理を確認してよい。どちらの表示もない場合は購入を進めず、次のStudio限定テストを使う。
Command Barからサーバーのテストイベントを動かす
PlayしたままWindow > Script > Command Barを開く。Testメニューのクライアント/サーバー表示切替を使い、ウィンドウ枠が緑色になるServer表示へ切り替える。Command Barへ次の1行を貼り付けて実行しよう。

local player = game.Players:GetPlayers()[1]
game.ServerStorage.StudioMockVipPurchase:Fire(player)
Testメニューからクライアント表示へ戻ると、カードが所有済みへ変わる。同じPlay中にVIPドアを通れ、所有済みボタンを押しても購入画面が開かなければ成功だ。

最後にStopして、もう一度Playしよう。テスト状態は残らないため、現在価格と購入ボタンへ戻る。本番では参加時にサーバーのUserOwnsGamePassAsync()が実際の所有権を確認する。Studioの現在セッション用テストと本番の所有権を混ぜないことが大切だ。

次の順番で見えれば、購入前、購入画面、完了直後、次のセッションを分けて確認できたことになる。
- 最初のPlayではStudioテスト設定による現在価格と購入ボタン
- 所有済み・請求なしの画面を閉じた後も、テスト状態は
falseでドアは閉じたまま - Studio限定テスト後は
所有済みでドアを通れる - 所有済みボタンから購入画面は開かない
- Stop後の新しいPlayではテスト状態が消える
うまく動かないとき
- ずっと
価格を確認中...:PassStoreが通常のScriptではなくLocalScriptか確認する 価格を取得できない:PASS_IDが0のままではないか確認し、Outputの警告を見てから価格を再読み込みを押す現在は販売していない: Creator Dashboardの収益化 > パス > 販売で販売中のアイテムがオンか確認する- 価格がDashboardの数字と違う: 地域価格やRoblox Plusの割引が反映される場合がある。画面へ固定価格を書かず、
PriceInRobuxを使う - テスト後もドアを通れない: サーバー表示のCommand Barで実行したか、2か所の
PASS_IDが同じか確認する StudioMockVipPurchaseが見つからない: Stop中やクライアント表示ではなく、Play中のサーバー表示でCommand Barを実行する- Stop後に未所有へ戻った: Studio限定テストの正常な境界だ。本番の購入履歴や所有権は作られない
- テスト購入の表示がない: 購入を進めず画面を閉じ、Studio限定のサーバーテストを使う
完成
これで、未所有者にはAPIから取得したPass名と現在価格が表示され、購入画面を開けるようになった。購入完了時はサーバーが現在のセッションへVIP権限を付け、所有済みの人へ再購入を求めない。Studio限定テストでも、Robuxを使わず同じ完了処理とStop後のリセットを確認できた。
次は、何度でも購入できるDeveloper Productを作り、購入1回につき100コインを増やす仕組みへ進む。

