전투 공격 판정
전에 올린 포스팅에서는 공격을 판정하기 위해 아주 간단한 방법만 사용했다.
간략하게 공격을 판정하는 방법은 3가지로 나눌 수 있다.
- 공격 타이밍에 공격 범위에 해당되는 콜리전을 만들어서 체크하기
- 공격하고자 하는 무기에 콜리전을 설치해 닿는 경우 체크하기
- 공격하는 애니메이션에 NotifyState를 이용해 Tick 마다 무기 위치 체크하기
저번엔 1번에 대해 포스팅 해봤으니
이번에는 3번을 해보자.
기본 아이디어
공격을 한다는건 무기를 휘두른다는 것과 같다.
그렇다면 무기의 칼 끝과 칼 손잡이 부분에 Socket을 지정하고
그 사이를 라인트레이싱해서 피격된 물체가 있는지 확인하자.
Socket 설정


이렇게 시작과 끝 부분을 지정해서 나중에 이 사이에 어떤 물체가 닿았는지 판별할 것이다.
AnimNotifyState
애님 노티파이 스테이트는 몽타주에서 시작과 끝 타이밍을 체크할 수 있으며, Tick 을 이용해 매 프레임 확인이 가능하다.
이점을 이용해서 애니메이션에서 공격하는 타이밍을 지정하고 체크해보자.
AttackTo 라고 하는 애님 노트파이 스테이트를 만들자.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "AnimNotifyState_AttackTo.generated.h"
/**
*
*/
UCLASS()
class PROJECTRAS_API UAnimNotifyState_AttackTo : public UAnimNotifyState
{
GENERATED_BODY()
public:
UAnimNotifyState_AttackTo();
virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration) override;
virtual void NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime, const FAnimNotifyEventReference& EventReference) override;
virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Attack)
int32 AttackNum;
protected:
virtual void MakeLineTrace(USkeletalMeshComponent* Attacker);
UPROPERTY()
TSet<TObjectPtr<AActor>> HitActors;
};
그리고 몽타주에서 공격하고자 하는 부분에 노티파이 스테이트를 설정한다.

이제 Tick을 구현해보자.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Animation/NotifyState/AnimNotifyState_AttackTo.h"
#include "Interface/RASBattleInterface.h"
#include "Engine/World.h"
#include "DrawDebugHelpers.h"
#include "Character/RASCharacterBase.h"
#include "Utils/RASCollisionChannels.h"
UAnimNotifyState_AttackTo::UAnimNotifyState_AttackTo()
{
}
void UAnimNotifyState_AttackTo::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration)
{
Super::NotifyBegin(MeshComp, Animation, TotalDuration);
HitActors.Empty();
}
void UAnimNotifyState_AttackTo::NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime, const FAnimNotifyEventReference& EventReference)
{
Super::NotifyTick(MeshComp, Animation, FrameDeltaTime, EventReference);
if (AActor* Owner = MeshComp->GetOwner())
{
if (ARASCharacterBase* Attacker = Cast<ARASCharacterBase>(Owner))
{
MakeLineTrace(MeshComp);
}
}
}
void UAnimNotifyState_AttackTo::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
Super::NotifyEnd(MeshComp, Animation);
if (MeshComp)
{
IRASBattleInterface* BattleInterface = Cast<IRASBattleInterface>(MeshComp->GetOwner());
if (BattleInterface)
{
BattleInterface->EndAttack();
}
}
}
void UAnimNotifyState_AttackTo::MakeLineTrace(USkeletalMeshComponent* Attacker)
{
if (Attacker == nullptr) return;
const FName StartSocketName(TEXT("WeaponStart"));
const FName EndSocketName(TEXT("WeaponEnd"));
if (Attacker->DoesSocketExist(StartSocketName) && Attacker->DoesSocketExist(EndSocketName))
{
FVector StartLocation = Attacker->GetSocketLocation(StartSocketName);
FVector EndLocation = Attacker->GetSocketLocation(EndSocketName);
AActor* Owner = Attacker->GetOwner();
if (Owner == nullptr) return;
FHitResult HitResult;
FCollisionQueryParams CollisionParams;
CollisionParams.AddIgnoredActor(Owner);
UWorld* World = Owner->GetWorld();
if (World == nullptr) return;
bool bHit = World->LineTraceSingleByChannel(
HitResult,
StartLocation,
EndLocation,
ECC_RASChannel,
CollisionParams
);
AActor* HitActor = HitResult.GetActor();
if (bHit == false || HitActor == nullptr || HitActors.Contains(HitResult.GetActor()))
{
DrawDebugLine(World, StartLocation, EndLocation, FColor::Blue, false, 0.1f, 0, 1.0f);
return;
}
HitActors.Add(HitActor);
DrawDebugLine(World, StartLocation, EndLocation, FColor::Red, false, 1.0f);
DrawDebugSphere(World, HitResult.ImpactPoint, 10.0f, 12, FColor::Yellow, false, 1.0f);
IRASBattleInterface* BattleInterface = Cast<IRASBattleInterface>(HitActor);
ARASCharacterBase* From = Cast<ARASCharacterBase>(Owner);
if (BattleInterface == nullptr)
return;
// TODO AttackNum에 따라 데미지 가져오기
BattleInterface->HitFromActor(From, 1);
}
}
중복되는 오브젝트가 없도록 Set을 이용해서 오브젝트를 관리해주고 피해를 주게된다.
피격 및 패링
공격 판정이 성공적으로 됐다면 오브젝트에 데미지를 줘야할 것이다.
따라서 전투 관련된 인터페이스를 하나 만들고 이를 구현해보도록 하자.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "RASBattleInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class URASBattleInterface : public UInterface
{
GENERATED_BODY()
};
DECLARE_DELEGATE(FCharacterAttackFinished)
/**
*
*/
class PROJECTRAS_API IRASBattleInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
virtual void SetAttackFinishedDelegate(const FCharacterAttackFinished& InOnAttackFinished) = 0;
virtual void StartAttackMontage(int InAttackNumber = 0) = 0;
virtual void HitFromActor(class ARASCharacterBase* InFrom, int InDamage) = 0;
virtual void EndAttack() = 0;
virtual void SetVisibleIndicator(bool InbIsVisible) = 0;
};
HitFromActor에서 피격에 대한 처리를 해주자.
이때, 플레이어가 우클릭을 통해 패링 중이라면 이렇게 처리하자.
void ARASPlayer::HitFromActor(class ARASCharacterBase* InFrom, int InDamage)
{
Super::HitFromActor(InFrom, InDamage);
if(LockOnTarget == nullptr)
SetLockedOnTarget(InFrom);
if (bIsParrying == true)
{
// TODO : 패링
// 스테미나 감소
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance == nullptr)
return;
if (Stat->GetStamina() <= 0)
{
AnimInstance->Montage_Play(ParryingMontage);
AnimInstance->Montage_JumpToSection(TEXT("ParryingBreak"), ParryingMontage);
bIsBreaking = true;
FOnMontageEnded MontageEndedDelegate;
MontageEndedDelegate.BindLambda([this, AnimInstance](UAnimMontage* Montage, bool bInterrupted)
{
bIsParrying = false;
bIsBreaking = false;
});
AnimInstance->Montage_SetEndDelegate(MontageEndedDelegate, ParryingMontage);
}
else
{
AnimInstance->Montage_Play(ParryingMontage);
AnimInstance->Montage_JumpToSection(TEXT("ParryingHit"),ParryingMontage);
}
}
else if (bIsParrying == false)
{
if (float ActualDamage = Stat->ApplyDamage(InDamage) > 0)
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance == nullptr)
return;
AnimInstance->Montage_Stop(0.1f);
ComboAttack->EndCombo(true, 1.f);
AnimInstance->Montage_Play(HitMontage);
if (ActualDamage >= KnockbackFigure)
{
AnimInstance->Montage_JumpToSection(TEXT("Knockback"), HitMontage);
}
else
{
AnimInstance->Montage_JumpToSection(TEXT("Hit"), HitMontage);
}
}
}
}
이렇게 해서 각 애니메이션에 맞게 처리해준다.
Locked On
락온 시스템은 Tab키를 눌러서 주변의 적들을 찾아 그 적을 타겟팅 하는 시스템이다.
물론 내가 타겟팅을 하지 않았어도 피격 당하거나 먼저 공격을 했을 경우 자연스레 그 물체에 타겟팅 되어야한다.
따라서 흐름은 다음과 같다.
- Tab 키 누르기 / 적에게 피격당함/ 적에게 피해를 입힘
- 주변의 적들 파악 / 그 적을 타겟팅함
- 가장 가까운 적 추출
- 추출된 적 타겟팅(카메라가 항상 쳐다봄)
- 만약 이미 타겟팅된 적이 있다면 새로운 타겟팅 객체를 찾음
순서대로 코드를 나열해보겠다.
Tab 키를 맵핑해준다.
void ARASPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* EnhancedInput = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);
//...//
EnhancedInput->BindAction(LockOnAction, ETriggerEvent::Triggered, this, &ARASPlayer::PressTab);
}
Tab키를 누르면
이렇게 처리한다.
void ARASPlayer::PressTab()
{
FindAllEnemyInRange();
if (LockOnTarget == nullptr)
{
SetClosestLockedOnTarget();
}
else
{
CycleLockOnTarget();
}
}
범위 내 모든 적을 찾는다.
void ARASPlayer::FindAllEnemyInRange()
{
TargetEnemys.Empty();
FVector Center = GetActorLocation();
const float SphereRadius = 2000.f;
FCollisionShape CollisionSphere = FCollisionShape::MakeSphere(SphereRadius);
FCollisionQueryParams Params(FName(TEXT("FindEnemy")), false, this);
TArray<FOverlapResult> Results;
DrawDebugSphere(GetWorld(), Center, SphereRadius, 12, FColor::Green, false, 1.0f, 0, 2.0f);
bool bIsHit = GetWorld()->OverlapMultiByChannel(
Results,
Center,
FQuat::Identity,
ECC_RASChannel,
CollisionSphere,
Params
);
if (bIsHit)
{
for (const FOverlapResult& Result : Results)
{
ARASCharacterBase* FindEnemy = Cast<ARASCharacterBase>(Result.GetActor());
if (FindEnemy != nullptr)
{
TargetEnemys.Add(FindEnemy);
}
}
}
}
가장 가까운 적을 타겟팅 한다.
void ARASPlayer::SetClosestLockedOnTarget()
{
if (LockOnTarget != nullptr)
return;
ARASCharacterBase* Target = nullptr;
float MinDistance = 21000.f;
for (ARASCharacterBase* Enemy : TargetEnemys)
{
float CurDistance = GetDistanceTo(Enemy);
if (CurDistance < MinDistance)
{
MinDistance = CurDistance;
Target = Enemy;
}
}
SetLockedOnTarget(Target);
}
또는 새로운 타겟팅을 찾는다.
void ARASPlayer::CycleLockOnTarget()
{
if (TargetEnemys.Num() == 0)
return;
TArray<ARASCharacterBase*> EnemyArray = TargetEnemys.Array();
EnemyArray.Sort([this](const ARASCharacterBase& A, const ARASCharacterBase& B)
{
return GetDistanceTo(&A) < GetDistanceTo(&B);
});
int32 CurrentIndex = EnemyArray.IndexOfByKey(LockOnTarget);
int32 NextIndex = (CurrentIndex + 1) % EnemyArray.Num();
SetLockedOnTarget(EnemyArray[NextIndex]);
}
타겟팅을 설정한다.
void ARASPlayer::SetLockedOnTarget(ARASCharacterBase* Target)
{
if (LockOnTarget)
{
LockOnTarget->SetVisibleIndicator(false);
}
LockOnTarget = Target;
if (LockOnTarget)
{
LockOnTarget->SetVisibleIndicator(true);
}
else
{
LockOff();
}
}
타겟팅이 존재하면 Tick 함수에서 카메라를 조절한다.
void ARASPlayer::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (LockOnTarget != nullptr)
{
float CurDistanceToTarget = GetDistanceTo(LockOnTarget);
if (CurDistanceToTarget > 2000.f)
{
LockOnTarget = nullptr;
PressTab();
return;
}
FRotator CurrentControlRot = GetControlRotation();
FVector PlayerLocation = GetActorLocation();
FVector TargetLocation = LockOnTarget->GetActorLocation();
FRotator TargetRotation = UKismetMathLibrary::FindLookAtRotation(PlayerLocation, TargetLocation);
FRotator NewRot = FMath::RInterpTo(CurrentControlRot, TargetRotation, DeltaTime, 5.f);
FRotator FinalRotation(CurrentControlRot.Pitch, NewRot.Yaw, CurrentControlRot.Roll);
GetController()->SetControlRotation(FinalRotation);
}
}
void ARASPlayer::LockOn()
{
URASPlayerAnimInstance* MyAnimInstance = Cast<URASPlayerAnimInstance>(GetMesh()->GetAnimInstance());
if (MyAnimInstance == nullptr)
return;
bUseControllerRotationYaw = true;
MyAnimInstance->SetLockOn(bUseControllerRotationYaw);
}
void ARASPlayer::LockOff()
{
URASPlayerAnimInstance* MyAnimInstance = Cast<URASPlayerAnimInstance>(GetMesh()->GetAnimInstance());
if (MyAnimInstance == nullptr)
return;
bUseControllerRotationYaw = false;
MyAnimInstance->SetLockOn(bUseControllerRotationYaw);
}
이렇게 해서 락온 기능을 만들 수 있다.
결과
'Unreal Engine 5 > ProjectRAS' 카테고리의 다른 글
| [UE5] ProjectRAS - 스테미나 추가, 패링 성공 시 Blur, 캐릭터 죽음 (0) | 2025.04.02 |
|---|---|
| [UE5] ProjectRAS - 패링으로 적 밀격 하기, UI 만들기 (0) | 2025.03.20 |
| [UE5] ProjectRAS - 시점 고정과 일반 몬스터 (0) | 2025.03.06 |
| [UE5] ProjectRAS - 콤보 공격 과 Rolling (0) | 2025.02.19 |
| [UE5] ProjectRAS - 몬스터 AI 제작하기 (0) | 2025.02.06 |