지연 보상
개요
서버 모드
와 호스트 모드
에서만 사용할 수 있습니다지연 보상은 빠른 속도의 멀티플레이어 게임에서 근본적인 문제를 해결합니다. 즉, 클라이언트를 완전히 신뢰할 수 없는 상황에서도 클라이언트에게 '보이는 대로' (What You See Is What You Get) 경험을 제공하는 것입니다.
문제는 네트워크상의 어느 머신도 게임의 정확히 동일한 틱에 있지 않다는 점입니다. 한 클라이언트가 보는 것과 그에 기반해 행동하는 것은 오직 자신의 관점에서만 100% 정확합니다. 대표적인 예로, 먼 거리의 대상을 정확하게 맞추는 샷 감지를 들 수 있습니다. — 클라이언트가 조준선에 목표를 정확히 포착하고 있더라도, 실제로는 이미 목표가 이동한 상태일 수 있습니다.
- 만약 권한을 가진 서버가 오직 자신의 세계 인식만을 기반으로 히트 감지를 한다면, 아무도 의도적으로 타격하지 않을 것입니다.
- 반면, 클라이언트에게 권한을 부여하여 자신이 무엇을 맞췄는지 서버에 전달하도록 한다면, 시스템은 단순한 게임 파괴적 익스플로잇에 매우 취약해집니다.
지연 보상을 통해 서버는 각 클라이언트의 관점에서 잠시 동안 세계를 바라보고, 그들이 실제로 불가능해 보이는 샷을 할 수 있는 위치에 있었는지 판단할 수 있습니다. 불행하게도, 이 과정은 대상이 플레이어의 예상과 달리 이미 안전하게 벽 뒤로 숨어있다고 느껴지더라도 맞게 될 수 있음을 의미합니다. 그러나 이러한 현상은 눈에 띄게 될 가능성이 훨씬 적습니다.
Fusion은 히트박스가 이전에 어디에 위치했는지의 기록을 보관하며, 각 클라이언트의 시점이 현재 서버 상태에 비해 얼마나 뒤처져 있는지 알고 있습니다. 이 기록을 사용하여 Fusion은 과거의 레이캐스트를 통해 지연을 보상할 수 있습니다.
초고정밀을 위해, Fusion은 지연 보상을 한 단계 더 진행합니다. AAA 게임의 프레임률은 보통 네트워크 틱률보다 높기 때문에, 플레이어가 화면에서 실제로 보는 것은 개별 틱이 아니라 두 틱 사이의 보간 결과입니다. Fusion은 지연 보상이 적용된 레이캐스트가 두 틱 사이에서 정확히 어느 정도 진행되었는지 알고 있으며, 이를 사용하여 서브 틱(sub-tick) 레이캐스트 정확도를 달성할 수 있습니다.
지연 보상 기능
Fusion을 사용하는 게임 개발자에게 이 모든 마법은 거의 보이지 않습니다. 필요한 것은 미리 제작된 HitboxRoot
와 Hitbox
컴포넌트뿐입니다.
Hitbox Root
지연 보상 히트박스를 네트워크 오브젝트에 설정하기 위해서는, 먼저 게임 오브젝트의 최상위 노드에 HitboxRoot
컴포넌트를 부착해야 합니다. HitboxRoot
는 게임 오브젝트 및 자식 오브젝트에서 발견되는 모든 Hitbox
컴포넌트를 그룹화하는 역할을 합니다.
HitboxRoot
는 또한 히트박스들을 위한 경계 구(sphere)를 제공하는데, 이는 지연 보상 시스템의 브로드페이즈 데이터 구조에서 사용됩니다. 이 볼륨은 BroadRadius
와 Offset
필드를 통해 구성할 수 있으며, 예를 들어 애니메이션으로 인해 오브젝트의 히트박스가 변화할 수 있는 경우를 포함하여 그룹화된 모든 히트박스를 포함해야 합니다.
Hitbox
각 Hitbox
는 지연 보상을 통해 쿼리 할 수 있는 단일 볼륨을 나타냅니다. 네트워크 게임 오브젝트에 지연 보상 히트박스를 설정하려면 다음 단계를 따릅니다:
게임 오브젝트의 최상위 노드에
HitboxRoot
컴포넌트가 필요합니다.일반 유니티
Collider
컴포넌트를 가진 동적 오브젝트는 모든Hitbox
노드 및 정적 지오메트리와는 별도의 레이어에 배치해야 합니다. 이는 동적 Collider에 의한 충돌 없이도 정적 지오메트리에는 지연 보상 레이캐스트가 히트박스를 통해 정상적으로 감지되도록 하는 가장 빠르고 신뢰할 수 있는 방법입니다. 동적 Collider를 완전히 제거하는 것은 물리 상호작용에 필요하므로 불가능하지만, 동적 오브젝트에 대한 지연 보상 히트 감지는 전적으로 Hitbox에 의존해야 합니다. 해결책은 동적 Collider를 지연 보상 레이캐스트에서 무시할 수 있는 레이어에 배치하는 것입니다.
주의 히트박스는 자신이 정의된 게임 오브젝트의 레이어를 사용합니다. 같은 HitboxRoot
아래에 그룹화되어 있더라도, 서로 다른 히트박스는 동일한 레이어를 공유할 필요가 없습니다. 히트박스는 등록될 때 그 레이어가 캐시 되며, 이후 레이어가 변경될 경우에는 Hitbox.SetLayer(int layer)
메서드를 사용하여 새로운 레이어를 올바르게 변경하고 캐시 해야 합니다.
주의 하나의 HitboxRoot
당 최대 31개의 Hitbox
노드가 존재할 수 있습니다. 하나의 오브젝트나 프리팹에 더 많은 자식 노드가 필요한 경우, 계층 구조를 분할하여 여러 루트에 분산시켜야 합니다.
히트박스 계층 구조의 구체적인 구성은 각 게임의 필요에 따라 완전히 달라질 수 있습니다.
쿼리
Fusion의 지연 보상 API에는 PhysX와 매우 유사한 구문으로 레이캐스트, 구체 겹침(sphere overlap) 및 박스 겹침(box overlap)과 같은 물리 쿼리를 지원하는 기능이 포함되어 있습니다. 특정 Hitbox
가 가졌던 정확한 위치와 회전을 쿼리 하는 것도 가능합니다.
C#
using System.Collections.Generic;
using Fusion;
using UnityEngine;
public class LagCompensationExampleBehaviour : NetworkBehaviour {
public float rayLength = 10.0f;
public float cooldownSeconds = 1.0f;
public LayerMask layerMask = -1;
[Networked]
public TickTimer ActiveCooldown { get; set; }
private readonly List<LagCompensatedHit> _hits = new List<LagCompensatedHit>();
public override void FixedUpdateNetwork() {
if (GetInput<NetworkInputPrototype>(out var input)) {
if (ActiveCooldown.ExpiredOrNotRunning(Runner) && input.Buttons.IsSet(NetworkInputPrototype.BUTTON_FIRE)) {
// reset cooldown
ActiveCooldown = TickTimer.CreateFromSeconds(Runner, cooldownSeconds);
// perform lag-compensated query
Runner.LagCompensation.RaycastAll(transform.position, transform.forward, rayLength, player: Object.InputAuthority, _hits, layerMask, clearHits: true);
for (var i = 0; i < _hits.Count; i++) {
// proceed with gameplay logic
}
}
}
}
}
player
매개변수는 쿼리가 어느 클라이언트의 관점에서 해결되어야 하는지를 지정하며, 보통 이 값은 히트스캔/투사체를 제어하는 Object.InputAuthority
에 해당합니다. 즉, 레이캐스트는 해당 입력을 폴링 한 플레이어 클라이언트가 보는 시간 프레임에 맞춰 데이터를 기반으로 수행됩니다. 이 모든 것은 자동으로 이루어지며, 복잡한 수학 계산을 할 필요가 없지만, 정확한 틱 및 보간 파라미터를 제공하는 옵션도 있습니다.
IncludePhysX
플래그가 히트 옵션에 지정되면, 지연 보상 레이캐스트는 일반 PhysX 콜라이더도 쿼리 할 수 있습니다. 단, 이 경우 일반 유니티 콜라이더는 지연 보상이 적용되지 않고, 쿼리 해결 시점의 현재 상태로 나타납니다.
주의: 동일한 유니티 인스턴스에서 여러 피어가 실행될 때, 각 피어의 물리 씬은 Runner.GetPhysicsScene
또는 GetPhysicsScene2D
를 통해 각각 검색할 수 있습니다. 이는 일반 Unity 콜라이더를 포함하는 지연 보상 쿼리에 의해 자동으로 수행되지만, 멀티 피어 환경에서 일반 물리 쿼리를 수행할 때는 주의가 필요합니다.
서브 틱 정확도
기본적으로 지연 보상 쿼리는 클라이언트의 보간 시점에서 사용되는 "From" 또는 "To" 상태에 대해 해결됩니다. 이는 보간 정규화 계수(alpha)와 시각화가 어느 상태에 더 가까운지에 따라 결정됩니다.
많은 게임에서는 이 정도 정확도가 충분할 수 있으나, 히트 옵션에 SubtickAccuracy
플래그를 포함하여 쿼리 하면, 입력이 폴링 된 시점에 플레이어가 실제로 본 보간 계수를 사용해 두 틱 사이의 상태를 쿼리함으로써 더욱 정밀한 쿼리 정확도를 달성할 수 있습니다.
C#
Runner.LagCompensation.OverlapSphere(position, radius, player: Object.InputAuthority, hits, options: HitOptions.SubtickAccuracy);
히트 필터링
레이어 마스크와 플래그
모든 지연 보상 쿼리는 레이어 마스크를 사용하여 어떤 레이어를 고려할지 정의하고, 다른 레이어에 있는 히트박스를 필터링합니다. hit options에 IgnoreInputAuthority
플래그를 포함하면, 쿼리를 수행하는 입력 권한을 가진 오브젝트에 속한 모든 히트박스가 자동으로 무시됩니다.
C#
// this can be cached or defined on the Inspector with a LayerMask field
var layerMask = LayerMask.GetMask("Player", "Destructible");
var options = HitOptions.IgnoreInputAuthority;
Runner.LagCompensation.Raycast(transform.position, transform.forward, rayLength, player: Object.InputAuthority, out var hit, layerMask, options);
필터링 콜백
브로드페이즈 해상 단계에서 발견된 모든 HitboxRoot
항목을 사전 처리(pre-process)하기 위한 콜백을 제공할 수도 있어, 보다 게임에 특화된 필터링 로직을 적용할 수 있습니다.
C#
using System.Collections.Generic;
using Fusion;
using Fusion.LagCompensation;
public class MyBehaviourFoo : NetworkBehaviour {
private readonly List<LagCompensatedHit> _hits = new List<LagCompensatedHit>();
private PreProcessingDelegate _preProcessingCachedDelegate;
private void Awake() {
// Caching a delegate avoids recurrent delegate allocation from method.
// Using a lambda expression (not a closure) will also prevent that (delegate cached by compiler).
_preProcessingCachedDelegate = PreProcessHitboxRoots;
}
public override void FixedUpdateNetwork() {
if (GetInput<NetworkInputPrototype>(out var input) && input.Buttons.IsSet(NetworkInputPrototype.BUTTON_FIRE)) {
var hitsCount = Runner.LagCompensation.RaycastAll(transform.position, transform.forward, 10, Object.InputAuthority, _hits, preProcessRoots: _preProcessingCachedDelegate);
if (hitsCount > 0) {
// proceed with gameplay logic
}
}
}
private static void PreProcessHitboxRoots(ref Query query, List<HitboxRoot> candidates, HashSet<int> preProcessedColliders) {
// HB root candidates can be iterated over (in reverse order, in order to remove entries while iterating)
for (var i = candidates.Count - 1; i >= 0; i--) {
var root = candidates[i];
if (root.name == "Please Ignore Me") {
// removed roots will not be processed any further
candidates.RemoveAt(i);
continue;
}
// it is possible to iterate over the hitboxes of each root
for (var j = 0; j < root.Hitboxes.Length; j++) {
var hb = root.Hitboxes[j];
// e.g. bypass the layer mask and Hitbox Active-state checks
if (hb.name == "Always Narrow-Check Me") {
preProcessedColliders.Add(hb.ColliderIndex);
}
}
}
}
}
오브젝트는 지연 보상 쿼리와 함께 사용자 정의 인자로도 전달될 수 있으며, 이를 통해 사전 처리 메서드나 람다가 클로저를 생성하지 않고 자체 포함되도록 할 수 있습니다.
C#
var query = SphereOverlapQuery.CreateQuery(transform.position, 10.0f, player: Object.InputAuthority,
preProcessRoots: (ref Query query, List<HitboxRoot> candidates, HashSet<int> preProcessedColliders) => {
if (query.UserArgs is MyCustomUserArgs myUserArgs) {
Log.Info($"User Arg: {myUserArgs.Arg}");
}
});
query.UserArgs = new MyCustomUserArgs { Arg = 42 };
var hitCount = Runner.LagCompensation.ResolveQuery(ref query, _hits);
2D 게임에서의 지연 보상
현재, Hitbox
는 3D 형태(구 또는 박스)로만 기술할 수 있습니다. 그러나 이 형태들은 위치를 고정된 평면(예: XY 평면)에서 배치하고 쿼리함으로써 2D 원이나 박스를 모방할 수 있습니다.
C#
if (Runner.LagCompensation.Raycast(transform.position, Vector2.right, length: 10, Object.InputAuthority, out var hit)) {
if (hit.Hitbox.TryGetComponent(out NetworkRigidbody2D nrb)) {
nrb.Rigidbody.AddForceAtPosition(Vector2.up, hit.Point);
}
}
주의 쿼리 옵션에서 IncludePhysX
플래그를 사용하면 항상 Unity의 3D 물리 엔진(PhysX)을 쿼리 합니다. 일반 2D 콜라이더를 쿼리 하기 위해서는, 해당 피어의 Physics2D 씬에서 별도의 쿼리를 수행해야 합니다.
C#
var hitboxHitsCount = Runner.LagCompensation.RaycastAll(transform.position, Vector2.right, 10, Object.InputAuthority, hitboxHits);
var colliderHitsCount = Runner.GetPhysicsScene2D().Raycast(transform.position, Vector2.right, 10, results: colliderHits);
Back to top