UE5 멀티대전게임 일기

UE5 멀티대전게임 일기(8) 캐릭터 스킬데미지, 콤보평타데미지 구현방식

언리얼배우기 프로젝트 2025. 4. 7. 23:01

 

스킬(특수기)의 범위 공격 판정 & 데미지 적용

 

콤보 평타에서 손에 박스를 붙여 충돌이 일어났을 때 데미지 주기

 


스킬(특수기)의 범위 공격 판정 & 데미지 적용

 

 

간단 요약

  1. 어떤 캐릭터가 "대쉬 스킬"을 씀
  2. 애니메이션이 실행되면서 스킬 ApplyDamageNotify가 발동됨
  3. 함수 GetTargetsInRange("DashSkill")를 호출함
  4. 내부적으로 “이 스킬의 데미지 범위가 얼마지?”, “공격 범위는 어디까지지?” 확인함
  5. 그 위치에 Overlap 검사를 해서 범위 안의 적들이 있으면 데미지를 줌

 

1. 스킬 데이터 구조 설계

CharacterBase.h


...
USTRUCT(BlueprintType)
struct FSkillData
{
    float Damage = 10.f;             // 얼마나 아프게 때릴지
    ESkillAttackType AttackType;     // 어떤 판정 방식? (Sphere, Box, Trace 등)
    FVector Range;                   // 얼마나 넓게/길게 타격할건지
    FVector Offset;                  // 캐릭터 기준 어느 방향으로 나가는지
};
....

 

스킬 하나하나의 속성 값들을 저장해주는 구조체이다.

 

2. 각각의 캐릭터는 이걸 Map형식으로 저장해둔다.

CharacterBase.h


...

protected:

    // 스킬 데이터 (기본값은 자식 클래스에서 설정)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
    TMap<FName, FSkillData> SkillDataMap;
...

 

3. 애니메이션에서 ApplyDamage Notify를 호출

SkillAnimNotify.cpp


...
else if (NotifyEventName == "ApplyDamage")
{
	if (SkillName.IsNone()) return;

	TArray<AActor*> Targets = Character->GetTargetsInRange(SkillName);
	Character->ApplySkillDamage(SkillName, Targets);
}
...

 

 

ApplyDamage노티파이를 사용할때는 캐릭터 생성자함수에 정의된 스킬 Skill Name에 맞춰서 스킬에 맞는 SkillName을 넣어줘야 각 스킬에맞는 데미지나 콜리전이 실행된다.

그럼 ApplyDamage노티파이부분에 애니메이션몽타주가 실행되다가 만나면

Dubu.cpp

Dubu.cpp생성자함수에서 SkillData에 스킬이름마다 Damage와 타입, 크기, 범위등을 초기화한 값을가지고

 

ApplySkillDamage(SkillName, Targets)함수가 실행된다.

CharacterBase.h
CharacterBase.cpp

그러면 FSkillData 구조체에서 데미지, 타입(SkillAttackType에서 들고옴), 타입 범위, 크기 등을 가져와서 스킬범위 내에 캐릭터에게 데미지를 주게 된다.

 

4. 실제로 적을 찾는 핵심 로직

CharacterBase.h

TArray<AActor*> ASW_CharacterBase::GetTargetsInRange_Implementation(FName SkillName)
{
    TArray<AActor*> HitActors;
    TArray<AActor*> ActorsToIgnore;
    ActorsToIgnore.Add(this);

    const FSkillData* SkillData = SkillDataMap.Find(SkillName);
    if (!SkillData) return HitActors;

    FVector Location = GetActorLocation() + GetActorRotation().RotateVector(SkillData->Offset);

    switch (SkillData->AttackType)
    {
    case ESkillAttackType::MeleeSphere:
    {
        float Radius = SkillData->Range.X;

        // 디버그용 시각화
        DrawDebugSphere(GetWorld(), Location, Radius, 12, FColor::Green, false, 1.0f);

        UKismetSystemLibrary::SphereOverlapActors(
            GetWorld(),
            Location,
            Radius,
            TArray<TEnumAsByte<EObjectTypeQuery>>{ UEngineTypes::ConvertToObjectType(ECC_Pawn) },
            ACharacter::StaticClass(),
            ActorsToIgnore,
            HitActors
        );
        break;
    }

    case ESkillAttackType::MeleeBox:
    {
        FVector Extent = SkillData->Range;

        // 디버그용 시각화
        DrawDebugBox(GetWorld(), Location, Extent, GetActorQuat(), FColor::Red, false, 1.0f);

        UKismetSystemLibrary::BoxOverlapActors(
            GetWorld(),
            Location,
            Extent,
            TArray<TEnumAsByte<EObjectTypeQuery>>{ UEngineTypes::ConvertToObjectType(ECC_Pawn) },
            ACharacter::StaticClass(),
            ActorsToIgnore,
            HitActors
        );
        break;
    }

    case ESkillAttackType::RangedTrace:
    {
        FVector Start = Location;
        FVector End = Start + GetActorForwardVector() * SkillData->Range.X;
        FHitResult HitResult;

        // 디버그용 시각화
        DrawDebugLine(GetWorld(), Start, End, FColor::Blue, false, 1.0f, 0, 2.0f);

        UKismetSystemLibrary::LineTraceSingle(
            GetWorld(),
            Start,
            End,
            UEngineTypes::ConvertToTraceType(ECC_Pawn),
            false,
            ActorsToIgnore,
            EDrawDebugTrace::None,
            HitResult,
            true
        );

        if (HitResult.bBlockingHit && HitResult.GetActor())
        {
            HitActors.Add(HitResult.GetActor());
        }
        break;
    }

    case ESkillAttackType::RangedProjectile:
    {
        // 디버그용: 스폰 지점 표시
        DrawDebugSphere(GetWorld(), Location, 20.f, 8, FColor::Yellow, false, 1.0f);

        if (SkillData->ProjectileClass)
        {
            FActorSpawnParameters SpawnParams;
            SpawnParams.Owner = this;
            SpawnParams.Instigator = GetInstigator();

            AActor* Projectile = GetWorld()->SpawnActor<AActor>(
                SkillData->ProjectileClass,
                Location,
                GetActorRotation(),
                SpawnParams
            );
        }
        break;
    }

    default:
        break;
    }

    return HitActors;
}

 

GetTargetsInRange_Implementation(FName SkillName)함수를 통해 범위 내에 적 캐릭터를 찾는다.

 

ex) SphereOverlap (원형 범위)일때

UKismetSystemLibrary::SphereOverlapActors(
    GetWorld(),
    Location,    // 캐릭터 위치 + Offset
    Radius,      // 얼마나 넓은 원을 검사할지
    ...
    HitActors    // 적 목록을 여기에 저장
);

ex) BoxOverlap (박스 형태 범위)일때

DrawDebugBox(...);
UKismetSystemLibrary::BoxOverlapActors(
    GetWorld(),
    Location, Extent,
    ...
    HitActors
);

 

아까 ApplySkillDamage함수에서 적들을 전부 순회하여 데미지를 적용한다.

 

 

추가적으로

 

각 캐릭터마다 SkillData 구조체는 상속받지말고 캐릭터마다 각각 가져야하지 않을까싶다. 아니면 지금처럼 돌려쓰거나...

(나중에 확인해봐야할 듯)


콤보 평타에서 손에 박스를 붙여 충돌이 일어났을 때 데미지 주기

 

간단 설명 

 

콤보 애니메이션 1타, 2타, 3타 등에서 캐릭터 손에 Boxcomponent를 붙여놓고, 애니메이션 중 특정 타이밍에만 On/Off해서 타격 판정을 한다. (펀치하기전부터 콜리전에 적 캐릭터가 닿아서 데미지가 들어가면 좀 이상해서 On/Off방식을 추가)

 

두부캐릭터는 양손을 사용해서 공격하기대문에 양손에 콜리전을 넣어줬다.

그래서 두부캐릭터로 예시를 들겠다.

 

1. 애니메이션에서 충돌 켜고 끄지

Dubu.h


...
// 애니메이션 노티파이용 함수
void RightHandStart();
void RightHandEnd();
void LeftHandStart();
void LeftHandEnd();
Dubu.cpp


...
// === 애님 노티파이에서 호출될 함수들 ===
void ASW_Dubu::RightHandStart()
{
	AlreadyHitActors.Empty(); // 매 콤보마다 초기화
	RightHandCollider->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
}

void ASW_Dubu::RightHandEnd()
{
	RightHandCollider->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

void ASW_Dubu::LeftHandStart()
{
	AlreadyHitActors.Empty(); // 여기서도 초기화
	LeftHandCollider->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
}

void ASW_Dubu::LeftHandEnd()
{
	LeftHandCollider->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

 

그리고 Notify에 두부용으로 4개의 노티파이를 생성해준다.

그럼 이제 오른손으로 공격할떄는 오른손 공격용인 RigthHandStart 노티파이로 애니메이션 공격 타격지점을 설정해주고, RightHandEnd 노티파이로 공격이 끝나는 지점에 설정해주면 된다.

 

왼손도 마찬가지

콤보1 오른손 공격
콤보2 왼손 공격

 

콤보3 양손 공격

 

2. 적이 닿았을 때 데미지 처리 (손 콜리전에 적이 닿으면)

Dubu.h


...
UFUNCTION()
void OnRightHandOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
	bool bFromSweep, const FHitResult& SweepResult);

UFUNCTION()
void OnLeftHandOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
	bool bFromSweep, const FHitResult& SweepResult);
Dubu.cpp


...
// 왼손
void ASW_Dubu::OnRightHandOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
	bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor && OtherActor != this && !AlreadyHitActors.Contains(OtherActor))
	{
		int32 Damage = 0;

		if (CurrentComboIndex == 0) // 1타
			Damage = 10;
		else if (CurrentComboIndex == 2) // 3타 양손
			Damage = 25;
		else
			return;

		FDamageEvent DamageEvent;
		OtherActor->TakeDamage(Damage, DamageEvent, GetController(), this);

		AlreadyHitActors.Add(OtherActor); // 중복 방지
	}
}

// 오른손
void ASW_Dubu::OnLeftHandOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
	bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor && OtherActor != this && !AlreadyHitActors.Contains(OtherActor))
	{
		int32 Damage = 0;

		if (CurrentComboIndex == 1) // 1타
			Damage = 10;
		else if (CurrentComboIndex == 2) // 3타 양손
			Damage = 25;
		else
			return;

		FDamageEvent DamageEvent;
		OtherActor->TakeDamage(Damage, DamageEvent, GetController(), this);

		AlreadyHitActors.Add(OtherActor); // 중복 방지
	}
}

 

이렇게 캐릭터 손 콜리전에 닿으면 적에게 가하는 공격 데미지를 1타 2타 3타에따라 구현할 수 있다.

 

AlreadyHitActors.Add(OtherActor);은 한번 데미지가 들어가면 또 데미지가 안들어가게해준다.

 

3. 가장 중요한 손 콜리전 추가방법

!!!!! 나중에 손에다가 콜리전 추가하는것보다 배 부분 앞으로 일정 크기만큼 박스콜리전 생성으로 바꿔야할 듯 !!!!!

케릭터 스켈레톤에서 타격에 적합한 부위 뼈대를 찾는다.

 

R_hand_Jnt를 오른손용 콜리전 생성 부위로 설정하고 L_hand_Jnt를 왼손용 콜리전 생성부위로 설정하면 좋을 것 같다.

 

캐릭터 생성자함수에 아래와 같이 양손 스켈레탈 이름을 넣어서 콜리전을 넣어준다.

 

그럼 캐릭터를 확인해보면 

 

콜리전이 잘 적용됐다.

 

콤보 평타 원리 요약

  1. 손에 충돌 박스 만들기
  2. 손 뼈에 붙여서 손이 움직일때 따라가게하기
  3. 공격 애니메이션 도중에만 충돌을 활성화해주기
  4. 누가 손에 닿았는지 감지해서 데미지 주기