2023. 7. 22. 16:05ㆍ언리얼이 리얼보다 쉬웠어요
※ UE5.2.1 기준으로 최신화된 글
기존 동기화 과정
기본적인 언리얼 데디케이티드 서버 구조 기반의 리플리케이트는 대략 이런 형식이다.
여기서 중요한 포인트는 '매 틱 같은 과정을 반복한다는 것'이다.
소규모 레벨, 적은 플레이 인원에서는 전혀 문제가 되지 않는다. 이 경우 '리플리케이트 고려 대상 리스트' 속에 들어 있는 객체는 (보이지 않는 액터까지 포함해서) 끽해봤자 몇십 개 가량이고, 2개의 커넥션을 대상으로 매번 루프를 돌아봤자 서버 입장에서 그렇게 큰 부하가 발생하지 않을 뿐더러 고려 대상 안에 있는 객체들은 어차피 높은 확률로 리플리케이트된다.
하지만 이 경우는? 어림잡아 계산해도 어마어마한 연산이 나온다. 심지어 부하를 감수하고 열심히 돌려봤자 최종적으로 걸러진 한 커넥션 당 리플리케이트되는 객체 수는 얼마 되지도 않는다. 이렇게 기존 리플리케이트 구조는 규모가 커질 수록 비효율적인 방식이 되는 것이다.
※ 기존 리플리케이션 시스템을 개선하기 위해 5.1부터 실험적 모델로 등장한 아이리스의 경우 리플리케이션 그래프를 지원하지 않으므로 주의.
리플리케이션 그래프
리플리케이션 그래프는 작명 그대로 노드 기반으로 돌아가는 구조고, 기존 동기화 과정을 완전히 대체한다. 기존 오브젝트에 달려 있던 리플리케이션용 변수도 여기선 사용되지 않거나 약간 비틀어서 불러와야 한다.(Relevancy나 NetCullDistance 등)
기존 동기화 과정의 경우 매 업데이트마다 리플리케이트를 고려할 액터 리스트를 처음부터 통째로 다시 구성하고 커넥션별로 관련이 있는(Relevant) 액터인지 또다시 따지기 때문에 액터 수가 많아질 수록 서버의 부담이 급증한다. 그래서 리플리케이션 그래프는 미리 액터 리스트를 보관하고 있다가 각 커넥션에게 건네는 방식으로 서버 CPU 연산의 부담을 대폭 해소시키는 것이다. 또 기본적으로 커스텀을 권장하는 구조기 때문에 리플리케이션 처리를 프로젝트에 맞게 최적화시킬 수 있다.
리플리케이션 그래프가 액터 정보를 관리하는 3단계
리플리케이션 그래프는 처음 월드를 초기화할 때, 그리고 새로운 액터가 추가될 때 기본적인 리플리케이션 정보를 구성하여 리스트에 보관하고, 그 액터의 리플리케이션 정보를 알맞는 노드에 라우팅한다. 리플리케이션 그래프는 주로 각 커넥션이 지니고 있는 액터 정보 맵에서 데이터를 참조하여 해당 커넥션에게 리플리케이트되는데, 그 정보는 몇 번의 미러링을 거쳐 캐시된 후 (특별한 조작이 있었다면) 그 커넥션에 맞게 재설정된 값이다. 큰 단위부터 나열하면 이렇다.
1. 클래스별 정보(FClassReplicationInfo)
액터 리플리케이션 방식에 대한 클래스별 데이터.
주요 변수
DistancePriorityScale: 0보다 클 시 거리 및 스케일 수치가 Priority 계산에 반영된다.
StarvationPriorityScale: 0보다 클 시 마지막으로 리플리케이트된 프레임 및 스케일 수치가 Priority 계산에 반영된다.
AccumulatedNetPriorityBias: AccumulatedPriority(Distance나 Starvation 등의 요소를 전부 계산 후 누적된 Priority)의 초기값
ReplicationPeriodFrame: 리플리케이트되기 위해 지나야 하는 최소 프레임. 기본값 1
CullDistanceSquared: 샘플 코드(Basic, Shooter) 기준 추가적인 설정이 없으면 CDO의 NetCullDistanceSquared 값으로 초기화
ActorChaneelFrameTimeout: 0보다 클 시 지정된 시간(프레임)이 지나도 리플리케이트되지 않으면 액터 채널이 닫히게 되며 그 계산식에도 반영된다.
2. 글로벌 액터 정보(FGlobalActorReplicationInfo)
리플리케이션 그래프 전체에서 글로벌인 액터 정보 데이터.
주요 변수
WorldLocation: 캐시된 액터의 위치 정보
Settings: FClassReplicationInfo. CDO 값 기준으로 초기화하지만 액터별로 변경 가능
DependentActorList: 종속 액터 리스트. 해당 액터가 리플리케이트될 때 DependantActorList에 들어 있는 액터들도 함께 리플리케이트함.
ParentActorList: 부모 액터 리스트. 해당 액터가 아무 노드에도 라우팅되어 있지 않더라도 자신을 DependantActor로 지정한 부모 액터가 있다면 부모 액터가 리플리케이트될 때 함께 리플리케이트됨
3. 커넥션별 액터 정보(FConnectionReplicationActorInfo)
커넥션별로 저장되는 액터 정보. 커넥션이 생성될 때 글로벌 액터 정보 기준으로 같이 초기화된다.
주요 변수
ReplicationPeriodFrame, CullDistanceSquared: 기본값은 글로벌 액터 정보 기준으로 초기화하지만 수정 가능. 실제 리플리케이트 과정에선 글로벌이 아닌 커넥션별 값 기준으로 체크
Channel: 액터 채널
ActorChannelCloseFrameNum: 실제 액터 채널이 닫히는 시간(프레임), 리플리케이트될 때마다 계산식↓에 의해서 업데이트
// Only update if the actor has a timeout set
if (GlobalData.Settings.ActorChannelFrameTimeout > 0)
{
const uint32 NewCloseFrameNum = FrameNum + ConnectionData.ReplicationPeriodFrame + GlobalData.Settings.ActorChannelFrameTimeout + GlobalActorChannelFrameNumTimeout;
ConnectionData.ActorChannelCloseFrameNum = FMath::Max<uint32>(ConnectionData.ActorChannelCloseFrameNum, NewCloseFrameNum); // Never go backwards, something else could have bumped it up further intentionally
}
리플리케이션 그래프의 노드 타입
리플리케이션 그래프의 노드는 크게 글로벌 노드와 커넥션 노드로 나뉜다.
기본적으로는 둘 다 UReplicationGraphNode 클래스가 베이스가 되는 노드라는 것은 변함이 없다. 이 두 가지를 구분하는 기준은 누가 소유하고 있는가다.
글로벌 노드들은 리플리케이션 그래프 본체가 들고 있다. 하지만 커넥션 노드는 커넥션 객체가 들고 있다.
(참고로 여기서 말하는 커넥션은 리플리케이션 그래프 기준으로 한번 래핑된 커넥션 관리용 객체를 뜻한다. 우리에게 익숙한 UNetConnection 객체는 그 속에서 포인터로 물고 있다.)
통상적인 경우 글로벌 노드는 전역적으로 영향을 끼치는 노드다. 모든 커넥션에서 사용된다.
새로운 네트워크 액터가 추가되면 리플리케이션 그래프는 모든 글로벌 노드를 순회하면서 NotifyAddNetworkActor 함수를 호출한다. 물론 제거될 때도 마찬가지(NotifyRemoveNetworkActor). 이때 액터 객체와 글로벌 정보를 노드의 용도에 맞게 라우팅시켜서 넣어줄 수 있다.
그 후 동기화 과정에서 커넥션별로 액터 수집을 할 때, 글로벌 노드는 해당 커넥션의 조건에 맞는 액터 정보 리스트를 넣어준다.
반대로 커넥션 노드는 단일 커넥션에만 영향을 끼치는 노드다. 노드를 소유한 커넥션만이 사용한다.
통상적으로는 새로운 네트워크 액터가 추가된다 해도 액터 객체나 정보가 그 순간 직접적으로 커넥션 노드에 들어가진 않는다.
리플리케이션 그래프의 액터 수집 과정에서 커넥션 노드의 순회보다 글로벌 노드의 순회가 먼저인 이유는 이것이다.
기본적으로 커넥션 노드는 소유하고 있는 커넥션, 그리고 플레이어 컨트롤러 외엔 아무것도 알 길이 없다.
글로벌 노드에서 수집해온 액터 정보를 이 단계에서 조작하거나, 플레이어 컨트롤러를 타고타고 가서 액터 정보를 긁어오거나 하는 것이 통상적인 커넥션 노드의 활용 방법이다.
글로벌 노드의 예시로는 AlwaysRelevant, 그리고 후술할 GridSpatialization2D 노드가 있고,
커넥션 노드의 예시로는 AlwaysRelevant_ForConnection가 있다.
(노드 이름에서부터 보이는 것처럼 리플리케이션 그래프는 Actor의 가상 함수 IsNetRelevantFor를 사용하지 않고 이렇게 노드 형태로 항상 모두에게 항상 Relevant, 해당 커넥션에게만 항상 Relevant인 상태를 관리한다)
엔진 코드에서 기본적으로 꽂아주는 대표적인 노드의 형태는 위와 같지만, 커스텀을 통해서 자유롭게 새로운 노드를 구축하고 연결시켜줄 수 있다.
샘플 코드
기본적인 리플리케이션 그래프의 활용 예시는 엔진에 기본적으로 들어 있는 BasicReplicationGraph나 Shooter Game에 들어 있는 ShooterReplicationGraph를 통해 확인할 수 있다. 그중에서도 슈터 게임의 코드는 조금만 개량해도 실전에 활용할 수 있고, 실제로 예전에 에픽측에서 리플리케이션 그래프를 설명하면서 빼꼼 보여줬던 포트나이트의 리플리케이션 그래프도 슈터 게임의 리플리케이션 그래프의 구조와 상당히 흡사한 형태였다.
리플리케이션 그래프는 그리드 노드에 대한 이야기를 빼먹을 수가 없는데, 그리드 얘기만 해도 상당히 길어지기 때문에 다음 글로 끊어서 써볼까 한다. 전반적인 것에 대해선 어느 정도 써놓은 것 같아서 일단은 요기까지 끗.
'언리얼이 리얼보다 쉬웠어요' 카테고리의 다른 글
[UE5] Garbage Collection (0) | 2023.08.31 |
---|---|
[UE5] 언리얼 인사이트로 안드로이드 프로파일링하기 (0) | 2023.08.21 |
[UE5] Octahedral Impostors (0) | 2023.08.18 |
[UE4/UE5] Replication Graph - GridSpatialization2D Node (0) | 2023.08.08 |