네트워크 시뮬레이션 루프
소개
Fusion은 사용 중인 네트워크 모드에 관계없이 일관된 시간 단계에서 개별 틱-기반 시뮬레이션을 실행합니다. 이를 처리하는 전반적인 프로세스를 "네트워크 시뮬레이션 루프"라고 합니다. 모든 로직은 NetworkBehaviour
또는 SimulationBehaviour
의 하위 클래스로 작성되며, 이는 이 시뮬레이션 루프의 중심이 일반 유니티의 게임 오브젝트임을 의미합니다.
Fusion은 월드 상태의 스냅샷을 앞으로 이동시키는 것 외에도 로컬 플레이어 입력이나 물리 기반 객체를 기반으로 미래의 상태를 예측할 수 있습니다(물리에 대한 클라이언트 측 예측을 설정하는 것은 선택 사항입니다).
현재 알려진 데이터와 사용자 정의 코드(예: 탄도 궤적 추정)를 기반으로 모든 객체에 대한 클라이언트 측 예측 로직을 작성할 수 있습니다.
이 문서에서는 Fusion의 시뮬레이션 루프가 작동하는 방식과 관련된 가장 중요한 개념에 대해 자세히 설명합니다.
틱
Fusion은 시뮬레이션을 시간별로 직접 구동하는 대신 틱 이라고 하는 추상적인 단위의 시간을 사용하여 처리합니다. 틱은 특정 클라이언트 및 호스트에 전달되는 실제 시간과 분리됩니다. 하드웨어 시간 대신 틱을 사용하면 네트워크 세션의 모든 클라이언트가 "시간" 개념에 대한 공통 참조 프레임을 공유할 수 있습니다. 틱은 움직이지 않으며 정밀도 문제에 영향을 받지 않으므로 여러 네트워크 시스템의 미래 및 과거 이벤트를 정확하게 추론하는 데 중요합니다.
각 틱 사이의 시간 간격은 NetworkProjectConfig
의 Simulation > Tick Rate
에 의해 정의됩니다. 틱 속도는 헤르츠(Hz)로 정의되므로 시간 간격이 60분의 1초의 델타와 같습니다. 이는 시스템이 이전과 현재 틱 사이에 실제 시간이 얼마나 지났는지에 관계없이 1/60초의 시간 간격을 사용하여 게임 상태를 진행한다는 것을 의미합니다. 코드에서 시간 간격은 NetworkRunner.DeltaTime
속성을 통해 접근할 수 있습니다.
중요: 시뮬레이션 루프에 사용되는 틱 속도는 렌더링 속도(초당 프레임 수)와 다릅니다!
각 틱에는 해당 시점의 세계의 "진실"을 명시적으로 설명하는 해당 상태 스냅샷이 있습니다. 다음 틱에 대한 새 스냅샷을 생성하기 위해 Fusion은 모든 SimulationBehaviour
및 NetworkBehaviour
에서 FixedUpdateNetwork()
를 호출하여 게임 객체의 전체 상태 부분을 업데이트합니다. Fusion은 주어진 GameObject
에 대한 상태 권한
을 갖는 것을 말합니다.
주어진 객체에 대해 상태 권한
을 갖는 머신은 딱 한대 입니다.
- 클라이언트/서버 설정에서는 항상 서버입니다.
- 각 클라이언트가 자신의 개체를 소유하는 클라이언트 권한 설정에서 각 클라이언트는 일반적으로 자신이 만든 객체에 대한 권한을 유지합니다.
입력 처리
Fusion은 입력을 사용하여 클라이언트 측 예측을 만들어줍니다. 클라이언트는 로컬(오래된) 정보와 로컬 플레이어의 입력을 기반으로 다음 서버 상태를 예측합니다. 실제 상태가 수신되면 클라이언트는 새로운 미래 상태를 재 시뮬레이션하기 위한 기준으로 이 새로운 실측 정보를 사용할 것입니다.
예측을 가능하게 하기 위해서, 입력 처리는 2 단계로 나누어집니다.
- 입력 폴링: Fusion은 특정 객체에 대한 입력 권한이 있는 클라이언트의 로컬 하드웨어에서 입력을 수집합니다.
- 입력 소비: 입력은 로컬 시뮬레이션에 의해 적용되고 권한 있는 시뮬레이션에 포함될 서버와 공유됩니다.
입력 정의, 폴링 및 사용에 대한 자세한 내용은 매뉴얼의 네트워크 입력
페이지를 참조하십시오.
예측
예측에는 클라이언트의 현재 로컬 정보를 기반으로 미래의 서버 상태를 예측하는 작업이 포함됩니다. 서버에서 받은 실제 게임 상태 스냅샷을 기반으로 한 로컬 정보는 네트워크 디바이스 간 지연 시간으로 인해 항상 구식이 됩니다. 클라이언트는 서버 게임 상태에 맞춰 로컬 게임 상태를 유지하기 위해 예측을 사용합니다. 이는 플레이어 환경에 미치는 네트워크 지연 시간의 영향을 완화하기 위해 필요합니다. 네트워크 지연 시간은 항상 서버에서 클라이언트까지 왕복하는 시간과 동일합니다. 별개의 틱 측면에서 합리적입니다.
다음과 같은 특성을 가진 시나리오가 있다고 가정해 보겠습니다:
- 현재 서버 틱: 100
- 틱 속도: 1/60 초
- 클라이언트 1의 레이턴시가 4 틱과 동일( 2틱이 서버에서 클라이언트로 + 2틱이 클라이언트에서 서버로)
- 클라이언트 2의 레이턴시가 6 틱과 동일 (3틱이 서버에서 클라이언트로 + 3틱이 클라이언트에서 서버로)
클라이언트는 각각의 대기 시간을 알고 있지만, 이를 따라잡기 위해 예측해야 하는 틱 수를 알려주는 것은 서버입니다. 예측 기능을 통해 클라이언트는 지정된 틱에 대해 서버에서 입력을 필요로 하는 시점에 입력을 보낼 수 있습니다. 클라이언트가 입력 권한을 가진 객체는 로컬 입력(예: 플레이어 캐릭터)을 사용하여 예측되므로 즉각적인 응답에 대한 착각을 일으킵니다.
예를 들어 클라이언트 1은 플레이어 캐릭터를 이동할 때 서버에서 받은 마지막 틱을 기준으로 미래 상태를 예측합니다. 이 예측을 기반으로 클라이언트 1은 서버 미래에 대해 2개의 틱(즉, 서버 미래에 대해)을 수행할 계획을 서버에 알려줍니다. (예, 틱 102)를 선택합니다. 이렇게 하면 입력이 회선을 통해 전송되고 서버가 제시간에 수신되어 틱 102의 유효 상태를 시뮬레이션하는 동안 사용할 수 있는 충분한 시간이 남습니다. 클라이언트 1의 플레이어 이동을 방해하는 요소가 없는 한 클라이언트 1의 예측된 틱 102와 서버의 유효한 틱 102는 동일한 상태가 됩니다.
클라이언트 1이 대기 시간을 보상하기 위해 앞으로 충분한 틱을 예측하도록 하기 위해 서버는 항상 서버 상태보다 두 개 이상의 틱이 선행되도록 지시합니다. 반면에 클라이언트 2는 대기 시간이 길기 때문에 미래의 3개 틱을 예측해야 합니다.
수치에서 알 수 있듯이 클라이언트 1과 클라이언트 2는 서버에 대한 상대적인 지연으로 인해 정확히 같은 상태가 아닙니다. 클라이언트들은 다른 클라이언트들의 정보가 아직 도달하지 않았기 때문에 클라이언트들의 "진실"이 잘못되었다는 사실을 알고 있습니다. 여기서 복제와 조정이 이루어집니다.
복제
Fusion은 작은 메모리 버퍼에서 상태 진행을 관리하고 시뮬레이션합니다. Fusion은 서버로부터 상태 스냅샷을 수신하면 메모리 버퍼에 스냅샷의 압축을 풉니다. 버퍼에 보관된 현재 상태는 예측 상태를 기반으로 하므로, 이 상태를 스냅샷의 상태로 바꾸면 클라이언트가 서버에서 받은 스냅샷과 연결된 틱에 효과적으로 다시 스냅됩니다. 그런 다음 Fusion은 수신된 상태와 클라이언트의 로컬 예측 상태 사이의 모든 틱에 대해 시뮬레이션 루프를 실행합니다. 이렇게 하면 더 정확한 예측 상태를 제외하고 스냅샷을 수신하기 전의 상태로 돌아갑니다.
조정
클라이언트가 보다 최신 상태(따라서 더 정확한 상태)의 새 스냅샷을 수신하면 클라이언트는 로컬 상태와 해당 상태를 조정합니다. 조정은 클라이언트가 수신한 마지막 서버 스냅샷을 복제하고 서버 스냅샷과 연결된 틱에서 현재 체크 표시까지 로컬 게임 상태를 다시 시뮬레이션하여 수행됩니다.
결과적으로 서버는 항상 견고한 조정 상태에 있으며 예상치 못한 이벤트가 발생할 경우 클라이언트는 점차적으로 자체 조정됩니다. 이 절차를 Fusion에서는 재시뮬레이션 이라고 하는데, 이는 클라이언트 상태를 서버와 조정하기 위해 새로운 검증된 상태에 따라 이전 입력의 효과를 다시 시뮬레이션하기 때문입니다.
이는 서버에서 클라이언트 1과 2가 충돌한 경우 특히 중요합니다. 지정된 틱에 대한 객체의 종료 상태를 독점적으로 결정하므로, 서버가 해당 위치를 확인합니다. 이것이 서버가 상태 권한이라고 하는 이유입니다.
예제
서버는 오래된 데이터의 지속적인 스트림을 클라이언트로 보냅니다. 클라이언트 1에서 검증된 틱 100이 수신되면 검증된 틱 100 상태를 메모리 버퍼로 복제하여 이전에 예측된 틱 100을 덮어씁니다.
- 예기치 않은 일이 발생하지 않으면 로컬에서 예측된 틱 100이 이미 올바른 상태일 것입니다. 즉, 클라이언트 1과 서버가 완벽한 동기화 상태임을 의미합니다. 단, 클라이언트 1은 서버보다 2개의 틱이 앞서 있습니다.
- 확인된 틱이 클라이언트 1의 예측과 일치하지 않으면 검증된 틱 100이 수정된 상태로 사용됩니다.
두 시나리오 모두 클라이언트는 새로운 틱 103에 도달하기 위해 100, 101, 102부터 순차적으로 입력을 다시 적용합니다.
클라이언트 1의 관점에서 시간을 앞당겨 수정한 틱 101을 받고 102와 103에 대한 입력을 적용하고 수정된 상태에 도착하면 틱 104가 됩니다.
클라이언트 2는 시뮬레이션 루프 동안 동일한 절차를 거칩니다. 유일한 차이점은 서버에서 검증된 틱 1 틱을 수신하는 데 약간의 지연이 있다는 것이며, 이는 서버 지연 시간이 더 길기 때문에 이를 보완할 수 있습니다.
FixedUpdateNetwork()
현재 틱 상태를 다음 틱 상태로 발전시키기 위해 Fusion은 씬의 모든 SimulationBehaviour
및 NetworkBehaviour
컴포넌트에 대해 FixedUpdateNetwork()
를 호출합니다. 복제 및 예측 프로세스 중에 모두 FixedUpdateNetwork()
가 호출되므로 로컬 상태는 네트워크 상태에서 파생 되어야 합니다. FixedUpdateNetwork
로부터 로컬 상태를 적용 진행형 델타 업데이트를 하면 너무 많은 변경 사항이 적용될 가능성이 높습니다.
스냅샷 보간
일반적으로 각 클라이언트는 단일 또는 선택된 소수의 GameObjects
에 대한 입력 권한만 가지고 있으며, 나머지 GameObjects
는 스냅샷 업데이트를 통해 클라이언트에 상태를 전달합니다. 예측된 상태와 달리 스냅샷은 서버에서 보내는 시간과 클라이언트가 수신 및 복제하는 시간 사이에 약간의 지연 시간이 있기 때문에 항상 서버보다 늦습니다.
Fusion은 이러한 "원격"으로 제어되는 GameObjects
를 Proxies 라고 부릅니다.
프록시의 현재(시각적) 상태에 스냅샷을 그대로 적용하면 클라이언트의 렌더링 속도 대신 시뮬레이션의 틱 속도로 애니메이션 및 업데이트되는 것으로 나타납니다. 다음과 같은 여러 가지 이유로 이 문제를 피해야 합니다.
- 대역폭을 절약합니다. 시뮬레이션은 렌더링보다 훨씬 낮은 주파수로 실행하는 것이 바람직합니다. 예를 들어 30Hz 네트워크 시뮬레이션 주파수와 120Hz 렌더링 주파수가 있습니다.
- 네트워크 패킷이 일관된 속도로 도착하지 않습니다. 게임의 렌더링 빈도를 랜덤 측면이 큰 이벤트로 묶으면 화면이 끊기는 현상이 발생합니다.
네트워크 패킷 빈도를 렌더링의 새로 고침 빈도에서 분리하는 동시에 부드러운 상태 전환을 제공하기 위해 Fusion은 두 스냅샷 사이에 "렌더 상태"를 보간합니다. 보간은 틱보다는 실시간으로 부드럽게 해주는 것을 목표로 하며, 이상적으로는 사용 가능한 가장 최근의 두 스냅샷을 기반으로 합니다. 그러나 스냅샷이 일정한 속도로 도착하지 않을 수 있습니다. 따라서 작은 차이(버퍼)로 인해 이 문제가 발생하지 않을 경우 일정하지 않은 네트워크 스냅샷을 일정 속도 보간법으로 처리할 위험이 있습니다.
Fusion의 많은 장점 중 하나는 현재 네트워크 상태에 따라 필요한 오프셋과 버퍼링을 조정하는 보간 알고리즘입니다. 이는 클라이언트에게 가능한 짧은 대기 시간을 제공하면서도 부드러운 시각적 효과를 제공합니다.
Back to top