언리얼 엔진 5/액션RPG 게임

[UE5] 시네마틱 보스배틀 게임 개발일지 (4) - 보스 공격 구현(GAS)

ciel45 2025. 9. 3. 18:28

이번엔 적의 공격을 구현해보았다.

 

원래는 그냥 애니메이션 몽타주 여러개를 준비해놓고 재생만 하게 하려고 했는데, 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