UE5/Network

Multiplayer Best Practices in UE #NotGDC

Pyxis 2024. 5. 7. 22:53

NotGDC 2022에서 Omid Kiarostami의 Multiplayer Best Practices in Unreal Engine을 듣고 요약한 필기

영어 강의를 듣고 이해한걸 필기해서 옮겼기 때문에 몇몇 개념은 부정확할 수 있다.

특히 Serialization은 듣기만 하다 처음 알게됐는데 추후에 기초 개념을 다지고와서 다시 필기를 수정해야 할 것 같다.

그래도 기본적인 내부 작동 방식은 많이 배운듯.

 

https://youtu.be/UstLLZbkmOQ?si=yS4AjoZWyhAaP3C7

 

 

[ Networking Primer ]

4가지 분류
A : Ordered, Unordered
B : Reliable, Unreliable
(ex)
UDP : Unordered, Unreliable
TCP : Ordered, Reliable
Ordered, Reliable -> *Expensive*

 

[ RPC ]

- Server RPC

Client가 Server에게 연락할 수 있는 유일한 방법
- Client, Mulicast RPC
가능하다면 가장 피해야할 것

 

[ Replication ]
1. 서버가 매우 빠르다고 가정, myVar를 1, 2, 3로 점점 매우 빠르게 올린다면 Client는 곧바로 3의 값만 복제받는다.
 : 강연자는 이걸 Replication이 Semi-reliable같은 방식이라고 한다.
2.
MyVar를 1에서 3으로 변경, MyVarZ를 A -> B로 변경
두 가지 작업을 동시에 했다면, 클라이언트는 두 변수의 복제 순서를 보장할 수 없음 (Atomicity)
그럼 두 복제되는 변수가 서로 의존적인 경우는 어떻게 해야하는가?
-> struct에 묶어서 한번에 보내면 복제 순서를 보장할 수 있게되는데, 이걸 Serialization이라고 함
만약 struct의 크기가 매우 크다면 Client는 그걸 받을 수 없어 게임에서 강제로 연결이 종료된다

[ Edge Cases - Relevancy ]
P라는 Player가 A라는 액터와 매우 멀리 떨어져있고, P가 보고있는 방향에도 A가 없다고 가정하자.
그럼 엔진은 A가 연관없다고 판단해서 A를 Destroy시킨다.
만약 P의 일정 Radius에도 들어오고, dot product를 통해 보고있는 방향에 속해있다고 판단한다면,
다시 A를 Spawn하게 되고 BeginPlay() 다시 호출, Replicated 변수들도 다시 복제한다
=> 이 때문에 개발과정에서 인지할 수 없는 엽기적인 버그가 발생할 수 있다
만약 빌딩같은 오브젝트가 하나 있다고 치자.
이 빌딩을 파괴하고 멀리 반대방향으로 떨어졌다가 다시 돌아와서 쳐다보면, Relevancy 때문에 빌딩이 파괴되는 애니메이션이 다시 재생되는걸 볼 수 있다.

subQ&A /

Q : Relevancy는 Culling같은 그래픽스 기능으로 인해 언리얼엔진의 모든 개발방식에서 적용되는 것인가요?

A : No, Multiplayer에 한함.

[ Q&A ]
Q : Why to avoid Client RPCs ?
여기서 Client RPCs는 Client와 Multicast RPC를 뜻함
일단 Client RPCs가 실행되고, 
강연자같은 경우 Client RPC를 안쓰고도 (출시된) 게임을 만든적이 있다.
한가지 예외로 채팅이 있다
기본적으로 TEXT를 누락없이 전체 Client에 보내야 하므로 Multicast RPC를 쓴다.
그리고 이건 RPC의 단점과도 귀결되는데, 도중에 들어온 Player는 이전의 채팅 내역을 볼 수 없다. (이전의 RPC 실행을 알 수 없다)
비용이 높으므로 채팅 메세지를 보내는데 반드시 제한을 둬야한다
일반적으로 Client RPC는 특수한 상황에 쓰라고 있는 것이다
-> 예를들면 Serializing large blocks of data, or 복제 순서를 보장하기 위해서

[ Roles & Autonomous Proxy's ]
Q : 만약 Client에서 Actor를 spawn한다면 (일반적으로 그래선 안되겠지만) 그 Actor는 Client내에서 ROLE_Authority를 가지나요?
A : Okay(긍정인가?). Actor의 variable은 확신할 수 없지만 그 Actor는 Client내에서만 존재하게 될 것이고 다른 Client에선 실제로 존재하지 않을 것임. (좋은 질문이에요.)

Q : When to use Reliable RPCs ?
기본적으로 UnReliable로 생각하자. 하지만 Reliable을 쓴다고 세상이 망하는건 아니다.
호출 빈도가 제한된 방식에 한하여는 많이 쓴다 -> 유저 인풋을 통해 n초에 한 번 call하는 경우
만약 매 frame마다 reliable을 보낸다면 backlog queue가 엄청나게 밀리게되고 서버가 뻗어버린다

[ Ways Replication is Better ]
Q : NetSerialize is less expensive than Reliable RPC?
A : Yes, 기본적으로 수신을 보장하지는 않기 때문이다. 하지만 최신 정보 수신을 보장할려고 노력함.
그리고 Replication 내부적으로 데이터 전송 방법에 많은 최적화가 있다. (load balance)
이 중에 Priority가 있는데, 예를들면 게임의 bandwidth가 제한된 상황에서 가까이 있는 Replicated Actor들은 높은 우선순위를 갖는다.

(충분히 멀리있는 Actor들은 복제 우선순위가 떨어지므로)

Q : 그럼 Reliable RPC는 위 같은 최적화를 하지 않나요?
A : Yes, Server로 보낸뒤에 Server가 Client에 잘 받았다고 다시 응답을 보낸다.
응답을 못 받았다면 성공할 때까지 Server로 보낸다 -> 타임아웃이 발생할 수도 있는데, 
수신 응답을 절대 받지 못하면 프로그래머의 관점에서 실패 조건을 이해할 수 없고, 무엇이 발생할지 알 수 없음
 반면에 Replication은 최종적인 값으로 동기화를 할 수는 있다. 최악의 경우 몇몇 값이 누락될 수 있지만
프로그래머는 그 상황은 확인할 수 있으므로 잠재적으로 누락될 수 있는걸 감안하는 코드를 작성 가능함.
-> Reliable RPC와 Replication의 각각 최악의 경우를 예시 들어주고, 프로그래머의 handling 관점에서 비교해서 설명해줌

[ How to model a Trigger with Replication ]
Replication을 통한 data packet 해석 트릭
1) 서버에서 총의 방아쇠를 당기고 놨을때 bool bTrigger 변수가 있다고 가정하자.
true가 replicate되고 그 패킷이 손실됐다면, 클라이언트에서 bTrigger의 값에 따라 총알 발사음을 내야할 경우 실행되지 않을 것이다.
2) 서버에서 총의 방아쇠를 당겼을 때와 놨을 때 전부를 기록하는, struct내에 int Activation, Deactivation 두 변수가 있다고 가정하자.
방아쇠를 당기고 놓는다면 변수 값이 각각 {0, 0} -> {1, 0} -> {1, 1}로 변경된다.
그리고 동일 frame에서 다시 한번 {1, 1} -> {2, 2}로 변경한다고 하자.
패킷을 받는 Client는 어느 패킷이 손실되더라도, {1, 0}을 받거나 {1, 1}, {2, 2} 중 하나만 받더라도 발사된 "사실" 자체는 해석할 수 있다.
{2, 2}만 받는다 하더라도 이전 값과 비교해서 서버내에서 두번 실행됐다는걸 추측할 수 있다.
또한 struct내에 Activation, Deactivation이 분리되어 있다면 Replication은 Unordered이므로 위 사실을 알지 못할 것이다.