스폰은 게임에서 객체나 캐릭터를 생성하는 과정이며, 리스폰은 캐릭터의 건강이 0에 도달하거나 맵에서 떨어지는 등의 제거 조건을 충족한 후 게임에 객체나 캐릭터를 다시 추가하는 과정입니다. 두 과정 모두 플레이어가 게임에 참여할 수 있도록 하고, 기술을 향상시키기 위해 계속 플레이할 수 있도록 보장하기 때문에 중요합니다.
샘플 레이저 태그 게임을 참조하여, 이 튜토리얼의 이 섹션에서는 스폰 및 리스폰을 처리하기 위해 Roblox의 내장 기능을 사용하는 방법과 사용자 정의하는 방법을 가르쳐 주며, 다음과 같은 스크립팅 지침을 포함합니다:
- 플레이어가 자신의 팀의 스폰 존에만 스폰될 수 있도록 스폰 위치 구성하기.
- 게임에 참여할 때 새로운 플레이어와 그들의 캐릭터를 라운드에 추가하기.
- 플레이어가 스폰 및 리스폰할 때 피해를 방지하는 포스 필드 사용자 정의하기.
- 적절한 시점에 게임 플레이가 올바르게 작동하도록 클라이언트 상태 처리하기.
- 라운드에서 태그 아웃된 캐릭터 리스폰하기.
- 게임 플레이 및 캐릭터 매개변수를 설정하는 데 중요한 작은 잡다한 작업 수행하기.
이 섹션에는 많은 스크립팅 내용이 포함되어 있지만, 게임을 만들 때 모든 것을 처음부터 작성하기보다는 기존 구성 요소를 활용하고 빠르게 반복하며 비전을 맞추기 위해 어떤 시스템이 사용자 정의 구현이 필요한지 파악하도록 권장합니다. 이 섹션을 완료한 후에는 점수를 추적하고 플레이어 상태를 모니터링하며 라운드 결과를 표시하는 라운드 기반 게임 플레이를 구현하는 방법을 배우게 됩니다.
스폰 위치 구성하기
지금 게임을 플레이 테스트하면 모든 플레이어가 녹색 팀의 스폰 존에 있는 SpawnLocation 객체 또는 분홍색 팀의 스폰 존에 있는 SpawnLocation 객체 중 하나에 무작위로 스폰됩니다. 이는 플레이어가 상대방의 포스 필드가 사라지자마자 각 스폰 존 내에서 서로 태그할 수 있는 게임 플레이 문제를 발생시킵니다.
이 문제를 해결하기 위해 샘플 레이저 태그 게임은 두 스폰 위치 모두에 Neutral 속성을 false로 설정하여 상대 팀의 플레이어가 잘못된 스폰 존에 스폰되는 것을 제한하고, TeamColor 속성을 이전 섹션의 팀 색상 할당에서 해당하는 Team.Color 값으로 설정합니다:


플레이어가 게임에 참여하면 ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ 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로 설정합니다. 이 부분은 튜토리얼의 후반부에서 더 자세히 배울 것입니다.
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
endWorkspace ⟩ World ⟩ Map ⟩ Spawns를 살펴보면 맵에 하나의 스폰 위치가 더 있는 것을 볼 수 있습니다: NeutralSpawn. 이 스폰 위치는 다른 스폰 위치와 다르게 TeamColor 속성이 게임의 두 팀 중 하나로 설정되어 있지 않습니다. 대신, 이 스폰 위치는 라운드가 활성화되어 있는지에 따라 변경되는 Neutral 속성을 가지고 있습니다.
예를 들어, 라운드가 활성화되어 있으면 Neutral 속성이 false로 설정되어 spawnPlayersInMap가 플레이어를 팀으로 정렬하고 아레나에 스폰할 수 있습니다. 그러나 라운드가 활성화되어 있지 않은 경우, 즉 한 라운드와 다음 라운드 사이의 시간에는 Neutral 속성이 true로 설정되어 플레이어가 팀 상태에 관계없이 그곳에 스폰할 수 있습니다. 이 과정이 Neutral 스폰 위치를 기능적인 로비로 만드는 것입니다.

예를 들어, ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ SpawnPlayersInLobby를 살펴보면, 라운드가 끝날 때 실행되며, players: { Player } 테이블에 전달된 각 플레이어에 대해 스크립트가 다음을 수행합니다:
- 플레이어의 Player.Neutral 속성을 true로 설정하여 자동으로 Player.Team을 nil로 재설정하여 라운드가 활성화되지 않았을 때 로비에서 리스폰할 수 있도록 합니다. 스폰 위치의 Neutral 속성도 true로 설정됩니다.
- 플레이어의 PlayerState를 InLobby로 변경하여 플레이어의 블래스터와 1인칭 UI 시각적 요소를 제거합니다.
중립 스폰 존과 각 라운드에 대한 기능에 대한 자세한 내용은 튜토리얼의 다음 섹션에서 라운드 추가를 참조하세요.
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에 전달하여 사용자 정의합니다. 이러한 값을 설정한 후, 스크립트는 캐릭터의 건강이 0에 도달할 때까지 기다립니다. 이 섹션의 후반부에서 리스폰에 대해 더 자세히 배울 것입니다.
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이 스크립트에서 중요한 점은 속성이 완전히 선택 사항이라는 것입니다. 즉, 함수의 처음 여섯 줄을 제거해도 게임은 여전히 제대로 작동합니다. 기능적 요구 사항이 아니라 각 속성은 게임 플레이 목표를 충족하는 디자인 결정을 내릴 수 있도록 해줍니다. 예를 들어:
- 캐릭터 이름이 더 가까운 거리에서 표시되도록 하려면 Humanoid.NameDisplayDistance 값을 줄입니다.
- 캐릭터의 건강이 100% 미만일 때만 표시되도록 하려면 Humanoid.HealthDisplayType을 DisplayWhenDamaged로 설정합니다.
- 캐릭터의 건강이 0에 도달했을 때 캐릭터가 분해되도록 하려면 Humanoid.BreakJointsOnDeath를 True로 설정합니다.
이러한 속성의 값을 변경하면 새로운 설정의 영향을 확인할 수 있도록 플레이 테스트를 하는 것이 중요합니다. Server & Clients 플레이 테스트에서 최소 두 개의 캐릭터를 선택하여 다중 클라이언트 시뮬레이션을 재현할 수 있습니다.

Players.PlayerAdded:Connect 이벤트의 또 다른 예는 ServerScriptService ⟩ PlayerStateHandler에 있습니다. 이전 예와 마찬가지로 PlayerStateHandler는 즉시 캐릭터를 확인합니다. 플레이어가 로비에 있지 않으면 스크립트는 플레이어 속성을 SelectingBlaster 상태로 설정합니다. 이는 플레이어가 아레나에 스폰된 후 두 가지 다른 블래스터 유형 중 하나를 선택할 수 있는 라운드의 초기 상태입니다. 이 상태에는 플레이어가 선택하는 동안 피해를 입지 않도록 하는 포스 필드도 포함됩니다.
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가 플레이어가 게임을 떠날 때 이를 연결 해제할 수 있도록 하기 위함입니다. 이 과정은 시간이 지남에 따라 연결 수가 계속 증가하는 것을 방지하는 일종의 메모리 관리 역할을 합니다.
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
endonPlayerAdded()의 두 연결된 함수가 모두 onPlayerStateChanged()를 호출하는 것을 볼 수 있습니다. 플레이어가 팀에 정렬된 후 초기 설정 중에 onPlayerAdded()는 PlayerState를 SelectingBlaster로 설정하므로 첫 번째 if 문이 false로 평가되어 BlasterState가 비활성화됩니다. 이후 블래스터 구현 섹션에서 이 과정에 대한 더 많은 세부 정보를 배우게 됩니다.
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()가 게임 전반에 걸쳐 자주 호출되는 것을 볼 수 있습니다: 예를 들어, 라운드의 초기 설정 중, 메인 코드 경로에 설정할 때, 플레이어가 블래스터를 선택할 때, 플레이어가 로비로 돌아갈 때 또는 Neutral 스폰 위치로 돌아갈 때입니다. 또한 플레이어가 블래스터를 선택한 후, ServerScriptService ⟩ BlasterSelectedHandler가 PlayerState를 Playing으로 설정하고, PlayerStateHandler는 scheduleDestroyForceField()를 호출하여 포스 필드를 제거할 수 있습니다.
포스 필드 사용자 정의하기
샘플 레이저 태그 게임은 플레이어가 블래스터를 선택하는 동안 피해를 입지 않도록 하기 위해 Studio의 내장 ForceField 클래스를 사용합니다. 이를 통해 플레이어가 포스 필드와 함께 스폰하기 위한 유일한 요구 사항은 SpawnLocation.Duration 속성이 0보다 큰 스폰 위치를 포함하는 것입니다. 샘플에서는 포스 필드를 활성화하기 위해 임의의 값인 9,999를 사용한 다음, ReplicatedStorage ⟩ ForceFieldClientVisuals에서 실제 지속 시간을 프로그래밍 방식으로 처리합니다.
setupHumanoidAsync와 유사하게, ForceFieldClientVisuals의 대부분의 줄은 선택 사항입니다. 예를 들어, 다음 스크립트처럼 함수의 내용을 주석 처리하면 게임은 StarterGui ⟩ ForceFieldGui의 육각형 스크립트 대신 기본 반짝이는 포스 필드를 사용합니다.
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사용자 정의 포스 필드는 새로운 ParticleEmitter가 아닌 GUI이기 때문에, ForceFieldClientVisuals 스크립트는 각 플레이어의 1인칭 시각적 요소에만 영향을 미치며, 다른 플레이어를 바라볼 때의 3인칭 시각적 요소에는 영향을 미치지 않습니다. 3인칭 시각적 요소는 기본 Roblox 모양을 유지합니다. 포스 필드를 수정하는 방법에 대한 자세한 내용은 ForceField.Visible를 참조하세요.


포스 필드는 플레이어가 스폰 및 리스폰하는 동안 적 플레이어에 대해 걱정할 필요 없이 충분한 시간을 제공하므로 유용하지만, 결국에는 주요 레이저 태그 게임 플레이를 위해 사라져야 합니다. 포스 필드 제거를 처리하는 스크립트는 ReplicatedStorage ⟩ scheduleDestroyForceField에 있으며, 세 가지 고유한 조건을 확인합니다:
- 플레이어가 블래스터를 선택한 후, 포스 필드는 플레이어가 주변에 적응할 수 있도록 충분히 지속되어야 합니다.
- 이 적응 시간 동안 포스 필드는 이점이 되어서는 안 되므로, 플레이어가 블래스터를 발사하는 순간 사라져야 합니다.
- 플레이어가 블래스터를 발사하기 전에 또는 포스 필드가 만료되기 전에 캐릭터를 리셋할 때 포스 필드는 사라져야 합니다.
scheduleDestroyForceField 스크립트의 각 체크는 이러한 조건에 대해 endForceField()를 호출합니다.
-- 플레이어가 발사할 경우 포스 필드 종료
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() 함수를 두 번 또는 세 번 호출할 수 있습니다. forceFieldEnded 불리언은 함수가 포스 필드를 한 번만 제거하려고 시도하도록 보장합니다.
local function endForceField()
if forceFieldEnded then
return
end
forceFieldEnded = true
attributeChangedConnection:Disconnect()
characterRespawnedConnection:Disconnect()
destroyForceField(player)
end클라이언트 상태 처리하기
이 섹션의 대부분은 ServerScriptService ⟩ PlayerStateHandler에 초점을 맞추고 있지만, ReplicatedStorage에도 같은 이름의 스크립트가 있습니다. 분할의 이유는 클라이언트-서버 아키텍처입니다:
클라이언트는 플레이어 상태 정보를 이해해야 하며, 이를 통해 적절하게 실시간으로 반응할 수 있습니다. 예를 들어, 올바른 사용자 인터페이스 요소를 표시하거나 플레이어가 이동하고 발사할 수 있도록 합니다.
서버는 이러한 모든 정보를 필요로 하며, 이를 통해 악용을 방지할 수 있습니다. 예를 들어, 서버는 캐릭터를 스폰하고 장비를 장착하며, 포스 필드를 비활성화하고 리더보드를 표시하는 등의 작업을 수행하기 위해 플레이어 상태가 필요합니다. 이 스크립트가 ReplicatedStorage에 있고 순수 클라이언트 측 위치에 있지 않은 이유입니다.
이 핵심 논리를 보려면, 사용자의 현재 상태를 확인한 다음 해당 상태에 대한 적절한 작업을 처리하는 함수를 호출하는 ReplicatedStorage ⟩ 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() 함수의 논리를 확인하면 플레이어가 블래스터를 선택하는 동안 클라이언트가 플레이어 이동을 비활성화하는 것을 볼 수 있습니다.
local function onSelectingBlaster()
togglePlayerCamera(true)
togglePlayerMovement(false)
setGuiExclusivelyEnabled(playerGui.PickABlasterGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
endonPlaying() 함수도 유사하게 간단합니다. 이동을 활성화하고, 메인 헤드업 디스플레이(HUD)로 전환하며, 블래스터를 활성화하고, 서버와 동일한 포스 필드 함수를 호출합니다.
local function onPlaying()
togglePlayerMovement(true)
setGuiExclusivelyEnabled(playerGui.HUDGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Ready)
scheduleDestroyForceField()
end캐릭터 리스폰하기
샘플 레이저 태그 게임은 ReplicatedStorage ⟩ PlayerStateHandler의 onTaggedOut() 상태를 통해 캐릭터를 라운드에 리스폰하는 것을 처리합니다. onSelectingBlaster() 및 onPlaying() 상태와 마찬가지로, onTaggedOut()은 playerState 속성의 변경에 따라 고유한 동작을 트리거합니다. 구체적으로, 플레이어 이동을 비활성화하고, 리스폰 UI를 표시하며, 블래스터를 비활성화합니다.
local function onTaggedOut()
-- 태그 아웃된 동안 제어 비활성화
togglePlayerMovement(false)
togglePlayerCamera(false)
setGuiExclusivelyEnabled(playerGui.OutStateGui)
-- 태그 아웃된 동안 블래스터 비활성화
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
end이 동작을 테스트하려면 Esc를 눌러 설정 탭으로 이동한 다음 캐릭터 리셋 버튼을 클릭하세요. 리스폰 화면을 트리거하면 이동하거나 카메라를 회전시키거나 블래스터를 발사할 수 없음을 알 수 있습니다.


이 스크립트는 실제로 캐릭터를 리스폰하지 않고, 단지 그들이 행동하지 못하도록 멈추고 서버가 캐릭터를 리스폰하고 있다는 시각적 피드백을 플레이어에게 제공합니다. 예를 들어, ServerScriptService ⟩ SetupHumanoid ⟩ setupHumanoidAsync ⟩ onHumanoidDied를 살펴보면, 스크립트는 PlayerState를 TaggedOut으로 설정하고(본질적으로 ReplicatedStorage ⟩ PlayerStateHandler에 알림), 몇 가지 시각적 지표를 추가합니다. 실제 리스폰 로직은 Roblox의 내장 동작입니다.
플레이어가 라운드에 리스폰할 때, 그들은 SpawnLocation.TeamColor 속성에 따라 자신의 팀의 스폰 위치에서 리스폰합니다. 리스폰 시간을 사용자 정의하려면 SetupHumanoid의 맨 위에 다음 줄을 추가할 수 있습니다. 이 기술에 대한 자세한 내용은 Players.RespawnTime을 참조하세요.
local Players = game:GetService("Players")
Players.RespawnTime = 10 -- 새 줄, 초 단위잡다한 설정
초기 설정의 일환으로, 샘플 레이저 태그 게임은 몇 가지 작지만 중요한 단계를 수행합니다:
게임에는 기본 Roblox 건강 재생을 비활성화하는 빈 스크립트인 StarterPlayer ⟩ StarterCharacterScripts ⟩ Health가 포함되어 있습니다. 이 속성의 동작에 대한 설명은 Humanoid.Health를 참조하세요.
게임은 StarterPlayer.CameraMode.LockFirstPerson 속성을 설정하여 1인칭 카메라를 사용합니다. 사용자가 1인칭과 3인칭 카메라 간에 전환할 수 있도록 하려면, Studio에서 한 번 설정하는 것뿐만 아니라 프로그래밍 방식으로 속성을 변경하고, 관점 변화에 따라 제어 및 UI를 수정해야 합니다.
이제 플레이어가 스폰하고, 블래스터를 선택하고, 1인칭 관점에서 조준할 수 있게 되었으므로, 다음 섹션에서는 라운드 기반 게임 플레이를 생성하는 스크립트에 대해 배웁니다.