미리보기

구현내용
플레이어와 적이 상호작용하며 서로 영향을 주고받는
이른바 제 2의 체력인 세키로의 체간 및 가드 시스템을 구현했습니다
- 플레이어의 체간 시스템
- 적의 체간 시스템
- 플레이어의 가드 & 패링
가드 와 패링 의 차이
| 가드 | 적의 공격을 막을 수 있음 하지만 체간 손상이 쌓임 |
| 패링 | 공격 직전에 타이밍을 맞춰 적의 공격을 무효화 체간 게이지가 쌓이지 않음 |
사용 클래스
| 사용 클래스 | 사용 목적 |
| HitInterface(인터페이스) | 플레이어와 적이 공격 / 방어 상호작용을 할 때 공격 이벤트를 전달하는 중간다리 역할 |
| StatComponent(액터컴포넌트) | 플레이어의 체력, 체간, 상태, 데미지 계산을 담당하는 가드나 패링 여부에 따라 받는 피해량을 계산하는 두뇌 |
| MainCharacter(캐릭터) | 가드 & 패링 을 실행하는 플레이어 |
| PostureWidget(위젯) | 플레이어의 체간 게이지 시각화를 담당하는 위젯 |
| EnemyStatusWidget(위젯) | 적의 체력 & 체간 게이지 시각화를 담당하는 위젯 |
구현C++
우선 적과 플레이어는 HitInterface를 상속받아 Hit이벤트를 주고받습니다
HitInterface를 상속받으면 상속받은 대상은 여러가지 Hit이벤트를 담당하는 데이터를 가진
ReceiveHit함수를 사용 할 수 있습니다
USTRUCT(BlueprintType)
struct FGameHitSystemData
{
GENERATED_BODY()
//공격 데미지
UPROPERTY(BlueprintReadWrite)
float damageAmount;
//공격 방향 (-180 ~ 180)
UPROPERTY(BlueprintReadWrite)
float hitDirection;
//패링가능 여부
UPROPERTY(BlueprintReadWrite)
bool bCanParry;
//넉백강도
UPROPERTY(BlueprintReadWrite)
float knockBackStrength;
//맞은곳
UPROPERTY(BlueprintReadWrite)
FVector hitLocation;
UPROPERTY(BlueprintReadWrite)
EDamageType damageType;
};
//interface
UINTERFACE(MinimalAPI)
class UHitInterface : public UInterface
{
GENERATED_BODY()
};
class IHitInterface
{
GENERATED_BODY()
public:
virtual void ReceiveHit(const FGameHitSystemData& damageData, AActor* attacker) = 0;
};
적과 플레이어 는 이와같이 인터페이스를 상속합니다

HitInterface의 ReceiveHit함수를 오버라이드 하여 사용하였습니다
virtual void ReceiveHit(const FGameHitSystemData& damageData, AActor* attacker) override;
우선 적의 체간 시스템입니다
체간 시스템의 계산은 데미지를 받는 피격자 에서 실행됩니다
각각 적은 외부에서 수정할수 있는 BreakHitLevel이 존재하며
해당 레벨만큼 히트 하게되면 가드브레이크가 실행됩니다
구현C++
.h
UPROPERTY(EditAnywhere, Category = "Posture")
int32 guardBreakHitLevel = 7;
UPROPERTY(EditAnywhere, Category = "Posture")
int32 currentPostureHit = 0;
.cpp
void AEnemyBaseCharacter::ApplyPosture()
{
//무적인경우 x
if (superArmorType == ESuperArmorType::Invincible || superArmorType == ESuperArmorType::SuperArmor)
return;
currentPostureHit++;
currentPosture = (float)currentPostureHit / guardBreakHitLevel * maxPosture;
currentPosture = FMath::Clamp(currentPosture, 0.f, maxPosture);
if (currentPosture >= maxPosture)
OnPostureBreak();
BroadCastPosture();
}
플레이어의 체간 시스템입니다
플레이어의 체간 StatComponent에서 계산된 Modifier에서 계산되어
가드 인지 패링 인지 아니면 그냥 히트 인지 에 따라서 계산됩니다
EGuardType::None -> 히트
EGuardType::Normal -> 가드
EGuardType::Perfect -> 패링
구현C++
struct FGuardModifier
{
float damageRate;
float postureRate;
};
static const TMap<EGuardType, FGuardModifier> guard_Modifiers =
{
//hp p / posture
{EGuardType::None, {1.f, 0.3f}},
{EGuardType::Normal, {0.f, 0.1f}},
{EGuardType::Perfect,{0.f, 0.f}}
};
void UStatComponent::ApplyGetDamage(float damageAmount, EGuardType guardType)
{
const FGuardModifier* guardModifier = guard_Modifiers.Find(guardType);
if (!guardModifier)
return;
float finalHpDamage = damageAmount * guardModifier->damageRate;
float finalPostureDamage = damageAmount * guardModifier->postureRate;
currentHp -= finalHpDamage;
bool bPostureIncreased = finalPostureDamage > 0.f;
if (bPostureIncreased)
currentPosture += finalPostureDamage;
currentHp = FMath::Clamp(currentHp, 0.f, GetMaxHp());
currentPosture = FMath::Clamp(currentPosture, 0.f, GetMaxPosture());
if (bPostureIncreased)
StartPostureRecover();
UpdateCurrentStats();
}
void UStatComponent::StartPostureRecover()
{
GetWorld()->GetTimerManager().ClearTimer(TH_PostureRecoverDelayTimer);
GetWorld()->GetTimerManager().ClearTimer(TH_PostureRecoverTimer);
float maxPosture = GetMaxPosture();
float delay = (currentPosture >= maxPosture) ? 3.f : postureRecoverDelay;
GetWorld()->GetTimerManager().SetTimer(TH_PostureRecoverDelayTimer, this, &UStatComponent::RecoverPostureTick, delay, false);
}
void UStatComponent::RecoverPostureTick()
{
if (currentPosture <= 0.f)
{
currentPosture = 0.f;
UpdateCurrentStats();
return;
}
float maxPosture = GetMaxPosture();
float postureRatio = currentPosture / maxPosture;
float recoverMultiplier;
if (currentPosture >= maxPosture)
recoverMultiplier = 300.f;
else
recoverMultiplier = FMath::Lerp(0.6f, 1.4f, postureRatio);
float recoverAmount = postureRecoverAmount * recoverMultiplier;
currentPosture -= recoverAmount;
currentPosture = FMath::Clamp(currentPosture, 0.f, maxPosture);
UpdateCurrentStats();
float interval = (currentPosture >= maxPosture) ? 0.01f : postureRecoverInterval;
GetWorld()->GetTimerManager().SetTimer(TH_PostureRecoverTimer,this,&UStatComponent::RecoverPostureTick,interval,false);
}
bool UStatComponent::IsPostureBroken() const
{
return currentPosture >= GetMaxPosture();
}
가드브레이크가 터지면 플레이어는 휘청이며 잠시동안 공격 및 가드실행이 불가능해집니다
플레이어의 가드 로직입니다
가드할때 가드 입력시간을 기록하여
해당 입력시간으로 구별됩니다
lastGuardInputTime = GetWorld()->GetTimeSeconds();
구현C++
EGuardType AMainCharacter::CheckGuardResult(float attackTime)
{
if (statComp->currentPosture >= statComp->GetMaxPosture())
return EGuardType::None;
if (!guardMode)
return EGuardType::None;
if (!IsFrontAttack(135.f))
return EGuardType::None;
const float perfectGuardWindow = 0.2f;
if (FMath::Abs(attackTime - lastGuardInputTime) <= perfectGuardWindow)
return EGuardType::Perfect;
return EGuardType::Normal;
}
bool AMainCharacter::IsFrontAttack(float allowedAngleDegress) const
{
float halfAngle = allowedAngleDegress * 0.5f;
return (hitDirection >= -halfAngle && hitDirection <= halfAngle);
}
플레이어가 가드를 하게되면 넉백되는 로직입니다
당연히 애니메이션으로 루트모션이 적용된 애니메이션이 있다면 편리하겠지만
1인개발이라 그런모션따위는 존재하지않았습니다
그래서 모션은 따로 구현해오고 밀리는건 직접 구현하였습니다
구현C++
void AMainCharacter::ReceiveKnockBack(float knockBackStrength, AActor* attacker)
{
FVector knockBackDir = (GetActorLocation() - attacker->GetActorLocation());
knockBackDir.Z = 0.f;
knockBackDir.Normalize();
knockBackStart = GetActorLocation();
knockBackEnd = knockBackStart + knockBackDir * knockBackStrength;
knockBackDuration = 0.25f;
knockBackElapsed = 0.f;
GetWorldTimerManager().SetTimer(TH_KnockBackTimerHandle, this, &AMainCharacter::UpdateKnockBack, 0.01f, true);
}
void AMainCharacter::UpdateKnockBack()
{
knockBackElapsed += 0.005f;
float alpha = FMath::Clamp(knockBackElapsed / knockBackDuration, 0.f, 1.f);
float knockAlpha = FMath::Pow(alpha, 0.5f);
FVector knockNewLoc = FMath::Lerp(knockBackStart, knockBackEnd, knockAlpha);
SetActorLocation(knockNewLoc, true);
if (alpha >= 1.f)
GetWorldTimerManager().ClearTimer(TH_KnockBackTimerHandle);
}
//적이 플레이어를 밀때 사용되는함수
void AEnemyBaseCharacter::ReceiveKnockBack(float knockBackStrength)
{
mainCharacter->ReceiveKnockBack(knockBackStrength, this);
}
void UEnemyBaseAnimInstance::AnimNotify_KnockBackMed()
{
if (!enemyCharacter)
return;
for (AActor* hitActor : enemyCharacter->weapon->alreadyHitActors)
enemyCharacter->ReceiveKnockBack(120.f);
}
넉백을 원하는 프레임구간에 추가하여 쉽게 사용 할 수 있습니다

결과
가드 (일반가드 - 체간게이지 쌓임 / 적의 공격을 막을 수 있음)

패링 (퍼펙트가드 - 체간게이지 쌓이지않음 / 적의 공격 무효화)

가드를 못했을 경우 ( 논가드 - 체간게이지 많이 쌓임 / 데미지를 받음)

1,2타는 가드 3타는 패링인 경우

적도 가드상태일때 플레이어의 공격을 방어할 수 있습니다

적을 때렸을 경우 적의 체간 게이지가 상승합니다

영상
'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - 죽음 / 리스폰 (0) | 2026.01.20 |
|---|---|
| Unreal - 적 공격 간파하기 (2) | 2026.01.16 |
| Unreal - 그래플링 훅 (0) | 2026.01.08 |
| Unreal - 처형( Execution ) (0) | 2026.01.05 |
| Unreal - 회피(Dodge) (0) | 2026.01.03 |