적이 Override하여 사용할 HitInterface를 만들어줍니다
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "HitInterface.generated.h"
USTRUCT(BlueprintType)
struct FGameHitSystemData
{
GENERATED_BODY()
//공격 데미지
UPROPERTY(BlueprintReadWrite)
float damageAmount;
//공격 방향
UPROPERTY(BlueprintReadWrite)
FVector hitDirection;
//패링가능 여부
UPROPERTY(BlueprintReadWrite)
bool bCanParry;
//넉백강도
UPROPERTY(BlueprintReadWrite)
float knockBackStrength;
};
//interface
UINTERFACE(MinimalAPI)
class UHitInterface : public UInterface
{
GENERATED_BODY()
};
class IHitInterface
{
GENERATED_BODY()
public:
virtual void ReceiveHit(const FGameHitSystemData& damageData, AActor* attacker) = 0;
};
그리고 hitinterface에 알려줄 무기에서 오버랩 트레이스 이벤트를 만들어줍니다
칼에 만들어주었습니다
현재칼에는 StartPoint와 EndPoint로 SceneComponent가 붙어있습니다
칼의 처음과 끝에 컴포넌트를 배치시켜줍니다
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "MySword")
class USceneComponent* capsureTraceStartPoint;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "MySword")
class USceneComponent* capsureTraceEndPoint;
ABasicSword::ABasicSword()
{
PrimaryActorTick.bCanEverTick = true;
sceneComp = CreateDefaultSubobject<USceneComponent>(TEXT("SceneComp"));
SetRootComponent(sceneComp);
swordMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("swordMesh"));
swordMesh->SetupAttachment(RootComponent);
static ConstructorHelpers::FObjectFinder<UStaticMesh> crowMesh(TEXT("/Game/MainCharacter/Sword/futuristic_sword_upload.futuristic_sword_upload"));
if (crowMesh.Succeeded())
{
swordMesh->SetStaticMesh(crowMesh.Object);
}
capsureTraceStartPoint = CreateDefaultSubobject<USceneComponent>(TEXT("startPoint"));
capsureTraceStartPoint->SetupAttachment(swordMesh);
capsureTraceStartPoint->SetRelativeLocation(FVector(31.27f, 0.f, 0.f));
capsureTraceStartPoint->SetRelativeRotation(FRotator(0.f, 4.54f, 0.f));
capsureTraceEndPoint = CreateDefaultSubobject<USceneComponent>(TEXT("EndPoint"));
capsureTraceEndPoint->SetRelativeLocation(FVector(372.8f, 40.87f, -2.6f));
capsureTraceEndPoint->SetupAttachment(swordMesh);
}
무기에서
Trace를 해줄 공격체크함수 , 그리고 중복히트를 방지한 TSet과 배열을 clear해줄 함수를 선언해줍니다
UFUNCTION()
void AttackHitCheck(float deltaTime);
UFUNCTION()
void ClearHitActors();
TSet<AActor*> alreadyHitActors;
해당함수에서는 alreadyHitActors배열을 초기화시켜줍니다
void ABasicSword::ClearHitActors()
{
alreadyHitActors.Empty();
}
공격을 감지하는 함수입니다
pawn채널만 감지를 하며 정해진대상만 체크를합니다
조건은 Enemy 라는 태그를 가진 대상만 공격 가능합니다
void ABasicSword::AttackHitCheck(float deltaTime)
{
if (!capsureTraceStartPoint || !capsureTraceEndPoint) return;
FVector traceStart = capsureTraceStartPoint->GetComponentLocation();
FVector traceEnd = capsureTraceEndPoint->GetComponentLocation();
TArray<FHitResult> traceHitResults;
FCollisionQueryParams hitParams;
hitParams.AddIgnoredActor(this);
hitParams.AddIgnoredActor(GetOwner());
//맞는대상 최적화코드 pawn, 그리고 정해진 대상만
FCollisionObjectQueryParams objQuery;
objQuery.AddObjectTypesToQuery(ECC_Pawn);
bool bHitTrace = GetWorld()->SweepMultiByObjectType(
traceHitResults, traceStart, traceEnd, FQuat::Identity, objQuery, FCollisionShape::MakeCapsule(10.f, 50.f),
hitParams
);
if (bHitTrace)
{
for (auto& hit : traceHitResults)
{
AActor* hitActors = hit.GetActor();
if (hitActors && hitActors != GetOwner())
{
if (alreadyHitActors.Contains(hitActors))
{
continue;
}
//태그체킹 Enemy확인
if (!hitActors->ActorHasTag(FName("Enemy")))
{
continue;
}
alreadyHitActors.Add(hitActors);
UE_LOG(LogTemp, Warning, TEXT("Sword hit: %s"), *hitActors->GetName());
}
}
}
// 디버그 캡슐
DrawDebugCapsule(GetWorld(),(traceStart + traceEnd) * 0.5f,(traceStart - traceEnd).Size() * 0.5f,10.f,
FRotationMatrix::MakeFromZ(traceEnd - traceStart).ToQuat(),
FColor::Red,
false,
0.1f
);
}
공격하고싶은 pawn에는 반드시 Enemy 태그를 붙혀주어야 작동합니다

애님인스턴스에서 배열을 클리어해주는 ClearHitActors함수를 호출해주는 노티파이를 만들어줍니다
void UMainCharacterAnimInstance::AnimNotify_AttackTraceArrayClear()
{
myPawn = TryGetPawnOwner();
if (myPawn)
{
mainCharacter = Cast<AMainCharacter>(myPawn);
if (mainCharacter->swordActorComp)
{
ABasicSword* sword = Cast<ABasicSword>(mainCharacter->swordActorComp->GetChildActor());
if (sword)
{
sword->ClearHitActors();
}
}
}
}
플레이어의 공격 몽타쥬부분에서 공격 시작전에 노티파이로 호출시켜줍니다

그다음 캐릭터에서 현재 사용하고있는 콤보 인덱스에서 사용하고있는 데이터테이블의 행의 값을 저장해야합니다
캐릭터에서 hitinterface에 값을 보내줄 변수를 선언
UPROPERTY()
int32 saveCurrentAttackDamage = 0;
UPROPERTY()
int32 saveCurrentComboIndex = 0;
공격함수에 추가해줍니다
//datatable의 hitdamage, index 저장
saveCurrentAttackDamage = static_cast<int32>(row->hitDamage);
saveCurrentComboIndex = row->comboIndexes;
적용한 전체코드
void AMainCharacter::PlayBasicAttackCombo()
{
if (GetCharacterMovement()->IsFalling()) return;
if (comboRows.Num() == 0) return;
bIsJumpAllowed = false;
// 처음 입력 또는 콤보가 끊긴 상태
if (!bCanNextCombo)
{
currentComboIndex = 0;
bCanNextCombo = true;
//다음 애니메이션 넘어갈떄의 제한
bCanReceiveInput = false;
}
else
{
if (!bCanReceiveInput) return;
// comboInputTime 안에 재입력 → 다음 콤보
currentComboIndex++;
if (currentComboIndex >= comboRows.Num())
{
currentComboIndex = 0; // 마지막 콤보 후 다시 처음
}
bCanReceiveInput = false;
}
FCharacterAnimDataTable* row = comboRows[currentComboIndex];
if (!row || !row->usingAnimation) return;
currentMontage = row->usingAnimation;
//datatable의 hitdamage, index 저장
saveCurrentAttackDamage = static_cast<int32>(row->hitDamage);
saveCurrentComboIndex = row->comboIndexes;
PlayAnimMontage(currentMontage);
GetWorld()->GetTimerManager().ClearTimer(th_comboAttackTimerHandle);
GetWorld()->GetTimerManager().SetTimer(th_comboAttackTimerHandle,this,&AMainCharacter::ResetCombo,row->comboInputTime,false
);
}
HitInterface를 맞을 대상 즉 적 캐릭터에게 Override 해줍니다
UCLASS()
class PORTFOLIOMS_API AEnemyBaseCharacter : public ACharacter
,public IHitInterface //hitinterface override
{
GENERATED_BODY()
public:
AEnemyBaseCharacter();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
virtual void OnConstruction(const FTransform& Transform) override;
//hitInterface//----------------------------------------------------------------------
virtual void ReceiveHit(const FGameHitSystemData& damageData, AActor* attacker) override;
무기에서 감지한 대상이 만약 hitinterface를 가지고있다면
복사된 데미지 수치를 인터페이스data에 복사해줍니다
밑 코드상에서는 FGameHitSystemData(hitinterface)에 있는 변수에 복사해주었습니다
if (hitActors->GetClass()->ImplementsInterface(UHitInterface::StaticClass()))
{
AMainCharacter* ownerChar = Cast<AMainCharacter>(GetOwner());
if (!ownerChar)
{
ownerChar = Cast<AMainCharacter>(GetAttachParentActor());
}
if (ownerChar)
{
FGameHitSystemData data;
data.damageAmount = ownerChar->saveCurrentAttackDamage;
UE_LOG(LogTemp, Warning, TEXT("DamageAmount: %d"), data.damageAmount);
}
}
적용한 전체코드
void ABasicSword::AttackHitCheck(float deltaTime)
{
if (!capsureTraceStartPoint || !capsureTraceEndPoint) return;
if (!bCanAttack) return;
FVector traceStart = capsureTraceStartPoint->GetComponentLocation();
FVector traceEnd = capsureTraceEndPoint->GetComponentLocation();
TArray<FHitResult> traceHitResults;
FCollisionQueryParams hitParams;
hitParams.AddIgnoredActor(this);
hitParams.AddIgnoredActor(GetOwner());
//맞는대상 최적화코드 pawn, 그리고 정해진 대상만
FCollisionObjectQueryParams objQuery;
objQuery.AddObjectTypesToQuery(ECC_Pawn);
bool bHitTrace = GetWorld()->SweepMultiByObjectType(
traceHitResults, traceStart, traceEnd, FQuat::Identity, objQuery, FCollisionShape::MakeCapsule(10.f, 50.f),
hitParams
);
if (bHitTrace)
{
for (auto& hit : traceHitResults)
{
AActor* hitActors = hit.GetActor();
if (hitActors && hitActors != GetOwner())
{
if (alreadyHitActors.Contains(hitActors))
{
continue;
}
//태그체킹 Enemy확인
if (!hitActors->ActorHasTag(FName("Enemy")))
{
continue;
}
alreadyHitActors.Add(hitActors);
UE_LOG(LogTemp, Warning, TEXT("Sword hit: %s"), *hitActors->GetName());
if (hitActors->GetClass()->ImplementsInterface(UHitInterface::StaticClass()))
{
AMainCharacter* ownerChar = Cast<AMainCharacter>(GetOwner());
if (!ownerChar)
{
ownerChar = Cast<AMainCharacter>(GetAttachParentActor());
}
if (ownerChar)
{
FGameHitSystemData data;
data.damageAmount = ownerChar->saveCurrentAttackDamage;
UE_LOG(LogTemp, Warning, TEXT("DamageAmount: %d"), data.damageAmount);
}
}
}
}
}
// 디버그 캡슐
DrawDebugCapsule(GetWorld(),(traceStart + traceEnd) * 0.5f,(traceStart - traceEnd).Size() * 0.5f,10.f,
FRotationMatrix::MakeFromZ(traceEnd - traceStart).ToQuat(),
FColor::Red,
false,
0.1f
);
}
여기까지 완료했다면 데이터 테이블에 있는 Hit Damage값이 HitInterface에 전달이되어
trace할때 수치를 주고받을수있게됩니다

sword trace쪽에 인터페이스의 ReceiveHit함수를 호출하고 데이터를 넘겨줍니다
그러면 hitinterface를 상속받는 대상에게는 data가 전달이되고 호출이됩니다
IHitInterface* hitInterface = Cast<IHitInterface>(hitActors);
if (hitInterface)
{
hitInterface->ReceiveHit(data, ownerChar);
UE_LOG(LogTemp, Warning, TEXT("Called ReceiveHit on %s"), *hitActors->GetName());
}
수정한 전체코드
void ABasicSword::AttackHitCheck(float deltaTime)
{
if (!capsureTraceStartPoint || !capsureTraceEndPoint) return;
if (!bCanAttack) return;
FVector traceStart = capsureTraceStartPoint->GetComponentLocation();
FVector traceEnd = capsureTraceEndPoint->GetComponentLocation();
TArray<FHitResult> traceHitResults;
FCollisionQueryParams hitParams;
hitParams.AddIgnoredActor(this);
hitParams.AddIgnoredActor(GetOwner());
//맞는대상 최적화코드 pawn, 그리고 정해진 대상만
FCollisionObjectQueryParams objQuery;
objQuery.AddObjectTypesToQuery(ECC_Pawn);
bool bHitTrace = GetWorld()->SweepMultiByObjectType(
traceHitResults, traceStart, traceEnd, FQuat::Identity, objQuery, FCollisionShape::MakeCapsule(10.f, 50.f),
hitParams
);
if (bHitTrace)
{
for (auto& hit : traceHitResults)
{
AActor* hitActors = hit.GetActor();
if (hitActors && hitActors != GetOwner())
{
if (alreadyHitActors.Contains(hitActors))
{
continue;
}
//태그체킹 Enemy확인
if (!hitActors->ActorHasTag(FName("Enemy")))
{
continue;
}
alreadyHitActors.Add(hitActors);
UE_LOG(LogTemp, Warning, TEXT("Sword hit: %s"), *hitActors->GetName());
if (hitActors->GetClass()->ImplementsInterface(UHitInterface::StaticClass()))
{
AMainCharacter* ownerChar = Cast<AMainCharacter>(GetOwner());
if (!ownerChar)
{
ownerChar = Cast<AMainCharacter>(GetAttachParentActor());
}
if (ownerChar)
{
FGameHitSystemData data;
data.damageAmount = ownerChar->saveCurrentAttackDamage;
//UE_LOG(LogTemp, Warning, TEXT("DamageAmount: %d"), data.damageAmount);
IHitInterface* hitInterface = Cast<IHitInterface>(hitActors);
if (hitInterface)
{
hitInterface->ReceiveHit(data, ownerChar);
UE_LOG(LogTemp, Warning, TEXT("Called ReceiveHit on %s"), *hitActors->GetName());
}
}
}
}
}
}
// 디버그 캡슐
DrawDebugCapsule(GetWorld(),(traceStart + traceEnd) * 0.5f,(traceStart - traceEnd).Size() * 0.5f,10.f,
FRotationMatrix::MakeFromZ(traceEnd - traceStart).ToQuat(),
FColor::Red,
false,
0.1f
);
}
다시한번
hitinterface를 사용할 적 캐릭터는 무조건 HitInterface를 Override해야지만 사용이 가능합니다
UCLASS()
class PORTFOLIOMS_API AEnemyBaseCharacter : public ACharacter
,public IHitInterface //hitinterface override
{
GENERATED_BODY()
public:
AEnemyBaseCharacter();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
virtual void OnConstruction(const FTransform& Transform) override;
//hitInterface//----------------------------------------------------------------------
virtual void ReceiveHit(const FGameHitSystemData& damageData, AActor* attacker) override;
//------------------------------------------------------------------------------------
OnConstruction함수에서 row에있는 maxHp를 가져온뒤 값을 최대 maxHp, 최소 0으로 제한해준뒤
확인용으로 몽타쥬를 플레이시켜보겠습니다
currnetHp즉 enemy의 현재 hp변수를 선언한뒤 maxHp와 연동해줍니다
beginplay에서 currentHp = maxHp를 해주었습니다
void AEnemyBaseCharacter::ReceiveHit(const FGameHitSystemData& damageData, AActor* attacker)
{
currentHp -= damageData.damageAmount;
currentHp = FMath::Clamp(currentHp, 0, maxHp);
UE_LOG(LogTemp, Warning, TEXT("CurrentHp : %d"), currentHp);
PlayAnimMontage(enemyDamagedMontage);
}
데이터테이블의 Enemy Id가 1번인 캐릭터의 Max Hp는 5000

1번공격 (1타) Damage = 300
2번공격 (1타) Damage = 100
3번공격 (2타) Damage = 200
으로 설정해주었습니다

값을 잘받아오는것을 로그로 확인할수 있습니다
HitInterface로 플레이어와 적이 DataTable에 있는 키값으로 서로 상호작용할수있는 구조가 완성되었습니다

'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - 캐릭터 회전 (0) | 2025.10.19 |
|---|---|
| Unreal - Posture Progress / Hit Direction (0) | 2025.08.29 |
| Unreal - Hash 기반 AI/Enemy 데이터 설계하기 (0) | 2025.08.23 |
| Unreal - 콤보 공격 (0) | 2025.08.22 |
| Unreal - 무기 Draw (0) | 2025.08.19 |