[UE5] 마우스 커서 방향으로 컴포넌트 회전

kkado·2024년 4월 4일
0

UE5

목록 보기
30/63
post-thumbnail

이제 탱크의 움직임은 모두 구현되었다. 이제 탱크의 포신 부분을 회전시키는 것을 구현해 보려고 한다.

원하는 것은 마우스 커서의 위치로 포신을 돌려, 나중에 구현할 조준 및 발사 기능까지 이어지도록 하는 것이다.

그러면 두 가지를 알아야 한다.

  1. 게임 필드와 마우스 커서가 닿는 곳을 구하기
  2. 그 곳으로 포신을 회전시키기

게임 필드와 마우스 커서가 닿는 곳을 구하기

검색을 조금 해 보니이전에 배웠던 라인 트레이스 개념을 이용할 수 있을 것 같다.
참고한 관련 영상 링크

'Visibility' 채널을 사용함으로써 카메라로부터 라인 트레이스를 수행할 수 있을 것으로 보인다.

그리고 좀 더 검색을 하다가 PlayerController 클래스 아래에 GetHitResultUnderCursor 이라는 함수가 있는 것을 확인할 수 있었다.

이 함수를 이용해서 카메라로부터 마우스 커서의 위치를 가져오고, 충돌이 발생한 곳의 ImpactPoint를 가져올 수 있을 것 같다.

PlayerController 객체 생성 및 가져오기

폰 클래스는 기본적으로 AController 객체를 가지고 있다.

Pawn 클래스에서 GetController() 함수를 찾을 수 있었고, 반환값이 AController 포인터임을 확인했다.

컨트롤러의 하위 클래스로는 AI Controller와 Player Controller가 있다.
폰은 항상 하나의 컨트롤러를 갖는다. 일대일 대응 관계라고 볼 수 있다.

우리가 필요한 것은 PlayerController 객체이지만, 폰 클래스로부터 얻을 수 있는 포인터는 Controller 객체이다. 따라서 캐스팅을 거쳐서 APlayerController 형으로 형변환 해주어야 한다. 사실 이것은 InputAction을 다룰 때 이미 해 보았던 작업이다.

void ATank::BeginPlay()
{
	Super::BeginPlay();
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* SubSystem =
			ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
			SubSystem->AddMappingContext(DefaultContext, 0);
	}

	PlayerControllerRef = Cast<APlayerController>(GetController());
}

PlayerController* PlayerControllerRef 을 만들고, 이를 BeginPlay 함수 내에서 타입캐스팅을 통해 가져오고 있다.

커서 위치 얻기

이제 이 PlayerControllerRef 포인터를 가지고 커서 트레이스를 진행해 보자.

마우스 커서가 움직이는 족족 포신이 회전해야 할 것이므로 Tick 함수 아래에 작성해야 할 것이다.

void ATank::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (PlayerControllerRef != nullptr)
	{
		FHitResult HitResult;
		PlayerControllerRef->GetHitResultUnderCursor(
			ECollisionChannel::ECC_Visibility,
			false,
			HitResult
		);
	}
}

이전에 액터를 그랩할 때 사용했던 SweepSingleByChannel 함수와 유사하게 동작한다. FHitResult 구조체를 만들고 이를 아웃풋 파라미터로 넣어 준다. 함수 호출 후에 HitResult에는 (만약 충돌이 발생했다면) 충돌한 액터에 대한 정보가 담겨 있을 것이다.

이제 이 HitResult 구조체에서 ImpactPoint를 얻을 수 있다.

먼저 제대로 가져오고 있는지 확인하기 위해 DrawDebugSphere 함수를 이용해서 구체를 생성해보자. (#include "DrawDebugHelpers.h" 필요)

		DrawDebugSphere(
			GetWorld(),
			HitResult.ImpactPoint,
			10,
			10,
			FColor::Red,
			false,
			-1
		);

구체가 얇아서 잘 안 보일 수도 있지만, 마우스 커서가 닿는 바닥 및 벽, 탱크 표면에 잘 생성되고 있다.

이제 이 지점으로 탱크의 포신을 회전시키기만 하면 된다!


포신을 회전시키기

포신을 회전하는 기능은 탱크뿐만 아니라 적 타워에도 필요한 기능이다.
Tank 클래스에 구현할 것이 아니라 그보다 상위 클래스인 BasePawn 클래스에 구현함으로써 좀 더 확장성을 고려해 보도록 하자.

또 다시, 두 가지 작업을 해야 한다.

  • 한 Location에서부터 다른 Location까지의 Rotation 얻기
  • 그 Rotation으로 회전시키기

RotateTurret 함수 선언

포신을 회전시키는 RotateTurret 함수를 만드려고 한다.
이 함수는 하위 클래스에서 사용할 수 있도록 protected으로 선언해야 할 것이며, 인자로는 특정 Location을 담고 있는 FVector를 필요로 한다.

protected:
	void RotateTurret(FVector LookAtTarget);

TargetRotation 얻기

어떤 위치로부터 다른 위치까지의 벡터는 사실 벡터의 뺄셈을 이용해서 간단히 구할 수 있다. 그리고 FVector 클래스에는 Rotation() 이라는 아주 보기 좋은 함수가 존재한다.

공식 문서 링크

친절하게 우리가 필요한 FRotator 형으로 반환해주는 것을 알 수 있다.

그리고 한가지 주의할 점이 있는데, 이 FRotator의 Yaw 값만 취해야 한다는 것이다.

탱크 포신의 각도에 대해서는 아직까지 구현 계획이 없으므로,
가까운 바닥을 바라보고 있을 때나 위를 바라보고 있을 때나 상관없이 그 '방향' 만을 가리키도록 우선 구현해보자. 즉 FRotator 값에서 피치와 롤 값을 0으로 만든 FRotator가 필요하다.

FVector ToTarget = LookAtTarget - GetActorLocation();
FRotator LookAtRotation = FRotator(0, ToTarget.Rotation().Yaw, 0);

포신 회전시키기

포신은 BasePawn 안에 TurretMesh 으로 분리하여 만들어 놓았다.

그냥 TurretMesh를 가져와서 -> SetWorldRotation 을 해 주면 전체 월드를 기준으로 한 Rotation 정보가 변경될 것이다.

TurretMesh->SetWorldRotation(LookAtRotation);

따라서 전체 RotateTurret 함수는 다음과 같다.

void ABasePawn::RotateTurret(FVector LookAtTarget)
{
	FVector ToTarget = LookAtTarget - GetActorLocation();
	FRotator LookAtRotation = FRotator(0, ToTarget.Rotation().Yaw, 0);
	TurretMesh->SetWorldRotation(LookAtRotation);
}

결과 확인

음... 일단 커서를 따라서 잘 회전하긴 하는데 두 가지 문제점이 보인다.

  1. 맵 바깥으로 커서가 나갔을 때 충돌 결과가 없어 이상하게 돌아간다
  2. 포신이 너무 홱홱 돌아가서 덜컥거리는 듯한 느낌을 준다.

이제 두 가지 문제점들을 해결해 보도록 한다.


수정

수정1. 맵 바깥의 커서 이동에 대한 처리

맵 바깥에는 트레이스 수행 결과 충돌하는 액터가 없어 Rotate가 잘 이루어지지 않는다.

맵의 테두리 부분에 보이지 않는 높은 벽을 세워서 트레이스가 그 벽에 닿게끔 하는 방법이 가장 먼저 떠오른다.

맵 가장자리 부분에 BlockingVolume을 생성하고 전체 맵을 커버하게끔 한다.

그리고 이 볼륨의 Collision 프리셋을 확인한다.

Visibility를 제외한 모든 타입을 Block하고 있음을 확인할 수 있다.
우리는 카메라로부터 Visibility 채널을 이용해서 트레이스를 수행하므로 Visibility 채널에 대한 response가 Ignore이면 곤란하다.

따라서 custom 프리셋으로 만든 후에 Visibility도 Block으로 설정해 준다.

그리고 이 볼륨을 복제해서 사방의 벽을 모두 막아준다.

이제 결과를 확인해 보면

벽 너머에 보이지는 않지만 충돌을 감지하는 볼륨에 트레이스가 찍히게 되면서 정상적으로 포신이 돌아가는 것을 확인할 수 있다.


수정2. 너무 빠르게 포신이 돌아가는 문제 해결

여기서 '보간(Interpolation)'이라는 개념을 사용할 수 있다.

이전에 움직이는 플랫폼을 구현할 때 잠깐 다루었던 내용인데 두 지점 간의 연결에 '궤적' 을 만들어서 서서히 변화하게끔 하는 방법이었다.

회전 값을 직접 설정하기보다는 보간 함수를 사용하면 된다.
FMath::RInterpTo 이라는 함수를 사용하는데, 이 함수는 Current Rotation에서부터 Target Rotation까지 특정한 계수만큼 보간시켜준다.

함수의 Syntax를 살펴보면 두 FRotator, 델타타임, 그리고 얼마나 보간할지에 대한 InterpSpeed 객체를 파라미터로 받고 있다.

델타타임은 GameplayStatics.h 에서 쉽게 얻을 수 있으며 InterpSpeed는 우리가 지정할 수 있다.

따라서 TurretMesh->SetWorldRotation(LookAtRotation()) 을 다음과 같이 변경한다.

	TurretMesh->SetWorldRotation(
		FMath::RInterpTo(
			TurretMesh->GetComponentRotation(),
			LookAtRotation,
            UGameplayStatics::GetWorldDeltaSeconds(),
			5));

결과를 확인하면 탱크를 가로질러서 Rotator에 급격한 변화를 주어도 포신이 홱홱 돌아가지 않고 서서히 돌아가는 모습을 확인할 수 있다.


profile
베이비 게임 개발자

0개의 댓글