본문 바로가기
Unreal 게임 개발/Unreal 강의 개인 정리

움직임 리플리케이션 & 디버깅- Unreal Network MultiPlayer Framework

by daisy0461 2025. 3. 11.

캐릭터 움직임 리플리케이션 플로우

(Chracter Movement Component에 구현되어 있음)

  1. 클라이언트에서 움직임 입력 명령
  2. 현재 Tick에서 입력 가속(입력을 가속도로 변환)과 움직이기 전의 캐릭터를 클래스에 기록한다.
    입력 가속은 이동 모드와 입력 변수를 기반으로 계산된다.
  3. Character Movement의 함수인 PerformMovement 실행
    Chracter Movement에 설정한 속성에 맞게 물리적으로 캐릭터가 이동하는 함수이다.
    움직이기 전의 상태를 기록한 클래스와 같은 클래스에 이동한 캐릭터의 움직임(FSavedMove_Character)를 기록한다.
    최적화를 위해서 유사한 움직임 항목(FSavedMove)들은 함께 결합시켜서 ServerMoveRPC를 호출한다.
  4. 해당 기록에서 중요한 정보를 모아서 ServerMoveRPC(서버RPC)를 호출한다.
    서버로 클라이언트 움직임 정보를 보낸다.
  5. 서버는 클라이언트로 받은 움직임 정보를 분석하고 클라이언트가 보낸 가속정보에 따라서
    MoveAutonomous()라는 함수를 실행해 캐릭터를 움직인다.
  6. 서버에서 움직임을 실행한 결과가 나오면 서버와 클라이언트의 움직임(최종위치)을 비교한다.
    비교했을 때 통과하면 이동이 유효하다는 신호를 클라이언트에게 보낸다.
  7. 위의 움직임을 비교했을 때 격차가 크다면 ClientAdjustPostionRPC를 호출한다.
    위 RPC를 통해 SavedMoves라는 대기열에서 서버가 처리한 움직임을 찾아 다시 올바른 최종 위치를 찾는다.
  8. 버에서 받은 움직임을 받은 클라이언트는 캐릭터의 위치를 변경한다.

움직임 주요 함수 Unreal Engine 함수

ReplicateMoveToServer()
클라이언트 캐릭터의 움직임을 보관하는 데이터를 생성하고 이를 추려서 서버로 보내는 역할을 수행한다.

  1. 클라이언트에 저장된 이전 움직임(OldMove)들 중에서 중요한 움직임을 참고한다.
  2. 현재 Tick에서 신규 움직임(NewMove)을 생성한다.
  3. NewMove엔 클라이언트 캐릭터가 움직이기 전에 시작한 위치(StartLocation)를 기록한다.
  4. 필요시 마지막으로 저장한 움직임과 현재 움직임을 합칠 수 있는지확인하고 합칠 수 있으면 합친다.
  5. 위의 준비가 끝나면 PerformMovement()함수를 호출하여 입력에 따른 움직임을 구현한다.
    위 함수의 결과에 따라 이동한 결과가 나오는데 이를 다시 신규 움직임(New Move)에 기록한다.
  6. ServerMove함수를 호출해 OldMove와 NewMove를 서버에 전송한다.
더보기

nullchek 밑에 FNetworkPredicationData_Client_Character라는 클라이언트 정보를 클래스를 제공해준다.

이 클래스에는 다양한 정보가 있지만

SaveMoves는 각각의 움직임을 FSavedMovePrt이라는 클래스를 통해서 움직임을 저장한다.

이러한 움직임의 정보를 FNetworkPredicationData_Client_Character가 보관하고 있다.

그 뒤에 OldMove에 이전의 움직임에서 중요한 움직임을 저장해준다.
여기서 IsImportantMove가 중요한 변화인지 감지해주는 함수이다.

그리고 현재 클라이언트가 진행하는 움직임을 NewMovePtr이라는 객체에 저장을 한다.

처음에는 SetMoveFor라는 함수를 통해 기본값을 저장해준다.

SetMoveFor를 보면 현재 캐릭터의 초기 위치와 상태를 저장하도록 되어있다.

이렇게 NewMove는 움직이기 전의 초기 위치와 상태를 저장한다.

그 다음에 꽤 긴 PendingMove관련 로직이 나오는데 간단하게 처리하지 못한 움직임을 처리하는 로직이다.

그 후 PerformMovement()를 호출해서 Character Movement Component가
캐릭터를 현재 상태에 맞춰서 움직이도록 한다.

이제 캐릭터가 이동을 했고

이제 NewMove에 PostUpdate()함수를 통해 움직임이 종료된 시점에 위치와 회전, 속도 값을 저장해준다.

Replication 옵션이 켜져있다면 ClientData에 NewMove를 추가해준다.

그리고 ServerMove를 보낸다면 ServerRPC를 실행한다.

두 함수는 ShouldUsePackedMovementRPCs()라는 압축된 RPC를 사용할거냐라는 결과에 따라서 다른 RPC가 호출된다.

 

ServerMove()

클라이언트의 최종 움직임 정보를 서버에 보내는 함수

ServerMove 인자

  1. TimeStamp : 움직임에 대한 시간 정보 (해당 움직임을 찾는 키로도 사용된다)
  2. Acceleration : 입력으로 발생된 최종 가속 정보를 작은 사이즈로 인코딩
  3. Location : 캐릭터의 월드의 최종 위치 정보. 캐릭터가 베이스 (플랫폼 위에 있는 경우 상대위치 사용)
  4. NewMove->GetCompressedFlags : 특수한 움직임 (점프, 웅크리기)에 대한 정보
  5. 회전 정보 : 압축된 회전 정보 (Yaw 회전 중심으로 저장하며 Roll과 Pitch는 하나로 묶어서 저장)
  6. ClientBaseBone : Skeletal Mesh Component 경우 기준이 되는 Bone 정보
  7. NewMove->EndPackedMovementMode : Character Component의 Movement Mode 정보
더보기

CallSeverMove에서 NewMove와 OldMove 두가지 정보를 서버로 보낸다.

OldMove가 있다면 ServerMoveOld라는 서버 RPC를 호출한다.

이 로직은 OldMove 중에 중요한 움직임을 일단 서버로 보내는 작업이다.

 그리고 ClientData 중에서 움직임을 처리하지 못한 데이터(PendingMove)들에 대해서 처리한다.

처리하지 못한 데이터는 루트모션과 딜레이된 움직임이 예시이다.

 

움직임을 처리하지 못한 데이터가 아니라면

위에서 설명한 ServerMove를 실행한다 .

Roll에대한 정보 변화는 거의 일어나지 않기에 8비트로 저장하고

Yaw와 Pitch는 32비트 정수로 저장한다.

해당 정보를 GetPackedAngle이라는 함수를 사용해서 압축해서 변수로 저장한다.
압축된 데이터는 이후 서버에서 디코딩한다.

 

ServerMove_Implementation()

서버 캐릭터의 움직임을 보관하는 네트워크용 서버 데이터를 생성한다.

  1. 클라이언트로 받은 타임 스탬프 값을 검증한다.
    (너무 오래 패킷이 도착하지 않았는데 클라이언트를 신뢰해서 비어있는 시간을 예측해서 캐릭터를 움직이면 위험요소가 존재한다. 그렇기에 상당한 시간 차가 감지되면 해킹방지를 위해 서버 Tick으로 빠르게 움직임을 예측한다.
    이후 네트워크 매너저 설정을사용해 클라이언트와 서버 시간을 서서히 균등화 시킨다.)
  2. 압축된 Acceleration, 회전 데이터를 디코딩하고 클라이언트와 서버의 타임 스탬프 정보를 기록한다.
  3. MoveAutonomous 함수를 호출해 서버 캐릭터를 이동시킨다.
  4. 최종위치가 나오면 클라이언트와의 차이를 비교하고 에러를 수정한다.
    (떨어지는 상황, 착지할 때의 상황에 따라 허용 가능 범위 내에서 클라이언트 데이터를 신뢰한다.
    상당한 시간 차가 감지되면, 수정 정보를 기록한다.(PendingAdjustMent))
더보기

ServerMove_Implementation이다.

위 사진의 가장 아래에 보면 위에서 설명한 것과 유사한 FNetworkPredictionData_Server_Character가 있다.

위는 FNetworkPredictionData_Client_Character가 있었다.

FNetworkPredictionData_Server_Character는 네트워크 동기화를 진행하기 위해서 저장하는 특별한 클래스이다.

해당 클래스에는 클라이언트에서 받은 데이터를 현재 서버 상태와 비교하는 값들로 이루어져있다.

 그 이후 클라이언트로 받은 타임 스탬프 값이 유효한지 검사한다.

유효한 경우에도 VerifyClientTimeStamp 안에서 

 해당 함수를 실행 시간 차에 관한 것들을 어떻게 처리할지에 대한 로직이 있다.

해당 로직이 서버 Tick으로 빠르게 움직임을 예측하고 네트워크 매니저에 설정되어 있는 값을 참고해서
클라이언트와 서버의 값을 서서히 동기화 시키는 작업을 한다.

 VerifyClientTimeStamp가 통과하면 회전 값과 Acceleration 값을 복원한다.

 그리고 Server에서 진행된 DeltaTime에 대한 정보를 받아

서버측에서 자율적으로 이동을 시키도록 MoveAutonomous 함수를 실행한다.

다음 함수를 호출하면 이동을 하게된다.

이제 이동에 대한 결과를 ServerMoveHandleClientError에서 클라이언트과 서버가 얼마나 차이가 나는지 비교한다.

허용하는 범위 내에선 차이가 나는 현상은 받아들인다.

 

ClientAdjustPosition()

서버와 클라이언트의 위치가 다를 때 클라이언트에게 수정할 위치 정보를 알려주는 함수

중복 없이 서버 틱의 마지막에서 수정이 필요할 때만 전송한다.

ClientAdjustPostion 인자

  1. TimeStamp : 클라이언트의 TimeStamp 값 (해당 값을 통해 현재 서버가 검증한 움직임을 찾는다.)
  2. DeltaTime : 서버의 DeltaTime
  3. Movement Mode : 압축된 Character Movement Mode 정보
  4. New Speed / New Location / Now Rotaion : 수정할 새로운 속도, 위치, 회전 정보
  5. 새로운 Base와 Base Bone 이름 : 수정할 Base에 대한 정보

ClientAdjustPosition_Implementaion()

서버에서 위치가 달라서 클라이언트에 위치를 수정하라는 요청을 받았을 때 클라이언트가 위치를 수정하는 함수이다.

  1. TimeStamp값을 통해 서버로부터 받은 움직임 정보를 찾고
    LastAckedMove라는 변수로 옮기며 이전에 저장된 움직임을 모두 제거한다.
  2. 서버에서 전달받은 위치로 루트 컴포넌트(캐릭터)의 위치를 변경한다.
  3. 서버에서 전달받은 New Speed로 Movement Component의 속도를 수정한다.
  4. Base정보와 위치를 수정한다.
  5. 서버에 의해 클라이언트 위치가 업데이트되었다고 기록한다.(bUpdatePosition에 기)
    클라이언트에서 움직임을 진행할 때 bUpdatePostion이 true라면
    서버의 수정 정보를 바탕으로 MoveAutonomous()함수를 호출해 클라이언트에서 남은 움직임을 재생한다.
더보기

ClientAdjustPosition_Implementation()함수에서 현재 저장된 Move중에 서버가 처리한 무브가 어떤 것인지 TimeStamp값을 사용해서 어떤 움직임을 서버가 처리했는지 확인한다.

확인이 끝난 후 AckMove()라는 함수를 호출한다.

이 함수의 역할은 서버가 처리한 움직임을 LastAckedMove라는 변수로 옮겨주는 역할이다.

그리고 해당 움직임보다 이전에 저장된 SavedMove에 대해 모두 FreeMove()를 호출해 제거한다.

이젠 LastAckedMove 이후에 저장된 움직임만 클라이언트에 남게 된다.

 이후 여러가지 처리 후에

위치에 대한 값을 업데이트 해준다.

그리고 ClientData에 bUpdatePosition이라는 플래그를 true로 설정한다.

이러면 함수가 종료하고

TickComponent()가 호출이 된다. 이 TickComponent()를 살펴보면

다음과 같은 로직이 있는데 bUpdatePosition 플래기가 true라면 캐릭터 움직임을 컨트롤하기 전에

클라이언트의 위치를 업데이트하는 ClientUpdatePostionAfterServerUpadate가 먼저 호출된다.

해당함수는 

LastAckedMove 이후에 저장된 움직임에 대해 하나씩 돌면서 MoveAutonomous()함수를 호출해서 클라이언트의 캐릭터를 이동시켜준다.

이렇게 남은 움직임을 모두 실행시켜준 다음에 

입력을 처리하는 ControlledCharacterMove()함수를 호출한다.

 

 

움직임 리플리케이션 디버깅 방법

RPC 디버깅 방법

DefaultEngin.ini에 다음과 같이 코드를 추가해준다.

다음과 같이 작성하면 클라이언트와 서버 간 진행되는 RPC의 모든 정보를 로그로 출력하게 된다.

 

위치 디버깅 방법

Unreal Editor에서 콘솔 명령어를 다음과 같이 입력해주면 
클라이언트 위치와 서버간의 위치의 이동차이가 발생하는 경우 DrawDebug함수가 호출되면서 캡슐이 보인다.

서버에서의 디버그는

전달받은 클라이언트의 위치를 붉은색으로 표시하고 서버에서 움직인 위치를 녹색으로 표시한다.

클라이언트에서의 디버그는 
클라이언트가 지정한 위치를 붉은색, 서버가 수정해준 위치를 녹색으로 표시한다.

클라이언트에서 수정은 발생했지만 서버와 클라이언트 위치가 거의 동일한 경우에 노란색으로 표시한다.