スポーンとは、エクスペリエンス内にオブジェクトやキャラクターを作成するプロセスを指し、リスポーンは、キャラクターのヘルスがゼロになる、またはマップから落ちるという除去条件を満たした後に、オブジェクトやキャラクターをエクスペリエンスに戻すプロセスを指します。これらのプロセスは重要であり、プレイヤーがエクスペリエンスに参加できるようにし、スキルを向上させ続けることができるからです。
サンプルレーザータグエクスペリエンスを参考に、このチュートリアルのセクションではスポーンとリスポーンを処理するために、Robloxの組み込み機能を使用してカスタマイズする方法を学びます。これには次のようなスクリプティングガイダンスが含まれます。
- プレイヤーが自分のチームのスポーンゾーンにのみスポーンできるようにスポーン場所を設定すること。
- 新しいプレイヤーとそのキャラクターを、経験に参加する際にラウンドに追加すること。
- プレイヤーがスポーン及びリスポーンする際に、ダメージを防ぐフォースフィールドをカスタマイズすること。
- ゲームプレイが適切なタイミングで正しく機能するように、クライアント状態を処理すること。
- ラウンドからアウトされたキャラクターをリスポーンさせること。
- ゲームプレイやキャラクターのパラメータを設定するための小さな雑多なアクションを実行すること。
このセクションには多くのスクリプトコンテンツが含まれていますが、経験を作成する際にすべてをゼロから書くのではなく、既存のコンポーネントを活用して迅速に反復し、ビジョンに合わせてカスタム実装が必要なシステムを見つけることを促します。このセクションを完了した後、ポイントを追跡し、プレイヤーの状態を監視し、ラウンドの結果を表示するラウンドベースのゲームプレイを実装する方法を学びます。
スポーン場所の設定
今、あなたがエクスペリエンスをプレイテストした場合、すべてのプレイヤーは緑チームのスポーンゾーンにある SpawnLocation オブジェクトか、ピンクチームのスポーンゾーンにある SpawnLocation オブジェクトのいずれかにランダムにスポーンします。これは、プレイヤーがスポーンゾーン内で相手のフォースフィールドが消えた瞬間に攻撃できるというゲームプレイの問題を引き起こします。
この問題に対抗するために、サンプルレーザータグエクスペリエンスは、どちらのスポーン場所も Neutral プロパティを false に設定して、相手チームのプレイヤーが間違ったスポーンゾーンにスポーンするのを制限し、 TeamColor プロパティを前のセクションのチームカラーの割り当てからの対応する Team.Color 値に設定しています。


プレイヤーがエクスペリエンスに参加すると、ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ spawnPlayersInMap は、各チームにすでに何人のプレイヤーがいるかをチェックし、最も少ないプレイヤー数のチームを返します。
spawnPlayersInMap
local function getSmallestTeam(): Team
local teams = Teams:GetTeams()
-- 小さい順にチームをソートする
table.sort(teams, function(teamA: Team, teamB: Team)
return #teamA:GetPlayers() < #teamB:GetPlayers()
end)
-- 最も小さいチームを返す
return teams[1]
end
最も少ないプレイヤー数のチームが分かると、そのチームにプレイヤーを振り分け、プレイヤーの Player.Neutral プロパティを false に設定し、プレイヤーがチームのスポーン場所にのみスポーンおよびリスポーンできるようにし、次に PlayerState を SelectingBlaster に設定します。この設定については、チュートリアルの後半で詳しく学びます。
spawnPlayersInMap
local function spawnPlayersInMap(players: { Player })
for _, player in players do
player.Team = getSmallestTeam()
player.Neutral = false
player:SetAttribute(PlayerAttribute.playerState, PlayerState.SelectingBlaster)
task.spawn(function()
player:LoadCharacter()
end)
end
end
Workspace ⟩ World ⟩ Map ⟩ Spawns を調べると、マップ内にもう一つのスポーン場所 NeutralSpawn があることがわかります。このスポーン場所は他のものとは異なり、TeamColor プロパティがエクスペリエンス内の2つのチームのいずれかに設定されていません。その代わりに、このスポーン位置はラウンドがアクティブかどうかによって変わる Neutral プロパティを持っています。
例えば、ラウンドがアクティブな場合、 Neutral プロパティは false に設定され、spawnPlayersInMap がプレイヤーをチームに振り分けてアリーナにスポーンできるようにします。しかし、ラウンドがアクティブでない場合、例えば一つのラウンドと次のラウンドの間など、 Neutral プロパティは true に設定されるため、プレイヤーはチームのステータスに関係なくスポーンすることができます。このプロセスが Neutral スポーン場所を実用的なロビーにしています。

例を示すために、ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ SpawnPlayersInLobby を調査すると、ラウンドの終了時に実行され、このスクリプトに渡された players: { Player } テーブルのすべてのプレイヤーについて、スクリプトが次の処理を行うことが分かります。
- プレイヤーの Player.Neutral プロパティを true に設定し、プレイヤーの Player.Team を自動的に nil にリセットし、ラウンドがアクティブでないときにロビーでリスポーンできるようにします。
- プレイヤーの PlayerState を InLobby に変更し、プレイヤーのブラスターと一人称UIビジュアルを削除します。
ロビーの中立スポーンゾーンとその機能についての詳細は、次のセクションのラウンドの追加を参照してください。
spawnPlayersInLobby
local function spawnPlayersInLobby(players: { Player })
for _, player in players do
player.Neutral = true
player:SetAttribute(PlayerAttribute.playerState, PlayerState.InLobby)
task.spawn(function()
player:LoadCharacter()
end)
end
end
新しいプレイヤーを接続する
StudioでのLuauコードは多くの場合イベント駆動型であり、スクリプトはRobloxサービスからのイベントをリッスンし、応答として関数を呼び出します。例えば、マルチプレイヤーエクスペリエンスに新しいプレイヤーを追加する際には、プレイヤーが正常に接続できるように必要なすべてを処理するイベントが必要です。サンプルレーザータグエクスペリエンスでは、この対応するイベントが Players.PlayerAdded:Connect です。
Players.PlayerAdded:Connect はエクスペリエンス内の複数のスクリプトの一部です。Ctrl/Cmd+Shift+F ショートカットを使って Players.PlayerAdded:Connect を検索すると、結果がエクスペリエンスの初期設定を理解するための良い出発点を提供します。

例を示すために、ServerScriptService ⟩ SetupHumanoid を開きます。Player と Character の違いは、このスクリプトを理解するための鍵です。
- プレイヤーはブラスターを選択し、リーダーボードに追加される必要があります。キャラクターはスポーンしてブラスターを受け取る必要があります。
SetupHumanoid はプレイヤーがキャラクターを持っているか(参加したばかりの場合)または持っていないか(リスポーン中)を即座に確認します。見つけたら、onCharacterAdded() を呼び出し、キャラクターから Humanoid モデルを取得し、それを ServerScriptService ⟩ SetupHumanoid ⟩ setupHumanoidAsync に渡してカスタマイズします。これらの値を設定した後、スクリプトはキャラクターのヘルスがゼロに達するのを待ちます。このリスポーンに関する詳細については、後でこのセクションで学びます。
setupHumanoidAsync
local function setupHumanoidAsync(player: Player, humanoid: Humanoid)
humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.Subject
humanoid.NameDisplayDistance = 1000
humanoid.HealthDisplayDistance = 1000
humanoid.NameOcclusion = Enum.NameOcclusion.OccludeAll
humanoid.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOn
humanoid.BreakJointsOnDeath = false
humanoid.Died:Wait()
onHumanoidDied(player, humanoid)
end
このスクリプトの重要な注意点は、プロパティが完全にオプショナルであるため、関数の最初の6行を削除してもエクスペリエンスは正常に動作します。機能要件としてではなく、各プロパティはゲームプレイの目標を満たすデザイン決定を行うことができます。例えば:
- キャラクター名をより近い距離で表示したい場合は、Humanoid.NameDisplayDistance の値を減少させます。
- キャラクターのヘルスが100%未満の場合にのみ表示したい場合は、Humanoid.HealthDisplayType を DisplayWhenDamaged に設定します。
- キャラクターのヘルスが0に達した時にキャラクターが崩壊するようにしたい場合は、Humanoid.BreakJointsOnDeath を True に設定します。
これらのプロパティの値を変更した場合は、新しい設定の影響を確認するためにプレイテストを行うことが重要です。メザニンからの Server & Clients プレイテストで少なくとも2キャラクターを選択することで、プレイヤーが経験することをマルチクライアントシミュレーションで再現することができます。

Players.PlayerAdded:Connect イベントの別の例は、ServerScriptService ⟩ PlayerStateHandler にあります。前の例と同様に、PlayerStateHandler はすぐにキャラクターをチェックします。プレイヤーがロビーにいない場合、スクリプトはプレイヤーの属性を SelectingBlaster ステートに設定します。これは、プレイヤーがアリーナにスポーンした後、2種類のブラスターから選択できるラウンドの初期ステートです。このステートにも、プレイヤーが選択を行う際にダメージを受けないようにするフォースフィールドが含まれています。
PlayerStateHandler
local function onPlayerAdded(player: Player)
player.CharacterAdded:Connect(function()
if not player.Neutral then
player:SetAttribute(PlayerAttribute.playerState, PlayerState.SelectingBlaster)
onPlayerStateChanged(player, PlayerState.SelectingBlaster)
end
end)
PlayerStateHandler の特定の変数には、attributeChangedConnectionByPlayer という名前が付けられています。このテーブルはすべてのプレイヤーとその Connections を格納しており、 GetAttributeChangedSignal に接続されています。この接続をテーブル内に保存する理由は、PlayerStateHandler がプレイヤーがエクスペリエンスを離れる際にその接続を 切断 できるようにするためです。このプロセスは、時間の経過とともに接続数が増加し続けるのを防ぐためのメモリ管理の一種です。
PlayerStateHandler
local attributeChangedConnectionByPlayer = {}
local function onPlayerAdded(player: Player)
-- プレイヤーの状態を将来のすべての更新を処理する
attributeChangedConnectionByPlayer[player] = player
:GetAttributeChangedSignal(PlayerAttribute.playerState)
:Connect(function()
local newPlayerState = player:GetAttribute(PlayerAttribute.playerState)
onPlayerStateChanged(player, newPlayerState)
end)
end
-- プレイヤーが離れたときに属性変更接続を切断
local function onPlayerRemoving(player: Player)
if attributeChangedConnectionByPlayer[player] then
attributeChangedConnectionByPlayer[player]:Disconnect()
attributeChangedConnectionByPlayer[player] = nil
end
end
onPlayerAdded() の両方の接続関数が onPlayerStateChanged() を呼び出すことがわかります。プレイヤーがチームに振り分けられた後の初期設定中に、onPlayerAdded() は PlayerState を SelectingBlaster に設定するため、最初の if 式は偽になり、 BlasterState が無効化されます。後のブラスターの実装セクションで、このプロセスの詳細について学びます。
PlayerStateHandler
local function onPlayerStateChanged(player: Player, newPlayerState: string)
-- ブラスターの状態は、プレイヤーの状態が「Playing」の場合のみ「Ready」
local newBlasterState = if newPlayerState == PlayerState.Playing then BlasterState.Ready else BlasterState.Disabled
-- プレイヤーがプレイを開始した際にフォースフィールドを破棄するロジックをスケジュールする
if newPlayerState == PlayerState.Playing then
scheduleDestroyForceField(player)
end
player:SetAttribute(PlayerAttribute.blasterStateServer, newBlasterState)
end
ブレークポイントを追加したり、単に print() ステートメントを追加すると、 onPlayerStateChanged() が、ラウンドの初期設定中やプレイヤーがブラスターを選択した後、ロビーに戻るときなど、エクスペリエンス全体で頻繁に呼び出されることがわかります。それに加えて、プレイヤーがブラスターを選択した後、ServerScriptService ⟩ BlasterSelectedHandler が PlayerState を Playing に設定し、 PlayerStateHandler が最終的に scheduleDestroyForceField() を呼び出すことでフォースフィールドを削除することができます。
フォースフィールドをカスタマイズする
カスタム実装を使用する代わりに、サンプルレーザータグエクスペリエンスは、プレイヤーがブラスターを選択している間にダメージを受けないようにするためにStudioの組み込み ForceField クラスを使用しています。これにより、プレイヤーがフォースフィールドとともにスポーンするための唯一の要件は、SpawnLocation.Duration プロパティが0より大きくなることです。サンプルでは、フォースフィールドを有効にするために任意の値9,999を使用し、実際の継続時間は ReplicatedStorage ⟩ ForceFieldClientVisuals でプログラム的に処理します。
setupHumanoidAsync と同様に、 ForceFieldClientVisuals でのほとんどの行は任意です。例えば、次のスクリプトのように関数の内容をコメントアウトすると、エクスペリエンスは StarterGui ⟩ ForceFieldGui の六角形スクリプトの代わりにデフォルトのスパークリングフォースフィールドを使用します。
Commenting Out Properties in ForceFieldClientVisuals
local function onCharacterAddedAsync(character: Model)
-- local forceField = character:WaitForChild("ForceField", 3)
-- if not forceField then
-- return
-- end
-- forceField.Visible = false
-- localPlayer.PlayerGui:WaitForChild("ForceFieldGui").Enabled = true
-- forceField.Destroying:Wait()
-- localPlayer.PlayerGui.ForceFieldGui.Enabled = false
end
カスタムフォースフィールドがGUIであり、各プレイヤーの一人称ビジュアルにのみ影響を与えるため、他のプレイヤーを見るときの三人称ビジュアルは、 Roblox のデフォルトの外観のままです。フォースフィールドを変更する方法についての詳細は、 ForceField.Visible を参照してください。


フォースフィールドは、プレイヤーがスポーンおよびリスポーンする間、敵プレイヤーを気にせずに十分な時間を提供するために便利ですが、最終的にはメインのレーザータグゲームプレイのために消える必要があります。フォースフィールドの削除を処理するスクリプトは ReplicatedStorage ⟩ scheduleDestroyForceField にあり、3つの独自の条件をチェックします。
- プレイヤーがブラスターを選択した後、フォースフィールドはプレイヤーが周囲に慣れるために十分な時間が必要です。
- この慣れの時間中、フォースフィールドがアドバンテージになってはいけないため、プレイヤーがブラスターを撃った瞬間に消える必要があります。
- プレイヤーがブラスターを撃つ前やフォースフィールドがタイムアウトする前に、キャラクターをリセットしたときにもフォースフィールドが消える必要があります。
scheduleDestroyForceField スクリプト内でこれらの各チェックが行われ、これらの条件に対して endForceField() が呼び出されます。
scheduleDestroyForceField
-- プレイヤーがブラスターを撃った場合、フォースフィールドを終了
local blasterStateAttribute = getBlasterStateAttribute()
attributeChangedConnection = player:GetAttributeChangedSignal(blasterStateAttribute):Connect(function()
local currentBlasterState = player:GetAttribute(blasterStateAttribute)
if currentBlasterState == BlasterState.Blasting then
endForceField()
end
end)
-- プレイヤーがリセットした場合、フォースフィールドを終了
characterRespawnedConnection = player.CharacterRemoving:Connect(endForceField)
-- 8秒後にフォースフィールドを終了
task.delay(MAX_FORCE_FIELD_TIME, endForceField)
endForceField() には、forceFieldEnded ブール値の周りに奇妙に見える if 文が含まれています。このチェックは順次実行されるため、スクリプトは endForceField() 関数を2回または3回呼び出すことがあります。forceFieldEnded ブール値は、関数がフォースフィールドを一度だけ破壊しようとすることを保証します。
scheduleDestroyForceField
local function endForceField()
if forceFieldEnded then
return
end
forceFieldEnded = true
attributeChangedConnection:Disconnect()
characterRespawnedConnection:Disconnect()
destroyForceField(player)
end
クライアント状態を処理する
このセクションのほとんどは ServerScriptService ⟩ PlayerStateHandler に焦点を合わせていますが、ReplicatedStorage に同名の別のスクリプトがあります。分割されている理由は、クライアント-サーバーアーキテクチャにあります。
クライアントは、リアルタイムで正確に応答できるようにプレイヤー状態情報を理解する必要があります。例えば、適切なユーザーインターフェース要素を表示したり、プレイヤーが動いたりブラスターを撃てるようにしたりします。
サーバーも同様の情報が必要で、これによりエクスプロイトを防ぐことができます。例えば、サーバーはキャラクターをスポーンし、装備させ、フォースフィールドを無効にし、リーダーボードを表示するなどのアクションを実行するためにプレイヤー状態が必要です。このため、このスクリプトは ReplicatedStorage にあり、純粋にクライアント側の場所にはありません。
このコアロジックを見るには、ユーザーの現在の状態を確認し、その状態に対応するアクションを処理する関数を呼び出す ReplicatedStorage ⟩ PlayerStateHandler の以下のスクリプトを確認してください。
PlayerStateHandler
local function onPlayerStateChanged(newPlayerState: string)
if newPlayerState == PlayerState.SelectingBlaster then
onSelectingBlaster()
elseif newPlayerState == PlayerState.Playing then
onPlaying()
elseif newPlayerState == PlayerState.TaggedOut then
onTaggedOut()
elseif newPlayerState == PlayerState.InLobby then
onInLobby()
else
warn(`無効なプレイヤー状態 ({newPlayerState})`)
end
end
すべてのイベント応答は、プレイヤーの操作、カメラの動き、どのUIレイヤーが表示されるかを有効または無効にするという類似の動作を必要とするため、このスクリプトに論理的にグループ化されています。例えば、ブラスター選択中、プレイヤーは無敵で移動できなくなる必要があります。サーバーは既にフォースフィールドを処理していますが、クライアントは移動を処理します。例を示すために、onSelectingBlaster() 関数のロジックを確認すると、ブラスターを選択している間にクライアントがプレイヤーの移動を無効にしていることがわかります。
PlayerStateHandler
local function onSelectingBlaster()
togglePlayerCamera(true)
togglePlayerMovement(false)
setGuiExclusivelyEnabled(playerGui.PickABlasterGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
end
onPlaying() 関数も同様に単純です。移動を有効にし、メインのヘッドアップディスプレイ(HUD)に移行し、ブラスターを有効にし、サーバーと同じフォースフィールド機能を呼び出します。
PlayerStateHandler
local function onPlaying()
togglePlayerMovement(true)
setGuiExclusivelyEnabled(playerGui.HUDGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Ready)
scheduleDestroyForceField()
end
キャラクターをリスポーンさせる
サンプルレーザータグエクスペリエンスは、ReplicatedStorage ⟩ PlayerStateHandler 内の onTaggedOut() ステートを通じて、キャラクターをラウンドにリスポーンさせる処理を行います。 onSelectingBlaster() と onPlaying() ステートと同様に、 onTaggedOut() は playerState 属性の変更に応じて独自の動作をトリガーします。具体的には、プレイヤーの移動を無効にし、リスポーンUIを表示し、ブラスターを無効にします。
PlayerStateHandler
local function onTaggedOut()
-- タグアウト中はコントロールを無効にする
togglePlayerMovement(false)
togglePlayerCamera(false)
setGuiExclusivelyEnabled(playerGui.OutStateGui)
-- タグアウト中はブラスターを無効にする
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
end
この動作をテストしたい場合は、Esc を押して Settings タブに移動し、 Reset Character ボタンをクリックします。リスポーン画面をトリガーすると、移動、カメラの回転、またはブラスターを撃つことはできないことを確認してください。


このスクリプトは実際にはキャラクターをリスポーンさせないことに留意することが重要です。それは、プレイヤーの行動を停止させ、サーバーがキャラクターをリスポーンさせていることを同じく視覚的にフィードバックを提供するだけです。これを示すために、ServerScriptService ⟩ SetupHumanoid ⟩ setupHumanoidAsync ⟩ onHumanoidDied を調べると、スクリプトは PlayerState を TaggedOut に設定し(本質的に ReplicatedStorage ⟩ PlayerStateHandler に通知)、いくつかの視覚的なインジケーターを追加します。リスポーンの実際のロジックは、Roblox に組み込まれた動作です。
プレイヤーがラウンドにリスポーンする際、彼らは SpawnLocation.TeamColor プロパティに基づいて自チームのスポーン場所にリスポーンします。リスポーン時間をカスタマイズするには、 SetupHumanoid の冒頭に次の行を追加します。この技術の詳細については、 Players.RespawnTime を参照してください。
SetupHumanoidlocal Players = game:GetService("Players")Players.RespawnTime = 10 -- 新しい行、秒単位
その他の設定
初期設定の一環として、サンプルレーザータグエクスペリエンスは、小さいながらも重要なステップをいくつか実行します:
エクスペリエンスには、デフォルトのRobloxヘルス再生を無効にする空のスクリプト、StarterPlayer ⟩ StarterCharacterScripts ⟩ Health が含まれています。このプロパティの動作についての説明は、 Humanoid.Health を参照してください。
プレイヤーがファーストパーソンカメラを使用できるように、 StarterPlayer.CameraMode.LockFirstPerson プロパティが設定されます。ユーザーがファーストパーソンとサードパーソンカメラを切り替えることを許可したい場合は、Studio内で一度設定するのではなく、プログラム的にプロパティを変更し、視点の変更に対処するためにコントロールとUIを修正する必要があります。
エクスペリエンスは、プレイヤーが他のプレイヤーをタグアウトするたびに獲得する「ポイント」という単位で、組み込みのRobloxリーダーボードを使用します。設定内容は ServerScriptService ⟩ SetupLeaderboard で確認できますが、ゲーム内リーダーボード が全概要を提供します。 onPlayerTagged はリーダーボードにポイントを追加し、これについては ラウンドの追加 や ヒットの検出 で学ぶことができます。
プレイヤーがスポーンし、ブラスターを選び、一人称視点から狙うことができるようになったので、次のセクションではラウンドベースのゲームプレイを作成するために必要なスクリプトについて学びます。