본문 바로가기

언리얼 개발자

[Unreal] AcknowledgedPawn

AcknowledgedPawn 변수의 역할

APlayerController 클래스에 AcknowledgedPawn 라는 변수가 존재합니다.

 

 

 부모 클래스인 AControllerPawn 변수와 AcknowledgedPawn 는 어떤 차이가 있을까요?

핵심은 언리얼이 주석처럼 Client 측의 Possess 를 인정해주는 역할을 합니다. 그리고 그 역할은 네트워크 상에서 유효합니다. 쉽게 말해, Server 측에서 Client가 Possess가 제대로 되었는지 확인할 수 있는 수단으로 사용될 수 있습니다. 

 

 만약 AcknowledgedPawnPawn 객체가 동일하지 않다면 UCharacterMovementComponent ::ReplicateMoveToServer() 함수의 예외에 걸려 이동조차 못할 수도 있습니다.

 

 

 

AcknowledgedPawn 값이 세팅되는 플로우

 Dedicated Server 환경이라고 했을 때 Possess는 보통 Authority를 갖고 있는 Server에서만 가능합니다.

'보통' 이라는 단어를 넣은 이유는 AController 클래스에 bCanPossessWithoutAuthority 변수가 있기 때문입니다. 아직 저 변수의 필요성을 느낀 적은 없지만, true로 변경하면 Possess 로직이 Authority를 갖지 않아도 돌긴 합니다.

 

 다시 돌아와서 Possess는 Server에서 이루어진다는 전제 하에 간략한 Possess Flow를 보겠습니다.

 

Possess Flow

 'Net' 이라는 Lifeline 은 단순히 RPC를 표현하기 위해 중간에 둔 것으로 큰 의미는 없습니다.

검정색 네모 박스가 있는 부분이 저희가 알아보려는 AcknowledgedPawn 변수가 세팅되는 부분입니다.

 

1. Server - APlayerController::OnPossess()

제일 먼저 빙의를 하는 과정에서 Server의 AcknowledgedPawnNULL로 세팅됩니다. Client의 Possess가 완료되었다는 RPC를 받기 전까진 NULL 값을 유지합니다. 그렇기에 AcknowledgedPawn != Pawn 조건을 통해 아직 해당 클라이언트가 준비가 안되었다는 걸 알 수 있습니다. 그 후에 ClientRestart() Client RPC를 호출합니다.

 

2. Client - APlayerController::ClientRestart_Implementation()

 Client 측에서는 ClientRestart() RPC에 함께 넘어온 빙의한 폰을 Pawn 변수에 할당하면서 AcknowledgedPawn 변수에도 할당합니다. 그 후에 ServerAcknowledgePossession() Server RPC를 호출합니다.

 

3. Server - APlayerController::ServerAcknowledgePossession() 

 Server에서는 RPC와 함께 넘어온 Pawn객체를 AcknowledgedPawn에 할당합니다.

 

 위 과정을 거치면서 AcknowledgedPawn 변수의 값이 서버, 클라가 동기화되고 서버에서는 클라이언트가 정상적으로 빙의를 마쳤음을 알 수 있습니다.

 

 

AcknowledgedPawn 값의 Server, Client 동기화 예외 처리

 살펴본 과정에 의하면 AcknowledgedPawn 변수는 주어진 역할에 따라 Property Replication이 아닌 RPC를 통해 동기화하고 있습니다. 그렇기 때문에 엔진에서는 추가적으로 Server, Client의 AcknowledgedPawn 변수가 같은 값을 유지할 수 있도록 추가 처리를 해주고 있습니다. Reliable RPC를 호출하면서 값을 맞추기 때문에 항상 같은 값을 가질 것 같지만, 아쉽게도 예외 상황이 있습니다. 그 중 하나의 예시를 보겠습니다.

 

Exception

 빨간색 네모 박스 부분의 ClientRestart()가 호출될 때 인자로 넘어온 Pawn이 nullptr인 경우가 있다면 어떨까요?

Client에서 빙의하려는 폰이 Net Culling되어 replicate 되지 않은 상태라면 충분히 일어날 수 있는 예외입니다. AcknowledgedPawn 변수는 ClientRestart()에 의해 값이 세팅되는 반면에 Pawn이라는 변수는 Replicate되는 변수로 결국에는 값이 할당됩니다. 그렇기 때문에 AcknowledgedPawnPawn 객체의 값이 달라지는 예외가 생깁니다.

 

 이해를 돕도록 아래 그림을 첨부합니다.

  

 Client에는 Net Culling되어 있는 'Pawn B' 에 Server가 빙의를 시도한다면, Client 측에서 ClientRestart() RPC를 받았을 때 는 인자로 넘어온 Pawn이 nullptr일 수 있습니다. 따라서 ClientRestart() 함수 내에서는 넘어온 Pawn이 nullptr일 경우 Reliable Server RPC ServerCheckClientPossessionReliable()를 호출합니다. 서버로 하여금 ClientRestart()을 다시 호출하도록 요청하는 겁니다. 서버는 현재 AcknowledgedPawn과 Pawn이 다른 지 확인하고 다를 경우에만 Reliable Client RPC ClientRetryClientRestart()를 통해 Client에서 ClientRestart() 로직이 돌 수 있도록 합니다.

 

 정리하면 처음 ClientRestart()가 불렸을 때 넘어온 폰이 nullptr이라면 서버에 다시 ClientRestart()를 호출을 요청하는 RPC를 쏘는 것입니다. 또한 Client의 APlayerController 에서 매 틱 AcknowledgedPawn과 Pawn을 비교하며 다를 경우 특정 Interval마다 ServerCheckClientPossession()를 호출하면서 동기화합니다.

 

 

최악의 경우, AcknowledgedPawn과 Pawn이 끝까지 다를 수 있다.

 쉽게 경험할 수 없는 최악의 경우가 있습니다. 정말 경험하기 힘든 케이스인데요, 아래의 순서일 때 발생합니다.

 

1. Server   : 빙의 시도, Client에 ClientRestart() 호출

2. Client    : ClientRestart() 에 Pawn이 제대로 넘어옴, Server에 Possess 완료 RPC 호출

3. Server   : Possess 완료, AcknowledgedPawn 에 세팅

4. Client    : 순간적으로 빙의한 폰이 Net Culling 되면서 AcknowledgedPawn, Pawn 모두 nullptr 세팅

5. Client    : Pawn 객체는 다시 Replicate 되면서 값이 세팅, AcknowledgedPawn 객체는 nullptr 유지 ( 값 다름 )

 

 위에서 말씀드린 것처럼 Client의 APlayerController 에서는 매 틱 AcknowledgedPawn과 Pawn이 다른 지 확인하고 다를 경우 Server에 RPC를 통해 알립니다. 하지만 Server에서는 3번 과정을 거쳤기 때문에 AcknowledgedPawn과 Pawn이 같은 값을 가지고 있고 해당 요청을 무시합니다. 즉 이 상황이 발생하면 Client는 Server에 주기적으로 의미없는 RPC를 쏘고 있고 이동이 불가능하며 여러 에러들이 발생할 겁니다.

 

 이 문제를 해결하려면 다시 Possess를 하거나 혹은 폰이 빙의한 채로 제거될 때 호출되는   AController::PawnPendingDestroy()를 override 하여 특수한 예외 처리를 하는 것인데, 깔끔한 방법을 찾진 못했습니다.

물론 위 문제는 빙의하는 폰을 자주 변경하며 각 폰 간의 거리가 먼 경우와 같은 정말 특수한 상황에서만 발생했었습니다.