[UE5] Blader 캐릭터 기초 조작 전투 구현, 모션 워핑을 사용한 타겟팅 시스템 (프로젝트ASR - 개발일지3)
이전 포스팅에 이어 작성하는 프로젝트 개발일지입니다.
목차
- 캐릭터, 전투 기획 및 로드맵 수립
- 두 번째 캐릭터 Blder 기초 조작, 전투 구현
- 콤보 시스템
- 타격 / 피격 판정, Enemy 클래스 추가
- 공중 공격
- 타겟팅 시스템 구현
캐릭터, 전투 기획 및 로드맵 수립
포스팅 내용에 대한 이해를 돕기 위해 간략하게 작성하면
총 3개의 캐릭터를 태그하며 전투를 진행하며 다수의 적을 상대하는 일반 전투, 다양한 공격 패턴을 가진 보스를 상대하는 보스 전투가 있다. 캐릭터는 각각 총기 / 카타나 / 대검을 사용하여 각기 다른 전투스타일과 스킬을 가지고 있으며 메쉬와 애니메이션도 모두 따로 제작 예정이다.
노션에 프로젝트 페이지를 만들었고 세부 기획과 로드맵, 일일개발일지를 작성하였다.
Blader 기초 조작, 전투 구현
콤보 시스템 구현
두 번째 캐릭터는 카타나를 주 무기로 사용하는 Blader이다. 총기를 사용하는 이전 캐릭터와 달리 콤보 공격이 주가 되는 캐릭터로 다른 캐릭터보다 속도감 있는 전투를 구현할 예정이다.
기본적인 로코모션 애니메이션은 이전과 같은 방식으로 State Machine과 Blend Space를 사용하여 제작하였다.
다음으로 콤보시스템을 제작하였다. 각 공격마다 Animation Montage를 만들고 AnimNotify와 AnimNotify State를 통해 정확한 시점에 다음 공격으로 넘어갈 수 있도록 하였다.
우선 Input action에 매핑된 함수인 Input_LightAttack에서는 캐릭터가 현재 공격 중이면 일반 공격 예약 여부를 나타내는 Bool값인 bIsLIghtAttackPending을 참으로 설정한다. 그렇지 않을 경우에는 LightAttack()을 호출한다.
void ABlader::Input_LightAttack(const FInputActionValue& Value)
{
bIsHeavyAttackPending = false;
if (CharacterState == EASRCharacterState::ECS_Attack)
{
bIsLightAttackPending = true;
}
else
{
LightAttack();
}
}
void ABlader::LightAttack()
{
if (CanAttack())
{
ResetHeavyAttack();
ExecuteLightAttack(LightAttackIndex);
}
else if (CanAttakInAir())
{
ExecuteLightAttackInAir(LightAttackIndex);
}
}
LightAttack에서는 피격 중 / 사망 / 체공 등 일반적인 공격이 가능한 상황인지 체크한 뒤 다른 콤보인 HeavyAttaack을 값들을 리셋하고 ExecuteLightAttack를 호출한다.
void ABlader::ExecuteLightAttack(int32 AttackIndex)
{
if (AttackIndex >= LightAttackMontages.Num())
{
LightAttackIndex = 0;
}
else
{
if (LightAttackMontages.IsValidIndex(AttackIndex) && LightAttackMontages[AttackIndex] != nullptr)
{
SetCharacterState(EASRCharacterState::ECS_Attack);
PlayAnimMontage(LightAttackMontages[AttackIndex]);
if (LightAttackIndex + 1 >= LightAttackMontages.Num())
{
LightAttackIndex = 0;
}
else
{
++LightAttackIndex;
}
}
}
}
ExecuteLightAttack에서는 현재 공격 횟수에 맞는 애니메이션을 재생하며 공격 Index를 1 증가시킨다.
이후 애니메이션 몽타주에 설정해 둔 ANS_LightAttackPending에서는 앞에서 설정한 bIsLIghtAttackPending를 확인하여 참일 경우 LightAttack 함수를 호출하여 바로 다음 공격을 진행한다.
우클릭의 경우 HeavyAttack으로 마찬가지로 콤보 시스템을 적용하였다. 위 LightAttack 애니메이션 몽타주에서도 예약된 HeavyAttack을 처리하는 AnimNotify state를 설정해 두어 좌클릭 -> 우클릭 -> 좌클릭과 같은 콤보도 가능하도록 설정하였다.
좌측 베기 -> 좌측 베기 / 우측 베기 -> 발차기처럼 현실적으로 전환하는데 시간이 더 오래 걸리는 동작들은 예약된 공격을 확인하고 시작하는 시점을 뒤로 늦추었다.
타격 / 피격 판정
AN_Trace라는 이름이 붙은 AnimNotify로 Trace 시점을 조절하였으며 무기 메쉬를 사용하여 Trace 하는 방식 / 플레이어의 위치를 기반으로 Sphere Trace를 사용하는 방식 / 무기 메쉬의 Collision을 사용하는 방식을 모두 고려해 보았는데 액션이 강조되는 캐릭터다 보니 빠른 속도의 공격에서도 안정적으로 누락 없이 이벤트를 발생시킬 수 있는 Sphere Trace로 결정하였다.
공격 이후의 처리는 Interface를 만들어, 대미지 타입 / 대미지 / HitResult를 전달하도록 하였다.
Enemy 클래스는 각 대미지 타입에 대한 애니메이션과 상태 변경을 Map 자료형으로 가지고 있어 상황에 맞는 애니메이션을 재생한다.
공중 공격
각 캐릭터의 스킬은 E, Q키 입력을 통해 시전 되는데 Blader 캐릭터의 E스킬에 해당하는 액션은 적을 띄우는 공격이다.
애니메이션에서는 Force Lock Root 옵션을 활성화하여 캐릭터가 움직이지 않도록 설정하였고 대신 TimelineComponent로 캐릭터의 움직임을 정의하였다.
간단한 보간을 사용하여도 괜찮지만 커브를 직접 설정하며 자연스러운 동작이 나올 때까지 테스트하기 위해서 TimelineComponent를 택하였다.
void ABlader::HandleTimelineUpdate(float Value)
{
if (GetWorld() != nullptr)
{
FVector NewLocation = UKismetMathLibrary::VLerp(LevitateLocation, LevitateLocation + FVector(0.f, 0.f, LevitateHeight), Value);
SetActorLocation(NewLocation, true);
}
}
피격당한 상대도 마찬가지로 같은 Curve를 사용하여 캐릭터와 같은 위치로 떠오를 수 있도록 하였다.
공중에서는 좌클릭으로 콤보를 이어갈 수 있으며 플레이어와 적 모두 MovementMode를 Flying으로 설정하여 공격받는 도중이나 공격하는 도중 떨어지지 않도록 하였다.
콤보의 마지막 동작에서는 상대를 지면으로 내리꽂도록 하였다. 마찬가지로 TimelineComponent와 직접 만든 Curve를 사용하여 자연스러운 연출을 시도하였다.
타겟팅 시스템 구현
구현까지는 하루 정도가 걸렸는데 이를 다듬고 여러 설정을 테스트하는데 그 이상의 시간을 사용하였다. 세키로와 명조의 타겟팅 시스템을 참고하였으며 크게 3개의 타겟팅 컨셉이 있다.
ActorComponent로 제작하여 TPS 게임을 위한 구조로 코드를 작성한 첫 번째 캐릭터에서도 쉽게 사용할 수 있도록 하였다.
1. 락온 타겟팅
우선 카메라부터 카메라가 바라보는 방향으로 일정거리를 더한 위치까지 Trace 하여 Enemy 클래스가 HitActor라면 시점을 고정시킨다. 만약 Hit이 발생하지 않았다면 이어서 바로 캐릭터를 기준으로 일정 거리를 Trace하여 가장 가까운 타겟을 찾는다.
찾은 타겟에게는 LookAtRotation을 사용하여 Rotator를 구하고 ControlRotation을 업데이트하여 카메라가 항상 대상을 향하도록 한다.
// Lock On Targeting - Camera
UCameraComponent* FollowCam = Owner->GetFollowCamera();
TraceEnd = FollowCam->GetComponentLocation() + FollowCam->GetForwardVector() * TargetingDistance;
bool bHit = UKismetSystemLibrary::SphereTraceSingleForObjects(
GetWorld(),
FollowCam->GetComponentLocation(),
TraceEnd,
// ...
// Lock On Targeting - Nearest
bool bHit = UKismetSystemLibrary::SphereTraceSingleForObjects(
GetWorld(),
Owner->GetActorLocation(),
Owner->GetActorLocation(),
500.f,
// ...
// Lock On
LookRotator = UKismetMathLibrary::FindLookAtRotation(Owner->GetActorLocation(), TargetActor->GetActorLocation() - CameraOffset);
NewControlRotator = FMath::RInterpTo(Owner->GetController()->GetControlRotation(), LookRotator, DeltaTime, CameraRotationSpeed);
Owner->GetController()->SetControlRotation(NewControlRotator);
//
2. 공격 방향 타겟팅
적이 있는 방향으로 공격을 보정해 주기 위해 적을 타겟팅하는 타겟팅모드이다. 직접 버튼을 눌러 타겟을 찾는 락온 타겟팅과 다르게 플레이어가 방향키를 누르면서 공격할 경우 자동으로 호출된다.
현재 캐릭터의 위치에서 Last Input Vector의 방향으로 일정 거리만큼 Trace 하고 찾은 Enemy를 저장해 둔다.
// Attack Direction Targeting
bool bHit = UKismetSystemLibrary::SphereTraceSingleForObjects(
GetWorld(),
Owner->GetActorLocation(),
Owner->GetActorLocation() + Owner->GetCharacterMovement()->GetLastInputVector().GetSafeNormal() * SubTargetingDistance,
// ...
이제 타겟팅할 적을 세팅되었기 때문에 캐릭터의 공격이 타깃에게 향햐도록 하여야 한다. 저번에 포스팅한 모션 워핑을 사용하여 이를 구현하였다.
MotionWarping AnimNotify State 시작 직전에 AN_FindTarget Anim Notify를 배치하여 Trace로 타겟을 찾도록 하고 타겟의 Transform 정보를 Warp Target으로 등록하도록 하였다.
// AN_FindTarget Notify
if (TargetingComponent->IsTargeting())
{
UE_LOG(LogTemp, Warning, TEXT("LockOn Mode"));
// Use LockOn Target Transform
FTransform TargetTransform = TargetingComponent->GetTargetTransform();
float WarpDistance = Blader->LightAttackWarpDistance > TargetTransform.GetLocation().Length() ? TargetTransform.GetLocation().Length() : Blader->LightAttackWarpDistance;
WarpTransform.SetLocation(Blader->GetActorLocation() + TargetTransform.GetLocation().GetSafeNormal() * WarpDistance);
WarpTransform.SetRotation(TargetTransform.GetRotation());
}
else
{
bool bFind = TargetingComponent->FindSubTarget();
if (bFind)
{
UE_LOG(LogTemp, Warning, TEXT("SubTarget Mode"));
FTransform TargetTransform = TargetingComponent->GetTargetTransform();
float WarpDistance = Blader->LightAttackWarpDistance > TargetTransform.GetLocation().Length() ? TargetTransform.GetLocation().Length() : Blader->LightAttackWarpDistance;
WarpTransform.SetLocation(Blader->GetActorLocation() + TargetTransform.GetLocation().GetSafeNormal() * WarpDistance);
WarpTransform.SetRotation(TargetTransform.GetRotation());
}
else
{
if (TargetingComponent->GetLastSubTargetActor() != nullptr)
{
FTransform TargetTransform = TargetingComponent->GetLastSubTargetTransform();
float WarpDistance = Blader->LightAttackWarpDistance > TargetTransform.GetLocation().Length() ? TargetTransform.GetLocation().Length() : Blader->LightAttackWarpDistance;
WarpTransform.SetLocation(Blader->GetActorLocation() + TargetTransform.GetLocation().GetSafeNormal() * WarpDistance);
WarpTransform.SetRotation(TargetTransform.GetRotation());
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Forward Mode"));
WarpTransform.SetLocation(Blader->GetActorLocation() + Blader->GetPendingMovementInputVector() * Blader->LightAttackWarpDistance);
WarpTransform.SetRotation(FQuat(Blader->GetActorRotation()));
}
}
}
WarpTransform.SetScale3D(FVector(1.f, 1.f, 1.f));
Blader->GetMotionWarpingComponent()->AddOrUpdateWarpTargetFromTransform(FName("Forward"), WarpTransform);
이미 락온한 타깃이 있다면 이를 우선하게 하였고 아무 타겟이 없을 경우, 플레이어가 입력한 방향으로 조금씩 이동하도록 설정해 두었다.
(AnimNotify를 C++로 다룰 때에는 Received_Notify가 아닌 Deprecated인 Notify를 오버라이드 해야 한다)
애니메이션 몽타주의 Motion Warping State에서 WarpTarget을 등록해 둔 "Forward"로 설정하여 공격 방향이 타겟을 향하도록 하였고, 타겟을 향해 조금씩 이동하도록 하였다.
진행중인 프로젝트 2개 모두 개발할 것들이 매우 많아져 포스팅에 시간을 쓰기가 어렵다. Github에 있는 소스코드도 추가하였으니 다음에는 코드나 구현방식을 조금 덜어내고 작성하려한다.