UE 5 스터디/Gameplay Ability System(GAS)

32-14. 로그 라이크 - (3) 게임플레이 어빌리티 - (5) 화염 폭풍 스펠

Crat3 2025. 6. 27. 15:03

0. 개요

범위를 지정하여 사용하면 적을 추적함과 동시에 적을 빨아들이며 도트(DoT ; Damage over Time) 데미지를 가하는 화염 폭풍 스펠을 설계한다.

 

 

1. 게임플레이 어빌리티 기초 설계

(1) Native 게임플레이 태그 생성

	FGameplayTag Abilities_Fire_Firenado;
	GameplayTags.Abilities_Fire_Firenado = UGameplayTagsManager::Get().AddNativeGameplayTag(
		FName("Abilities.Fire.Firenado"),
		FString("Firenado Ability")
	);

 

 

(2) 스펠 메뉴에 어빌리티 태그 할당

 

 

(3) DA_AbilityInfo에 데이터 추가

 

 

 

(4) 애님 몽타주 생성 및 모션워핑 노티파이, 게임플레이 큐 노티파이 추가

 

 

 

2. 파이어 토네이도 액터

(1) C++ 코드

class UCapsuleComponent;

UCLASS()
class AURA_API AAuraFireTornado : public AActor
{
	GENERATED_BODY()

	AAuraFireTornado();

protected:
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaSeconds) override;
	virtual void Destroyed() override;

public:
	void SetDamageDeltaSecond(float InTime) {DamageDeltaSecond = InTime;}
	void SetDamageRadius(float InDamageRadius) {DamageRadius = InDamageRadius;}
	void SetFollowRadius(float InFollowRadius) {FollowRadius = InFollowRadius;}

protected:
	UFUNCTION()
	void ApplyDamageAndKnockback();
	
	bool IsValidOverlap(AActor* OtherActor);

public:
	UPROPERTY(BlueprintReadWrite, meta=(ExposeOnSpawn = true))
	FDamageEffectParams DamageEffectParams;
	
protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	TObjectPtr<UCapsuleComponent> Capsule;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr<UStaticMeshComponent> Mesh;
	
	UPROPERTY(VisibleAnywhere)
	TObjectPtr<UAudioComponent> LoopingSoundComponent;
	
private:
	UPROPERTY(EditDefaultsOnly)
	float LifeSpan = 15.f;
	
	UPROPERTY(EditDefaultsOnly)
	float MovementSpeed = 1.f;
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(AllowPrivateAccess=true))
	float DamageDeltaSecond = 0.2f;

	UPROPERTY(EditDefaultsOnly)
	float FollowRadius = 600.f;
	
	UPROPERTY(EditDefaultsOnly)
	float DamageRadius = 300.f;

	UPROPERTY(EditDefaultsOnly)
	float SpinDegreePerSecond = -360.f;
	
	UPROPERTY(EditAnywhere)
	TObjectPtr<USoundBase> LoopingSound;

	UPROPERTY(BlueprintReadWrite, meta=(AllowPrivateAccess = true))
	TArray<AActor*> OverlappingActors;
};

데미지 로직이 액터 내에 위치하기 때문에, 적을 끌어오는 범위, 유지 시간, 데미지 범위를 외부에서(게임플레이 어빌리티에서) 설정할 수 있도록 해야 한다.

 

(1-1) 생성자 / 비긴 플레이 / 파괴

AAuraFireTornado::AAuraFireTornado()
{
	PrimaryActorTick.bCanEverTick = false;

	Mesh = CreateDefaultSubobject<UStaticMeshComponent>("Mesh");
	SetRootComponent(Mesh);

	Capsule = CreateDefaultSubobject<UCapsuleComponent>("Capsule");
	
	Capsule->SetCollisionObjectType(ECC_Projectile);
	Capsule->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	Capsule->SetCollisionResponseToAllChannels(ECR_Ignore);
	Capsule->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
	Capsule->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Overlap);
	Capsule->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
	Capsule->SetupAttachment(GetRootComponent());
}

void AAuraFireTornado::BeginPlay()
{
	Super::BeginPlay();
	SetLifeSpan(LifeSpan);
	SetReplicateMovement(true);
	
	LoopingSoundComponent = UGameplayStatics::SpawnSoundAttached(LoopingSound, GetRootComponent());
}

컴포넌트에 대한 기본 설정을 한다.

스태틱 메시를 루트로 지정하고 캡슐을 하위로 지정하여 메시 크기에 따라 캡슐도 유동적으로 스케일링되도록 한다.

 

void AAuraFireTornado::Destroyed()
{
	if (LoopingSoundComponent)
	{
		LoopingSoundComponent->Stop();
		LoopingSoundComponent->DestroyComponent();
	}
	
	Super::Destroyed();
}

 

 

(1-2) 틱 - 적을 향해 추적하여 따라가기

void AAuraFireTornado::Tick(float DeltaSeconds)
{
    Super::Tick(DeltaSeconds);

    FRotator DeltaRotation = FRotator(0.f, SpinDegreePerSecond * DeltaSeconds, 0.f);
    AddActorWorldRotation(DeltaRotation);

    if (!OverlappingActors.IsEmpty())
    {
       AActor* TargetActor = OverlappingActors.Last();

       if (!IsValid(TargetActor))
          return;

       FVector TornadoLocation = GetActorLocation();
       FVector ActorLocation = TargetActor->GetActorLocation();

       ActorLocation.Z = TornadoLocation.Z;

       FVector NewLocation = FMath::VInterpTo(TornadoLocation, ActorLocation, DeltaSeconds, MovementSpeed);
       SetActorLocation(NewLocation, true);
    }
}

데미지를 주는 함수 내에서 획득한 Overlapping Actors 배열을 이용하여 가장 외곽에 있는 적을 추적하게 한다.

선형 보간을 이용하여 서서히 움직이게 한다.

 

 

(1-3) 데미지, 넉백 함수

void AAuraFireTornado::ApplyDamageAndKnockback()
{
	OverlappingActors.Empty();
	
	TArray<AActor*> IgnoreActors;
	IgnoreActors.AddUnique(GetOwner());
	
	UAuraAbilitySystemLibrary::GetLivePlayersWithinRadius(
		this,
		OverlappingActors,
		IgnoreActors,
		FollowRadius,
		GetActorLocation());

	// 1. 중앙으로 끌어 당김
	// 2. 틱 데미지 부여
	// 3. 범위 밖의 액터를 향해 느리게 움직임
	
	for (AActor* OtherActor : OverlappingActors)
	{
		if (IsValidOverlap(OtherActor) == false)
			continue;

		if (HasAuthority())
		{
			if (auto* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
			{
				bool bIsKnockBack = true;
				
				// 벡터를 구하고 정규화
				FVector ToCenter = GetActorLocation() - OtherActor->GetActorLocation();
				ToCenter.Z = GetActorLocation().Z - Capsule->GetScaledCapsuleHalfHeight();
				ToCenter.Normalize();
				
				// 보스는 넉백 무효
				if (OtherActor->ActorHasTag(FName("Boss")))
				{
					bIsKnockBack = false;
					DamageEffectParams.KnockbackChance = 0.f;
				}

				if (bIsKnockBack)
				{
					const FVector KnockbackDirection = ToCenter;
					const FVector KnockbackForce = KnockbackDirection * DamageEffectParams.KnockbackForceMagnitude;
					DamageEffectParams.KnockbackForce = KnockbackForce;
				}

				FVector DistanceVector = (OtherActor->GetActorLocation()-GetActorLocation());
				float Distance = DistanceVector.Length();
				
				if (Distance <= DamageRadius)
				{
					const FVector DeathImpulse = ToCenter * DamageEffectParams.DeathImpulseMagnitude;
					DamageEffectParams.DeathImpulse = DeathImpulse;
					DamageEffectParams.TargetAbilitySystemComponent = TargetASC;
				
					UAuraAbilitySystemLibrary::ApplyDamageEffect(DamageEffectParams);
				}
			}
		}
	}
}

- 파이어 토네이도 액터 주위로 '견인 범위'를 검사하여 겹치는 액터를 반환한다.

- 보스가 아닌 적을 대상으로 파이어 토네이도 액터 중심으로 넉백 벡터를 지정한다.

- 파이어 토네이도 액터와 대상 사이의 거리를 체크하여 '데미지 범위' 내에 있으면 데미지 게임플레이 이펙트를 적용한다.

 

 

(1-4) 겹침 판정 검증 함수

bool AAuraFireTornado::IsValidOverlap(AActor* OtherActor)
{
	if (DamageEffectParams.SourceAbilitySystemComponent == nullptr)
		return false;

	AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();

	if (SourceAvatarActor == OtherActor)
		return false;

	if (!UAuraAbilitySystemLibrary::IsNotFriend(SourceAvatarActor, OtherActor))
		return false;

	return true;
}

본인과 아군 대상으로는 데미지를 입힐 수 없도록 함수를 구성한다.

 

 

(1-5) 타이머 설정

	FTimerHandle TimerHandle;
void AAuraFireTornado::BeginPlay()
{
	Super::BeginPlay();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// 타이머 설정
	FTimerDelegate TimerDelegate;
	TimerDelegate.BindUFunction(this, FName("ApplyDamageAndKnockback"));

	GetWorldTimerManager().SetTimer(
		TimerHandle,
		TimerDelegate,
		DamageDeltaSecond,
		true
		);
}

데미지 델타 초마다 콜백 함수가 호출되도록 지정한다.

 

 

(2) 블루프린트 생성

시간에 따라 제자리 회전하도록 할 것이다.

메시를 추가하고 머티리얼을 반투명한 재질로 변경한다. 이후 나이아가라 시스템을 추가하고 적용한다.

 

 

3. 화염 폭풍 게임플레이 어빌리티

(1) C++ 코드

class AAuraFireTornado;

UCLASS()
class AURA_API UAuraFirenado : public UAuraDamageGameplayAbility
{
	GENERATED_BODY()

public:
	virtual FString GetDescription(int32 Level, const UObject* WorldContextObject);
	virtual FString GetNextLevelDescription(int32 Level, const UObject* WorldContextObject);

	UFUNCTION(BlueprintCallable)
	AAuraFireTornado* SpawnTornado();

	UFUNCTION(BlueprintCallable)
	AAuraFireTornado* SpawnTornadoToLocation(const FVector& Location);
	
private:
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<AAuraFireTornado> FireTornadoClass;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(AllowPrivateAccess=true))
	float DamageDeltaSecond = 0.2f;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(AllowPrivateAccess=true))
	float SpawnTime = 5.f;
	
	UPROPERTY(EditDefaultsOnly)
	float FollowRadius = 600.f;
	
	UPROPERTY(EditDefaultsOnly)
	float DamageRadius = 300.f;
};

 

 

(1-1) 스펠 설명

FString UAuraFirenado::GetDescription(int32 Level, const UObject* WorldContextObject)
{
	const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
	const float ManaCost = FMath::Abs(GetManaCost(Level));
	const float Cooldown = GetCoolDown(Level);
	const float MagicAttackPower = UAuraAbilitySystemLibrary::GetAttributeValue(WorldContextObject, FAuraGameplayTags::Get().Attributes_Secondary_MagicAttackPower);
	const int32 MagicPowerDamage = MagicAttackPower * MagicPowerCoefficient.GetValue();
	
	return FString::Printf(TEXT(
		"<Title>화염 폭풍</>\n<Small>레벨 </><Level>%d</>\n<Small>마나 </><ManaCost>%.1f</>\n<Small>쿨타임 </><Cooldown>%.1f</>\n<Default>해당 범위에 총 </><Damage>%d</><Default>의 피해를 입히는 화염 기둥을 </><Num>%.1f초</><Default> 동안 소환합니다.</>\n<Small>최대 %.f의 거리에 있는 적을 추적하며, %.f 거리 내의 적에게 데미지를 입힙니다.</>"),
		Level,
		ManaCost,
		Cooldown,
		ScaledDamage + MagicPowerDamage,
		SpawnTime,
		FollowRadius,
		DamageRadius
	);
}

FString UAuraFirenado::GetNextLevelDescription(int32 Level, const UObject* WorldContextObject)
{
	const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
	const float ManaCost = FMath::Abs(GetManaCost(Level));
	const float Cooldown = GetCoolDown(Level);
	const float MagicAttackPower = UAuraAbilitySystemLibrary::GetAttributeValue(WorldContextObject, FAuraGameplayTags::Get().Attributes_Secondary_MagicAttackPower);
	const int32 MagicPowerDamage = MagicAttackPower * MagicPowerCoefficient.GetValue();
	
	return FString::Printf(TEXT(
		"<Title>다음 레벨: </>\n<Small>레벨 </><Level>%d</>\n<Small>마나 </><ManaCost>%.1f</>\n<Small>쿨타임 </><Cooldown>%.1f</>\n<Default>해당 범위에 총 </><Damage>%d</><Default>의 피해를 입히는 화염 기둥을 </><Num>%.1f초</><Default> 동안 소환합니다.</>\n<Small>최대 %.f의 거리에 있는 적을 추적하며, %.f 거리 내의 적에게 데미지를 입힙니다.</>"),
		Level,
		ManaCost,
		Cooldown,
		ScaledDamage + MagicPowerDamage,
		SpawnTime,
		FollowRadius,
		DamageRadius
	);
}

 

 

(1-2) 파이어 토네이도 위치에 소환하는 함수

AAuraFireTornado* UAuraFirenado::SpawnTornadoToLocation(const FVector& Location)
{
    FTransform SpawnTransform;
    SpawnTransform.SetLocation(Location);

    // 월드에 파이어볼 생성
    AAuraFireTornado* Tornado = GetWorld()->SpawnActorDeferred<AAuraFireTornado>(
       FireTornadoClass,
       SpawnTransform,
       GetOwningActorFromActorInfo(),
       CurrentActorInfo->PlayerController->GetPawn(),
       ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

    Tornado->SetDamageDeltaSecond(DamageDeltaSecond);
    Tornado->SetFollowRadius(FollowRadius);
    Tornado->SetDamageRadius(DamageRadius);
    
    Tornado->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
    Tornado->SetOwner(GetAvatarActorFromActorInfo());

    Tornado->FinishSpawning(SpawnTransform);

    return Tornado;
}

 

 

4. 게임플레이 어빌리티 블루프린트

- 클래스 디폴트

파이어 토네이도 클래스를 지정하고 값을 작성한다.

범위 지정 데칼을 사용하기 때문에 Activation Owned Tags에 WaitForExecute 태그를 추가한다.

 

 

(1) 범위 지정 데칼 사용 및 오토 런 중지

위의 게임플레이 큐를 적용하면 DA_MessageInfo를 통해 등록된 메시지가 출력된다.

 

 

(2) 마우스 액션 검증

마우스 입력, 혹은 어빌리티가 할당된 키가 눌리면 마우스 위치를 저장하고 모션 워핑을 시작한다.

애님 몽타주를 실행한다.

 

 

(3) 노티파이 대기 - 토네이도 소환

몽타주 내의 노티파이를 대기하는 노드를 추가하고, 마우스 위치에 토네이도를 소환한다.

 

 

(4) 토네이도 위치 조정, 소환 시간 설정, 이펙트 출력, 어빌리티 종료

메인 로직을 설정한다.

 

 

(5) EndAbility

어빌리티가 종료될 때 다시 한 번 캐릭터의 오토 런을 중단하고 타이머 핸들을 초기화한다.