언리얼 엔진5 Advanced - 움직임 리플리케이션

타입·2025년 4월 7일
0

언리얼 강의

목록 보기
33/47

자율 움직임 리플리케이션

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

클라이언트의 입력 정보를 서버로 보내고 서버에서 확인 후 수정을 거침

사용자 입력이 가속도 수치로 변환되어 전달
PerformMovement() 함수를 실행하여 이동
서버로 클라이언트의 움직임 정보를 RPC로 보냄

서버에선 MoveAutonomous() 함수를 실행하여 캐릭터 이동 시뮬레이션
차이가 크면 ClientAdjustPosition RPC 호출하여 클라이언트의 수정 요청

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/understanding-networked-movement-in-the-character-movement-component-for-unreal-engine
캐릭터 무브먼트 흐름은 "무브먼트 리플리케이션 개요" 항목을 자세히 보면 좋습니다.

Autonomous Proxy 클라이언트의 진행

  • UCharacterMovementComponent::ReplicateMoveToServer()

클라이언트의 캐릭터의 움직임을 보관하는 네트워크용 클라이언트 데이터 생성
클라이언트의 데이터에 저장된 움직임 중에 참고할 중요한 움직임 기록 (OldMove)
현재 틱의 움직임을 기록하는 신규 움직임 생성 (NewMove)
입력을 처리하기 전의 각종 초기화 상태를 저장 - 예) StartLocation
필요시 최종 움직임과 현재 움직임을 병합 시도
클라이언트 로컬에서의 움직임 진행 (PerformMovement)
신규 움직임(NewMove)에 움직임 결과 상태를 저장 - 예) SavedLocation
신규 움직임을 클라이언트 데이터에 추가
ServerMove() 함수를 호출해 OldMove와 NewMove를 서버에 전송

콜스택

  • TickComponent()
    • ControlledCharacterMove()
      • 클라이언트면 ReplicateMoveToServer()
      • 서버면 PerformMovement()

ReplicateMoveToServer()

// CharacterMovementComponent.cpp
void UCharacterMovementComponent::ReplicateMoveToServer(float DeltaTime, const FVector& NewAcceleration)
{
	...
    
    // 움직임 정보를 보관하는 클래스
	FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
	if (!ClientData)
	{
		return;
	}
    
	// Update our delta time for physics simulation.
	DeltaTime = ClientData->UpdateTimeStampAndDeltaTime(DeltaTime, *CharacterOwner, *this);
    
	// Find the oldest (unacknowledged) important move (OldMove).
	// Don't include the last move because it may be combined with the next new move.
	// A saved move is interesting if it differs significantly from the last acknowledged move
	FSavedMovePtr OldMove = NULL;
	if( ClientData->LastAckedMove.IsValid() )
	{
		const int32 NumSavedMoves = ClientData->SavedMoves.Num();
		for (int32 i=0; i < NumSavedMoves-1; i++)
		{
			const FSavedMovePtr& CurrentMove = ClientData->SavedMoves[i];
			if (CurrentMove->IsImportantMove(ClientData->LastAckedMove))
			{
            	// 중요한 변화가 일어난 경우 이전의 움직임 저장
				OldMove = CurrentMove;
				break;
			}
		}
	}
    
	// Get a SavedMove object to store the movement in.
	FSavedMovePtr NewMovePtr = ClientData->CreateSavedMove();
	FSavedMove_Character* const NewMove = NewMovePtr.Get();
	if (NewMove == nullptr)
	{
		return;
	}
    
    // 움직이기 전의 초기 정보와 상태 등을 저장
	NewMove->SetMoveFor(CharacterOwner, DeltaTime, NewAcceleration, *ClientData);
    
    ... // PendingMove 처리
    
	// Perform the move locally
	CharacterOwner->ClientRootMotionParams.Clear();
	CharacterOwner->SavedRootMotion.Clear();
    // 캐릭터를 현재 상태에 맞춰 움직이도록 처리
	PerformMovement(NewMove->DeltaTime);

	// 움직임이 종료된 시점의 위치/방향/속도 정보를 저장
	NewMove->PostUpdate(CharacterOwner, FSavedMove_Character::PostUpdate_Record);
    
	// Add NewMove to the list
	if (CharacterOwner->IsReplicatingMovement())
	{
    	// CharacterMovement가 리플리케이션 중인 경우 새롭게 저장한 움직임을 SavedMoves에 추가
		check(NewMove == NewMovePtr.Get());
		ClientData->SavedMoves.Push(NewMovePtr);
        
        ...
        
        // 업데이트된 시간을 저장
		ClientData->ClientUpdateRealTime = MyWorld->GetRealTimeSeconds();
        
        ...
        
        // ServerMove RPC 호출
		// Send move to server if this character is replicating movement
		if (bSendServerMove)
		{
			SCOPE_CYCLE_COUNTER(STAT_CharacterMovementCallServerMove);
			if (ShouldUsePackedMovementRPCs())
			{
				CallServerMovePacked(NewMove, ClientData->PendingMove.Get(), OldMove.Get());
			}
			else
			{
				CallServerMove(NewMove, OldMove.Get());
			}
		}
    }
    
    ...
}

클라이언트가 호출하는 서버 RPC

  • UCharacterMovementComponent::CallServerMove()
  • 클라이언트의 최종 움직임 정보를 서버에 보내는 함수
    • 타임스탬프: 움직임에 대한 시간 정보
    • 가속 정보: 입력으로 발생된 최종 가속 정보를 작은 사이즈로 인코딩
    • 위치 정보: 캐릭터의 최종 위치 정보, 캐릭터가 베이스 - 예) 플랫폼 위에 있는 경우는 상대 위치를 사용
    • 플래그: 특수한 움직임(점프, 웅크리기)에 대한 정보
    • 회전 정보: 압축된 회전 정보 (Yaw 회전 중심으로 저장)
    • 본 정보: 스케레탈 메시 컴포넌트인 경우, 기준이 되는 본 정보
    • 무브먼트 모드 정보: 캐릭터 컴포넌트의 무브먼트 모드 정보

CallServerMove()

void UCharacterMovementComponent::CallServerMove
	(
	const FSavedMove_Character* NewMove,
	const FSavedMove_Character* OldMove
	)
{
	check(NewMove != nullptr);

	// Compress rotation down to 5 bytes
	uint32 ClientYawPitchINT = 0;
	uint8 ClientRollBYTE = 0;
	NewMove->GetPackedAngles(ClientYawPitchINT, ClientRollBYTE);

	// send old move if it exists
	if (OldMove)
	{
    	// 캐릭터의 Server RPC 호출
        // 함수의 선언은 캐릭터에 되어있지만, 로직 수행은 캐릭터 무브먼트 컴포넌트에서 진행되는 특징
		ServerMoveOld(OldMove->TimeStamp, OldMove->Acceleration, OldMove->GetCompressedFlags());
	}

	FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
	if (const FSavedMove_Character* const PendingMove = ClientData->PendingMove.Get())
    {
    	... // PendingMove 처리
    }
    else
    {
		ServerMove(
			NewMove->TimeStamp,
			NewMove->Acceleration,
			SendLocation,
			NewMove->GetCompressedFlags(),
			ClientRollBYTE, // GetPackedAngles()로 압축
			ClientYawPitchINT,
			ClientMovementBase,
			ClientBaseBone,
			NewMove->EndPackedMovementMode
			);
    }
}

서버의 처리

  • UCharacterMovementComponent::ServerMove_Implementation()

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

클라이언트로부터 받은 타임스탬프 값을 검증
타임 스탬프 값을 다양한 방법으로 검증
상당한 시간 차가 감지되면, 해킹 방지를 위해 서버 틱으로 제한함
네트워크 매니저 설정의 보상 비율을 사용해 클라이언트와 서버 시간을 서서히 균등화시킴

압축된 가속, 회전 데이터를 디코딩하고 클라이언트와 서버의 타임스탬프 정보를 기록

MoveAutonomous() 함수를 호출해 서버 캐릭터를 이동시킴

클라이언트와의 차이를 비교하고 에러를 수정함
떨어지는 상황, 착지할 때의 상황에 따라 허용 가능 범위 내에서 클라이언트 데이터를 신뢰함
상당한 시간 차가 감지되면, 수정 정보를 기록함(PendingAdjustment)

ServerMove_Implementation()

void UCharacterMovementComponent::ServerMove_Implementation(
	float TimeStamp,
	FVector_NetQuantize10 InAccel,
	FVector_NetQuantize100 ClientLoc,
	uint8 MoveFlags,
	uint8 ClientRoll,
	uint32 View,
	UPrimitiveComponent* ClientMovementBase,
	FName ClientBaseBoneName,
	uint8 ClientMovementMode)
{
	SCOPE_CYCLE_COUNTER(STAT_CharacterMovementServerMove);
	CSV_SCOPED_TIMING_STAT(CharacterMovement, CharacterMovementServerMove);

	if (!HasValidData() || !IsActive())
	{
		return;
	}	

	// 서버측 데이터
    // 클라이언트와 달리 움직임 정보를 담아두는 것이 없고, PendingAdjustment 존재
	FNetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character();
	check(ServerData);
    
    // 타임스탬프 유효성 검사
    // 자세한 처리는 ProcessClientTimeStampForTimeDiscrepancy() 참고
	if( !VerifyClientTimeStamp(TimeStamp, *ServerData) )
    {
    	...
        
		return;
    }
    
    ...
    
    // 회전/가속 정보 복원
	// View components
	const uint16 ViewPitch = (View & 65535);
	const uint16 ViewYaw = (View >> 16);
	
	const FVector Accel = InAccel;

	const UWorld* MyWorld = GetWorld();
	const float DeltaTime = ServerData->GetServerMoveDeltaTime(TimeStamp, CharacterOwner->GetActorTimeDilation(*MyWorld));
    
    ...

	// Perform actual movement
	if ((MyWorld->GetWorldSettings()->GetPauserPlayerState() == NULL) && (DeltaTime > 0.f))
	{
		if (PC)
		{
			PC->UpdateRotation(DeltaTime);
		}

		// 최종 이동 처리
		MoveAutonomous(TimeStamp, DeltaTime, MoveFlags, Accel);
	}
    
    ...
    
    // 서버 이동 결과 클라이언트와 비교
	ServerMoveHandleClientError(TimeStamp, DeltaTime, Accel, ClientLoc, ClientMovementBase, ClientBaseBoneName, ClientMovementMode);
}

void UCharacterMovementComponent::ServerMoveHandleClientError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& RelativeClientLoc, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode)
{
	...
    
	// Offset may be relative to base component
	FVector ClientLoc = RelativeClientLoc;
	if (MovementBaseUtility::UseRelativeLocation(ClientMovementBase))
	{
		// 베이스 위에 있으면 상대좌표로 계산
        MovementBaseUtility::TransformLocationToWorld(ClientMovementBase, ClientBaseBoneName, RelativeClientLoc, ClientLoc);
	}
	else
	{
		ClientLoc = FRepMovement::RebaseOntoLocalOrigin(ClientLoc, this);
	}
    
    // 서버 위치 (클라이언트의 위치는 RPC로 받음)
	FVector ServerLoc = UpdatedComponent->GetComponentLocation();
    
    ...

	// Compute the client error from the server's position
	// If client has accumulated a noticeable positional error, correct them.
	bNetworkLargeClientCorrection = ServerData->bForceClientUpdate;
	if (ServerData->bForceClientUpdate || (!bFallingWithinAcceptableError && ServerCheckClientError(ClientTimeStamp, DeltaTime, Accel, ClientLoc, RelativeClientLoc, ClientMovementBase, ClientBaseBoneName, ClientMovementMode)))
	{
    	... // PendingAdjustment 기록
        
		ServerData->LastUpdateTime = GetWorld()->TimeSeconds;
		ServerData->PendingAdjustment.DeltaTime = DeltaTime;
		ServerData->PendingAdjustment.TimeStamp = ClientTimeStamp;
        // 좋은 움직임이 아님! 클라이언트 수정 필요
		ServerData->PendingAdjustment.bAckGoodMove = false;
		ServerData->PendingAdjustment.MovementMode = PackNetworkMovementMode();
        
        ...
    }
	else
	{
    	...
        
		// acknowledge receipt of this successful servermove()
		ServerData->PendingAdjustment.TimeStamp = ClientTimeStamp;
		ServerData->PendingAdjustment.bAckGoodMove = true;
    }
    
    ...
}

서버가 호출하는 클라이언트 RPC

  • UCharacterMovementComponent::ClientAdjustPosition()

클라이언트에게 수정할 위치 정보를 알려주는 함수

  • 중복 없이 서버 틱의 마지막에서 수정이 필요한 때만 전송함
    • 타임 스탬프: 클라이언트의 타임 스탬프 값
    • 델타 타임: 서버의 델타 타임
    • 무브먼트 모드 정보: 압축된 캐릭터 컴포넌트의 무브먼트 모드 정보
    • 새로운 속도: 수정할 새로운 속도 정보
    • 새로운 위치: 수정할 새로운 위치 정보
    • 새로운 회전: 수정할 새로운 회전 정보
    • 새로운 베이스와 베이스 본 이름: 수정할 베이스에 대한 정보

콜스택

  • UNetDriver::ServerReplicateActors()
    • UCharacterMovementComponent::SendClientAdjustment()
      필요한 경우에만 전송
      • UCharacterMovementComponent::ClientAdjustPosition()
void UCharacterMovementComponent::SendClientAdjustment()
{
	...

	const float CurrentTime = GetWorld()->GetTimeSeconds();
	if (ServerData->PendingAdjustment.bAckGoodMove)
    {
    	// 잘 받았다는 신호만 보냄
		// just notify client this move was received
		if (CurrentTime - ServerLastClientGoodMoveAckTime > NetworkMinTimeBetweenClientAckGoodMoves)
		{
			ServerLastClientGoodMoveAckTime = CurrentTime;
			if (ShouldUsePackedMovementRPCs())
			{
				ServerSendMoveResponse(ServerData->PendingAdjustment);
			}
			else
			{
				ClientAckGoodMove(ServerData->PendingAdjustment.TimeStamp);
			}
        }
    }
	else
	{
    	...
        {
        	...
            {
				...            
				else
				{
                	// 서버의 위치 수정 요청을 클라이언트로 전송
					ClientAdjustPosition
					(
						ServerData->PendingAdjustment.TimeStamp,
						ServerData->PendingAdjustment.NewLoc,
						ServerData->PendingAdjustment.NewVel,
						ServerData->PendingAdjustment.NewBase,
						ServerData->PendingAdjustment.NewBaseBoneName,
						ServerData->PendingAdjustment.NewBase != NULL,
						ServerData->PendingAdjustment.bBaseRelativePosition,
						ServerData->PendingAdjustment.MovementMode
					);
				}
            }
        }
    }
}

클라이언트의 수정 처리

  • UCharacterMovementComponent::ClientAdjustPosition_Implementation()

타임 스탬프 값을 통해 서버로부터 확인받은 움직임 정보를 기록
(LastAckedMove에 옮기고 이전에 저장된 움직임들을 모두 제거)

서버에서 전달받은 위치로 루트 컴포넌트(캐릭터)의 위치를 변경

서버에서 전달받은 속도로 무브먼트 컴포넌트의 속도를 수정

베이스 정보와 위치를 수정

서버에 의해 클라이언트 위치가 업데이트 되었다고 기록 (bUpdatePosition)
서버의 수정 정보를 바탕으로 MoveAutonomous() 함수를 호출해 클라이언트에서 남은 움직임을 재생함

ClientAdjustPosition_Implementation()

void UCharacterMovementComponent::ClientAdjustPosition_Implementation
	(
	float TimeStamp,
	FVector NewLocation,
	FVector NewVelocity,
	UPrimitiveComponent* NewBase,
	FName NewBaseBoneName,
	bool bHasBase,
	bool bBaseRelativePosition,
	uint8 ServerMovementMode,
	TOptional<FRotator> OptionalRotation /* = TOptional<FRotator>()*/
	)
{
	if (!HasValidData() || !IsActive())
	{
		return;
	}


	FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
	check(ClientData);
    
    ...
	
    // 타임스탬프를 키로 현재 저장된 움직임 중 서버가 처리한 움직임이 어떤 것인지 찾음
	// Ack move if it has not expired.
	int32 MoveIndex = ClientData->GetSavedMoveIndex(TimeStamp);
    
    ...

	// 해당 움직임을 LastAckedMove에 옮겨줌
	ClientData->AckMove(MoveIndex, *this);
    
    ...
	
    // 마지막 위치에 대한 정보를 업데이트
	LastUpdateLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector;
	LastUpdateRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity;
	LastUpdateVelocity = Velocity;

	UpdateComponentVelocity();
	ClientData->bUpdatePosition = true; //이후 TickComponent() 처리에서 사용
}

AckMove()

void FNetworkPredictionData_Client_Character::AckMove(int32 AckedMoveIndex, UCharacterMovementComponent& CharacterMovementComponent)
{
	// It is important that we know the move exists before we go deleting outdated moves.
	// Timestamps are not guaranteed to be increasing order all the time, since they can be reset!
	if( AckedMoveIndex != INDEX_NONE )
	{
		// Keep reference to LastAckedMove
		const FSavedMovePtr& AckedMove = SavedMoves[AckedMoveIndex];
		UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("AckedMove Index: %2d (%2d moves). TimeStamp: %f, CurrentTimeStamp: %f"), AckedMoveIndex, SavedMoves.Num(), AckedMove->TimeStamp, CurrentTimeStamp);
		if( LastAckedMove.IsValid() )
		{
			FreeMove(LastAckedMove);
		}
        // 서버가 처리한 움직임을 LastAckedMove에 저장
		LastAckedMove = AckedMove;

		// LastAckedMove 이후의 움직임만 클라이언트에 남게 됨
		// Free expired moves.
		for(int32 MoveIndex=0; MoveIndex<AckedMoveIndex; MoveIndex++)
		{
			const FSavedMovePtr& Move = SavedMoves[MoveIndex];
			FreeMove(Move);
		}

		// And finally cull all of those, so only the unacknowledged moves remain in SavedMoves.
		SavedMoves.RemoveAt(0, AckedMoveIndex + 1, EAllowShrinking::No);
	}
    
    ...
}

TickComponent()

void UCharacterMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
	...

	if (CharacterOwner->GetLocalRole() > ROLE_SimulatedProxy)
	{
		SCOPE_CYCLE_COUNTER(STAT_CharacterMovementNonSimulated);

		// If we are a client we might have received an update from the server.
		const bool bIsClient = (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy && IsNetMode(NM_Client));
		if (bIsClient)
		{
			FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
			if (ClientData && ClientData->bUpdatePosition)
			{
            	// ClientData의 bUpdatePosition 플래그가 세팅되었다면
                // 아래에서 입력에 대한 캐릭터 움직임을 처리하기 전에 클라이언트의 움직임을 업데이트
				ClientUpdatePositionAfterServerUpdate();
			}
		}
        
        ...
        
		// Perform input-driven move for any locally-controlled character, and also
		// allow animation root motion or physics to move characters even if they have no controller
		const bool bShouldPerformControlledCharMove = CharacterOwner->IsLocallyControlled() 
													  || (!CharacterOwner->Controller && bRunPhysicsWithNoController)		
													  || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion());

		if (bShouldPerformControlledCharMove)
		{
        	// 입력에 대해 캐릭터 움직임을 처리
			ControlledCharacterMove(InputVector, DeltaTime);
            
            ...
        }
    }
}

ClientUpdatePositionAfterServerUpdate()

bool UCharacterMovementComponent::ClientUpdatePositionAfterServerUpdate()
{
	...

	// 업데이트 되었으니 false 처리
	ClientData->bUpdatePosition = false;
    
    ...
    
	// Replay moves that have not yet been acked.
	UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ClientUpdatePositionAfterServerUpdate Replaying %d Moves, starting at Timestamp %f"), ClientData->SavedMoves.Num(), ClientData->SavedMoves[0]->TimeStamp);
	for (int32 i=0; i<ClientData->SavedMoves.Num(); i++)
	{
		FSavedMove_Character* const CurrentMove = ClientData->SavedMoves[i].Get();
		checkSlow(CurrentMove != nullptr);

        ...

		// LastAckedMove 이후에 저장된 움직임들을 순회하며 클라이언트 캐릭터 이동
		MoveAutonomous(CurrentMove->TimeStamp, CurrentMove->DeltaTime, CurrentMove->GetCompressedFlags(), CurrentMove->Acceleration);
        
        ...
        }

    ...
}

움직임 리플리케이션의 디버깅

  • DefaultEngine.ini
    움직임 리플리케이션에 대한 상세 정보 로그
    LogNetPlayerMovement=VeryVerbose

  • 서버에서의 오차 발생시 드로우 디버그
    전달받은 클라이언트 위치를 붉은색으로 표시
    서버에서 움직인 위치를 녹색으로 표시

  • 오차를 전달받은 클라이언트에서의 드로우 디버그
    클라이언트가 지정했던 위치를 붉은색으로 표시
    서버가 수정해준 위치를 녹색으로 표시
    수정은 발생했지만 서버와 클라이언트 위치가 거의 동일한 경우에는 노란색으로 표시

드로우 디버그를 위한 콘솔 변수 설정
p.NetShowCorrections 1

실습코드

https://github.com/dnjfs/ArenaBattle_Network/commit/456ec595285b35e273cda109d77366c4df155866

  • 공격 중 이동 불가 처리
    패킷 렉으로 인한 움직이지 못하는 상태에서 클라이언트의 이동 입력 방지
profile
주니어 언리얼 프로그래머

0개의 댓글