이번엔 적의 공격을 구현해보았다.
원래는 그냥 애니메이션 몽타주 여러개를 준비해놓고 재생만 하게 하려고 했는데, GAS에 대해 조사해보니 유용할 것 같다는 생각이 들었다.
처음엔 대형 RPG 게임도 아니고, 단순히 보스전 하나만 있는 게임에서 GAS를 사용하는것은 오버엔지니어링이 아닐까 하는 생각이 들었지만, 본 프로젝트의 GAS 사용의 이점은 다음과 같았다.
- UGameplayAbility_Montage 클래스를 통한 애니메이션 몽타주 관련 작업의 용이성
- 해당 클래스는 UGameplayAbility의 자식 클래스로, 멤버 변수인 AnimMontage를 적절히 등록해주고 ActivateAbility 함수만 호출해주면, 알아서 몽타주를 재생시켜준다.
- 본 프로젝트에서 모든 액션은 애니메이션 몽타주를 기반으로 동작할 것이기에, 단순 재생에 더불어 다양한 부가 기능을 제공해주는 UGameplayAbility_Montage를 쓰지 않을 이유가 없다고 생각했다.
- 공격의 쿨타임 관리, 페이즈 별 사용 가능 공격 관리
- 강력한 공격일 수록 쿨타임을 길게, 약한 공격일 수록 쿨타임을 짧게하는 작업이 용이하다.
- 2페이즈, 3페이즈에서 사용가능한 기술이 늘어난다고 가정할 때, 해당 사항들을 관리하는 작업 역시 용이하다.
- 태그 시스템을 활용해서 어떤 공격은 Phase2 태그가 있을 때만 사용가능하도록 만든다던가
- 스태미나와 같은 자원의 소모를 간편하게 처리 가능
이러한 장점들을 감안하여 GAS를 사용하기로 했다.
작성한 코드는 다음과 같다.
// BossCharacter.h
class TITAN_API ABossCharacter : public ACharacter, public IAbilitySystemInterface
{
//...
UFUNCTION(Category = "Abilites|Melee")
bool ActivateMeleeAbility(bool AllowRemoteActivation = true);
//...
protected:
//...
virtual void SetMeleeAbility();
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "Abilities")
UAbilitySystemComponent* AbilitySystemComponent;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Abilities|Melee")
TSubclassOf<class UGameplayAbility> MeleeAbility;
UPROPERTY()
FGameplayAbilitySpecHandle MeleeAbilitySpecHandle;
//...
};
// BossCharacter.cpp
//...
#include "AbilitySystemComponent.h"
// Sets default values
ABossCharacter::ABossCharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UBossMovementComponent>(ACharacter::CharacterMovementComponentName)) // 커스텀 MovementComponent 사용
{
//...
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
}
// Called when the game starts or when spawned
void ABossCharacter::BeginPlay()
{
Super::BeginPlay();
SetMeleeAbility();
}
//...
void ABossCharacter::SetMeleeAbility()
{
if (!AbilitySystemComponent)
return;
MeleeAbilitySpecHandle = AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(MeleeAbility));
}
bool ABossCharacter::ActivateMeleeAbility(bool AllowRemoteActivation)
{
if (!AbilitySystemComponent || !MeleeAbilitySpecHandle.IsValid())
{
return false;
}
return AbilitySystemComponent->TryActivateAbility(MeleeAbilitySpecHandle);
}
AbilitySystemComponent, 사용할 Ability를 만들어 세팅해주었다.
이제 ActivateMeleeAbility를 호출하는 것으로 밀리 공격이 나가게 할 것이다.
어빌리티 클래스를 만들기 전에, 활용할 UGameplayAbility_Montage 클래스를 보면 다음과 같다.
// GameplayAbility_Montage.h
* A gameplay ability that plays a single montage and applies a GameplayEffect
*/
UCLASS(MinimalAPI)
class UGameplayAbility_Montage : public UGameplayAbility
{
//...
UE_API virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* OwnerInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
UPROPERTY(EditDefaultsOnly, Category = MontageAbility)
TObjectPtr<UAnimMontage> MontageToPlay;
UPROPERTY(EditDefaultsOnly, Category = MontageAbility)
float PlayRate;
UPROPERTY(EditDefaultsOnly, Category = MontageAbility)
FName SectionName;
/** GameplayEffects to apply and then remove while the animation is playing */
UPROPERTY(EditDefaultsOnly, Category = MontageAbility)
TArray<TSubclassOf<UGameplayEffect>> GameplayEffectClassesWhileAnimating;
/** Deprecated. Use GameplayEffectClassesWhileAnimating instead. */
UPROPERTY(VisibleDefaultsOnly, Category = Deprecated)
TArray<TObjectPtr<const UGameplayEffect>> GameplayEffectsWhileAnimating;
UE_API void OnMontageEnded(UAnimMontage* Montage, bool bInterrupted, TWeakObjectPtr<UAbilitySystemComponent> AbilitySystemComponent, TArray<struct FActiveGameplayEffectHandle> AppliedEffects);
UE_API void GetGameplayEffectsWhileAnimating(TArray<const UGameplayEffect*>& OutEffects) const;
};
#undef UE_API
void UGameplayAbility_Montage::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
return;
}
UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
if (MontageToPlay != nullptr && AnimInstance != nullptr && AnimInstance->GetActiveMontageInstance() == nullptr)
{
TArray<FActiveGameplayEffectHandle> AppliedEffects;
// Apply GameplayEffects
TArray<const UGameplayEffect*> Effects;
GetGameplayEffectsWhileAnimating(Effects);
if (Effects.Num() > 0)
{
UAbilitySystemComponent* const AbilitySystemComponent = ActorInfo->AbilitySystemComponent.Get();
for (const UGameplayEffect* Effect : Effects)
{
FActiveGameplayEffectHandle EffectHandle = AbilitySystemComponent->ApplyGameplayEffectToSelf(Effect, 1.f, MakeEffectContext(Handle, ActorInfo));
if (EffectHandle.IsValid())
{
AppliedEffects.Add(EffectHandle);
}
}
}
float const Duration = AnimInstance->Montage_Play(MontageToPlay, PlayRate);
FOnMontageEnded EndDelegate;
EndDelegate.BindUObject(this, &UGameplayAbility_Montage::OnMontageEnded, ActorInfo->AbilitySystemComponent, AppliedEffects);
AnimInstance->Montage_SetEndDelegate(EndDelegate);
if (SectionName != NAME_None)
{
AnimInstance->Montage_JumpToSection(SectionName);
}
}
}
상술했듯 멤버변수로서 AnimMontage, SectionName, PlayRate가 이미 준비되어있고, ActivateAbility 함수에서는 이것들을 고려해 몽타주를 알아서 재생시켜준다.
이걸 토대로 작성한 어빌리티 클래스는 다음과 같다.
// GA_RampageMelee.h
UCLASS()
class TITAN_API UGA_RampageMelee : public UGameplayAbility_Montage
{
GENERATED_BODY()
public:
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
};
// GA_RampageMelee.cpp
#include "GA_RampageMelee.h"
#include "Titan/Utils/Debug.h"
void UGA_RampageMelee::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
int32 SectionName_Int = FMath::RandRange(0, 1);
FString SectionName_Str = FString::FromInt(SectionName_Int);
SectionName = FName(*SectionName_Str);
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
CommitAbility(Handle, ActorInfo, ActivationInfo);
}
상술했듯이, UGameplayAbility_Montage 클래스가 몽타주 재생과 같은 귀찮은 일들은 전부 담당해주기 때문에 코드가 길 필요가 없었다.
이제 저 클래스를 상속받은 BPGA_RampageMelee를 만들어, 애니메이션 몽타주를 넣어주고, 그 BPGA_RampageMelee를 MeleeAbility로서 등록해주었다.



아직 히트박스 구현, 데미지 판정 등 할게 많지만, 공격 애니메이션은 잘 나와주는걸 볼 수 있었다.
작업 내용: https://github.com/sys010611/Titan/commit/e846a7636928c5b989df995bf6a4a5decf7d2218
보스 공격 구현 · sys010611/Titan@e846a76
github.com
'언리얼 엔진 5 > 액션RPG 게임' 카테고리의 다른 글
| [UE5] 시네마틱 보스배틀 게임 개발일지 (6) - 리팩토링, 데미지 시스템 구현 (0) | 2025.09.28 |
|---|---|
| [UE5] 시네마틱 보스배틀 게임 개발일지 (5) - 플레이어 준비 + 콤보 공격 구현(모션 매칭, GAS) (1) | 2025.09.25 |
| [UE5] 시네마틱 보스배틀 게임 개발일지 (3) - 보스 움직임 구현(BT + EQS) (2) | 2025.08.30 |
| [UE5] 시네마틱 보스배틀 게임 개발일지 (2) - 레벨, 보스 에셋 준비 (0) | 2025.08.27 |
| [UE5] 시네마틱 보스배틀 게임 개발일지 (1) - 시작 (0) | 2025.08.26 |