24 분 소요


Networked Physics by Glenn Fiedler을 정리한 글입니다.

Glenn Fiedler 는 Network Next 의 설립자이자 CEO입니다 . Network Next 는 프리미엄 네트워크 전송을위한 마켓 플레이스를 만들어 게임용 인터넷을 수정하고 있습니다.


Networked Physics

Introduction to Networked Physics

소개

이 시리즈에서는 결정론적 잠금 단계, 스냅 샷 보간 및 상태 동기화의 세 가지 방법으로 물리 시뮬레이션을 네트워크화 할 것입니다.

시작하기 전에 이 시리즈에서 네트워크로 연결할 물리 시뮬레이션을 살펴 보겠습니다.



여기에서는 오픈 소스 물리 엔진 ODE 에서 큐브의 간단한 시뮬레이션을 설정했습니다.
플레이어는 질량 중심에 힘을 가하여 움직입니다. 물리 시뮬레이션은 이 선형 모션을 취하고 큐브가 지면과 충돌할 때 마찰을 계산하여 롤링 및 텀블링 모션을 유도합니다.

이것이 구 대신 큐브를 선택한 이유입니다.
이 복잡하고 예측할 수 없는 움직임이 필요합니다. 왜냐하면 일반적으로 강체(rigid body)는 모양에 따라 흥미로운 방식으로 움직이기 때문입니다.

인터랙티브한 세계

네트워크 물리는 플레이어가 물리적으로 시뮬레이션 된 다른 오브젝트와 상호 작용할 때 특히 흥미롭습니다.
해당 오브젝트가 플레이어의 움직임에 영향을 미치면 더욱 그렇습니다.

이제 시뮬레이션에 큐브를 더 추가해 보겠습니다



플레이어가 큐브와 상호 작용하면 빨간색으로 변합니다. 큐브가 멈춰 있으면 다시 회색으로 바뀝니다(상호 작용하지 않음).

구르고 다른 큐브와 상호 작용하는 것은 멋지지만, 정말로 원했던 것은 많은 큐브를 밀어내는 방법이었습니다.
그래서 생각해 낸 것은 다음과 같습니다.



보시다시피 상호 작용은 직접적인 것이 아닙니다. 플레이어에 의해 밀린 빨간색 큐브는 다른 큐브도 빨간색으로 바꿉니다.
이렇게하면 상호 작용이 팬 아웃되어 영향을 받는 모든 개체를 덮습니다.


복잡한 경우

나는 또한 플레이어와 비플레이어 큐브 사이의 매우 복잡한 결합 동작을 원했는데, 그들은 하나의 시스템이 됩니다:
제약조건에 의해 결합되는 강체의 그룹입니다.

이것을 구현하기 위해 나는 내가 좋아하는 게임 Katamari Damacy 중 하나에서와 같이 플레이어가 구르고 큐브 공을 만들 수 있다면 멋질 것이라고 생각했습니다.



플레이어로부터 일정 거리 내에 있는 큐브는 큐브 중앙을 향해 힘이 가해집니다. 이 큐브는 katamari 공에 있는 동안 물리적으로 시뮬레이션된 상태로 유지되며 원래 게임에서처럼 플레이어에게 “고정”된 것이 아닙니다.

이것은 네트워크 물리학에 있어서 매우 어려운 상황입니다!


Deterministic Lockstep

소개

앞의 글 Introduction to Networked Physics에서 이 시리즈의 네트워크로 물리 시뮬레이션을 살펴보았습니다.
이 기사에서는 특히 결정론적 잠금 단계를 사용하여 이 물리 시뮬레이션을 네트워크로 연결하려고 합니다.

결정론적 잠금 단계는 해당 시스템의 상태가 아닌 해당 시스템을 제어하는 입력만 전송하여 한 컴퓨터에서 다른 컴퓨터로 시스템을 네트워킹하는 방법입니다. 물리 시뮬레이션 네트워킹의 맥락에서 이것은 위치, 방향, 선형 속도 및 각속도와 같은 개체 당 상태 전송을 피하면서 소량의 입력을 전송한다는 의미입니다.

이점은 대역폭이 시뮬레이션의 개체 수가 아니라 입력 크기에 비례한다는 것입니다. 예, 결정론적 잠금 단계를 사용하면 단 하나와 동일한 대역폭으로 백만 개의 객체에 대한 물리 시뮬레이션을 네트워크화 할 수 있습니다.

이것은 이론 상으로는 훌륭하게 들리지만 실제로는 대부분의 물리 시뮬레이션이 결정론적이지 않기 때문에 결정론적 잠금 단계를 구현하기가 어렵습니다. 컴파일러, OS 및 명령어 세트 간의 부동 소수점 동작의 차이로 인해 부동 소수점 계산에 대한 결정성을 보장하는 것은 거의 불가능합니다.

결정론(Determinism)

결정론은 동일한 초기 조건과 동일한 입력 세트가 주어지면 시뮬레이션이 정확히 동일한 결과를 제공한다는 의미입니다. 그리고 정확히 같은 결과를 의미 합니다 .

가깝거나 충분한 게 아니라 정확히 동일한 것입니다. 비트 수준까지 정확합니다.
정확히는, 각 프레임의 끝에 있는 전체 물리 상태를 체크섬으로 가져갈 수 있고, 그것도 동일한 것입니다.



위에서 거의 결정론적인 시뮬레이션을 볼 수 있습니다.
왼쪽의 시뮬레이션은 플레이어가 제어합니다. 오른쪽의 시뮬레이션은 동일한 초기 조건에서 시작하여 2 초 지연을 적용하여 정확히 동일한 입력을 적용합니다. 두 시뮬레이션 모두 동일한 델타 시간(정확하게 동일한 결과를 보장하는 데 필요한 전제 조건)으로 앞으로 나아가고 두 시뮬레이션 모두 동일한 입력을 적용합니다. 가장 작은 발산 후 시뮬레이션이 점점 더 동기화되지 않는지 확인해보면, 이 시뮬레이션은 비결정론적입니다.

사용하고 있는 물리 엔진(Open Dynamics Engine)은 Solver 내부의 난수 생성기를 사용하여 제약 조건 처리 순서를 무작위로 지정하여 안정성을 향상시킵니다. 오픈 소스입니다. 한 번보세요! 불행히도 이것은 왼쪽의 시뮬레이션이 오른쪽의 시뮬레이션과 다른 순서로 제약 조건을 처리하여 약간 다른 결과를 가져 오기 때문에 결정론을 깨뜨립니다.

다행히도 동일한 바이너리와 동일한 OS에서 ODE를 결정론적으로 만드는 데 필요한 모든 것은 dSetRandomSeed를 통해 시뮬레이션을 실행하기 전에 내부 랜덤 시드를 현재 프레임 번호로 설정하는 것입니다. 일단 이것이 실행되면, ODE는 정확히 동일한 결과를 제공하며 왼쪽과 오른쪽 시뮬레이션은 동기화 상태를 유지합니다.



그리고 이제 경고의 한마디입니다. 위의 시뮬레이션이 동일한 기계에서 결정론적이라 하더라도, 반드시 다른 컴파일러, 다른 OS 또는 다른 기계 아키텍처(예: PowerPC vs. 인텔)에서는 결정론적이지도 않을 것입니다. 사실, 부동소수 최적화 때문에 디버그 빌드와 릴리스 빌드도 결정론적이지 않습니다.

부동 소수점 결정론은 복잡한 주제고 만능의 해결 방법은 없습니다.

자세한 내용은 Floating Point Determinism 문서를 참조하십시오.


네트워킹 입력

이제 구현을 시작하겠습니다.

이 물리 시뮬레이션 예제는 키보드 입력에 의해 구동됩니다. 화살표 키는 힘을 적용하여 플레이어 큐브를 움직이고, 공간을 유지하면 큐브를 들어 올리고 다른 큐브를 날려 버리고, ‘z’를 누르고 있으면 카타마리 모드가 활성화됩니다.

이런 입력을 어떻게 네트워크화 할 수 있을까요? 키보드의 전체 상태를 보내야할까요? 아닙니다.
전체 키보드 상태를 보낼 필요는 없고 시뮬레이션에 영향을 주는 키의 상태만 보내면 됩니다.
그러면 Key PressedKey Released 이벤트는 어떤가요? 이것 또한 좋은 전략이 아닙니다.
정확히 동일한 입력이 오른쪽에 적용되도록 해야하기 때문에, TCP를 통해 Key Pressed, Key Released 이벤트를 그냥 보낼 수는 없습니다.

대신 구조체를 사용하여 입력을 나타내고 왼쪽의 각 시뮬레이션 프레임 시작 부분에 키보드에서 이 구조체를 샘플링하는 것입니다.

    struct Input
    {
        bool left;
        bool right;
        bool up;
        bool down;
        bool space;
        bool z;
    };


다음으로, 오른쪽 시뮬레이션이 입력이 프레임 n에 속한다는 것을 알 수 있도록 왼쪽 시뮬레이션에서 오른쪽 시뮬레이션으로 입력을 보냅니다.

그리고 여기 핵심 부분이 있습니다. 오른쪽의 시뮬레이션은 해당 프레임에 대한 입력이 있을 때만 프레임 n을 시뮬레이션 할 수 있습니다. 입력이 없으면 기다려야 합니다.

예를 들어, TCP를 사용하여 전송하는 경우 단순히 입력만 보낼 수 있고 다른 쪽에서는 들어오는 패킷을 읽을 수 있으며 수신된 각 입력은 시뮬레이션이 진행할 수 있는 한 프레임에 해당합니다. 주어진 렌더 프레임에 대한 입력이 도착하지 않으면 오른쪽이 앞으로 나아갈 수 없으며 다음 입력이 도착할 때까지 기다려야 합니다.

이제 TCP를 사용하여 진행해 보겠습니다. Nagle의 알고리즘을 비활성화하고 프레임 당 한 번 (초당 60 회) 왼쪽에서 오른쪽 시뮬레이션으로 입력을 보냅니다.

여기에서 약간 복잡해집니다. 다음 프레임에 대한 입력이 없으면 시뮬레이션을 진행할 수 없기 때문에 네트워크를 통해 도착하는 입력을 취한 다음 입력이 도착할 때 시뮬레이션을 실행하는 것만으로는 충분하지 않습니다. 결과가 매우 불안정할 수 있기 때문입니다. 60HZ로 네트워크를 통해 전송된 데이터는 일반적으로 각 패킷 사이의 1/60 초 간격으로 적절하게 도착하지 않습니다.

이런 종류의 동작을 원한다면 직접 구현해야 합니다.


재생 지연 버퍼(Playout Delay Buffer)

이러한 장치를 재생 지연 버퍼라고합니다.

불행히도 재생 지연 버퍼의 주제는 특허 지뢰밭입니다.
직장에서 “재생 지연 버퍼” 또는 “적응형 재생 지연”을 검색하는 것은 권장하지 않습니다.
그러나 간단히 말해서, 짧은 시간 동안 패킷을 버퍼링하여 실제로는 다소 불안정한 상태로 도착하더라도 일정한 속도로 도착하는 것처럼 보이도록 하는 기술입니다.

여기서 하는 작업은 비디오를 스트리밍 할 때 Netflix가 하는 작업과 유사합니다.
일부 패킷이 늦게 도착할 경우 버퍼를 확보하기 위해 초기에 약간 일시 중지한 다음 지연이 경과하면 비디오 프레임이 올바른 시간 간격으로 표시됩니다. 버퍼가 충분히 크지 않으면 비디오 재생이 중단됩니다. 결정론적 잠금 단계를 사용하면 시뮬레이션이 정확히 동일한 방식으로 작동합니다. 버퍼가 지터를 완화할만큼 충분히 크지 않을 때 장애를 표시합니다. 물론 버퍼 크기를 늘리는 데 드는 비용은 추가 대기 시간이므로 모든 문제를 해결할 수는 없습니다. 어느 시점에서 사용자는 충분히 말합니다! 너무 많은 지연 시간이 추가되었습니다. 1초 더 지연된 상태로 당신의 게임을 하지 않겠습니다 :)

내 플레이 아웃 지연 버퍼 구현은 정말 간단합니다. 프레임별로 인덱싱 된 입력을 추가하고 첫 번째 입력이 수신되면 수신기 시스템에 현재 현지 시간을 저장하고 그 시점부터 패킷이 해당 시간 + 100ms에 재생되어야 한다고 가정하여 패킷을 전달합니다.
실제 상황을 위해서는 좀 더 복잡한 것이 필요할 것 같은데, 아마도 시계 드리프트를 다루는 것, 그리고 전체 대기 시간을 최소화하면서 상당한 양의 버퍼링 안전성을 유지하기 위해 시뮬레이션 속도가 약간 빨라지거나 느려질 때(“적응적”)를 감지해야 합니다. 이것은 상당히 복잡하고 아마도 그 주제만으로도 한 기사를 작성할 수 있습니다.

목표는 평균 조건에서 재생 지연 버퍼가 프레임 n, n+1, n+2 등에 대해 일정한 간격의 1/60 초 간격으로 극적인 상황없이 안정적인 입력 스트림을 제공하는 것입니다. 최악의 경우 시간이 프레임 n 에 도달하고 입력이 아직 도착하지 않은 경우 null을 반환하고 시뮬레이션이 강제로 대기합니다. 패킷이 묶여서 늦게 전달되는 경우 프레임 당 대기열에서 제거할 준비가 된 여러 입력이 있을 수 있습니다. 이 경우에는 렌더 프레임 당 시뮬레이션 된 프레임을 4 개로 제한하므로 시뮬레이션이 따라 잡을 기회가 있지만 훨씬 뒤쳐 질 정도로 오랫동안 시뮬레이션하지 않습니다. “죽음의 나선”과 같습니다.

TCP가 충분합니까?

이 플레이 아웃 버퍼 전략을 사용하고 TCP를 통해 입력을 전송하여 모든 입력이 안정적이고 순서대로 도착하도록 합니다.
이것은 편리하며 결국 TCP는 정확히 이 상황, 즉 신뢰할 수있는 순서의 데이터를 위해 설계되었습니다.

사실 인터넷에서 전문가들이 다음과 같은 말을 하는 것은 흔합니다:

하지만 이런 생각이 완전히 틀렸다 고 말하고 싶습니다.



위에서는 100ms 지연 및 1% 패킷 손실에서 TCP를 통한 결정론적 잠금 단계를 사용하여 네트워크로 연결된 시뮬레이션을 볼 수 있습니다. 오른쪽을 자세히 보면 몇 초마다 끊김 현상을 볼 수 있습니다. 여기서 일어나는 일은 패킷이 손실될 때마다 TCP가 재전송되는 동안 RTT2* 를 기다려야한다는 것입니다(실제로 훨씬 더 나쁠 수 있습니다). 결정론적 잠금 단계를 사용하면 올바른 시뮬레이션이 입력 n 없이 프레임 n 을 시뮬레이션할 수 없기 때문에 문제가 발생합니다. 따라서 입력 n 이 재전송되기를 기다리기 위해 일시 ​​중지해야합니다!

그게 다가 아닙니다. 지연 시간과 패킷 손실이 증가하면 훨씬 더 악화됩니다. 다음은 250ms 지연 및 5% 패킷 손실에서 TCP를 통한 결정론적 잠금 단계를 사용하여 네트워크로 연결된 동일한 시뮬레이션입니다.



패킷 손실이 없거나 대기 시간이 매우 적으면 TCP로 허용 가능한 결과를 얻을 수 있다는 것은 인정합니다.
그러나 TCP를 사용하면 네트워크 상태가 좋지 않을 때 심하게 동작한다는 점에 유의해야 합니다.


TCP 보다 더 잘 만들 수 있나요?

자체 게임에서 TCP를 능가할 수 있습니까? 신뢰할 수 있는 배송을 원하십니까?
대답은 단호한 입니다. 그러나 우리가 게임의 규칙을 바꾸는 경우에서만 해당됩니다.

여기에 트릭이 있습니다. 우리는 모든 입력이 안정적이고 순서대로 도착하도록 해야합니다. 그러나 UDP 패킷으로 입력을 보내면 해당 패킷 중 일부가 손실됩니다. 사실 이후 패킷 손실을 감지하고 손실된 패킷을 재전송하는 대신 상대방이 수신한 것을 확인할 때까지 각 UDP 패킷에 모든 입력을 중복해서 포함하면 어떨까요?

입력은 6 비트로 매우 작습니다. 초당 60 개의 입력(60fps 시뮬레이션)을 보내고 있고 왕복 시간이 30-250ms 범위에 있다고 가정해 보겠습니다. 재미로 최악의 경우에는 최대 2 초가 될 수 있다고 가정해 보겠습니다.
이 시점에서 연결 시간을 초과 할 것입니다. 즉, 평균적으로 2-15 개의 입력 프레임 만 포함하면되며 최악의 경우 120 개의 입력이 필요합니다. 최악의 경우는 120 * 6 = 720 비트입니다. 90 바이트의 입력에 불과합니다! 아주 합리적인 수준입니다.

TCP보다 더 잘 만들 수 있습니다. 입력이 모든 프레임을 변경하는 것은 일반적이지 않습니다. 대신 패킷을 보낼 때 가장 최근 입력의 시퀀스 번호, 첫 번째 (가장 오래된) 입력의 6 비트, 확인되지 않은 입력의 수로 시작하면 어떨까요? 그런 다음 이러한 입력을 반복하여 패킷에 쓸 때 다음 입력이 이전 입력과 다르면 단일 비트 (1)을 쓰고 입력이 동일하면 (0)을 씁니다.
따라서 입력이 이전 프레임과 다른 경우 7 비트(드물게)를 씁니다. 입력이 동일하면 하나(일반적으로)만 작성합니다. 입력이 드물게 변경되는 경우 이것은 큰 승리이며 최악의 경우에도 그렇게 나쁘지 않습니다. 120 비트의 추가 데이터가 전송되었습니다. 최악의 경우 오버 헤드가 15 바이트뿐입니다.

물론 오른쪽 시뮬레이션에서 왼쪽으로 다른 패킷이 필요하므로 왼쪽에서 어떤 입력이 수신되었는지 알 수 있습니다. 오른쪽 시뮬레이션은 각 프레임을 재생 지연 버퍼에 추가하기 전에 네트워크에서 입력 패킷을 읽고 가장 최근에 수신한 입력을 추적하고 이를 “ack”또는 입력 승인으로 왼쪽으로 다시 보냅니다.

왼쪽이 확인을 받으면 가장 최근에 수신된 입력보다 오래된 모든 입력을 버립니다. 이렇게하면 두 시뮬레이션 사이의 왕복 시간에 비례하여 실행 중에 적은 수의 입력만 포함할 수 있습니다.


완벽한 승리

우리는 게임의 규칙을 변경하여 TCP를 이겼습니다.

“UDP 위에 TCP의 95 %를 구현”하는 대신 완전히 다른 것을 구현했고 요구 사항에 더 잘 맞았습니다.
입력이 작다는 것을 알고 있기 때문에 중복으로 입력을 전송하는 프로토콜이므로 재전송을 기다릴 필요가 없습니다.

그래서 TCP를 통해 입력을 보내는 것보다 이 접근 방식이 정확히 얼마나 낫습니까?

한 번 보겠습니다…



위의 비디오는 2 초의 지연 시간과 25 % 패킷 손실로 이 기술을 사용하여 UDP를 통해 동기화된 결정론적 잠금 단계를 보여줍니다. 이러한 조건에서 TCP가 얼마나 끔찍한 지 상상해보십시오.

결론적으로, 신뢰할 수 있는 순서의 데이터에 의존하는 유일한 네트워킹 모델인 TCP가 가장 유리한 부분에서도 UDP 위에 구축 된 간단한 프로토콜을 사용하면 더 좋은 결과를 얻을 수 있습니다.


Snapshot Interpolation


Snapshot Compression


State Synchronization




Networked Physics in Virtual Reality

소개

Glenn은 GDC에서 강연을 한 후 오큘러스에서 VR의 네트워크 물리에 대한 제안을 받았습니다.
진행하기 전에 두 가지 조건을 요청했습니다.
첫째: 가장 좋은 제품을 만들기 위해 개발한 소스 코드를 오픈 소스 라이선스로 게시하는 것과,
둘째: 내가 다 작성했을 때, 샘플을 개발하기 위해 진행했던 단계를 소개하기 위한 글을 쓰는 것이었습니다.

오큘러스도 이에 동의하여, 이 글을 쓰게 되었습니다!
또 네트워크 물리 샘플의 출처는 fbsamples/oculus-networked-physics-sample 에 있고, 코드는 BSD 라이선스로 공개되었습니다.
차세대 프로그래머들이 네트워크 물리학에 대한 Glenn의 연구를 통해 배우고 정말 멋진 것들을 만들어 낼 수 있기를 바랍니다.
행운을 빕니다!


우리는 무엇을 만들고 있습니까?

오큘러스와 처음 토론을 시작했을 때, 우리는 4 명의 플레이어가 테이블에 앉아 물리적으로 시뮬레이션된 큐브와
상호 작용할 수 있는 테이블을 만드는 것을 상상했습니다.
예를 들어, 큐브를 던지고, 잡고 쌓아 올리거나 손으로 스와이프하여 서로의 스택을 넘어뜨릴 수 있는 것이었습니다.

하지만 며칠 동안 Unity와 C#을 배우고 나서 실제로 틈 사이에에 있는 자신을 발견했습니다.
VR에서 규모는 매우 중요합니다. 큐브가 작을 때는 모든 것이 훨씬 덜 흥미로워졌지만
큐브가 1 미터로 확장되었을 때 모든 것이 정말 멋진 규모감을 가졌습니다.
이 거대한 큐브 스택을 최대 20 ~ 30 미터 높이로 만들 수 있습니다. 이것은 정말 멋졌습니다!

VR 밖에서의 느낌을 시각적으로 전달하는 것은 어렵지만, 표현하면 이렇게 생겼습니다…



… 터치 컨트롤러를 사용하여 큐브를 선택하고, 잡고, 던질 수 있으며, 손에서 놓는 큐브는 시뮬레이션에서
다른 큐브와 상호 작용합니다.
큐브를 한 무더기의 큐브에 던져 넘어뜨릴 수 있고, 각 손에 있는 큐브를 집어서 곡예를 할 수 있고,
정육면체 더미를 쌓아서 얼마나 높게 만들 수 있는 지 볼 수도 있습니다.


이것은 매우 재미있었지만, 모든 것이 동화같지 않았습니다.
오큘러스와 함께 일하면서, 실제로 작업을 시작하기 전에 결과물을 정의해야 했습니다.

저는 성공을 정의하기 위해 다음과 같은 기준을 제안했습니다.

  1. 플레이어는 대기 시간 없이 큐브를 집어 던지고 잡을 수 있어야 한다.
  2. 플레이어는 큐브를 쌓을 수 있어야 하며, 이 스택은 안정적이어야 하며(휴식하기 위해) 눈에 보이는 지터가 없어야 한다.
  3. 플레이어가 던진 큐브가 시뮬레이션과 상호작용하는 경우, 가능한 경우 이러한 상호작용은 대기 시간이 없어야 한다.

동시에 나는 최소한의 위험을 감수하고 일하기 위한 일련의 과제를 만들었습니다.
이것은 연구개발이었기 때문에 우리가 하고자 하는 일에 실제로 성공할 것이라는 보장은 없었습니다.


네트워크 모델(Network Models)

먼저 네트워크 모델을 선택해야했습니다.
네트워크 모델은 기본적으로 어떻게 지연시간을 숨기고 시뮬레이션을 동기화할 것인지에 대한 전략입니다.

선택할 수 있는 네트워크 모델은 크게 세 가지가 있습니다:

  1. 결정론적 잠금 단계(Deterministic Lockstep)
  2. 클라이언트가 예측하는 클라이언트/서버
  3. 권한 체계를 사용한 분산 시뮬레이션

나는 즉시 올바른 네트워크 모델: 플레이어가 상호 작용하는 큐브의 권한을 인수하는 분산 시뮬레이션 모델을 확신했습니다.
이것에 대한 나의 추론을 공유하겠습니다.

첫째, Unity(PhysX) 내부의 물리 엔진이 결정론적이지 않기 때문에 결정론적 잠금 단계 네트워크 모델을 간단히 배제할 수 있습니다.
게다가 PhysX가 결정론적이라 할지라도 플레이어가 시뮬레이션과 상호 작용할 때 지연 시간이 없어야 하므로 여전히 배제 할 수 있습니다.

그 이유는 결정론적 잠금 단계로 지연 시간을 숨기기 위해 시뮬레이션의 두 복사본을 유지하고 렌더링(GGPO 스타일) 전에 로컬 입력으로 권한있는 시뮬레이션을 미리 예측해야 했기 때문입니다.
90Hz 시뮬레이션 속도에서, 그리고 숨길 수 있는 최대 250ms의 대기 시간으로, 이것은 각 시각 렌더 프레임에 대해 25개의 물리 시뮬레이션 단계를 의미했습니다. CPU 집약적인 물리 시뮬레이션에서는 25배 비용이 현실적이지 않았습니다.

이것으로 두 가지 옵션이 남았습니다:
클라이언트가 예측하는 클라이언트/서버 네트워크 모델(아마도 전용 서버일 것이다)과
덜 안전한 분산 시뮬레이션 네트워크 모델이었습니다.

이것은 비경쟁적인 샘플이었기 때문에 전용 서버 운영 비용을 부담할 명분이 거의 없었습니다.
따라서 내가 클라이언트가 예측하는 클라이언트/서버 모델을 구현했든 분산 시뮬레이션 모델을 구현했든,
보안은 사실상 동일했습니다.
유일한 차이점은 이론적으로 경기에 참가한 선수들 중 한 명만 부정행위를 할 수 있는 지,
아니면 그들 모두가 부정행위를 할 수 있는 지 입니다.

이 때문에 분산 시뮬레이션 모델이 가장 일리가 있었습니다. 플레이어가 상호 작용하는 큐브에 대한 권한을 가져 다른 플레이어에 큐브 상태를 보내기 때문에, 플레이어는 실질적으로 동일한 양의 보안을 가지고 있었고, 값비싼 롤백과 재조정을 요구하지 않았습니다.


권한 체계(Authority Scheme)

상호 작용하는 객체에 대해 권한을 갖는 것(서버와 같은 동작)은 지연 시간을 숨길 수 있어서 직관적입니다.
서버는 지연을 경험하지 않기 때문입니다.
확실하지 않은 것은 갈등을 해결하는 방법입니다.

두 명의 플레이어가 동일한 스택과 상호 작용하면 어떻게 될까요?
지연 시간으로 가려진 두 명의 플레이어가 같은 큐브를 잡으면 어떨까요?
갈등의 경우에는 누가 이기고 누가 시정되며 어떻게 결정됩니까?

이 시점에서 나의 직관은 빠르게 (초당 최대 60회 이상) 물체와 상태를 교환하고 있을 것이기 때문에,
이벤트보다는 네트워크 프로토콜을 통해 플레이어 간에 교환되는 상태에서의 인코딩으로 이것을 실행하는 것이
최선일 것이라는 것이었다.

나는 이것에 대해 잠시 생각하고 두 가지 핵심 개념을 생각해냈습니다.

  1. 권한
  2. 소유권


각 큐브는 기본(흰색)으로 설정되거나 마지막으로 상호 작용한 플레이어의 색상으로 설정되는 권한이 있습니다.
만약 다른 플레이어가 물체와 상호작용을 한다면, 권한은 그 플레이어로 전환되어 업데이트됩니다.
원래는 던져진 물체와 장면의 상호작용을 위해 권한을 사용할 계획이었습니다.
나는 플레이어2 가 던진 큐브가 그것이 상호작용하는 물체와, 그리고 다시 그 물체와 상호 작용하는 다른 물체에
대해서도 반복적으로 권한을 가질 수 있다고 상상했었습니다.


소유권은 좀 달랐습니다. 일단 플레이어가 큐브를 소유하면, 플레이어가 소유권을 없애기 전까지
다른 플레이어가 소유권을 가질 수 없습니다.
플레이어들이 큐브를 집어든 후 다른 플레이어의 손에서 큐브를 꺼내지 못하게 하고 싶었기 때문에
큐브를 잡는 것에 소유권을 사용할 계획이었습니다.


나는 내가 보낸 큐브-당 서로 다른 두 개의 순서 번호, 즉 권한 순서 번호와 소유 순서 번호를 포함시킴으로써
권한과 소유권을 상태로 나타내고 전달할 수 있다는 생각이 있었습니다.
이 생각은 궁극적으로 옳다고 증명되었지만, 예상했던 것보다 훨씬 더 복잡하게 실행되었습니다.
이에 대해서는 나중에 더 이야기하겠습니다.


상태 동기화(State Synchronization)

위에서 설명한 권한 규칙을 구현할 수 있다고 믿고, 첫 번째 작업은 한 방향의 흐름으로 물리를 동기화하는 것이
실제로 Unity 및 PhysX에서 동작할 수 있다는 것을 증명하는 것이었습니다.
이전 작업에서 ODE로 구축된 네트워크 시뮬레이션이 있었기 때문에 정말 가능할지 전혀 몰랐습니다.

확인하기 위해 Unity에서 큐브가 플레이어 앞에서 더미로 떨어지는 루프백 씬을 설정했습니다.
두 세트의 큐브가 있습니다. 왼쪽의 큐브는 권한이 있는 쪽을 나타내고, 오른쪽의 큐브는 권한이 없고 왼쪽의 큐브와
동기화되기를 원하는 쪽입니다.



처음부터 두 큐브 세트가 동일한 초기 상태에서 시작하더라도, 큐브를 동기화할 수 있는 어떤 것도 없어서 약간 다른 최종 결과를 보여줍니다.
하향식(top-down)에서는 이를 가장 쉽게 볼 수 있습니다.



이것은 PhysX가 비결정론적이기 때문에 발생합니다.
비결정론적인 작업에 주의를 기울이는 대신, 왼쪽(권한)에서 상태를 파악하고 이를 오른쪽(비권한)에
초당 10 번을 적용하여 비결정론과 싸웠습니다.



각 큐브에서 얻은 상태는 다음과 같습니다:

struct CubeState
{
    Vector3 position;
    Quaternion rotation;
    Vector3 linear_velocity;
    Vector3 angular_velocity;
};


그리고 이 상태를 오른쪽의 시뮬레이션에 적용하면 각 큐브의 위치, 회전, 선형(linear) 및 각도 속도(angular)를
왼쪽에서 캡처한 상태로 간단히 스냅할 수 있습니다.

이 간단한 변화는 좌우 시뮬레이션을 일치시키기에 충분합니다.
PhysX는 눈에 띄는 팝을 보여줄 수 있는 업데이트 사이에 1/10초 동안 충분히 분산되지도 않습니다.



이는 네트워킹을 위한 상태 동기화 기반 접근법이 PhysX와 작동할 수 있음을 증명합니다.(안도의 한숨)
물론 유일한 문제는 압축되지 않은 물리 상태를 보내는 것이 너무 많은 대역폭을 사용한다는 것입니다.


대역폭 최적화(Bandwidth Optimization)

네트워크 물리 샘플을 인터넷에서 재생하기 위해서는 대역폭 제어가 필요했습니다.

가장 쉬운 이득은 단순히 나머지 큐브의 상태를 더 효율적으로 인코딩하는 것이었습니다.
예를 들어, 선형 속도(0,0,0)와 정지 큐브 각 속도(0,0,0)를 반복해서 보내는 대신, 나는 단지 1비트만 보냈습니다.

[position] (vector3)
[rotation] (quaternion)
[at rest] (bool)
<if not at rest>
{
    [linear_velocity] (vector3)
    [angular_velocity] (vector3)
}

이것은 어떤 식으로든 네트워크를 통해 전송되는 상태를 바꾸지 않기 때문에 무손실 기술(lossless) 입니다.
또한 매우 효과적입니다, 통계적으로 말하면, 대부분의 큐브들이 정지해 있기 때문입니다.

대역폭을 더 최적화하려면 손실(lossy) 기술을 사용해야 합니다.
예를 들어, 우리는 어떤 최소/최대 범위의 경계 위치를 정량화하고 그것을 센티미터의 1/1000의 해상도로 정량화하여
그 정량화된 위치를 정수값으로 송신함으로써 네트워크를 통해 전송되는 물리 상태의 정밀도를 줄일 수 있습니다.
선형 및 각도 속도에 동일한 기본 접근법을 사용할 수 있습니다.
회전을 위해 나는 쿼터니온의 가장 작은 세 개의 표현을 사용했습니다.

하지만 이것은 대역폭을 절약하는 반면, 위험도를 증가시킵니다.
내 걱정은 우리가 큐브 스택(예를 들어, 10개 또는 20개의 큐브를 서로 위에 배치된)을 네트워킹하고 있다면, 아마도 양자화(quantization)는 그 스택에 지터(Jitter)를 추가하는 오류를 만들 것입니다.
어쩌면 스택이 나에게는 문제가 없는데, 특히 짜증나고 디버깅하기 힘든 방법으로, 다른 플레이어가 내 상태를 보는 원격 보기(예: 비권한 시뮬레이션)에서만 스택이 불안정해질 수 있습니다.

내가 찾은 이 문제에 대한 최선의 해결책은 양쪽의 상태를 정량화(quanize)하는 것입니다.
즉, 각 물리 시뮬레이션 단계 전에 네트워크를 통해 물리 상태를 전송할 때와 정확히 동일한 방식으로 물리 상태를 캡처하고 정량화한 다음, 이 정량화된 상태를 다시 로컬 시뮬레이션에 적용한다는 것입니다.

이제 권한 없는 쪽의 정량화된 상태에서의 추정은 권한 시뮬레이션과 정확히 일치하여 큰 스택의 지터를 최소화합니다. 적어도 이론상으로는.

quantize?(양자화), extrapolation?(추정, 외삽)


쉬어간다(Coming To Rest)

그러나 물리 상태를 양자화하면서 매우 흥미로운 부작용이 생겼습니다!

  1. PhysX는 모든 프레임이 시작될 때 각 강체(rigid body)의 상태를 강제하는 것을 좋아하지 않으며 CPU를 많이 차지하여 확실히 알 수 있습니다.

  2. 양자화는 PhysX가 수정하기 위해 매우 열심히 시도하는 위치에 오류를 추가하여 거대한 폭발로 즉시 큐브를 튕겨냅니다!

  3. 회전도 정확하게 표현할 수 없고 다시 관통이 발생합니다. 흥미롭게도 이 경우 큐브가 바닥을 가로 질러 미끄러지는 피드백 루프에 갇힐 수 있습니다!

  4. 큰 스택의 큐브는 정지된 것처럼 보이지만 편집기를 자세히 살펴보면 큐브가 표면 바로 위에서 양자화되어 그쪽으로 떨어지기 때문에 실제로는 작은 양으로 흔들리는 것으로 나타났습니다.


PhysX CPU 사용량에 대해 할 수 있는 것은 별로 없지만, 침투(Depenetration)에 대해 찾은 해결책은 각 강체(rigid body)에 maxDepenetrationVelocity 를 설정하여 큐브들이 밀어내는 속도를 제한하는 것이었습니다. 초당 1미터가 매우 잘 작동했습니다.

큐브를 안정되게 쉬게 하는 것은 훨씬 더 어려웠습니다. 찾은 해결책은 정지 상태에서 PhysX를 완전히 비활성화하고 그것을 링버퍼의 위치 및 큐브-당 회전으로 대체하는 것이었습니다. 만약 큐브가 지난 16프레임에서 크게 움직이거나 회전하지 않았다면, 강제로 정지시켰습니다. 쾅. 양자화로 완벽하게 스택이 안정되었습니다.

이건 해킹처럼 보일지도 모르지만, 실제로 PhysX 소스 코드를 입력해서 PhysX solver 를 다시 쓰는 것, 그리고 정지 상태에서 계산하는 것 외에는 다른 방법이 보이지 않았습니다.
그래도 내가 틀렸다는 것이 입증되면 기쁘기 때문에 더 좋은 방법을 찾으면 알려주세요 :)


우선순위 누산기(Priority Accumulator)

다음으로 한 큰 대역폭 최적화는 각 패킷에 큐브의 하위 집합만 보내는 것이었습니다.
이를 통해 최대 패킷 크기를 설정하고 각 패킷에 맞는 업데이트 세트만 전송하여 전송되는 대역폭 양을 세밀하게 제어할 수 있었습니다.

실제로 작동하는 방법은 다음과 같습니다:

  1. 각 큐브에는 각 프레임에서 계산되는 우선순위 요소가 있습니다. 값이 높을수록 전송될 가능성이 높습니다. 음수 값은 “이 큐브를 보내지 않음”을 의미합니다.
  2. 우선순위 요소가 양수이면 해당 큐브의 우선 순위 누산기 값에 추가됩니다. 이 값은 시뮬레이션 업데이트 사이에 유지되어 우선순위 누적기가 각 프레임을 증가시키므로 우선순위가 높은 큐브가 우선순위가 낮은 큐브보다 빠르게 상승합니다.
  3. 음의 우선순위 요소는 우선 순위 누적기를 -1.0으로 지웁니다.
  4. 패킷을 보낼 때 큐브는 우선순위가 가장 높은 누적기에서 가장 낮은 순서로 정렬됩니다. 처음 n 개의 큐브는 패킷에 잠재적으로 포함할 큐브 세트가 됩니다. 우선순위 누산기 값이 음수인 개체는 제외됩니다.
  5. 패킷이 기록되고 큐브는 중요도에 따라 패킷에 직렬화됩니다. 큐브 업데이트는 현재 상태에 따라 인코딩이 가변적이기 때문에 모든 상태 업데이트가 반드시 패킷에 맞지는 않습니다. 따라서 패킷 직렬화는 패킷에 포함되었는지 여부를 나타내는 큐브별 플래그를 반환합니다.
  6. 패킷으로 전송된 큐브의 우선순위 누산기 값은 0.0으로 지워져 다른 큐브가 다음 패킷에 포함될 수 있는 공정한 기회를 제공합니다.


이 데모에서 높은 에너지 충돌은 비결정론적 결과로 가장 큰 차이의 원인이었기 때문에, 최근에 높은 에너지 충돌과 관련된 큐브에 우선순위를 높이는 값을 발견했습니다. 또, 최근에 플레이어들이 던진 큐브의 우선순위도 높였다.

다소 반직관적으로, 정지된 큐브의 우선순위를 줄인 것이 나쁜 결과를 낳았습니다. 생각해보면 시뮬레이션이 양쪽에서 실행되기 때문에 정지된 큐브는 약간 동기화되지 않고 빠르게 수정되지 않아 다른 큐브와 충돌할 때 분열을 일으키는 것이 원인인 것 같습니다.


델타 압축(Delta Compression)

지금까지의 모든 기술에도 불구하고 여전히 최적화는 충분하지 않았습니다. 4 명의 플레이어와 함께 플레이어 당 비용을 256kbps 이하로 낮춰서, 호스트에서 전체 시뮬레이션이 1Mbps에 맞추고 싶었습니다.

마지막 트릭이 하나 남았습니다: delta compression.

1인칭 슈터들은 종종 이전 상태에 비해 전 세계의 상태를 압축함으로써 델타 압축을 시행합니다.
이 기법에서는 이전의 완전한 세계 상태나 ‘스냅샷’이 기준 역할을 하며, 기준과 현재 스냅샷 사이의 차이점, 즉 델타가 생성되어 클라이언트로 전송됩니다.

이 기법은 모든 객체의 상태가 각 스냅샷에 포함되기 때문에 (상대적으로) 구현이 용이하며, 따라서 서버가 해야 할 일은 각 클라이언트가 수신한 가장 최근의 스냅샷을 추적하고, 해당 스냅샷에서 현재까지의 델타(deltas)를 생성하는 것입니다.

그러나 우선순위 누산기를 사용하면 패킷에 모든 개체에 대한 업데이트가 포함되어 있지 않아 델타 인코딩이 더욱 복잡해집니다.
이제 서버(또는 권한 측면)는 이전 스냅샷 번호와 관련하여 큐브를 단순히 인코딩할 수 없습니다.
대신, 기준선을 큐브별로 지정해야 하므로, 수신자는 각 큐브가 어떤 상태에 대해 인코딩되는지 알 수 있습니다.

지원 시스템과 데이터 구조도 훨씬 더 복잡합니다:

  1. 가장 최근에 수신된 스냅샷 # 뿐만 아니라 수신된 패킷을 Sender 에게 다시 보고할 수 있는 신뢰성있는 시스템이 필요합니다.
  2. Sender 는 전송된 각 패킷에 포함된 상태를 추적해야 하므로 패킷 수준 ack를 전송된 상태에 매핑하고 가장 최근에 acked한 큐브-당 상태를 업데이트할 수 있습니다. 다음에 큐브를 보낼 때, 큐브의 델타는 이 상태를 기준으로 인코딩됩니다.
  3. Receiver 는 큐브-당 수신된 상태를 링 버퍼에 저장해야 하므로 이 링 버퍼에서 기준선을 조회하여 델타에서 현재 큐브 상태를 재구성 할 수 있습니다.

하지만 궁극적으로, 이 시스템은 대역폭 사용량을 동적으로 조정할 수 있는 유연성과 델타 인코딩을 통해 얻을 수 있는 몇 배의 대역폭 개선을 결합하기 때문에 복잡성이 추가되는 것을 감수할만한 가치는 있습니다.



델타 인코딩(Delta Encoding)

이제 지원할 구조가 준비되었으므로 실제로 이전 기준 상태와 관련된 큐브의 델타를 인코딩해야합니다. 어떻게하나요?

가장 간단한 방법은 기준 값에서 변경되지 않은 큐브를 한 비트: not changed 로 인코딩하는 것 입니다.
이것은 또한 얻을 수 있는 가장 쉬운 이득이기도 합니다. 언제든 대부분의 큐브가 휴지(rest) 상태에 있기 때문에 상태를 변경하지 않기 때문입니다.

보다 진보된 전략은 현재 값과 기준 값 간의 델타를 인코딩하여 더 적은 비트로 작은 델타를 인코딩하는 것입니다.
예를 들어 델타 위치는 기준선에서 (-1, + 2, + 5) 일 수 있습니다. 선형 값에 대해서는 잘 작동하지만 쿼터니언의 가장 큰 구성 요소가 기준선과 현재 회전 사이에서 종종 다르기 때문에 가장 작은 세 쿼터니언 표현의 델타에 대해 분석됩니다.

게다가, 델타를 인코딩하면 약간의 이득을 얻을 수 있지만 기대했던 규모는 아니었습니다. 필사적인 마지막 희망으로, 예측을 포함하는 델타 인코딩 전략을 생각해냈습니다. 이 접근 방식에서는 큐브가 중력으로 인해 가속 상태에서 탄도처럼(Ballistically) 움직이고 있다고 가정하고 기준선에서 현재 상태를 예측합니다.

예측이 고정 소수점으로 작성되어야 한다는 사실때문에 복잡했습니다. 부동 소수점 계산이 반드시 결정론적이라고 보장할 수는 없기 때문입니다. 그러나 며칠 간의 조정과 실험 끝에 약 90%로 PhysX 통합자와 일치하는 위치, 선형 및 각도 속도에 대한 탄도 예측 변수를 작성할 수 있었습니다.

이 행운의 큐브는 완벽한 예측이라는 또 다른 비트로 인코딩되어 또 다른 규모의 향상을 가져옵니다. 예측이 정확히 일치하지 않는 경우 예측과 관련된 작은 오류 오프셋을 인코딩했습니다.

시간을 보냈지만 회전에 대한 좋은 예측 변수를 얻을 수 없었습니다.
원인을 가장 작은 세 가지 표현으로 설명하면, 이는 특히 고정 소수점에서 매우 수치적으로 불안정합니다.
앞으로는 양자화된 회전에 대해 가장 작은 세 가지 표현을 사용하지 않을 것입니다.

또한 비트 패커를 사용하는 것이 이러한 양을 읽고 쓰는 가장 좋은 방법이 아니라는 점은 델타와 오류 오프셋을 인코딩하는 동안 고통스럽게 분명했습니다. 분수 비트를 표현할 수 있는 범위 코더 또는 산술 압축기와 같은 것이 훨씬 더 나은 결과를 제공할 것이라고 확신합니다. 그러나 저는 이미 이 시점에서 대역폭 예산 내에 있었고 더이상 누들링을 정당화 할 수 없었습니다. :)


아바타 동기화(Synchronizing Avatars)

몇 달의 작업 끝에 다음과 같은 진전을 이루었습니다:

  • 상태 동기화가 Unity 및 PhysX에서 작동한다는 증거
  • 양 쪽에서 상태를 양자화하면서 원격 보기에서 안정적인 스택
  • 4 명의 플레이어가 모두 1Mbps에 들어갈 수 있는 지점까지 대역폭 감소

다음으로 구현한 것은 터치 컨트롤러를 통한 시뮬레이션과 상호 작용이었습니다. 이 부분은 정말 재미 있었고 프로젝트에서 제가 가장 좋아하는 부분이었습니다. :)

이러한 상호 작용을 즐기시기 바랍니다. 집어 올리기, 던지기, 손에서 손으로 전달과 같은 간단한 일을 기분 좋게 만들기 위해 많은 실험과 튜닝이 있었고, 던지기 작업을 훌륭하게 하기 위한 미친 조정조차도 높은 스택 위에 물건을 올려놓는 것은 여전히 높은 정확도로 이루어질 수 있었습니다.

그러나 네트워킹에 관한 한, 이 경우에 게임 코드는 중요하지 않았습니다.
네트워킹이 신경 쓰는 것은 추적된 헤드셋과 터치 컨트롤러 위치 및 방향에 의해 움직이는 아바타로 머리와 두 손으로 표현됩니다.

이를 동기화하기 위해 나머지 물리 상태와 함께 FixedUpdate 에서 아바타 구성 요소의 위치와 방향을 캡처하고 이 상태를 원격보기의 아바타 구성 요소에 적용했습니다.

하지만 처음 시도했을 때는 정말 끔찍해 보였습니다. 이유는?

여러 번의 디버깅을 한 후 업데이트에서 렌더링 시 터치 하드웨어에서 아바타 상태가 렌더링 프레임레이트로 샘플링되고 다른 머신의 FixedUpdate에서 적용되어 아바타 샘플 시간이 원격 뷰의 현재 시간과 맞지 않아 지터를 발생시킨다는 것을 알아냈습니다.

이를 해결하기 위해 아바타 상태를 샘플링할 때 물리와 렌더 시간의 차이를 저장했고, 이것을 각 패킷의 아바타 상태에 포함시켰습니다. 그리고 100ms 지연이 있는 지터 버퍼를 수신 패킷에 추가해 패킷 전달의 시간 분산에서 네트워크 지터를 해결하고 아바타 상태 간의 보간이 정확한 시간에 샘플을 재구성할 수 있도록 했습니다.

아바타가 가지고 있는 큐브를 동기화하기 위해 큐브가 아바타의 손의 부모가 되는 동안 큐브의 우선순위 요인을 -1로 설정해 일반 물리 상태 업데이트로 보내는 것을 막았습니다. 큐브가 손에 붙어 있는 동안, 나는 큐브의 id와 상대적 위치, 회전 등을 아바타 상태의 일부로 포함시켰습니다. 리모트 뷰에서 큐브는 첫 번째 아바타 상태가 모체화된 상태로 도착할 때 아바타 손에 부착되며, 던져지거나 방출되는 큐브에 해당하는 정기적인 물리 상태 업데이트가 재개될 때 분리되었습니다.

양방향 흐름

이제 플레이어가 터치 컨트롤러로 작업하는 장면과 상호 작용했으므로 두 번째 플레이어도 장면과 상호 작용할 수 있는 방법에 대해 생각하기 시작했습니다.

항상 두 헤드셋을 정신없이 전환하지 않기 위해(!!!) 1번 플레이어(왼쪽)와 2번 플레이어(오른쪽)의 컨텍스트를 전환할 수 있도록 유니티 테스트 장면을 확장했다.

첫 번째 플레이어를 “호스트”로, 두 번째 플레이어를 “게스트”라고 정했습니다. 이 모델에서 호스트는 실제 시뮬레이션이며 기본적으로 모든 큐브를 게스트 플레이어에 동기화하지만 게스트가 월드와 상호 작용할 때 이러한 개체에 대한 권한을 가져와 호스트 플레이어에게 상태를 다시 보냅니다.

명백한 충돌을 일으키지 않고 이 작업을 수행하기 위해 호스트와 게스트는 권한과 소유권을 가져오기 전에 큐브의 로컬 상태를 확인합니다. 예를 들어, 호스트는 이미 게스트가 소유하고 있는 큐브에 대한 소유권을 가지지 않으며, 그 반대의 경우도 마찬가지이며, 권한을 가져가는 동안 플레이어는 큐브를 다른 사람의 스택에 던져 큐브를 만드는 동안 넘어 뜨릴 수 있습니다.

네트워크로 연결된 물리 샘플에서 4 명의 플레이어로 더 일반화하면 모든 패킷이 호스트 플레이어를 통과하여 호스트가 중재자가 됩니다. 사실상, 진정한 피어 투 피어가 아니라 게임의 모든 게스트가 호스트 플레이어와 만 통신하는 토폴로지가 선택됩니다. 이를 통해 호스트는 수락할 업데이트와 무시하고 나중에 수정할 업데이트를 결정할 수 있습니다.

이러한 수정을 적용하려면 호스트가 게스트를 무시하고 “아니오”라고 말할 수 있는 방법이 필요했는데, 이 큐브에 대한 권한/소유권이 없으므로 이 업데이트를 수락해야 합니다. 또 호스트에게 세계와의 게스트 상호 작용에 대한 순서를 결정할 수 있는 어떤 방법이 필요했으므로 한 클라이언트가 갑자기 지연을 경험하고 패킷을 늦게 전달하면 이러한 패킷이 다른 게스트의 최근 작업보다 우선하지 않도록 했습니다.

이전에 내 직감에 따라 이것은 큐브-당 두 개의 시퀀스 번호로 달성되었습니다.

  1. 권한 시퀀스
  2. 소유권 시퀀스

이러한 시퀀스 번호는 각 상태 업데이트와 함께 전송되며 플레이어가 큐브를 보유할 때 아바타 상태에 포함됩니다.
호스트는 게스트의 업데이트를 수락해야 하는 지 여부를 결정하고 게스트는 서버의 상태 업데이트가 더 최근이고 수락해야 하는 지 여부를 결정하는 데 시퀀스가 사용됩니다. 게스트가 큐브에 대한 권한이나 소유권이 있다고 생각하는 경우에도 마찬가지입니다.

권한 순서는 플레이어가 큐브에 대해 권한을 가질 때마다 그리고 플레이어의 권한 아래 큐브가 쉬게 될 때마다 증가합니다.
큐브에 게스트 머신에 대한 권한이 있으면 기본 권한으로 돌아가기 전에 호스트로부터 확인을 받을 때까지 해당 머신에 대한 권한을 보유합니다. 이렇게 하면 상당한 패킷 손실이 발생하더라도 게스트 권한의 큐브에 대한 최종 미사용 상태가 호스트에 다시 커밋됩니다.

소유권 순서는 플레이어가 큐브를 잡을 때마다 증가합니다. 소유권은 권한보다 강하므로 소유권 순서의 증가가 권한 순서 번호의 증가보다 우선합니다. 예를 들어, 다른 플레이어가 큐브를 잡기 직전에 플레이어가 큐브와 상호 작용하면 그것을 이미 잡고 있는 플레이어가 승리합니다.

이 데모 작업으로 얻은 경험에서 이러한 규칙이 충돌을 해결하는 데 충분하고 호스트 및 게스트 플레이어가 세계와 지연없이 상호 작용할 수 있도록 하는 것으로 나타났습니다. 수정이 필요한 충돌은 상당한 지연 시간에서도 실제로 거의 발생하지 않으며, 발생하면 시뮬레이션이 일관된 상태로 빠르게 수렴됩니다.


결론

분산 시뮬레이션 네트워크 모델을 사용하여 Unity 및 PhysX 에서 안정적인 큐브 스택의 고품질 네트워크 물리가 가능합니다.

이 접근 방식은 전용 서버 및 클라이언트 측 예측을 통해 서버 권한이있는 네트워크 모델의 보안을 제공하지 않으므로 협력 환경에만 가장 잘 사용됩니다.

작업을 후원하고 이 연구를 가능하게 해준 오큘러스에게 감사드립니다!

네트워크 물리 샘플의 소스 코드는 fbsamples/oculus-networked-physics-sample에서 다운로드 할 수 있습니다.




참고자료

댓글남기기