타겟팅 시스템
컨트롤러와 캐릭터의 회전을 분리한 타겟팅 시스템
타겟팅 시스템
목표
- 타겟팅(락 온) 시스템을 구현한다.
타겟팅 시스템?
액션 RPG를 해본 사람에게는 굉장히 친숙한 기능이다. 특정 대상을 타깃삼아 시점을 고정하거나 특수 행동을 하는 그 기능이 맞다. 본 프로젝트에서는 플레이어 컨트롤러의 정면 방향을 특정 대상을 향해 고정하는 동작을 수행한다.
설계
많은 게임들에서 구현된 기능이기에 그 변주 또한 다양하다. 구현에 앞서 몇 가지 요소들을 미리 생각해 보자.
- 적을 선택하는 기준
- 화면 중앙과 가장 가까운 적을 타겟팅
- 플레이어와 가장 가까운 적을 타겟팅
- 타겟 마커
- 3D 모델이 WidgetComponent를 가지고 마커 부착
- 플레이어 HUD에 마커 부착
- 타겟팅 시스템 분리
- 캐릭터 클래스 내 함수
- 별도 컴포넌트
우선 적을 선택하는 기준이다. 처음에는 전자를 생각했으나 사용할 에셋들을 떠올려보고 후자로 결정했다. 적과 환경요소가 시각적으로 명확하게 구분되지 않는 상황에서는 가장 가까운 위협을 찾아내는 후자가 더 도움이 될 것이라 판단했기 때문이다.
타겟 마커는 플레이어의 HUD에 부착하는 방식을 택했다. 플레이어를 기준으로 타겟팅 시스템을 구성하고 싶었다. 타깃과 멀어질 경우 마커의 거리감 문제가 발생할 수 있지만, 일정 거리 이상 멀어지면 마커가 해제되도록 하여 이를 방지했다.
타겟팅 시스템은 별도로 분리하지 않고 플레이어 캐릭터 클래스의 함수로 포함시켰다. 오로지 캐릭터만이 가진 전용 기능이며, 이후에 다른 플레이어 캐릭터를 늘릴 생각이 없기에 내린 선택이다.
타겟팅 시 플레이어의 움직임
타겟팅이 적용되면 마우스를 통한 플레이어 컨트롤러 회전 입력이 차단되고, 타겟을 향하여 플레이어 컨트롤러의 정면 방향이 고정된다. 그렇다면 캐릭터는 어떨까? 나는 논타겟 상태와 타겟 상태 모두에서 컨트롤러와 캐릭터의 회전이 독립적으로 작동하기를 원했다.
이를 위해서는 3인칭 템플릿의 캐릭터 & 카메라 분석 포스트에서 언급했던 2가지 요소를 잠깐 살펴봐야한다.
bUseControllRotation
bOrientRotationToMovement
bUseControllRotation
를 사용하면 컨트롤러의 회전을 캐릭터가 그대로 적용받는다. 플레이어가 바라보는 방향이 곧 캐릭터가 바라보는 방향이며, 플레이어는 일반적으로 캐릭터의 뒷면만을 보게 된다. TPS 슈팅 게임에서처럼 시선을 정면에 고정한 채 옆으로 움직이거나 뒷걸음질 치는 모습을 보인다.
bOrientRotationToMovement
은 캐릭터가 이동하는 방향을 바라보게 한다. bUseControllRotation
을 해제해야 제대로 적용된다. 플레이어는 캐릭터가 정면을 향해 달려가거나, 본인을 향해 달려오는 모습을 볼 수 있다.
타겟팅에서 이들이 어떻게 적용될까? 플레이어 컨트롤러의 방향이 고정되고, 캐릭터 또한 타겟을 바라봐야 한다면 bUseControllRotation = true && bUseOrientationMovement = false
인 상황이 된다. 이게 일반적인 타겟팅이다.
문제는 내가 정면 구르기 에셋만을 보유했다는 것이다. 논타겟 상태에서 bUseControllRotation = false && bUseOrientationMovement = true
로 설정하여 캐릭터가 입력받은 방향을 바라보게 하고, 그 상태에서 정면 구르기를 진행해왔었다. 위의 타겟팅을 적용한다면, 오로지 타깃을 향해 구르는 캐릭터가 되어버린다.
그래서 다른 접근법을 사용했다. bUseControllRotation
은 계속 false로 놔두고, 컨트롤러의 방향과 캐릭터의 방향을 매 틱마다 보정하는 방식이다. 그 결과물이 위의 영상이다. 타겟팅 중임에도 원하는 방향으로 구를 수 있다!
구현 - 종합 부분
// IIIVLockOnTargetMarker.h
UINTERFACE(MinimalAPI)
class UIIVLockOnTargetMarker : public UInterface
{
GENERATED_BODY()
};
타겟팅 대상이 될 몬스터 클래스는 IIIVLockOnTargetMarker 인터페이스를 구현한다. 별다른 함수가 없는 타겟 대상 판별용 인터페이스이다.
// IVPlayerCharacter.cpp
void AIVPlayerCharacter::LockOn()
{
// 락온 대상 탐색을 위한 콜리전 설정
TArray<FHitResult> HitResults;
FVector Start = GetActorLocation();
FVector End = Start; // 원점
FQuat Rotation = FQuat::Identity;
FCollisionShape CollisionShape = FCollisionShape::MakeSphere(LockOnDistance);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
if (GetWorld()->SweepMultiByObjectType(
HitResults,
Start,
End,
Rotation,
FCollisionObjectQueryParams(ECollisionChannel::ECC_Pawn),
CollisionShape,
QueryParams))
{
AActor* ClosestActor = nullptr;
float ClosestDistance = LockOnDistance;
// 락온 대상 중 가장 가까운 액터 선택
for (const FHitResult& HitResult : HitResults)
{
AActor* HitActor = HitResult.GetActor();
if (HitActor && HitActor->Implements<UIIVLockOnTargetMarker>()) // 락온 대상 조건 확인
{
if (IsTargetVisibleByLineTrace(HitActor)) // 시야에 있는지 확인
{
float Distance = FVector::Distance(Start, HitActor->GetActorLocation());
if (Distance < ClosestDistance)
{
ClosestDistance = Distance;
ClosestActor = HitActor;
}
}
}
}
...
}
락온 과정은 크게 두 부분으로 구성된다. 우선 범위 내의 타겟팅 가능 대상들을 탐지하여 그중 가장 가까운 대상을 타겟으로 정하는 과정이다. 타겟팅 가능 대상은 3가지 조건을 만족해야 한다.
- 타겟팅 대상으로써 UIIVLockOnTargetMarker을 구현하는가?
- 플레이어와 타깃 사이에 장애물이 없는가?
- 타겟팅 범위 내 대상 중 플레이어와 가장 가까이 위치해있는가?
// IVPlayerCharacter.cpp
void AIVPlayerCharacter::LockOn()
{
...
// 실질 락온 작업
if (ClosestActor)
{
// 실제로 대상이 있어야 타겟팅 모드로 돌입
CharacterStatComponent->SetCharacterTargetingState(ETargetingState::OnTargeting);
// 플레이어 입력 및 거리에 따른 종료 조건 제어
LockOnActor = ClosestActor;
GetController()->SetIgnoreLookInput(true); // 락온 중 시점 입력 무시
GetWorldTimerManager().SetTimer(LockOnCheckTimer, this, &AIVPlayerCharacter::CheckLockOnDistance, 0.1f, true);
// 타겟 액터에 마킹 위젯 표시
if (PlayerController)
{
PlayerController->ShowTargetMarker(ClosestActor);
}
// 모션 워핑 대상으로 지정
if (MotionWarpingComponent)
{
MotionWarpingComponent->AddOrUpdateWarpTargetFromTransform(FName("Target"), ClosestActor->GetActorTransform());
}
// 회전제어
GetCharacterMovement()->bOrientRotationToMovement = false; // 자동 회전 방지
}
}
}
위의 과정을 거쳐 타겟을 선정한 후, 타겟팅 동작을 수행한다. CheckLockOnDistance
함수는 일정 주기로 호출되어 타겟이 범위 내에 존재하는지, 가려지지는 않았는지를 계속 확인한다.
// IVPlayerCharacter.cpp
void AIVPlayerCharacter::LockOff()
{
LockOnActor = nullptr;
GetController()->SetIgnoreLookInput(false); // 락온 해제 시 시점 입력 허용
GetWorldTimerManager().ClearTimer(LockOnCheckTimer);
// 타겟 액터에 마킹 위젯 해제
if (PlayerController)
{
PlayerController->HideTargetMarker();
}
// 모션 워핑 대상 해제
if (MotionWarpingComponent)
{
MotionWarpingComponent->RemoveWarpTarget(FName("Target"));
}
// 회전 제어
GetCharacterMovement()->bOrientRotationToMovement = true; // 자동 회전
}
타겟팅 해제는 위의 역으로 진행된다. 플레이어가 타겟팅을 해제하거나 타겟팅 대상 혹은 플레이어가 사망한 경우에 이를 진행한다.
구현 - 보정 부분
void AIVPlayerCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (CharacterStatComponent)
{
// 락온 중이라면 락온 대상을 따라 시점을 조정한다
if (CharacterStatComponent->GetCharacterTargetingState() == ETargetingState::OnTargeting && LockOnActor)
{
FRotator LookAtRotation = UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), LockOnActor->GetActorLocation());
FRotator NewRotation = FMath::RInterpTo(GetControlRotation(), LookAtRotation, DeltaTime, 5.0f);
PlayerController->SetControlRotation(NewRotation);
}
}
}
틱에서는 타겟팅 시 플레이어 컨트롤러의 회전을 제약한다. 플레이어가 다른 방향을 보더라도 타겟을 향해 시선을 돌리도록 매번 보정하는 것이다. RInterpTo
를 사용하여 부드럽게 시선을 옮기도록 했다.
// IVPlayerCharacter.cpp
void AIVPlayerCharacter::BasicMove(const FInputActionValue& Value)
{
...
if (CharacterStatComponent && CharacterStatComponent->GetCharacterTargetingState() == ETargetingState::OnTargeting)
{
// 동작이 어색해서 임시 주석 처리
//if (bInComboAttack) return; // 공격 재생중이면 회전 설정 X
// 현재 이동중인 방향으로 캐릭터 회전
FVector MoveDirection = GetVelocity();
MoveDirection.Z = 0.0f;
if (!MoveDirection.IsNearlyZero())
{
// 해당 방향으로 보간하여 부드럽게 회전
FRotator TargetRotation = UKismetMathLibrary::MakeRotFromX(MoveDirection);
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), TargetRotation, GetWorld()->GetDeltaSeconds(), 5.0f);
SetActorRotation(NewRotation);
}
}
}
BasicMove는 WASD를 통해 플레이어의 평면 움직임을 제어하는 함수다. 타겟팅 중 입력이 들어오면 이동 방향을 향해 캐릭터가 부드럽게 회전하도록 함으로써 bOrientRotationToMovement
를 모방했다. 해당 동작은 매 틱 진행되는 것이 아닌 매 입력마다 진행되므로, 입력 버튼을 누르고 있는 동안 회전이 진행된다.
개선점
일부 게임에서는 논타겟 상태에서 구르기를 사용하고, 타겟팅 상태에서는 닷지를 통해 짧은 회피를 수행하는 모습을 볼 수 있다. 이때, 굳이 방향을 풀거나 회전 보정을 하지 않는 데는 이유가 있다.
우선, 공격이 잘 안 맞는다. 캐릭터는 캐릭터의 정면을 향해 공격하지만 이동 입력에 따라 정면 방향이 바뀌기 때문에 앞쪽으로 조금 이동한 후에야 공격을 수행할 수 있다. 시선은 타깃을 추적하지만 공격 방향과 일치하지 않으니 반쪽짜리 타겟팅이라고 할 수 있다. 이를 보완하기 위하여 모션 워핑을 적용하여 공격 시 타깃을 향해 회전하도록 해두었다.
그리고 방향 제어가 복잡하다. 모션 워핑으로 회전했다가, 다른 동작을 수행하며 몸을 틀었다가, 다시 입력에 따라 회전하는 과정에서 이상한 회전들이 발생하곤 한다. 콤보 시스템과의 연계에서도 문제가 있는 듯하니 추후 수정해 나가야겠다.