새소식

개발/UE4

[UE4/CameraManager] 서버의 CameraManager와 Client POV

  • -

PlayerCameraManager의 멀티 환경 옵션에는 'bUseClientSideCameraUpdates' 라는 옵션이 있다.

해당 옵션은 PlayerController의 TickActor 를 Entry로 잡고 확인해볼 필요가 있다.

// Send a camera update if necessary.
// That position will be used as the base for replication
// (i.e., the origin that will be used when calculating NetCullDistance for other Actors / Objects).
// We only do this when the Pawn will move, to prevent spamming RPCs.
if (bIsClient && bIsLocallyControlled && GetPawn() && PlayerCameraManager->bUseClientSideCameraUpdates)
{
	UPawnMovementComponent* PawnMovement = GetPawn()->GetMovementComponent();
	if (PawnMovement != nullptr &&
	!PawnMovement->IsMoveInputIgnored() &&
	(PawnMovement->GetLastInputVector() != FVector::ZeroVector || PawnMovement->Velocity != FVector::ZeroVector))
	{
		PlayerCameraManager->bShouldSendClientSideCameraUpdate = true;
	}
}

위 코드는 PlayerController의 TickActor의 일부이다. 이를 확인해보면 알 수 있듯, 클라이언트가 로컬 컨트롤되고 있으며, 
'bUseClientSideCameraUpdates' 변수가 활성화 되어있으면, PlayerCameraManager의 'bShouldSendClientSideCameraUpdate' 가 활성화 됨을 알 수 있다.
그럼 이제 PlayerCameraManager에서 'bShouldSendClientSideCameraUpdate' 가 사용되는 로직을 살펴보자.

if ((PCOwner->Player && PCOwner->IsLocalPlayerController()) || !bUseClientSideCameraUpdates || bDebugClientSideCamera)
{
	DoUpdateCamera(DeltaTime);
	const float TimeDilation = FMath::Max(GetActorTimeDilation(), KINDA_SMALL_NUMBER);
	TimeSinceLastServerUpdateCamera += (DeltaTime / TimeDilation);
	if (bShouldSendClientSideCameraUpdate && IsNetMode(NM_Client))
	{
		SCOPE_CYCLE_COUNTER(STAT_ServerUpdateCamera);

		const AGameNetworkManager* const GameNetworkManager = GetDefault<AGameNetworkManager>();
		const float ClientNetCamUpdateDeltaTime = GameNetworkManager->ClientNetCamUpdateDeltaTime;
		const float ClientNetCamUpdatePositionLimit = GameNetworkManager->ClientNetCamUpdatePositionLimit;

		FMinimalViewInfo CurrentPOV = GetCameraCachePOV();
		FMinimalViewInfo LastPOV = GetLastFrameCameraCachePOV();

		FVector ClientCameraPosition = FRepMovement::RebaseOntoZeroOrigin(CurrentPOV.Location, this);
		FVector PrevClientCameraPosition = FRepMovement::RebaseOntoZeroOrigin(LastPOV.Location, this);

		const bool bPositionThreshold = (ClientCameraPosition - PrevClientCameraPosition).SizeSquared() > (ClientNetCamUpdatePositionLimit * ClientNetCamUpdatePositionLimit);

		if (bPositionThreshold || (TimeSinceLastServerUpdateCamera > ClientNetCamUpdateDeltaTime))
		{
			// compress the rotation down to 4 bytes
			int32 const ShortYaw = FRotator::CompressAxisToShort(CurrentPOV.Rotation.Yaw);
			int32 const ShortPitch = FRotator::CompressAxisToShort(CurrentPOV.Rotation.Pitch);
			int32 const CompressedRotation = (ShortYaw << 16) | ShortPitch;

			int32 const PrevShortYaw = FRotator::CompressAxisToShort(LastPOV.Rotation.Yaw);
			int32 const PrevShortPitch = FRotator::CompressAxisToShort(LastPOV.Rotation.Pitch);
			int32 const PrevCompressedRotation = (PrevShortYaw << 16) | PrevShortPitch;

			if ((CompressedRotation != PrevCompressedRotation) || !ClientCameraPosition.Equals(PrevClientCameraPosition) || (TimeSinceLastServerUpdateCamera > ServerUpdateCameraTimeout))
			{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
				if (ClientCameraPosition.X > 1048576.0f || ClientCameraPosition.X < -1048576.0f ||
					ClientCameraPosition.Y > 1048576.0f || ClientCameraPosition.Y < -1048576.0f ||
					ClientCameraPosition.Z > 1048576.0f || ClientCameraPosition.Z < -1048576.0f)
				{
					UE_LOG(LogPlayerCameraManager, Warning, TEXT("ClientCameraPosition %f %f %f doesn't fit in FVector_NetQuantize for ServerUpdateCamera, capping"), ClientCameraPosition.X, ClientCameraPosition.Y, ClientCameraPosition.Z);
				}
#endif //!(UE_BUILD_SHIPPING || UE_BUILD_TEST)
					
				const float MaxQuantize = 1048575.f;
				const float MinQuantize = -1048575.f;
				ClientCameraPosition.X = FMath::Clamp(ClientCameraPosition.X, MinQuantize, MaxQuantize);
				ClientCameraPosition.Y = FMath::Clamp(ClientCameraPosition.Y, MinQuantize, MaxQuantize);
				ClientCameraPosition.Z = FMath::Clamp(ClientCameraPosition.Z, MinQuantize, MaxQuantize);

				PCOwner->ServerUpdateCamera(ClientCameraPosition, CompressedRotation);

				TimeSinceLastServerUpdateCamera = 0.0f;
			}
		}

		bShouldSendClientSideCameraUpdate = false;
	}
}

위 코드를 살펴보면, 클라이언트 환경에서 로컬컨트롤 되고 있고 'bShouldSendClientSideCameraUpdate' 가 활성화 되어있는 경우, 카메라의 변위차가 임계보다 큰 경우 PlayerController의 ServerUpdateCamera를 통해 클라이언트 상 카메라의 Location과 Rotation 값을 업데이트 할 수 있도록 송신하는 것을 알 수 있다.

이렇게 데이터를 수신 받은 서버쪽 PlayerController의 ServerUpdateCamera에서는 PlayerCameraManager의 FillCameraCache를 수행하는데, 이를 통해 ServerSide에서 수정된 POV를 갖게 된다.

 

이 과정은 다음과 같은 상황에서 주의해야 한다.

- 클라이언트의 캐릭터가 일시적으로 MovementComponent가 비활성화된 상태로 다른 액터의 하위로 붙어있어야 하는 경우, 'bShouldSendClientSideCameraUpdate' 가 활성화될 수 없어 서버쪽에서 클라이언트의 PlayerCameraManager가 가진 POV가 갱신되지 않아 'GetPlayerViewPoint' 함수에서 미갱신된 값을 반환할 수 있다.

위 상황의 결과는 네트워크상에서 IsNetForRelevant나 WorldComposition, WorldPartition과 같이 클라이언트의 위치값을 'GetPlayerViewPoint'로 인지하는 모듈에서 치명적인 결과를 발생시킨다.

 

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.