이제 탱크의 움직임은 모두 구현되었다. 이제 탱크의 포신 부분을 회전시키는 것을 구현해 보려고 한다.
원하는 것은 마우스 커서의 위치로 포신을 돌려, 나중에 구현할 조준 및 발사 기능까지 이어지도록 하는 것이다.
그러면 두 가지를 알아야 한다.
검색을 조금 해 보니이전에 배웠던 라인 트레이스 개념을 이용할 수 있을 것 같다.
참고한 관련 영상 링크
'Visibility' 채널을 사용함으로써 카메라로부터 라인 트레이스를 수행할 수 있을 것으로 보인다.
그리고 좀 더 검색을 하다가 PlayerController 클래스 아래에 GetHitResultUnderCursor
이라는 함수가 있는 것을 확인할 수 있었다.
이 함수를 이용해서 카메라로부터 마우스 커서의 위치를 가져오고, 충돌이 발생한 곳의 ImpactPoint를 가져올 수 있을 것 같다.
폰 클래스는 기본적으로 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
클래스에 구현함으로써 좀 더 확장성을 고려해 보도록 하자.
또 다시, 두 가지 작업을 해야 한다.
포신을 회전시키는 RotateTurret
함수를 만드려고 한다.
이 함수는 하위 클래스에서 사용할 수 있도록 protected으로 선언해야 할 것이며, 인자로는 특정 Location을 담고 있는 FVector를 필요로 한다.
protected:
void RotateTurret(FVector LookAtTarget);
어떤 위치로부터 다른 위치까지의 벡터는 사실 벡터의 뺄셈을 이용해서 간단히 구할 수 있다. 그리고 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);
}
음... 일단 커서를 따라서 잘 회전하긴 하는데 두 가지 문제점이 보인다.
이제 두 가지 문제점들을 해결해 보도록 한다.
맵 바깥에는 트레이스 수행 결과 충돌하는 액터가 없어 Rotate가 잘 이루어지지 않는다.
맵의 테두리 부분에 보이지 않는 높은 벽을 세워서 트레이스가 그 벽에 닿게끔 하는 방법이 가장 먼저 떠오른다.
맵 가장자리 부분에 BlockingVolume을 생성하고 전체 맵을 커버하게끔 한다.
그리고 이 볼륨의 Collision 프리셋을 확인한다.
Visibility를 제외한 모든 타입을 Block하고 있음을 확인할 수 있다.
우리는 카메라로부터 Visibility 채널을 이용해서 트레이스를 수행하므로 Visibility 채널에 대한 response가 Ignore이면 곤란하다.
따라서 custom 프리셋으로 만든 후에 Visibility도 Block으로 설정해 준다.
그리고 이 볼륨을 복제해서 사방의 벽을 모두 막아준다.
이제 결과를 확인해 보면
벽 너머에 보이지는 않지만 충돌을 감지하는 볼륨에 트레이스가 찍히게 되면서 정상적으로 포신이 돌아가는 것을 확인할 수 있다.
여기서 '보간(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에 급격한 변화를 주어도 포신이 홱홱 돌아가지 않고 서서히 돌아가는 모습을 확인할 수 있다.