Robloxでゲームパスを作る:VIPドアへ反映する方法
Passの所有状態をサーバーで確認し、未所有者は止まり、所有者だけが通れるVIPドアをCollision Groupで作ろう。

水色のVIPドアを置き、Studioの所有テストで内側まで通り抜けられる状態を作る。完成までの目安は8分だ。Passの作成とStudioでの動作確認にRobuxは使わない。

手順4では同じ入口を使い、未所有テストでは手前で止まり、所有テストでは通り抜ける違いを2本のGIFで確かめる。この記事では実際のPassを購入しない。Studioだけで有効になるサーバー側のテスト設定を使い、未所有と所有の両方を再現する。本番の所有状態はRoblox公式APIを使ってサーバーが決めるため、クライアントからテスト設定を本番のVIP所有へすり替えることはできない。
まだゲームをRobloxへ保存していない場合は、先にPrivateでPublishする記事を進めよう。PartとScriptの追加に迷う場合は、Part操作とサーバーScriptの基本も確認できる。
1. Creator DashboardでPassを作る
Creator DashboardでPublish済みのゲームを開き、左側の収益化 > パスへ進む。パスを作成を選び、名前と詳細を入力して作成しよう。アイコン画像は任意なので、空のままでも作れる。Roblox公式のPass手順でも、Passを作る前にゲームをPublishしておくよう案内している。

作成したPassの⋯メニューを開き、パスIDをコピーを選ぶ。コピーした数字がPass IDだ。あとでScriptのPASS_IDへ入れるため、テキストエディターなどへ一時的に控えておこう。Passの販売と価格設定は、購入ボタンを作る次の記事で行う。

Tip — Passは一度買う権利
Passは、VIPエリアのような権利を1回購入して使い続ける仕組みだ。同じ商品を何度も買うDeveloper Productとは役割が違う。この記事では購入せず、Studio内の安全なテスト設定で権限だけを再現する。
2. VipDoorを置く
Studioへ戻り、WorkspaceへPartを追加してVipDoorへ名前を変える。入口をふさぐ大きさにし、AnchoredとCanCollideをオンにしよう。色やMaterialは自由だが、通常の壁と見分けられる水色などにすると確認しやすい。

このドアを全員に対して開閉すると、VIPが通る瞬間に未所有者まで入れてしまう。今回はCollision Groupを使い、VIPのキャラクターとドアの組み合わせだけを「ぶつからない」にする。
Tip — Collision Groupは衝突の組み合わせ表
Collision Groupは、どの種類同士をぶつけるか決める表だ。今回は
VipVisitorsはドアにぶつかり、VipMembersだけがVipDoorを通り抜ける。
3. VipAccessを追加する
ServerScriptServiceへ通常のScriptを追加し、VipAccessへ名前を変える。中身をすべて消し、次のコードへ置き換えよう。
Tip — コードのコメントは人向けのメモ
Luauでは、
--からその行の終わりまでがコメントになる。コメントはコードを読む人へ残すメモで、Robloxは命令として実行しない。たとえばlocal PASS_ID = 1234567890 -- VIP AccessのPass IDなら、左側の数字は使われるが、右側の説明はゲームの動作に影響しない。次のコードにある-- 「パスIDをコピー」で取得した数字へ変更するも、あとで直す場所を忘れないためのメモだ。
local MarketplaceService = game:GetService("MarketplaceService")
local PhysicsService = game:GetService("PhysicsService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local PASS_ID = 0 -- 「パスIDをコピー」で取得した数字へ変更する
local STUDIO_TEST_OWNED = false
local GUEST_GROUP = "VipVisitors"
local MEMBER_GROUP = "VipMembers"
local DOOR_GROUP = "VipDoor"
local vipDoor = workspace:WaitForChild("VipDoor")
local function ensureCollisionGroup(groupName)
if not PhysicsService:IsCollisionGroupRegistered(groupName) then
PhysicsService:RegisterCollisionGroup(groupName)
end
end
ensureCollisionGroup(GUEST_GROUP)
ensureCollisionGroup(MEMBER_GROUP)
ensureCollisionGroup(DOOR_GROUP)
PhysicsService:CollisionGroupSetCollidable(GUEST_GROUP, DOOR_GROUP, true)
PhysicsService:CollisionGroupSetCollidable(MEMBER_GROUP, DOOR_GROUP, false)
vipDoor.CollisionGroup = DOOR_GROUP
local function setCharacterGroup(character, hasVipPass)
local groupName = hasVipPass and MEMBER_GROUP or GUEST_GROUP
for _, descendant in character:GetDescendants() do
if descendant:IsA("BasePart") then
descendant.CollisionGroup = groupName
end
end
end
local function ownsVipPass(player)
if RunService:IsStudio() then
return STUDIO_TEST_OWNED
end
if PASS_ID <= 0 then
warn("PASS_IDを作成したPass IDへ変更してください")
return false
end
local success, ownsPass = pcall(function()
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, PASS_ID)
end)
if not success then
warn("Pass ownership check failed:", ownsPass)
return false
end
return ownsPass
end
local function setVipAccess(player, hasVipPass)
local hasVipPassValue = player:FindFirstChild("HasVipPass")
if hasVipPassValue then
hasVipPassValue.Value = hasVipPass
end
end
local function setupPlayer(player)
if player:FindFirstChild("HasVipPass") then
return
end
local hasVipPassValue = Instance.new("BoolValue")
hasVipPassValue.Name = "HasVipPass"
hasVipPassValue.Value = false
hasVipPassValue.Parent = player
local function setupCharacter(character)
setCharacterGroup(character, hasVipPassValue.Value)
character.DescendantAdded:Connect(function(descendant)
if descendant:IsA("BasePart") then
descendant.CollisionGroup =
hasVipPassValue.Value and MEMBER_GROUP or GUEST_GROUP
end
end)
end
player.CharacterAdded:Connect(setupCharacter)
hasVipPassValue:GetPropertyChangedSignal("Value"):Connect(function()
if player.Character then
setCharacterGroup(player.Character, hasVipPassValue.Value)
end
end)
if player.Character then
setupCharacter(player.Character)
end
setVipAccess(player, ownsVipPass(player))
end
Players.PlayerAdded:Connect(setupPlayer)
for _, player in Players:GetPlayers() do
task.spawn(setupPlayer, player)
end
3つのCollision Groupを最初に用意する
ensureCollisionGroup()は、同じ名前がまだ登録されていないときだけGroupを作る。続く2行が、今回のドアの組み合わせ表だ。
PhysicsService:CollisionGroupSetCollidable(GUEST_GROUP, DOOR_GROUP, true)
PhysicsService:CollisionGroupSetCollidable(MEMBER_GROUP, DOOR_GROUP, false)
VipVisitorsとドアはぶつかり、VipMembersとドアはぶつからない。床や普通の壁との設定は変えていないため、VIPになっても地面をすり抜けるわけではない。PhysicsServiceの公式リファレンスにあるように、Groupの登録と衝突関係の変更はサーバー側で行う。
ドアのCanCollide自体をオフにする方法との違いも大切だ。CanCollide = falseなら全員が通る。Collision Groupなら、同じ瞬間に未所有者と所有者が入口へ来ても、それぞれ別の結果になる。
キャラクターの全部品を同じGroupへ入れる
Robloxのキャラクターは、胴体だけでなく頭、手足、アクセサリーなど複数のBasePartでできている。setCharacterGroup()は、現在ある全部品をVipVisitorsかVipMembersへまとめて入れる。
参加後にアクセサリーが追加される場合もあるため、DescendantAddedで新しいBasePartを同じGroupへ入れる。ここを省くと、体は通れたのに帽子だけがドアへ引っかかることがある。再出現したときはCharacterAddedが新しい体へ同じ設定を行う。
HasVipPassを変更すると現在の体にも反映する
HasVipPassは「この参加者がVIP Passを持つか」を表すサーバー側の印だ。setVipAccess(player, hasVipPass)が値を変更すると、GetPropertyChangedSignal("Value")が現在のキャラクターへGroupを設定し直す。
この記事では参加時の所有確認だけで値を決める。次の購入ボタン記事では、Studio限定の完了テストを行った現在のセッションだけ、この同じ値をサーバーからtrueへ変える。ドア側の処理を作り直さず、権限を決める入口だけを追加できる。
PASS_ID = 0は未設定を表す仮の値だ。Creator Dashboardでコピーした数字へ必ず変更しよう。本番でIDが未設定だったり所有確認APIが失敗したりした場合、コードはfalseを返してドアを開けない。
HasVipPassはVipAccessというサーバーScriptがPlayerの中へ作るBoolValueだ。trueならキャラクターをVipMembersへ移し、falseならVipVisitorsへ移す。画面側から見える値でも、VIPを決定する処理はサーバーだけに置かれている。
重要 — ドアは通常プレイの入口
プレイヤー側がキャラクター物理を動かすため、Collision Groupのドアだけを不正対策にはできない。VIP限定アイテムの付与、報酬、テレポート、保存データの変更を追加するときは、その処理の直前にもサーバーで
HasVipPassを確認しよう。公式のnetwork ownership解説でも、移動や物理の結果をサーバー側で検証する必要性を説明している。
4. 未所有と所有をStudioで確かめる
最初はSTUDIO_TEST_OWNED = falseのままPlayし、ドアへ正面から歩こう。キャラクターが手前で止まれば未所有側は成功だ。
この値は、制作者がRoblox上で実際にPassを所有していても、Studio内ではチュートリアル用の未所有状態を優先して作る。falseは「このアカウントがRoblox上で未所有」という証明ではなく、ドアと画面の未所有側を再現するテスト設定だ。

StopしてSTUDIO_TEST_OWNED = trueへ変え、もう一度Playする。今度はHasVipPass = trueになり、同じドアを通り抜けられる。

この切り替えはRunService:IsStudio()の内側だけで使われる。本番サーバーでは無視され、UserOwnsGamePassAsync()の結果だけが権限になる。確認後はSTUDIO_TEST_OWNED = falseへ戻しておこう。
2回のテストでは、同じSpawnLocationから同じ方向へ歩こう。未所有側だけ別の角度から当たると、ドアではなく横の壁で止まった可能性を切り分けにくい。入口の床へ小さな目印を置き、どちらもそこからまっすぐ進むと比較しやすい。
通れない、または全員が通れるとき
- Studioの所有テストでも全員が止まる:
VipDoorの名前と、所有テスト時のSTUDIO_TEST_OWNED = trueを確認する - 全員が通れる:
VipDoor.CanCollideがオンで、ドアのCollisionGroupがVipDoorになっているか確認する - 赤いエラーが出る:
VipAccessがLocalScriptではなくServerScriptServiceの通常のScriptか確認する - アクセサリーだけ引っかかる:
DescendantAddedから下のコードを省いていないか確認し、StopしてPlayし直す - 所有確認APIが失敗する: 権限を開けずに警告を出すのが安全な状態だ。Pass IDと通信状態を直してから新しいPlayで確認する
完了チェック
WorkspaceにAnchored、CanCollideオンのVipDoorがあるServerScriptServiceにVipAccessがあるSTUDIO_TEST_OWNED = falseでは入口の外に止まるSTUDIO_TEST_OWNED = trueでは同じ入口を通れる- Stop後に
STUDIO_TEST_OWNED = falseへ戻した - 実際のPass購入やRobux消費を行っていない
完成
これで、通常プレイでは未所有者が止まり、Pass所有者が通れるVIPドアの土台ができた。ドア全体を消すのではなく、参加者ごとのサーバー所有状態とCollision Groupを結び付けている。VIP限定の報酬や操作を追加するときは、ドアとは別にサーバーで権限を再確認しよう。

