서버 권한 기술

*이 콘텐츠는 AI(베타)를 사용해 번역되었으며, 오류가 있을 수 있습니다. 이 페이지를 영어로 보려면 여기를 클릭하세요.

이 가이드는 서버 권한 모델을 사용하여 고품질의 부드러운 멀티플레이어 게임을 만들기 위한 다양한 기술을 설명합니다.

예측 인스턴스 생성 (인스턴스 스티칭)

인스턴스 스티칭은 클라이언트 스크립트가 RunService:BindToSimulation() 콜백 내에서 Instances를 예측적으로 생성할 수 있도록 합니다. 클라이언트는 서버의 왕복 통신을 기다리지 않고 즉시 Instance를 생성합니다. 서버의 권한 있는 복사본이 도착하면 클라이언트에서 생성한 인스턴스와 서버의 권한 있는 복사본이 하나로 병합됩니다. 스크립트 관점에서 볼 때, Instance는 즉시 존재하며 서버와 일치합니다.

인스턴스 스티칭은 인스턴스가 가능한 한 빨리 클라이언트에서 보이고 활성 상태여야 하는 경우에 유용합니다. 서버는 클라이언트가 필요한 모든 인스턴스를 복제하지만, 이 과정은 서버 통신으로 인해 적어도 하나의 왕복 지연이 발생합니다. 예를 들어 로켓 발사기를 발사하거나 물리 제약을 생성하는 경우 스티칭이 없으면 클라이언트는 멀리서 로켓이 튀어 나오는 것을 보거나 새로운 제약이 복제될 때 약간의 떨림을 경험하게 됩니다.

기술적 동작

인스턴스 스티칭은 클라이언트와 서버 모두에서 동일한 결정론적 GUID를 생성하여 작동합니다. GUID는 생성되는 Instance 유형, 소스의 고유성(아래 참조), 현재 시뮬레이션 프레임, 매 프레임마다 재설정되는 스크립트별 호출 카운터의 네 가지 입력에서 파생됩니다.

클라이언트와 서버가 입력에 동의하면 일치하는 GUID를 생성하고 스티치가 성공합니다.

구현

인스턴스 스티칭을 활용하기 위해서는 ModuleScript에서 클라이언트 서버에서 요구되는 RunService:BindToSimulation() 콜백 내에서 Instance.new(), Instance:Clone(), 또는 Instance.fromExisting()를 호출하십시오. 귀하 쪽에서 추가로 필요한 것은 없습니다; 시스템이 GUID 할당 및 조정을 자동으로 처리합니다.

비- 시뮬레이션 액세스 속성인 Name, Size, 또는 Parent와 같은 속성을 인스턴스가 DataModel에 부모로 설정되기 전에 자유롭게 설정할 수 있습니다.

시뮬레이션 (ModuleScript) - BindToSimulation() 콜백에서 인스턴스 생성
local RunService = game:GetService("RunService")
local Simulation = {}
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local part = Instance.new("Part")
part.Name = "PredictedPart"
part.Size = Vector3.new(2, 2, 2)
part.Parent = workspace -- Part가 이제 데이터 모델에 있음; 비 시뮬레이션 액세스 변경은 이 이후에 오류가 발생합니다
-- Part는 클라이언트에서 즉시 존재하며 서버와 일치하게 조정됩니다
end)
end
return Simulation

Instance:Clone()Instance.fromExisting()는 소스 인스턴스가 클라이언트와 서버 모두에 복제되었을 때 올바르게 스티칭됩니다. 두 측 모두는 일치하는 소스 GUID에서 복제하며 일치하는 예측 GUID를 생성합니다.

시뮬레이션 (ModuleScript) - BindToSimulation() 콜백에서 인스턴스 복제
local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- 복제된 인스턴스
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- 복제된 계층 구조는 서버의 권한 있는 복사본과 스티칭됩니다
end)
end
return Simulation

위치 스무딩

예측된 동기화된 객체의 위치가 잘못 예측된 경우 시각적으로 부드럽게 만들어 재현할 수 있습니다.

  1. 시뮬레이션된 객체를 보이지 않게 만듭니다.
  2. 렌더러 객체를 질량이 없고 충돌하지 않으며 시각적으로만 추적할 수 있는 복제로 만듭니다.
  3. 렌더러 객체에 보이지 않는 시뮬레이션된 객체의 위치를 부드럽게 추적하는 스크립트를 부착합니다. 렌더링과 시뮬레이션 간의 이 분리는 렌더러 객체의 위치를 변경하여 시각적으로 부드러운 경험을 만들 수 있게 해줍니다.

다음의 샘플 Script에서 렌더링된 객체(부모)는 시뮬레이션된 객체를 부드럽게 추적합니다. 렌더링된 객체는 항상 시뮬레이션된 객체보다 약간 "뒤"에 있으며, 일반적으로 괜찮지만 특정 상황에서는 바람직하지 않을 수 있습니다.

렌더러 파트를 사용하여 BasePart 위치 부드럽게 추적
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
-- 부드럽게 추적할 객체
local smoothTarget:BasePart = workspace.SimulatedPart
-- 부드럽게 될 시각적 객체
local renderer:BasePart = script.Parent
-- 부드럽게 할 시간; 작을수록 빠름
local smoothTime = 0.07
-- 부드러운 위치를 계산하는 데 필요한 데이터 저장
local smoothVelocity = Vector3.new()
-- 렌더러 객체의 물리학 비활성화
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- 대상 객체를 부드럽게 추적
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)

축구 예제 게임은 이 기술의 변형을 사용하여 축구공의 위치 스무딩을 더욱 지능적으로 켜고 끕니다. 특히, 축구공은 시뮬레이션된 공이 렌더링된 공에서 "멀리 점프"했을 때만 위치를 부드럽게 합니다. 이 접근 방식은 두 세계의 장점을 제공합니다: 축구공은 정상적인 조건에서 시각적 지연이 없으며, 게임은 시뮬레이션된 공이 예기치 않게 새로운 위치로 점프한 이후에만 위치를 부드럽게 보간합니다. 이는 네트워크 아티팩트 또는 서버 측 변화로 인한 것입니다.

애니메이션 코드 작성

서버 권한 아래에서는 서버가 잘못된 예측을 수정할 때 클라이언트의 시뮬레이션이 롤백되고 재시뮬레이션될 수 있습니다. 롤백 중에는 애니메이션 상태가 되돌려지므로, 이전 프레임에서 캐시한 AnimationTrack가 더 이상 유효하지 않을 수 있습니다.

애니메이션 논리 미러링

모든 핵심 게임 플레이 논리와 마찬가지로 애니메이션을 제어하는 논리는 서버와 클라이언트 간에 동기화되어야 하며, 그렇지 않으면 잘못된 예측과 떨림이 발생할 수 있습니다. RunService:BindToSimulation()을 통해 함수 바인딩하는 패턴에 대해 더 자세한 내용은 시뮬레이션 동기화를 참조하십시오.

트랙 캐싱 피하기

서버 권한 스크립트에서 흔히 있는 패턴은 로드 시 AnimationTrack 객체를 캐시하고 무한히 재사용하는 것입니다. 이 패턴은 서버가 잘못된 예측을 수정할 때 서버 권한 게임에서 실패합니다. 클라이언트가 수정된 데이터를 가지고 시뮬레이션을 롤백/재생할 경우, 스크립트가 정지되었거나 대체된 트랙에 대한 참조를 여전히 보유하고 있다면 AdjustWeight() 또는 AdjustSpeed()와 같은 호출은 더 이상 시각적으로 표현되지 않는 트랙에서 작동하게 됩니다.

클라이언트에서 트랙 캐시 (신뢰할 수 없음)
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
-- 애니메이션 트랙 캐시
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)

트랙 객체를 보유하는 대신, 애니메이션 ID(또는 Animation 인스턴스)를 저장하고 필요할 때 Animator에서 라이브 트랙을 쿼리하십시오. 이를 위해 사용할 수 있는 두 가지 API가 있습니다:

  • Animator:GetTrackByAnimationId() — 특정 애니메이션 ID에 대한 현재 활성 트랙을 반환하거나, 해당 ID에 대해 활성 애니메이션이 없으면 nil을 반환합니다. 특정 애니메이션을 알고 있을 때 이 메서드를 사용하십시오.
  • Animator:GetPlayingAnimationTracks() — 모든 활성 트랙(재생 중, 페이드 아웃 중, 또는 일시 정지 중)을 반환합니다. 모든 활성 트랙을 반복해야 하는 경우 이 메서드를 사용하십시오(예: 모든 애니메이션을 멈추거나 트랙을 기준으로 찾을 때).

ModuleScript 이름이 CustomAnimate인 경우 ReplicatedStorage:

CustomAnimate
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- 애니메이션 참조 저장(로드되지 않은 트랙)
local animations = {
WalkForward = ReplicatedStorage.Animations.WalkForward,
}
local function getOrLoadTrack(animator: Animator, animation: Animation): AnimationTrack
local track = animator:GetTrackByAnimationId(animation.AnimationId)
if not track then
track = animator:LoadAnimation(animation)
end
return track
end
CustomAnimate.SyncAnimations = function(character)
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
RunService:BindToSimulation(function(dt: number)
local walkTrack = getOrLoadTrack(animator, animations.WalkForward)
if not walkTrack.isPlaying then
walkTrack.Looped = true
walkTrack.Priority = Enum.AnimationPriority.Core
walkTrack:Play()
end
walkTrack:AdjustSpeed(1 + math.cos(time()))
end)
end
return CustomAnimate

소리 및 시각 효과 재생

예측된 시뮬레이션에서 클라이언트가 발생할 것으로 예측한 이벤트에 대한 효과나 소리를 트리거 하는 것이 가능합니다. 렌더링 시스템은 잘못 예측된 효과를 "되돌릴" 준비를 해야 합니다. 예를 들어, 클라이언트는 수류탄이 폭발했다고 예측하고 입자를 생성할 수 있지만, 다른 플레이어가 수류탄을 해체했으면 클라이언트는 입자 효과를 숨겨야 합니다.

예측된 시뮬레이션을 렌더링하기 위한 좋은 전략은 시뮬레이션 루프 내에서 상태 머신 패턴을 동기화하고 렌더 단계 함수에서 상태 변화를 렌더링하는 것입니다. 다음 예제는 상태 머신 패턴으로 수류탄을 시뮬레이션합니다.

수류탄 추적을 위한 간단한 상태 머신 (ModuleScript)
local module = {}
module.GrenadeStates = {
Idle = 0,
Lit = 1,
Exploded = 2,
Defused = 3,
}
module.GrenadeExplodeTime = 3.0
module.Initialize = function(grenade)
RunService:BindToSimulation(function(deltaTime)
-- 초기 빈 수류탄 상태 설정
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- 수류탄 타이머 증가
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- 불이 붙은 수류탄 폭발
if grenadeState == module.GrenadeStates.Lit then
if timer >= module.GrenadeExplodeTime then
grenadeState = module.GrenadeStates.Exploded
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
end
end)
end
return module

이전 상태 머신이 준비되면, 상태 동기화에 따라 별도의 스크립트 내에서 RunService.RenderStepped 연결에서 수류탄 효과를 렌더링할 수 있습니다:

동기화된 수류탄 상태에 따라 입자 및 소리 렌더링
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- 수류탄 상태 표시를 위한 하이라이트 인스턴스
local highlight = Instance.new("Highlight")
highlight.Parent = grenade
highlight.FillTransparency = 1
highlight.OutlineTransparency = 1
highlight.DepthMode = Enum.HighlightDepthMode.Occluded
RunService.RenderStepped:Connect(function(deltaTime: number)
local grenadeState = grenade:GetAttribute("State")
local grenadeTimer = grenade:GetAttribute("Timer")
-- 수류탄이 불붙은 경우 입자 방출
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- 수류탄이 방금 폭발한 경우 폭발 입자 방출
if previousGrenadeState ~= grenadeState then
if grenadeState == Simulation.GrenadeStates.Exploded and grenadeTimer < 0.2 then
grenade.ExplosionEmitter:Emit(100)
grenade.ExplosionSound:Play()
end
previousGrenadeState = grenadeState
end
-- 상태 및 시간에 따라 수류탄의 하이라이트 색상 변경
if grenadeState == Simulation.GrenadeStates.Lit then
highlight.FillColor = Color3.fromRGB(255, 0, 0)
highlight.FillTransparency = 1 - (grenadeTimer / Simulation.GrenadeExplodeTime)
elseif grenadeState == Simulation.GrenadeStates.Idle then
highlight.FillTransparency = 1
elseif grenadeState == Simulation.GrenadeStates.Exploded then
highlight.FillTransparency = 1
elseif grenadeState == Simulation.GrenadeStates.Defused then
highlight.FillColor = Color3.fromRGB(0, 255, 125)
highlight.FillTransparency = 0.5
end
end)

네트워크 지연을 고려한 설계

특정 게임플레이 메커니즘은 다른 메커니즘보다 네트워크 멀티플레이어에 더 잘 적응합니다. 플레이어는 다른 플레이어가 행동을 수행하는 시점과 해당 플레이어의 입력을 받는 시점 사이에 항상 약간의 지연이 있습니다. 초고속 멀티플레이어 게임을 만들기 위한 가장 좋은 방법은 이러한 제한 사항을 염두에 두고 게임을 설계하는 것입니다.

예를 들어 플레이어 이동에서 가속이 느린 게임은 가속이 높은 게임보다 더 부드럽게 보입니다. 이는 네트워크 지연으로 인해 입력으로 인해 발생하는 위치 차이가 높은 가속 게임에 비해 적기 때문입니다.

또 다른 예로, 플레이어가 입력을 눌러 대폭발을 즉시 유발할 수 있는 게임플레이 메커니즘은, 폭발이 입력 후 지연되는 경우보다 더 많은 네트워크 아티팩트를 갖게 됩니다. 이는 점화 및 굴착 폭발을 유도하는 대신 핀 효과에서 다시 시뮬레이션 하게 하여 더 주목할만한 네트워크 아티팩트를 줄입니다.

다른 플레이어 입력 예측

기본적으로 Roblox는 각 클라이언트의 입력을 다른 클라이언트에게 전달하지 않습니다. 이것이 귀하의 게임에 적합한지는 게임의 설계에 달려 있습니다:

  • 기본 휴머노이드 이동의 경우, 기본 동작은 다른 플레이어 캐릭터의 움직임이 권한 있는 서버 상태에서 추출되지 않음을 의미하며, 결과적으로 다른 플레이어 캐릭터는 잘못된 예측을 하지 않지만 약간 과거에 렌더링됩니다.
  • 반면 레이싱 게임의 경우 기본 동작은 클라이언트가 다른 플레이어가 스로틀이나 다른 입력을 적용하고 있는지 알지 못함을 의미하며, 그로 인해 다른 차량은 실제로 앞에 있다 하더라도 로컬 플레이어 뒤에 나타날 수 있습니다. 이를 완화하기 위해 서버에 속성 형태로 플레이어 입력을 저장하고, 다음 코드 샘플과 레이싱 템플릿에서 시연한 것처럼 RunService:BindToSimulation()를 사용하여 이러한 동기화된 속성에서 작동할 수 있습니다. 이 접근 방식은 속성을 입력으로 사용하여 플레이어 입력을 완전히 복제할 수 있습니다.
속성에 플레이어 입력 저장 (ModuleScript)
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local module = {}
module.storePlayerInput = function(player:Player, humanoidRootPart:BasePart)
local inputContext:InputContext = player.PlayerGui.InputContext
local throttle = inputContext.DefuseAction:GetState()
humanoidRootPart:SetAttribute("Throttle", throttle)
-- 다른 입력을 속성에 기록...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- 서버에서 모든 클라이언트로 입력 전달
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- 로컬 플레이어 입력을 속성으로 기록
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- 게임에 대한 입력으로 속성 사용
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- 플레이어의 차량에 스로틀 적용
end
end
end)
end
return module

디버깅

서버 권한 게임을 디버깅하는 데 사용할 수 있는 새로운 도구와 기법이 있습니다.

서버 권한 시각화

CtrlShiftF6 (Windows) 또는 ShiftF6 (Mac)을 눌러 Studio의 서버 권한 시각화기를 열면 여러 핵심 정보 조각을 보여줍니다:

세부사항설명
인스턴스 예측 성공률지난 8초 동안 올바르게 예측된 인스턴스의 비율입니다.
입력 수락률서버에서 제 시간에 도착한 모든 플레이어 입력의 비율입니다. 늦은 입력은 이 숫자를 낮추게 됩니다.
클라이언트-서버 스텝 델타클라이언트와 서버 간의 프레임 수입니다. 클라이언트가 조인하는 시간도 포함됩니다. 이 숫자의 안정성은 서버에 대한 연결의 안정성을 나타냅니다.
RCC 하트비트 FPS서버에서의 시뮬레이션의 프레임 속도입니다. 이 숫자가 59 이하로 떨어지면 서버가 시뮬레이션을 따라갈 수 없고 게임 품질이 저하됩니다.
예측된 인스턴스 수클라이언트가 예측하고 있는 인스턴스의 수입니다.
입력 드롭 원인 수

서버가 각 이유로 입력을 드롭한 횟수입니다:

  • [x] 너무 오래됨 — 입력이 늦게 도착하여 네트워크에 문제가 생기거나 클라이언트가 시뮬레이션을 따라갈 수 없었습니다.
  • [x] 순서가 틀림 — 입력이 재배열되어 삭제된 네트워킹 오류가 발생했습니다.
  • [x] 버퍼가 가득 참 — 서버가 입력을 버퍼링할 수 없었습니다. 네트워크가 갑자기 개선되었거나 서버가 시뮬레이션을 따라갈 수 없었습니다.

시뮬레이션 반경

자동 예측(Enum.PredictionMode.Automatic)에 의존하는 경우, Studio의 설정에서 영역이 활성화되었는가를 활성화하여 플레이어 캐릭터 주변의 예측 반경을 시각화할 수 있습니다 (AltS Windows; S Mac). 녹색 실린더는 인스턴스가 예상되는 반경을 나타내며, 장치의 성능 특성에 따라 반경이 커지고 줄어들 수 있습니다.

서버 권한이 실행 중인 플레이어 캐릭터 주변의 시뮬레이션 반경
©2026 Roblox Corporation. Roblox 및 Roblox 로고, 'Powering Imagination'은 미국 및 기타 국가 내 당사의 등록 및 미등록 상표입니다.