Unreal - HitInterface 상호작용

2025. 8. 23. 17:37·Unreal 프로젝트 다이어리/두번째 프로젝트

적이 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
'Unreal 프로젝트 다이어리/두번째 프로젝트' 카테고리의 다른 글
  • Unreal - 캐릭터 회전
  • Unreal - Posture Progress / Hit Direction
  • Unreal - Hash 기반 AI/Enemy 데이터 설계하기
  • Unreal - 콤보 공격
lucodev
lucodev
커피와 노트북 그리고 개발
  • lucodev
    루코 개발테이블
    lucodev
  • 전체
    오늘
    어제
    • 분류 전체보기 (209) N
      • Unreal 프로젝트 다이어리 (106) N
        • 첫번째 프로젝트 (73)
        • 두번째 프로젝트 (33) N
      • Unreal 팁 (8)
      • Unreal 디버깅 (8)
      • C++ 프로그래머스 (52)
        • Stack,Queue (7)
        • Hash (4)
        • Heap (2)
        • Sort (5)
        • Exhaustive search (5)
        • Greedy (2)
        • BFS , DFS (7)
        • Graph (2)
        • Dynamic Programming (1)
        • C++ Math (2)
        • 기타 문제 (14)
      • C++ 백준 (4)
      • C++ 팁 (1)
      • 개인 코테 & 스타디 <비공개> (29)
        • 코드 개인보관함 (9)
        • 코딩테스트+@ (11)
        • 알고리즘 스타디 (6)
        • 알고리즘 스타디 과제 (3)
        • 비공개 (0)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 태그

    언리얼 시퀀스
    unreal 모션매칭
    unreal 파쿠르
    Unreal Parkour
    unreal inventory
    언리얼 behavior tree
    언리얼 파쿠르
    언리얼 motionmatching
    언리얼 모션매칭
    unreal 시퀀스
    언리얼
    unreal 인벤토리
    언리얼 상호작용
    언리얼 인벤토리
    언리얼 parkour
    언리얼 프로그래스바
    언리얼 컷씬
    언리얼 비헤이비어트리
    언리얼 ui
    언리얼 behaviortree
  • hELLO· Designed By정상우.v4.10.3
lucodev
Unreal - HitInterface 상호작용
상단으로

티스토리툴바