서버 권한 기술

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

이 가이드는 서버 권한 모델을 사용하여 고품질 및 원활한 멀티플레이어 게임을 만드는 다양한 기술을 설명합니다.

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

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

인스턴스 스티칭은 인스턴스가 가능한 한 빨리 클라이언트에서 보이고 활성화되어야 하는 경우에 유용합니다. 서버는 클라이언트가 필요한 모든 인스턴스를 복제하게 되지만, 이 과정은 서버 통신으로 인해 최소한 하나의 왕복 지연이 발생합니다. 예로는 로켓 발사기를 발사하거나 물리적 제약을 만드는 경우가 있습니다 — 스티칭이 없으면 클라이언트는 멀리서 로켓이 갑자기 나타나거나, 새로운 제약이 그들에게 복제될 때 약간의 떨림을 볼 것입니다.

기술적 동작

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

클라이언트와 서버가 입력에 동의하면 일치하는 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 -- 파트가 이제 DataModel에 있음; 이 후 비시뮬레이션 액세스 변경은 오류를 발생시킬 수 있음
-- 클라이언트에서 즉시 존재하고 서버와 조정될 것입니다
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 Workspace = game:GetService("Workspace")
-- 부드럽게 추적할 객체
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에 대해 현재 활성 트랙을 반환하거나, 활성 애니메이션이 없으면 nil을 반환합니다. 특정 애니메이션을 찾을 때 이를 사용하십시오.
  • Animator:GetPlayingAnimationTracks() — 모든 활성 트랙(재생 중, 페이드 아웃 중 또는 일시 정지 중)을 반환합니다. 활성 상태를 반복할 필요가 있을 때 이를 사용하십시오(예를 들어 모든 애니메이션을 중지하거나 특정 기준으로 트랙을 찾기 위해).

ModuleScript이며 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 Workspace = game:GetService("Workspace")
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의 설정에서 **지역 사용 가능 여부(Are Regions Enabled)**를 켜면 플레이어 캐릭터 주변의 예측 반경을 시각화할 수 있습니다(AltS on Windows; S on Mac). 초록색 실린더는 인스턴스가 예측되는 캐릭터 주변의 범위를 나타내며, 반경은 장치의 성능 특성에 따라 커지거나 작아집니다.

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