정확한 타이밍에 패링하기
다크 소울류 게임을 보면 패링을 정확한 타이밍에 하면
적의 스테미나를 한번에 많이 감소시킨다던지 등 다양한 효과가 존재한다.
그래서 이번에는 정확한 타이밍에 적의 공격을 막아내면 주변 적들을 넉백 시켜보도록 하겠다.
정확한 타이밍 체크하기
패링을 하는건 우클릭으로 할 수 있다.
그랬을 때, 패링한 시간을 정확히 기억한다.
그리고 적의 공격을 받았고 내가 패링중이라면, 공격받은 타이밍과 기억해둔 시간과의 차이가 0.1초 일 경우에
정확한 타이밍이라고 인식하도록 한다.
void ARASPlayer::PressRightClick()
{
if (CombatState == EPlayerCombatState::Breaking || CombatState == EPlayerCombatState::Armoring)
return;
UAnimInstance* MyAnimInstance = GetMesh()->GetAnimInstance();
if (!MyAnimInstance)
return;
ComboAttack->EndCombo(false, 0.f);
CombatState = EPlayerCombatState::Parrying;
MyAnimInstance->Montage_Play(ParryingMontage);
ParryingTime = RASUtils::GetCurrentPlatformTime();
MyAnimInstance->Montage_JumpToSection(TEXT("Parrying"), ParryingMontage);
}
소수점을 제대로 추출하기 위해 다음과 같이 한다.
static const float GetCurrentPlatformTime()
{
static const double StartTime = FPlatformTime::Seconds();
return static_cast<float>(FPlatformTime::Seconds() - StartTime);
}
0.1초 인지 파악해서 ParryingExact를 실행한다.
void ARASPlayer::HitFromActor(ARASCharacterBase* InFrom, int InDamage)
{
if (CombatState == EPlayerCombatState::Rolling || CombatState == EPlayerCombatState::Armoring)
return;
Super::HitFromActor(InFrom, InDamage);
if (LockOnTarget == nullptr)
SetLockedOnTarget(InFrom);
if (CombatState == EPlayerCombatState::Parrying)
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (!AnimInstance)
return;
float CurrentTime = RASUtils::GetCurrentPlatformTime();
UE_LOG(LogTemp, Log, TEXT("Parrying time diff: %f"), CurrentTime - ParryingTime);
if (CurrentTime - ParryingTime <= 0.1f)
{
AnimInstance->Montage_Play(ParryingMontage);
AnimInstance->Montage_JumpToSection(TEXT("ParryingExact"), ParryingMontage);
CombatState = EPlayerCombatState::Armoring;
FOnMontageEnded MontageEndedDelegate;
MontageEndedDelegate.BindLambda([this, AnimInstance](UAnimMontage* Montage, bool bInterrupted)
{
CombatState = EPlayerCombatState::Idle;
});
AnimInstance->Montage_SetEndDelegate(MontageEndedDelegate, ParryingMontage);
return;
}
}
}
...
}
ParryingExact Notify
적들을 밀쳐내기 위해 몽타주의 노타피이를 이용해 밀쳐내는 모션이 동작하면
적들을 다 밀어내도록 해보자.

KnockBackTrigger라는 노티파이를 생성한다.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Animation/Notify/AnimNotify_KnockBackTrigger.h"
#include "Interface/RASBattleInterface.h"
#include "Utils/RASCollisionChannels.h"
#include "DrawDebugHelpers.h"
#include "Engine/OverlapResult.h"
UAnimNotify_KnockBackTrigger::UAnimNotify_KnockBackTrigger()
{
}
void UAnimNotify_KnockBackTrigger::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
Super::Notify(MeshComp, Animation, EventReference);
AActor* MyActor = MeshComp->GetOwner();
FVector Center = MyActor->GetActorLocation();
const float SphereRadius = 500.f;
TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams CollisionQueryParam(SCENE_QUERY_STAT(KnockBack), false, MyActor);
bool bResult = MyActor->GetWorld()->OverlapMultiByChannel(
OverlapResults,
Center,
FQuat::Identity,
ECC_RASChannel,
FCollisionShape::MakeSphere(SphereRadius),
CollisionQueryParam
);
DrawDebugSphere(MyActor->GetWorld(), Center, SphereRadius, 16, FColor::Green, false, 0.2f);
if (bResult)
{
for (auto& Overlap : OverlapResults)
{
FVector TargetLocation = Overlap.GetActor()->GetActorLocation();
IRASBattleInterface* BattleInterface = Cast<IRASBattleInterface>(Overlap.GetActor());
if (BattleInterface)
{
FVector Direction = TargetLocation - Center;
BattleInterface->KnockbackToDirection(MyActor, Direction, 1800.f);
}
}
}
}
코드는 간단하다. 채널 컬리전을 이용해 주변 적들을 전부 추출한후 추출된 적들을 KnockbackToDirection이라는 함수를 통해 밀쳐내면 된다. 방향은 적의 위치 - 내 위치 를 해서 내가 적을을 바라보는 방향으로 날려주겠다.
이 함수는 다음과 같다.
넉백
void ARASCharacterBase::KnockbackToDirection(class AActor* InFrom, FVector Direction, float InPower)
{
if (!Direction.IsNearlyZero())
{
Direction.Normalize();
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance)
{
AnimInstance->Montage_Play(HitMontage);
AnimInstance->Montage_JumpToSection(TEXT("Knockback"));
AnimInstance->SetRootMotionMode(ERootMotionMode::IgnoreRootMotion);
}
float KnockbackStrength = InPower;
LaunchCharacter(Direction * KnockbackStrength, true, true);
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, [this, AnimInstance]()
{
if (AnimInstance)
{
AnimInstance->SetRootMotionMode(ERootMotionMode::RootMotionFromMontagesOnly);
}
}, 0.2f, false);
}
}
넉백 애니메이션을 실행하고 LaunchCharacter를 통해 방향으로 날려주면 된다.
문제는 루트모션이 재생중이라면 LaunchCharacter 함수가 동작하지 않기 때문에, 일시적으로 RootMotion을 Ignore 처리하고 0.2초 뒤 다시 원래대로 되돌리면 된다.
UI
사용할 UI가 많지는 않다.
플레이어의 상태를 나타내는 UI와 적들의 상태를 나타내는 UI 그리고 락온 했을 때 락온한 몬스터가 무엇인지 나타내는 UI 마지막으로 보스의 상태를 나타내는 UI일 것이다. 보스는 보스를 제작한 후에 UI를 만들도록 하겠다.
락온 UI
적을 락온 했을 때, 적의 몸 부분에 흰색 점을 나타내 표시하도록 하자.
Widget을 만들어준다.

그리고 적들을 한번에 죽일 수 있는 상태가 되면 표현될 에임도 추가한다.

그리고 Widget을 상속받아 클래스를 만든다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "RASAimWidget.generated.h"
/**
*
*/
UCLASS()
class PROJECTRAS_API URASAimWidget : public UUserWidget
{
GENERATED_BODY()
public:
URASAimWidget(const FObjectInitializer& ObjectInitializer);
void SetupAim();
void VisibleAim(bool bVisible);
void VisibleLastAim(bool bVisible);
protected:
UPROPERTY(meta=(BindWidget))
TObjectPtr<class UImage> Aim;
UPROPERTY(meta=(BindWidget))
TObjectPtr<class UImage> Last_Aim;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "UI/RASAimWidget.h"
#include "Components/Image.h"
URASAimWidget::URASAimWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
}
void URASAimWidget::SetupAim()
{
Aim->SetVisibility(ESlateVisibility::Collapsed);
Last_Aim->SetVisibility(ESlateVisibility::Collapsed);
}
void URASAimWidget::VisibleAim(bool bVisible)
{
if(bVisible)
{
Aim->SetVisibility(ESlateVisibility::Visible);
}
else
{
Aim->SetVisibility(ESlateVisibility::Collapsed);
}
}
void URASAimWidget::VisibleLastAim(bool bVisible)
{
if(bVisible)
{
Last_Aim->SetVisibility(ESlateVisibility::Visible);
Aim->SetVisibility(ESlateVisibility::Collapsed);
}
else
{
Last_Aim->SetVisibility(ESlateVisibility::Collapsed);
Aim->SetVisibility(ESlateVisibility::Visible);
}
}
이렇게 해서 원할때 몹에게 접근해서 원하는 Aim을 키고 끄고 할 수 있다.
적 상태 UI
적의 상태는 체력과 스테미나이다.
이는 Progress Bar를 이용해서 구현한다.

적당한 크기의 Bar를 생성하고 색상을 넣어준다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "RASStatusBarWidget.generated.h"
/**
*
*/
UCLASS()
class PROJECTRAS_API URASStatusBarWidget : public UUserWidget
{
GENERATED_BODY()
public:
URASStatusBarWidget(const FObjectInitializer& ObjectInitializer);
void BindHP(class URASStatComponent* InStatComponent);
void BindStamina(class URASStatComponent* InStatComponent);
protected:
UFUNCTION()
void UpdateHp(float InHp);
UFUNCTION()
void UpdateStamina(float InStamina);
TWeakObjectPtr<class URASStatComponent> StatComponent;
UPROPERTY(meta=(BindWidget))
TObjectPtr<class UProgressBar> HP_Bar;
UPROPERTY(meta=(BindWidget))
TObjectPtr<class UProgressBar> Stamina_Bar;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "UI/RASStatusBarWidget.h"
#include "Component/Stat/RASStatComponent.h"
#include "Components/ProgressBar.h"
URASStatusBarWidget::URASStatusBarWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
}
void URASStatusBarWidget::BindHP(class URASStatComponent* InStatComponent)
{
StatComponent = InStatComponent;
if (InStatComponent != nullptr)
{
InStatComponent->OnHpChanged.AddUObject(this, &URASStatusBarWidget::UpdateHp);
}
}
void URASStatusBarWidget::BindStamina(class URASStatComponent* InStatComponent)
{
StatComponent = InStatComponent;
if (InStatComponent != nullptr)
{
InStatComponent->OnStaminaChanged.AddUObject(this, &URASStatusBarWidget::UpdateStamina);
}
}
void URASStatusBarWidget::UpdateHp(float InHp)
{
if (StatComponent->IsValidLowLevel())
{
HP_Bar->SetPercent(InHp / StatComponent->GetMaxHp());
}
}
void URASStatusBarWidget::UpdateStamina(float InStamina)
{
if (StatComponent->IsValidLowLevel())
{
Stamina_Bar->SetPercent(InStamina / StatComponent->GetMaxStamina());
}
}
이렇게 만들어주고 몬스터에게 위젯 컴포넌트를 생성해서 몹에게 부착해준다.
StatusBarWidgetComponent = CreateDefaultSubobject<UWidgetComponent>(TEXT("StatusBar"));
StatusBarWidgetComponent->SetupAttachment(GetMesh());
void ARASCommonMonster::PostInitializeComponents()
{
Super::PostInitializeComponents();
StatusBarWidgetComponent->InitWidget();
auto Widget = Cast<URASStatusBarWidget>(StatusBarWidgetComponent->GetUserWidgetObject());
if (Widget == nullptr) return;
Widget->BindHP(Stat);
Widget->BindStamina(Stat);
Stat->SetHp(100000);
Stat->SetStamina(100000);
}
이렇게 Bind 해 놓으면 체력과 스테미나가 변동되었을 때, 알아서 값이 수정될 것이다.
플레이어 상태 UI
플레이어는 추후 추가될 아이템과 스킬을 포함한 체력과 스테미나가 표현되어야 한다.
이는 언리얼 무료 에셋을 가져와 제작하겠다.

대충 배치는 이렇게 해준다.
그리고 애니메이션을 통해 맨 처음 UI가 생성되었을 때 입체감 있게 해준다.

이제 코드로 가보자.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "RASPlayerHUDWidget.generated.h"
/**
*
*/
UCLASS()
class PROJECTRAS_API URASPlayerHUDWidget : public UUserWidget
{
GENERATED_BODY()
public:
URASPlayerHUDWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeConstruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
void BindHP(class URASStatComponent* InStatComponent);
void BindStamina(class URASStatComponent* InStatComponent);
protected:
UFUNCTION()
void UpdateHp(float InHp);
UFUNCTION()
void UpdateStamina(float InStamina);
protected:
float TargetHpPercentage;
float TargetStaminaPercentage;
TWeakObjectPtr<class URASStatComponent> StatComponent;
UPROPERTY(meta=(BindWidget))
TObjectPtr<class UImage> HpOrb;
UPROPERTY(meta=(BindWidget))
TObjectPtr<class UImage> StaminaOrb;
UPROPERTY(Meta = (BindWidgetAnim), Transient)
TObjectPtr<class UWidgetAnimation> UI_Start;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "UI/RASPlayerHUDWidget.h"
#include "Component/Stat/RASStatComponent.h"
#include "Components/ProgressBar.h"
#include "Components/Image.h"
#include "UMG.h"
URASPlayerHUDWidget::URASPlayerHUDWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
}
void URASPlayerHUDWidget::NativeConstruct()
{
PlayAnimation(UI_Start, 0.f, 1, EUMGSequencePlayMode::Reverse, 2.f);
}
void URASPlayerHUDWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
float CurrentHpPercentage = 0;
float CurrentStaminaPercentage = 0;
float IntervalTime = 5.f;
HpOrb->GetDynamicMaterial()->GetScalarParameterValue(TEXT("Percentage"), CurrentHpPercentage);
StaminaOrb->GetDynamicMaterial()->GetScalarParameterValue(TEXT("Percentage"), CurrentStaminaPercentage);
if (CurrentHpPercentage != TargetHpPercentage)
{
CurrentHpPercentage = FMath::FInterpTo(CurrentHpPercentage, TargetHpPercentage, InDeltaTime, IntervalTime);
if (HpOrb && HpOrb->GetDynamicMaterial())
{
HpOrb->GetDynamicMaterial()->SetScalarParameterValue(TEXT("Percentage"), CurrentHpPercentage);
}
}
if (CurrentStaminaPercentage != TargetStaminaPercentage)
{
CurrentStaminaPercentage = FMath::FInterpTo(CurrentStaminaPercentage, TargetStaminaPercentage, InDeltaTime, IntervalTime);
if (StaminaOrb && StaminaOrb->GetDynamicMaterial())
{
StaminaOrb->GetDynamicMaterial()->SetScalarParameterValue(TEXT("Percentage"), CurrentStaminaPercentage);
}
}
}
void URASPlayerHUDWidget::BindHP(class URASStatComponent* InStatComponent)
{
StatComponent = InStatComponent;
if (InStatComponent != nullptr)
{
InStatComponent->OnHpChanged.AddUObject(this, &URASPlayerHUDWidget::UpdateHp);
}
}
void URASPlayerHUDWidget::BindStamina(class URASStatComponent* InStatComponent)
{
StatComponent = InStatComponent;
if (InStatComponent != nullptr)
{
InStatComponent->OnStaminaChanged.AddUObject(this, &URASPlayerHUDWidget::UpdateStamina);
}
}
void URASPlayerHUDWidget::UpdateHp(float InHp)
{
if (StatComponent->IsValidLowLevel())
{
TargetHpPercentage = InHp / StatComponent->GetMaxHp();
}
}
void URASPlayerHUDWidget::UpdateStamina(float InStamina)
{
if (StatComponent->IsValidLowLevel())
{
TargetStaminaPercentage = InStamina / StatComponent->GetMaxStamina();
}
}
이 역시 바인딩을 통해 자동으로 값이 갱신 되지만 Orb는 Material Instance이기 때문에 여기서 Percentage 값을 가져와 수정하므로써 값을 채울 수 있다.
결과
'Unreal Engine 5 > ProjectRAS' 카테고리의 다른 글
| [UE5] ProjectRAS - 맵 디자인 및 블프화 (0) | 2025.04.16 |
|---|---|
| [UE5] ProjectRAS - 스테미나 추가, 패링 성공 시 Blur, 캐릭터 죽음 (0) | 2025.04.02 |
| [UE5] ProjectRAS - 전투 공격 판정 과 패링, 적 타겟팅 시스템(락온) (0) | 2025.03.12 |
| [UE5] ProjectRAS - 시점 고정과 일반 몬스터 (0) | 2025.03.06 |
| [UE5] ProjectRAS - 콤보 공격 과 Rolling (0) | 2025.02.19 |