SoftTarget 기능
소프트타겟, 소프트락 개념
- 플레이어 공격 버튼 입력 시, 플레이어 정면을 기준으로 일정 거리 및 각도 내에 적이 있을 때 그 적을 향해 회전 및 다가가면서 공격하는 기능
- 충분히 가까운 걱리에서 적 캐릭터를 향해 속도와 방향을 준다 (AutoDash 기능)
- 시점 고정은 하지 않고, 캐릭터들의 위치 및 방향에 따라 그때 그때 달라진다는 점에서 하드락과 다르다
문제 상황
1. 공격 시 계속 적 캐릭터에 붙어서 비비는 현상
- 플레이어의 목표 지점은 적 캐릭터의 ActorLocation이다
- 적 몬스터의 캐릭터 크기가 클수록, 목표 지점을 향해 다가가려 하면서 비빈다
- 캐릭터 간의 거리가 일정 거리 이내가 되면 멈추게 하더라도, 이 거리 값 설정을 플레이어 캐릭터 공격 모션 에셋에 설정하기에, 적 캐릭터 크기가 크면 의미가 없다
2. 플레이어 캐릭터의 공격 모션 재생 중, 적 캐릭터가 이동하면서 타겟 위치가 바뀌거나 타겟 액터 자체가 바뀌는 현상
- 특정 속도로 접근하도록 세팅된 값인데, 거리가 멀어지니 이동 시간이 짧아진다
- 겁나 빠르게 이동한다
- 처음 목표로 한 지점을 플에이어 공격 모션이 끝날 때까지 유지해줘여 한다
해결 방법
적 캐릭터의 중앙이 아닌 가장 가까운 위치를 타겟 위치로 잡는다
- 각 캐릭터는 캡슐 형태의 피격 볼륨을 가진다
- 플레이어 캐릭터가 공격 모션을 재생하면, 적 캐릭터에 Attach된 피격 CapsuleComponent를 조사한다
- 이 Component들 중 캐릭터의 Forward를 기준으로 즉, 플레이어 캐릭터 정면에서 가장 가까운 캡슐 표면 위치를 구한다
- 그리고 이 CapsuleComponent, 그리고 Capsule의 Owner Actor를 플레이어 캐릭터의 공격 모션이 끝날 때까지 유지한다
void UCustomTargetingComponent::SoftTarget_Init(bool bUseTopPriorityPolicy)
{
if (FL::HasTag(OwnerCharacter, Tags.Status.Dead))
{
SoftTarget_Clear();
return;
}
// SoftTarget 탐색 세팅을 순회한다
const int32 Size = bUseTopPriorityPolicy ? 1 : SoftTargetSettings.Num();
for (int32 Index = 0; Index < Size; ++Index)
{
const auto& CurrentSoftTargetSettings = SoftTargetSettings[Index];
if (CurrentSoftTargetSettings.bEnable == false)
{
SoftTarget_Clear();
continue;
}
// ...
const FVector OwnerLocation = OwnerCharacter->GetActorLocation();
const FQuat ActorQuat = OwnerCharacter->GetActorRotation().Quaternion();
// SoftTarget을 체크하는 기점은 플레이어 캐릭터의 발끝지점
const FVector SoftTargetCheckLocation = OwnerLocation - FVector(0.f, 0.f, OwnerCharacter->GetCapsuleComponent()->GetScaledCapsuleHalfHeight());
// 기준 위치로부터 조건을 충족하는 CapculeComponent 리스트를 받는다
const TArray<FOverlapResult> OverlapResults =
CheckOverlap(SoftTargetCheckLocation, ActorQuat,
CurrentSoftTargetSettings.CollisionChannels, CurrentSoftTargetSettings.InitTraceHalfHeight, CurrentSoftTargetSettings.InitTraceDistance);
// 검출된 대미지 캡슐이 있다면
if (OverlapResults.IsEmpty() == false)
{
bool bFoundTarget = false;
const FVector TargetedActorLocation = OverlapResults[0].GetActor()->GetActorLocation();
float NearestDist = MAX_FLT;
FVector NearestPoint = TargetedActorLocation;
UCapsuleComponent* NearestCapsule = nullptr;
// Segment를 정의한다
const FVector SelfActorForward = OwnerCharacter->GetActorForwardVector();
const FVector SegmentStart = OwnerLocation;
const FVector SegmentEnd = OwnerLocation + (bHasInput ? InputVector : SelfActorForward) * MAX_FLT;
// Intersection이 전혀 없는지 확인
bool bFoundAnyIntersection = false;
// 피격 캡슐을 Iterate해서 가장 가까운 포인트를 찾는다
for (const auto& OverlapResult_Capsule : OverlapResults)
{
if (UCapsuleComponent* TargetCapsule = Cast<UCapsuleComponent>(OverlapResult_Capsule.Component))
{
// ...
const FVector TargetCenter = TargetCapsule->GetComponentLocation();
const float CapsuleRadius = TargetCapsule->GetScaledCapsuleRadius();
const float CapsuleHalfHeight = TargetCapsule->GetScaledCapsuleHalfHeight();
const FVector UpVector = TargetCapsule->GetUpVector();
// 캡슐의 위 아래 반구 모양을 제외하고 원통형 부분만 고려했을 때, 맨 위와 아래 지점을 구한다
// 이는 아래의 SegmentCapsuleIntersections 함수에 필요한 인자를 구하기 위함
const FVector BeginPoint = TargetCenter - UpVector * (CapsuleHalfHeight - CapsuleRadius);
const FVector EndPoint = TargetCenter + UpVector * (CapsuleHalfHeight - CapsuleRadius);
// 직선과 캡슐의 교차점을 구하는 함수 실행
TArray<FVector> OutIntersections;
const bool bFoundIntersections = FL::SegmentCapsuleIntersections(BeginPoint, EndPoint, CapsuleRadius, SegmentStart, SegmentEnd, OutIntersections);
// 여태껏 아무런 교차점을 못 찾았는지 기록
bFoundAnyIntersection |= bFoundIntersections;
if (bFoundIntersections)
{
// 해당 지점이 소프트 타겟 체크 범위 내에 있는지와 중간에 장애물이 있는지 확인
if (SoftTarget_FoundTarget(OutIntersections[0], SoftTargetCheckLocation, TargetCapsule, NearestDist, NearestPoint, NearestCapsule))
{
bFoundTarget = true;
}
}
else
{
// 교차점이 하나도 없었다면 끼인각이 가장 작은 위치를 다시 탐색
// 앞서 정의한 Segment와 캡슐의 중심축 Segment와 가장 가까운 지점 탐색
const FVector ClosestPoint = FMath::ClosestPointOnSegment(SegmentEnd, BeginPoint, EndPoint);
if (FL::SegmentCapsuleIntersections(BeginPoint, EndPoint, CapsuleRadius, SegmentStart, ClosestPoint, OutIntersections)
&& SoftTarget_FoundTarget(OutIntersections[0], SoftTargetCheckLocation, TargetCapsule, NearestDist, NearestPoint, NearestCapsule))
{
bFoundTarget = true;
}
}
}
}
// Target으로 삼을 CapsuleComponent와 표면 상의 Target 위치를 잡았는지 확인
if (bFoundTarget)
{
// 캡슐 표면 위치 및 해당 캡슐의 Owner 반환
TargetLocationOnSoftTarget = NearestPoint;
TargetCapsuleOnSoftTarget = NearestCapsule;
bActiveSoftTarget = true;
CurrentSoftTargetSettingIndex = Index;
FL::ActorMessage::Publish(OwnerCharacter, FGM_Character_ActivatedSoftTarget { true, TargetCapsuleOnSoftTarget->GetOwner() });
break;
}
}
}
// SoftTarget 탐색을 시도했으나 실패했을 경우
if (bActiveSoftTarget == false)
{
SoftTarget_Clear();
}
}
- 플레이어 캐릭터의 Forward 방향으로 캡슐과의 접점을 탐색한다
- 없다면 캡슐의 중앙을 관통하는 선분 위의 점 중, 플레이어 캐릭터의 Forward Vector와 가장 가까운 위치를 찾는다
- 감지된 모든 CapculeComponent를 모두 순회한 뒤, 가장 가까운 지점을 최종 타겟으로 확정한다
일정 거리 내로 더 다가오지 못하게 막기
- 우리 프로젝트는 Abl이라는 플러그인을 사용해 캐릭터의 액션 단위를 정의했다
- 캐릭터의 액션 하나를 실행하기 위해 애니메이션 실행, 위치 보정, GE 적용 등을 AblTask 또는 AblAction 이라는 단위로 실행했다
- 아래는 그 중 AblAction에 해당하는, 타겟으로 삼은 Actor와의 거리를 유지하기 위한 로직의 일부이다
void UAblAction_KeepDistance::OnActionStart(const TWeakObjectPtr<const UAblAbilityContext>& Context) const
{
if (AActor* SelfActor = Context->GetSelfActor()
; IsValid(SelfActor) && FL::IsLocalPlayerCharacter(SelfActor))
{
// 최초 소프트 타겟 대상 지정
if(UCustomTargetingComponent* TargetingComponent = FL::GetActorComponent<UCustomTargetingComponent>(SelfActor))
{
TargetingComponent->SoftTarget_Init(bUseTopPriorityPolicy);
// ...
}
}
}
void UAblAction_KeepDistance::OnActionTick(const TWeakObjectPtr<const UAblAbilityContext>& Context, float DeltaTime) const
{
Super::OnActionTick(Context, DeltaTime);
// 전방 일정 범위 내에 타겟이 있는 경우, CombatMove를 실행시키지 않는 옵션 처리
if (AActor* SelfActor = Context->GetSelfActor()
; IsValid(SelfActor) && FL::IsLocalPlayerCharacter(SelfActor))
{
if (UCombatMoveComponentBase* CombatMoveComponent = FL::GetActorComponent<UCombatMoveComponentBase>(SelfActor)
; CombatMoveComponent->IsEnableCombatMove() && bDisableCombatMoveWithTarget)
{
FVector TargetLocation;
UCapsuleComponent* CapsuleComponent = nullptr;
const bool bHasLockTarget = FL::GetTargetInfo(SelfActor, TargetLocation, CapsuleComponent);
if (bHasLockTarget && IsValid(CapsuleComponent))
{
const float OffsetDistance = KeepDistance + CapsuleComponent->GetScaledCapsuleRadius();
bool bInsideDistance = false;
switch (DistanceType)
{
case EAistanceType::XYZ:
{
const float ActualDistSq = FVector::DistSquared(SelfActor->GetActorLocation(), TargetLocation);
bInsideDistance = ActualDistSq <= OffsetDistance * OffsetDistance;
}
break;
// ...
}
if (bInsideDistance)
{
// 타겟이 잡히면 CombatMove Velocity를 0으로
CombatMoveComponent->SetOverrideVelocity(FVector::ZeroVector);
}
else
{
// 타겟이 없으면 OverrideVelocity 적용을 멈추고, 다시 애니메이션 커브 데이터에 기반한 Velocity를 적용한다
CombatMoveComponent->ClearOverrideVelocity();
}
}
}
}
}
- CombatMoveComponent는 이동량을 메타 데이터르 갖고 있는 애니메이션의 커브를 추출해 캐릭터의 이동에 관여하는 Component이다
- 애니메이션을 재생하면서 실제로 이동할 때, 이동량을 조정하는 기능을 추가하기 위한 목적이다
- GetTargetInfo 함수를 통해 SelfActor의 현재 SoftTarget Actor를 가져오는 역할을 한다
- OnActionStart에서 초기화한 SoftTarget 정보는 UCustomTargetingComponent에 계속 유지되고 있다
- 위 로직은 문제 상황 발생 전부터 있던 로직이다
- 매 Tick SoftTarget을 찾던 로직을 AblAction Start 타이밍에 1번만 하도록 수정했다
- 기존에는 플레이어 캐릭터 공격 시, 적 캐릭터와의 거리를 유지하는 역할만 했다
- 가장 가까운 캡슐의 표면으로 TargetLocation을 정의하도록 변경했다
- 이로써, 상기한 큰 스케일의 적 캐릭터를 향해 공격 시 CombatMoveComponent에 의해 비비는 현상을 막는 역할까지 한다