収益化初級プラスRobux不要

Robloxでゲームパスを作る:VIPドアへ反映する方法

Passの所有状態をサーバーで確認し、未所有者は止まり、所有者だけが通れるVIPドアをCollision Groupで作ろう。

公開日
VIPドアの内側まで通り抜けたStudio所有テスト中のキャラクター

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

VIPドアの内側まで通り抜けたStudio所有テスト中のキャラクター

手順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しておくよう案内している。

Creator DashboardでVIP Passの名前と詳細を入力する作成画面

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

作成したVIP PassのメニューからパスIDをコピーする場所

Tip — Passは一度買う権利

Passは、VIPエリアのような権利を1回購入して使い続ける仕組みだ。同じ商品を何度も買うDeveloper Productとは役割が違う。この記事では購入せず、Studio内の安全なテスト設定で権限だけを再現する。

2. VipDoorを置く

Studioへ戻り、WorkspaceへPartを追加してVipDoorへ名前を変える。入口をふさぐ大きさにし、AnchoredCanCollideをオンにしよう。色やMaterialは自由だが、通常の壁と見分けられる水色などにすると確認しやすい。

WorkspaceのVipDoorと、CanCollideを確認しているProperties

このドアを全員に対して開閉すると、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()は、現在ある全部品をVipVisitorsVipMembersへまとめて入れる。

参加後にアクセサリーが追加される場合もあるため、DescendantAddedで新しいBasePartを同じGroupへ入れる。ここを省くと、体は通れたのに帽子だけがドアへ引っかかることがある。再出現したときはCharacterAddedが新しい体へ同じ設定を行う。

HasVipPassを変更すると現在の体にも反映する

HasVipPassは「この参加者がVIP Passを持つか」を表すサーバー側の印だ。setVipAccess(player, hasVipPass)が値を変更すると、GetPropertyChangedSignal("Value")が現在のキャラクターへGroupを設定し直す。

この記事では参加時の所有確認だけで値を決める。次の購入ボタン記事では、Studio限定の完了テストを行った現在のセッションだけ、この同じ値をサーバーからtrueへ変える。ドア側の処理を作り直さず、権限を決める入口だけを追加できる。

PASS_ID = 0は未設定を表す仮の値だ。Creator Dashboardでコピーした数字へ必ず変更しよう。本番でIDが未設定だったり所有確認APIが失敗したりした場合、コードはfalseを返してドアを開けない。

HasVipPassVipAccessというサーバー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上で未所有」という証明ではなく、ドアと画面の未所有側を再現するテスト設定だ。

未所有状態のキャラクターがVipDoorの手前で止まる動き

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

Studioの所有テスト状態でキャラクターがVipDoorを通り抜ける動き

この切り替えはRunService:IsStudio()の内側だけで使われる。本番サーバーでは無視され、UserOwnsGamePassAsync()の結果だけが権限になる。確認後はSTUDIO_TEST_OWNED = falseへ戻しておこう。

2回のテストでは、同じSpawnLocationから同じ方向へ歩こう。未所有側だけ別の角度から当たると、ドアではなく横の壁で止まった可能性を切り分けにくい。入口の床へ小さな目印を置き、どちらもそこからまっすぐ進むと比較しやすい。

通れない、または全員が通れるとき

  • Studioの所有テストでも全員が止まる: VipDoorの名前と、所有テスト時のSTUDIO_TEST_OWNED = trueを確認する
  • 全員が通れる: VipDoor.CanCollideがオンで、ドアのCollisionGroupVipDoorになっているか確認する
  • 赤いエラーが出る: VipAccessがLocalScriptではなくServerScriptServiceの通常のScriptか確認する
  • アクセサリーだけ引っかかる: DescendantAddedから下のコードを省いていないか確認し、StopしてPlayし直す
  • 所有確認APIが失敗する: 権限を開けずに警告を出すのが安全な状態だ。Pass IDと通信状態を直してから新しいPlayで確認する

完了チェック

  • WorkspaceにAnchored、CanCollideオンのVipDoorがある
  • ServerScriptServiceVipAccessがある
  • STUDIO_TEST_OWNED = falseでは入口の外に止まる
  • STUDIO_TEST_OWNED = trueでは同じ入口を通れる
  • Stop後にSTUDIO_TEST_OWNED = falseへ戻した
  • 実際のPass購入やRobux消費を行っていない

完成

これで、通常プレイでは未所有者が止まり、Pass所有者が通れるVIPドアの土台ができた。ドア全体を消すのではなく、参加者ごとのサーバー所有状態とCollision Groupを結び付けている。VIP限定の報酬や操作を追加するときは、ドアとは別にサーバーで権限を再確認しよう。

次は、未所有者だけに現在価格と購入ボタンを表示する。

公式リファレンス

画像の拡大表示