32-14. 로그 라이크 - (3) 게임플레이 어빌리티 - (5) 화염 폭풍 스펠
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
어빌리티가 종료될 때 다시 한 번 캐릭터의 오토 런을 중단하고 타이머 핸들을 초기화한다.