이펙트를 액터 컴포넌트에서 ObjectPool로 관리하는 기능을 구현했습니다.
이펙트를 액터를 사용하여 구현할 필요는 없지만, 나중에 구현할 TimeStop 기능에서 GlobalTimeDilation 대신 액터의 CustomTimeDilation을 사용할 수 있도록 하기 위해서 액터와 이를 관리할 액터 컴포넌트를 사용하여 구현하였습니다.
기능 설명
- DataTable의 정보를 기반으로 이펙트를 생성합니다. 이때 생성된 이펙트는 초기에 비활성화 상태입니다.
- EffectSystem의 EffectPool에서 특정 Key를 사용하여 해당 이펙트의 Pool을 찾습니다. 이때, Pool이 존재하지 않는 경우 동적으로 Pool을 생성합니다.
- 이펙트의 Pool에서 활성화할 수 있는 이펙트를 검색합니다. 활성화 가능한 이펙트가 있는 경우 해당 이펙트를 활성화시킵니다. 만약 활성화 가능한 이펙트가 없는 경우에는 새로운 이펙트를 동적으로 생성하여 활성화합니다.
- 활성화된 이펙트의 수명이 다한 경우 해당 이펙트를 비활성화합니다.
- 동적으로 생성된 이펙트는 수명이 다하면 비활성화되고, 이후에 일정 시간이 지나면 제거됩니다.
부모 이펙트 액터 클래스
부모 이펙트 액터 초기화
// APREffect.h
/**
* 이펙트를 초기화하는 함수입니다.
*
* @param NewEffectOwner 이펙트의 소유자
* @param NewPoolIndex 이펙트 풀의 Index
* @param NewLifespan 이펙트의 수명
*/
UFUNCTION(BlueprintCallable, Category = "PREffect")
virtual void InitializeEffect(AActor* NewEffectOwner = nullptr, int32 NewPoolIndex= -1, float NewLifespan = 0.0f);
// APREffect.cpp
void APREffect::InitializeEffect(AActor* NewEffectOwner, int32 NewPoolIndex, float NewLifespan)
{
// 이펙트를 비활성화 상태로 설정합니다.
bActivate = false;
SetActorHiddenInGame(!bActivate);
EffectOwner = NewEffectOwner;
PoolIndex = NewPoolIndex;
EffectLifespan = NewLifespan;
// 이펙트에 설정된 모든 타이머를 초기화합니다.
GetWorldTimerManager().ClearAllTimersForObject(this);
// 비활성화 델리게이트에 바인딩된 함수를 제거합니다.
OnEffectDeactivateDelegate.Clear();
}
이펙트 액터가 월드에 Spawn되면 초기화 함수를 호출하여 이펙트 액터를 비활성화 상태로 설정합니다. 비활성화 상태로 설정된 이펙트 액터는 SetActorHiddenInGame 함수를 사용하여 게임에서 숨깁니다. 이후, EffectOwner(이펙트를 사용하는 액터), PoolIndex(EffectSystem의 EffectPool에서 사용하는 Index), EffectLifespan(이펙트 액터의 수명)을 설정합니다. 설정 후, 이펙트 액터에 설정된 모든 타이머를 초기화하며, 이펙트 액터를 비활성화할 때 실행하는 델리게이트에 바인딩된 함수를 제거합니다.
부모 이펙트 액터 활성화 / 비활성화
void APREffect::Activate()
{
bActivate = true;
SetActorHiddenInGame(!bActivate);
// 이펙트의 수명을 설정합니다. 이펙트의 수명이 끝나면 이펙트를 비활성화합니다.
SetEffectLifespan(EffectLifespan);
}
void APREffect::Deactivate()
{
bActivate = false;
SetActorHiddenInGame(!bActivate);
// 이펙트에 설정된 모든 타이머를 초기화합니다.
GetWorldTimerManager().ClearAllTimersForObject(this);
// 비활성화 델리게이트를 호출합니다.
OnEffectDeactivateDelegate.Broadcast(this);
}
이펙트 액터를 활성화 함수는 SetActorHiddenInGame 함수를 사용하여 이펙트 액터를 게임에서 보이게 합니다. 보이게 한 이펙트 액터의 수명을 설정하며, 수명을 설정하는 동안 타이머도 같이 설정합니다.
이펙트 액터 비활성화 함수는 SetActorHiddenInGame 함수를 사용하여 이펙트 액터를 게임에서 숨깁니다. 이펙트 액터에 설정된 모든 타이머를 초기화하고 비활성화 델리게이트를 호출합니다.
두 함수 모두 자식 이펙트 액터 클래스에서 오버라이딩하여 사용합니다.
// APREffect.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEffectDeactivate, APREffect*, Effect);
public:
/** 이펙트가 비활성화될 때 실행하는 델리게이트입니다. */
FOnEffectDeactivate OnEffectDeactivateDelegate;
비활성화 델리게이트는 부모 이펙트 클래스를 인자로 받습니다.
이펙트 액터 수명 설정
// APREffect.cpp
void APREffect::SetEffectLifespan(float NewLifespan)
{
EffectLifespan = NewLifespan;
if(bActivate)
{
if(NewLifespan > 0.0f)
{
// 수명이 0보다 클 경우, 즉 새로운 수명이 설정된 경우 타이머를 설정합니다.
GetWorldTimerManager().SetTimer(EffectLifespanTimerHandle, this, &APREffect::Deactivate, NewLifespan);
}
else
{
// 수명이 0보다 작거나 같을 경우, 즉 이펙트의 수명이 무한대인 경우 이전에 설정한 타이머를 지워 제한된 수명을 가지지 않게 합니다.
GetWorldTimerManager().ClearTimer(EffectLifespanTimerHandle);
}
}
}
수명이 0.0f보다 클 경우, 즉 새로운 수명이 설정된 경우 타이머를 설정하여 수명이 다할 경우 이펙트 액터를 비활성화하는 함수를 실행합니다. 수명이 0.0f 보다 작을 경우, 즉 이펙트의 수명이 무한대인 경우 이전에 설정한 타이머를 제거하여 제한된 수명을 가지지 않게 합니다.
자식 이펙트(NiagaraEffect, ParticleEffect) 액터 클래스
부모 이펙트 액터 클래스를 상속하는 자식 이펙트 액터 클래스는 사용하는 이펙트에 따라 NiagaraEffect 액터 클래스와 ParticleEffect 액터 클래스로 구분하여 사용합니다.
// APRNiagaraEffect.h
private:
/** Spawn한 NiagaraEffect입니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PRNiagaraEffect", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UNiagaraComponent> NiagaraEffect;
// APRNiagaraEffect.cpp
APRNiagaraEffect::APRNiagaraEffect()
{
NiagaraEffect = CreateDefaultSubobject<UNiagaraComponent>(TEXT("NiagaraEffect"));
SetRootComponent(NiagaraEffect);
}
// APRParticleEffect.h
private:
/** Spawn한 ParticleEffect입니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PRParticleEffect", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UParticleSystemComponent> ParticleEffect;
// APRParticleEffect.cpp
APRParticleEffect::APRParticleEffect()
{
ParticleEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("ParticleEffect"));
SetRootComponent(ParticleEffect);
}
NiagaraEffect 액터는 NiagaraComponent를 RootComponent로 설정합니다.
ParticleEffect 액터는 ParticleSystemComponent 를 RootComponent로 설정합니다.
이펙트 액터 초기화
// APRNiagaraEffect.h
public:
/**
* 인자로 받은 NiagaraSystem을 기반으로 NiagaraEffect를 초기화하는 함수입니다.
*
* @param NiagaraSystem 사용할 이펙트
* @param NewEffectOwner 이펙트의 소유자
* @param NewPoolIndex 이펙트 풀의 Index
* @param NewLifespan 이펙트의 수명
*/
UFUNCTION(BlueprintCallable, Category = "PRNiagaraEffect")
void InitializeNiagaraEffect(UNiagaraSystem* NiagaraSystem = nullptr, AActor* NewEffectOwner = nullptr, int32 NewPoolIndex= -1, float NewLifespan = 0.0f);
// APRNiagaraEffect.cpp
void APRNiagaraEffect::InitializeNiagaraEffect(UNiagaraSystem* NiagaraSystem, AActor* NewEffectOwner, int32 NewPoolIndex, float NewLifespan)
{
InitializeEffect(NewEffectOwner, NewPoolIndex, NewLifespan);
if(IsValid(NiagaraSystem))
{
NiagaraEffect->SetAsset(NiagaraSystem);
}
}
NiagaraEffect 액터는 초기화할 때 NiagaraComponent의 Asset을 사용할 NiagaraSystem으로 설정합니다.
ParticleEffect 액터를 초기화할 때 ParticleSystemComponent의 Template를 사용할 ParticleSystem으로 설정합니다.
이펙트 관리 컴포넌트(EffectSystemComponent) 클래스
Onwer인 캐릭터가 사용하는 이펙트를 NiagaraEffect 액터와 ParticleEffect 액터로 구분하여 관리하는 컴포넌트 클래스입니다. 각각 사용하는 클래스와 이름만 다를뿐 관리하는 방법은 동일합니다.
// UPREffectSystemComponent.h
/**
* NiagaraEffect의 풀입니다.
*/
USTRUCT(Atomic, BlueprintType)
struct FPRNiagaraEffectPool
{
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "PRNiagaraEffectPool")
TArray<TObjectPtr<APRNiagaraEffect>> Effects;
};
/**
* 동적으로 생성한 NiagaraEffect 및 해당 NiagaraEffect를 제거할 때 사용하는 TimerHandle을 저장한 구조체입니다.
*/
USTRUCT(Atomic, BlueprintType)
struct FPRDynamicNiagaraEffectPool
{
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "DynamicNiagaraEffectPool")
TMap<TObjectPtr<APRNiagaraEffect>, FTimerHandle> Effects;
};
private:
/** 월드에 Spawn한 NiagaraEffect의 Pool입니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "PREffectSystem|NiagaraEffect", meta = (AllowPrivateAccess = "true"))
TMap<TObjectPtr<UNiagaraSystem>, FPRNiagaraEffectPool> NiagaraEffectPool;
/** 월드에 동적 Spawn한 NiagaraEffect의 Pool입니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "PREffectSystem|NiagaraEffect", meta = (AllowPrivateAccess = "true"))
TMap<TObjectPtr<UNiagaraSystem>, FPRDynamicNiagaraEffectPool> DynamicNiagaraEffectPool;
이펙트 액터는 오브젝트 풀 패턴으로 관리합니다. 이펙트 풀은 TMap를 사용하며, 각 이펙트가 사용하는 이펙트 시스템 클래스 포인터를 Key값으로 사용합니다. Value값은 배열을 사용하기 위해 구조체를 선언하고, 구조체 내에서 이펙트 액터 클래스 포인터의 배열을 변수로 사용합니다.
EffectPool 초기화
// APRBaseCharacter.h
private:
/** 이펙트를 관리하는 ActorComponent 클래스입니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "EffectSystem", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UPREffectSystemComponent> EffectSystem;
public:
/** ObjectPoolSystem을 반환하는 함수입니다. */
FORCEINLINE class UPREffectSystemComponent* GetEffectSystem() const { return EffectSystem; }
// APRBaseCharacter.cpp
APRBaseCharacter::APRBaseCharacter()
{
...
// EffectSystem
EffectSystem = CreateDefaultSubobject<UPREffectSystemComponent>(TEXT("EffectSystem"));
}
void APRBaseCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
...
// EffectSystem
GetEffectSystem()->InitializeEffectPool();
}
// UPREffectSystemComponent.cpp
void UPREffectSystemComponent::InitializeEffectPool()
{
// 기존의 EffectPool을 초기화합니다.
EmptyAllEffectPool();
// DataTable의 정보를 기반으로 NiagaraEffectPool을 생성합니다.
if(NiagaraEffectSettingsDataTable != nullptr)
{
TArray<FName> RowNames = NiagaraEffectSettingsDataTable->GetRowNames();
for(const FName& RowName: RowNames)
{
const FPRNiagaraEffectSettings* NiagaraEffectSettings = NiagaraEffectSettingsDataTable->FindRow<FPRNiagaraEffectSettings>(RowName, FString(""));
if(NiagaraEffectSettings != nullptr)
{
CreateNiagaraEffectPool(*NiagaraEffectSettings);
}
}
}
// DataTable의 정보를 기반으로 ParticleEffectPool을 생성합니다.
if(ParticleEffectSettingsDataTable != nullptr)
{
TArray<FName> RowNames = ParticleEffectSettingsDataTable->GetRowNames();
for(const FName& RowName: RowNames)
{
const FPRParticleEffectSettings* ParticleEffectSettings = ParticleEffectSettingsDataTable->FindRow<FPRParticleEffectSettings>(RowName, FString(""));
if(ParticleEffectSettings != nullptr)
{
CreateParticleEffectPool(*ParticleEffectSettings);
}
}
}
}
컴포넌트를 캐릭터에 부착하고 캐릭터의 PostInitializeComponent 함수에서 InitializeEffectPool 함수를 사용하여 모든 EffectPool을 초기화한 후, 데이터 테이블의 정보를 기반으로 EffectPool을 새로 생성합니다.
EffectPool 생성
// UPREffectSystemComponent.h
/**
* NiagaraEffectPool의 설정 값을 나타내는 구조체입니다.
*/
USTRUCT(Atomic, BlueprintType)
struct FPRNiagaraEffectSettings : public FTableRowBase
{
GENERATED_BODY()
public:
FPRNiagaraEffectSettings()
: NiagaraSystem(nullptr)
, PoolSize(0)
, Lifespan(0.0f)
{}
FPRNiagaraEffectSettings(TObjectPtr<UNiagaraSystem> NewNiagaraSystem, int32 NewPoolSize, float NewLifespan)
: NiagaraSystem(NewNiagaraSystem)
, PoolSize(NewPoolSize)
, Lifespan(NewLifespan)
{}
public:
/** 생성할 NiagaraSystem입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PRNiagaraEffectSettings")
TObjectPtr<UNiagaraSystem> NiagaraSystem;
/** Pool에 넣을 초기에 생성할 NiagaraEffect의 수입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PRNiagaraEffectSettings")
int32 PoolSize;
/** 이펙트의 수명입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PRNiagaraEffectSettings")
float Lifespan;
public:
/**
* 인자로 받은 NiagaraEffectSettings와 같은지 판별하는 ==연산자 오버로딩입니다.
*
* @param NewNiagaraEffectSettings 비교하는 NiagaraEffectSettings와 같은지 판별할 NiagaraEffectSettings입니다.
* @return 인자로 받은 NiagaraEffectSettings와 같을 경우 true를 다를 경우 false를 반환합니다.
*/
FORCEINLINE bool operator==(const FPRNiagaraEffectSettings& NewNiagaraEffectSettings) const
{
return this->NiagaraSystem == NewNiagaraEffectSettings.NiagaraSystem
&& this->PoolSize == NewNiagaraEffectSettings.PoolSize
&& this->Lifespan == NewNiagaraEffectSettings.Lifespan;
}
/**
* 인자로 받은 NiagaraEffectSettings와 다른지 판별하는 !=연산자 오버로딩입니다.
*
* @param NewNiagaraEffectSettings 비교하는 NiagaraEffectSettings와 같은지 판별할 NiagaraEffectSettings입니다.
* @return 인자로 받은 NiagaraEffectSettings와 다를 경우 true를 같을 경우 false를 반환합니다.
*/
FORCEINLINE bool operator!=(const FPRNiagaraEffectSettings& NewNiagaraEffectSettings) const
{
return this->NiagaraSystem != NewNiagaraEffectSettings.NiagaraSystem
&& this->PoolSize != NewNiagaraEffectSettings.PoolSize
&& this->Lifespan != NewNiagaraEffectSettings.Lifespan;
}
};
private:
/** 인자로 받은 NiagaraEffectSettings를 기반으로 NiagaraEffectPool을 생성하는 함수입니다. */
UFUNCTION(BlueprintCallable, Category = "PREffectSystem|NiagaraEffect")
void CreateNiagaraEffectPool(FPRNiagaraEffectSettings NiagaraEffectSettings);
// UPREffectSystemComponent.cpp
void UPREffectSystemComponent::CreateNiagaraEffectPool(FPRNiagaraEffectSettings NiagaraEffectSettings)
{
if(GetWorld() != nullptr && NiagaraEffectSettings.NiagaraSystem != nullptr)
{
// NiagaraEffectPool에 추가할 Pair를 초기화하고 NiagaraEffect를 생성하여 추가합니다.
FPRNiagaraEffectPool Pair;
for(int32 Index = 0; Index < NiagaraEffectSettings.PoolSize; Index++)
{
APRNiagaraEffect* SpawnNiagaraEffect = SpawnNiagaraEffectInWorld(NiagaraEffectSettings.NiagaraSystem, Index, NiagaraEffectSettings.Lifespan);
if(IsValid(SpawnNiagaraEffect))
{
Pair.Effects.Emplace(SpawnNiagaraEffect);
}
}
// 초기화된 Pair를 NiagaraEffectPool에 추가하고 ActivateNiagaraEffectIndexList와 UsedNiagaraEffectIndexList를 생성합니다.
NiagaraEffectPool.Emplace(NiagaraEffectSettings.NiagaraSystem, Pair);
CreateActivateNiagaraEffectIndexList(NiagaraEffectSettings.NiagaraSystem);
CreateUsedNiagaraEffectIndexList(NiagaraEffectSettings.NiagaraSystem);
}
}
void UPREffectSystemComponent::CreateActivateNiagaraEffectIndexList(UNiagaraSystem* NiagaraSystem)
{
ActivateNiagaraEffectIndexList.Emplace(NiagaraSystem);
}
void UPREffectSystemComponent::CreateUsedNiagaraEffectIndexList(UNiagaraSystem* NiagaraSystem)
{
FPRNiagaraEffectPool* Pair = NiagaraEffectPool.Find(NiagaraSystem);
if(Pair == nullptr)
{
// 지정된 NiagaraEffect가 업습니다.
return;
}
FPRUsedIndexList UsedEffectIndexList;
for(const auto& NiagaraEffect : Pair->Effects)
{
UsedEffectIndexList.Indexes.Add(NiagaraEffect->GetPoolIndex());
}
UsedNiagaraEffectIndexList.Emplace(NiagaraSystem, UsedEffectIndexList);
}
EffectPool은 FPREffectSettings 구조체를 인자로 받아 생성합니다. EffectSettings 구조체는 생성할 이펙트의 System 포인터와 EffectPool의 크기(생성할 이펙트 액터의 수), 이펙트의 수명을 변수로 가지고 있습니다.
PoolSize만큼 이펙트 액터를 월드에 Spawn한 후, 생성한 이펙트 액터를 배열로 보관하는 FPREffectPool 구조체에 저장한 후 생성한 이펙트의 System 포인터를 Key값으로 하고, FPREffectPool 구조체를 Value값으로 하여 EffectPool에 저장합니다.
활성화된 이펙트의 Index를 저장하는 ActivateEffectIndexList와 이펙트 액터를 생성하여 사용된 이펙트의 Index를 저장하는 UsedEffectIndexList를 생성합니다.
UsedEffectIndexList는 PoolSize를 초과하여 이펙트 액터를 생성하게 되거나 데이터 테이블에 존재하지 않는 이펙트 액터를 생성할 때 사용하게 되는 동적 생성에서 사용하게 됩니다.
Spawn 이펙트 액터 InWorld
// UPREffectSystemComponent.h
private:
/** 인자로 받은 NiagaraSystem를 월드에 APRNiagaraEffect로 Spawn하는 함수입니다. */
UFUNCTION(BlueprintCallable, Category = "PREffectSystem|NiagaraEffect")
APRNiagaraEffect* SpawnNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem, int32 PoolIndex = -1, float Lifespan = 0.0f);
// UPREffectSystemComponent.cpp
APRNiagaraEffect* UPREffectSystemComponent::SpawnNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem, int32 PoolIndex, float Lifespan)
{
if(!GetWorld() || !NiagaraSystem || !GetPROwner())
{
return nullptr;
}
// NiagaraEffect를 생성합니다.
APRNiagaraEffect* NiagaraEffect = GetWorld()->SpawnActor<APRNiagaraEffect>(APRNiagaraEffect::StaticClass());
if(!IsValid(NiagaraEffect))
{
// NiagaraEffect 생성에 실패하면 함수를 종료하고 nullptr을 반환합니다.
return nullptr;
}
// NiagaraEffect를 초기화합니다.
NiagaraEffect->InitializeNiagaraEffect(NiagaraSystem, GetPROwner(), PoolIndex, Lifespan);
// NiagaraEffect의 OnEffectDeactivateDelegate 이벤트에 대한 콜백 함수를 바인딩합니다.
NiagaraEffect->OnEffectDeactivateDelegate.AddDynamic(this, &UPREffectSystemComponent::OnNiagaraEffectDeactivate);
return NiagaraEffect;
}
void UPREffectSystemComponent::OnNiagaraEffectDeactivate(APREffect* Effect)
{
APRNiagaraEffect* NiagaraEffect = Cast<APRNiagaraEffect>(Effect);
if(IsValid(NiagaraEffect) && NiagaraEffect->GetNiagaraEffect() != nullptr)
{
FPRActivateIndexList* ActivateIndexList = ActivateNiagaraEffectIndexList.Find(NiagaraEffect->GetNiagaraEffectAsset());
if(ActivateIndexList != nullptr)
{
// 비활성화된 NiagaraEffect의 Index를 ActivateIndexList에서 제거합니다.
ActivateIndexList->Indexes.Remove(NiagaraEffect->GetPoolIndex());
}
}
}
이펙트 액터는 SpawnActor 함수를 사용하여 월드에 Spawn한 후, 각 이펙트 액터의 InitializeEffect 함수를 호출하여 초기화합니다.
이후에 이펙트 액터가 수명을 다하여 비활성화될 때 호출하는 델리게이트에 OnEffectDeactivate 함수를 바인딩합니다.
OnEffectDeactivate 함수는 활성화된 이펙트 액터의 Index를 저장한 ActivateEffectIndexList에서 비활성화된 이펙트 액터의 Index를 제거하는 함수입니다.
이펙트 액터 SpawnAtLocation / SpawnAttached
// UPREffectSystemComponent.h
public:
/**
* 이펙트를 지정한 위치에 Spawn하는 함수입니다.
*
* @param SpawnEffect Spawn할 이펙트
* @param Location 이펙트를 생성할 위치
* @param Rotation 이펙트에 적용한 회전 값
* @param Scale 이펙트에 적용할 크기
* @param bEffectAutoActivate true일 경우 이펙트를 Spawn하자마다 이펙트를 실행합니다. false일 경우 이펙트를 실행하지 않습니다.
*/
UFUNCTION(BlueprintCallable, Category = "PREffectSystem|NiagaraEffect")
APRNiagaraEffect* SpawnNiagaraEffectAtLocation(UNiagaraSystem* SpawnEffect, FVector Location, FRotator Rotation = FRotator::ZeroRotator, FVector Scale = FVector(1.0f), bool bEffectAutoActivate = true);
/**
* 이펙트를 지정한 Component에 부착하여 Spawn하는 함수입니다.
*
* @param SpawnEffect Spawn할 이펙트
* @param Parent 이펙트를 부착할 Component
* @param AttachSocketName 부착할 소켓의 이름
* @param Location 이펙트를 생성할 위치
* @param Rotation 이펙트에 적용한 회전 값
* @param Scale 이펙트에 적용할 크기
* @param bEffectAutoActivate true일 경우 이펙트를 Spawn하자마다 이펙트를 실행합니다. false일 경우 이펙트를 실행하지 않습니다.
*/
UFUNCTION(BlueprintCallable, Category = "PREffectSystem|NiagaraEffect")
APRNiagaraEffect* SpawnNiagaraEffectAttached(UNiagaraSystem* SpawnEffect, USceneComponent* Parent, FName AttachSocketName, FVector Location, FRotator Rotation, FVector Scale, bool bEffectAutoActivate = true);
// UPREffectSystemComponent.cpp
APRNiagaraEffect* UPREffectSystemComponent::SpawnNiagaraEffectAtLocation(UNiagaraSystem* SpawnEffect, FVector Location, FRotator Rotation, FVector Scale, bool bEffectAutoActivate)
{
APRNiagaraEffect* ActivateableNiagaraEffect = GetActivateableNiagaraEffect(SpawnEffect);
if(IsValid(ActivateableNiagaraEffect))
{
// NiagaraEffect를 활성화하고 Spawn할 위치와 회전값, 크기, 자동실행 여부를 적용합니다.
ActivateableNiagaraEffect->SpawnEffectAtLocation(Location, Rotation, Scale, bEffectAutoActivate);
// 활성화된 NiagaraEffect의 Index를 ActivateNiagaraEffectIndexList에 추가합니다.
ActivateNiagaraEffectIndexList.Find(SpawnEffect)->Indexes.Add(ActivateableNiagaraEffect->GetPoolIndex());
}
return ActivateableNiagaraEffect;
}
APRNiagaraEffect* UPREffectSystemComponent::SpawnNiagaraEffectAttached(UNiagaraSystem* SpawnEffect, USceneComponent* Parent, FName AttachSocketName, FVector Location, FRotator Rotation, FVector Scale, bool bEffectAutoActivate)
{
APRNiagaraEffect* ActivateableNiagaraEffect = GetActivateableNiagaraEffect(SpawnEffect);
if(IsValid(ActivateableNiagaraEffect))
{
// NiagaraEffect를 활성화하고 Spawn하여 부착할 Component와 위치, 회전값, 크기, 자동실행 여부를 적용합니다.
ActivateableNiagaraEffect->SpawnEffectAttached(Parent, AttachSocketName, Location, Rotation, Scale, bEffectAutoActivate);
// 활성화된 NiagaraEffect의 Index를 ActivateNiagaraEffectIndexList에 추가합니다.
ActivateNiagaraEffectIndexList.Find(SpawnEffect)->Indexes.Add(ActivateableNiagaraEffect->GetPoolIndex());
}
return ActivateableNiagaraEffect;
}
이펙트 액터를 지정한 위치에 Spawn하거나 Component에 부착할 때 GetActivateableEffect 함수를 사용하여 활성화할 수 있는 이펙트 액터를 가져옵니다. 가져온 각 이펙트 액터를 Spawn한 후, 활성화된 이펙트 액터의 Index를 ActivateEffectIndexList에 추가합니다.
활성화할 수 있는 이펙트 액터 반환
// UPREffectSystemComponent.h
private:
/** 인자에 해당하는 활성화할 수 있는 NiagaraSystem을 반환하는 함수입니다. */
UFUNCTION(BlueprintCallable, Category = "PREffectSystem|NiagaraEffect")
APRNiagaraEffect* GetActivateableNiagaraEffect(UNiagaraSystem* NiagaraSystem);
private:
/** 동적으로 생성한 Effect의 수명입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PREffectSystem", meta = (AllowPrivateAccess = "true"))
float DynamicLifespan;
/** 월드에 Spawn한 NiagaraEffect의 Pool입니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "PREffectSystem|NiagaraEffect", meta = (AllowPrivateAccess = "true"))
TMap<TObjectPtr<UNiagaraSystem>, FPRNiagaraEffectPool> NiagaraEffectPool;
/** 월드에 동적 Spawn한 NiagaraEffect의 Pool입니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "PREffectSystem|NiagaraEffect", meta = (AllowPrivateAccess = "true"))
TMap<TObjectPtr<UNiagaraSystem>, FPRDynamicNiagaraEffectPool> DynamicNiagaraEffectPool;
// UPREffectSystemComponent.cpp
APRNiagaraEffect* UPREffectSystemComponent::GetActivateableNiagaraEffect(UNiagaraSystem* NiagaraSystem)
{
// 해당 NiagaraEffect에 해당하는 Pool이 생성되었는지 확인하고, 없으면 생성합니다.
if(!IsCreateNiagaraEffectPool(NiagaraSystem))
{
FPRNiagaraEffectSettings NiagaraEffectSettings = FPRNiagaraEffectSettings(NiagaraSystem, 1, DynamicLifespan);
CreateNiagaraEffectPool(NiagaraEffectSettings);
}
// NiagaraEffectPool에서 해당 NiagaraEffect의 Pool을 얻습니다.
FPRNiagaraEffectPool* Pair = NiagaraEffectPool.Find(NiagaraSystem);
if(Pair == nullptr)
{
// 지정된 NiagaraEffect가 업습니다.
return nullptr;
}
APRNiagaraEffect* ActivateableNiagaraEffect = nullptr;
// Pair에서 활성화되지 않은 NiagaraEffect를 찾아 활성화합니다.
for(const auto& NiagaraEffect : Pair->Effects)
{
if(IsValid(NiagaraEffect) && !IsActivateNiagaraEffect(NiagaraEffect))
{
ActivateableNiagaraEffect = NiagaraEffect;
break;
}
}
// Pair의 모든 NiagaraEffect가 활성화되었을 경우 새로운 NiagaraEffect를 생성합니다.
if(ActivateableNiagaraEffect == nullptr)
{
ActivateableNiagaraEffect = SpawnDynamicNiagaraEffectInWorld(NiagaraSystem);
}
// 동적으로 생성된 NiagaraEffect일 경우 DynamicEffectDestroyTimer를 정지합니다.
FPRDynamicNiagaraEffectPool* DynamicEffectPool = DynamicNiagaraEffectPool.Find(ActivateableNiagaraEffect->GetNiagaraEffectAsset());
if(DynamicEffectPool != nullptr)
{
FTimerHandle* DynamicDestroyTimer = DynamicEffectPool->Effects.Find(ActivateableNiagaraEffect);
if(DynamicDestroyTimer != nullptr)
{
GetWorld()->GetTimerManager().ClearTimer(*DynamicDestroyTimer);
}
}
return ActivateableNiagaraEffect;
}
인자로 받은 이펙트에 해당하는 EffectPool이 생성되었는지 확인하고, 없을 경우 EffectPool을 생성합니다. 이 때 생성하는 EffectPool의 크기는 1이며, 생성한 이펙트 액터의 수명은 DynamicListspan 값입니다.
EffectPool이 이미 생성되었을 경우에는 Pool을 가져옵니다. 가져온 Pool에서 활성화되지 않은 이펙트 액터를 가져옵니다.
만약 Pool의 모든 이펙트 액터가 활성화되어 있는 경우에는 SpawnDynamicEffectInWorld 함수를 사용하여 새로운 이펙트 액터를 생성합니다.
활성화할 이펙트 액터가 동적으로 생성된 이펙트 액터일 경우 동적으로 생성한 이펙트 액터를 제거하는 DynamicEffectDestroyTimer를 정지하고, 활성화할 이펙트 액터를 반환합니다.
이펙트 액터 동적 생성
// UPREffectSystemComponent.h
private:
/**
* 사용 가능한 Index를 찾아 반환하는 함수입니다.
*
* @param UsedIndexes 이미 사용 중인 Index 목록
* @return 사용가능한 Index
*/
UFUNCTION(BlueprintCallable, Category = "PREffectSystem")
int32 FindAvailableIndex(const TSet<int32>& UsedIndexes);
private:
/** 인자로 받은 NiagaraSystem를 월드에 APRNiagaraEffect로 동적 Spawn하는 함수입니다. */
UFUNCTION(BlueprintCallable, Category = "PREffectSystem|NiagaraEffect")
APRNiagaraEffect* SpawnDynamicNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem);
/** 인자로 받은 NiagaraEffect의 설정 값을 데이터 테이블에서 가져오는 함수입니다. */
UFUNCTION(BlueprintCallable, Category = "PREffectSystem|NiagaraEffect")
FPRNiagaraEffectSettings GetNiagaraEffectSettingsFromDataTable(UNiagaraSystem* NiagaraSystem) const;
/** 인자로 받은 NiagaraEffect의 설정 값을 데이터 테이블에서 가져오는 함수입니다. */
UFUNCTION(BlueprintCallable, Category = "PREffectSystem|NiagaraEffect")
FPRNiagaraEffectSettings GetNiagaraEffectSettingsFromDataTable(UNiagaraSystem* NiagaraSystem) const;
private:
/**
* 동적으로 생성된 NiagaraEffect의 Index를 추적하기 위한 Index 목록입니다.
* 동적으로 생성한 NiagaraEffect의 Index를 부여할 때 해당 Index를 여기에 저장합니다.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "PREffectSystem|NiagaraEffect", meta = (AllowPrivateAccess = "true"))
TMap<TObjectPtr<UNiagaraSystem>, FPRUsedIndexList> UsedNiagaraEffectIndexList;
// UPREffectSystemComponent.cpp
int32 UPREffectSystemComponent::FindAvailableIndex(const TSet<int32>& UsedIndexes)
{
int32 NewIndex = 0;
while(UsedIndexes.Contains(NewIndex))
{
NewIndex++;
}
return NewIndex;
}
APRNiagaraEffect* UPREffectSystemComponent::SpawnDynamicNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem)
{
APRNiagaraEffect* ActivateableNiagaraEffect = nullptr;
FPRUsedIndexList* UsedEffectIndexList = UsedNiagaraEffectIndexList.Find(NiagaraSystem);
if(UsedEffectIndexList == nullptr)
{
// 지정된 NiagaraEffect가 없습니다.
return nullptr;
}
// 사용 가능한 Index를 구합니다.
const int32 NewIndex = FindAvailableIndex(UsedEffectIndexList->Indexes);
// 사용 가능한 Index를 UsedEffectIndexList에 저장합니다.
UsedEffectIndexList->Indexes.Add(NewIndex);
// 새로운 NiagaraEffect를 생성하고 초기화합니다.
const FPRNiagaraEffectSettings NiagaraEffectSettings = GetNiagaraEffectSettingsFromDataTable(NiagaraSystem);
if(NiagaraEffectSettings != FPRNiagaraEffectSettings())
{
// 데이터 테이블에 NiagaraEffect의 설정 값을 가지고 있을 경우 설정 값의 Lifespan을 적용합니다.
ActivateableNiagaraEffect = SpawnNiagaraEffectInWorld(NiagaraSystem, NewIndex, NiagaraEffectSettings.Lifespan);
}
else
{
// 데이터 테이블에 NiagaraEffect의 설정 값을 가지고 있지 않을 경우 DynamicLifespan을 적용합니다.
ActivateableNiagaraEffect = SpawnNiagaraEffectInWorld(NiagaraSystem, NewIndex, DynamicLifespan);
}
if(!IsValid(ActivateableNiagaraEffect))
{
// 지정된 NiagaraEffect가 없습니다.
return nullptr;
}
// ActivateableNiagaraEffect의 OnNiagaraEffectDeactivate 이벤트에 대한 콜백 함수를 바인딩합니다.
// 동적으로 생성된 NiagaraEffect에 대한 추가로 비활성화하는 함수입니다.
ActivateableNiagaraEffect->OnEffectDeactivateDelegate.AddDynamic(this, &UPREffectSystemComponent::OnDynamicNiagaraEffectDeactivate);
// NiagaraEffectPool에서 해당 NiagaraEffect의 Pool을 얻습니다.
FPRNiagaraEffectPool* Pair = NiagaraEffectPool.Find(NiagaraSystem);
if(Pair == nullptr)
{
// 지정된 NiagaraEffect가 업습니다.
return nullptr;
}
// 새로 생성한 NiagaraEffect를 Pair에 저장합니다.
Pair->Effects.Emplace(ActivateableNiagaraEffect);
return ActivateableNiagaraEffect;
}
FPRNiagaraEffectSettings UPREffectSystemComponent::GetNiagaraEffectSettingsFromDataTable(UNiagaraSystem* NiagaraSystem) const
{
if(NiagaraEffectSettingsDataTable != nullptr)
{
TArray<FName> RowNames = NiagaraEffectSettingsDataTable->GetRowNames();
for(const FName& RowName: RowNames)
{
FPRNiagaraEffectSettings* NiagaraEffectSettings = NiagaraEffectSettingsDataTable->FindRow<FPRNiagaraEffectSettings>(RowName, FString(""));
if(NiagaraEffectSettings != nullptr && NiagaraEffectSettings->NiagaraSystem == NiagaraSystem)
{
return *NiagaraEffectSettings;
}
}
}
return FPRNiagaraEffectSettings();
}
동적으로 이펙트 액터를 생성하는 함수는 독립적으로 사용하지 않습니다. 활성화 가능한 이펙트 액터를 가져오는 함수에서만 사용합니다. 만약 인자로 받은 이펙트의 EffectPool이 없어서 EffectPool을 생성할 때 UsedEffectIndexList도 같이 이 생성되기 때문에, 인자로 받은 이펙트의 UsedEffectIndexList가 존재하지 않을 경우 인자로 받은 이펙트의 EffectPool이 없으므로 nullptr을 반환하고 종료합니다.
UsedEffectIndexList 함수를 사용하여 동적으로 생성할 이펙트 액터가 사용 가능한 Index를 가져옵니다. 가져온 사용 가능한 Index는 UsedEffectIndexList에 저장됩니다.
새로운 이펙트 액터를 생성할 때 데이터 테이블에 인자로 받은 이펙트에 해당하는 설정 값(EffectSettings 구조체)가 있을 경우 설정 값의 Lifespan(수명)을 적용하여 생성합니다. 데이터 테이블에 설정 값이 존재하지 않을 경우 이펙트 액터의 수명은 DynamicLifespan 값을 적용하여 생성합니다.
이펙트 액터를 생성하였을 경우, 이펙트 액터가 비활성화 될때 실행하는 OnEffectDeactivateDelegate 델리게이트에 OndynamicEffectDeactivate 함수를 바인딩합니다.
새로 생성한 이펙트 액터를 EffectPool에 저장하고, 새로 생성한 이펙트 액터를 반환합니다.
이펙트 액터의 비활성화
// APREffect.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEffectDeactivate, APREffect*, Effect);
public:
/** 이펙트가 비활성화될 때 실행하는 델리게이트입니다. */
FOnEffectDeactivate OnEffectDeactivateDelegate;
// APREffect.cpp
void APREffect::Activate()
{
bActivate = true;
SetActorHiddenInGame(!bActivate);
// 이펙트의 수명을 설정합니다. 이펙트의 수명이 끝나면 이펙트를 비활성화합니다.
SetEffectLifespan(EffectLifespan);
}
void APREffect::Deactivate()
{
bActivate = false;
SetActorHiddenInGame(!bActivate);
// 이펙트에 설정된 모든 타이머를 초기화합니다.
GetWorldTimerManager().ClearAllTimersForObject(this);
// 비활성화 델리게이트를 호출합니다.
OnEffectDeactivateDelegate.Broadcast(this);
}
앞에서 설명한대로, 각 이펙트 액터를 각자 수명을 가지며 좌표나 컴포넌트에 부착하여 활성화할 경우 수명만큼의 타이머를 설정합니다. 수명이 다할 때 타이머가 작동하여 이펙트는 비활성화되고, OnEffectDeactivateDelegate 델리게이트가 호출됩니다.
EffectSystem에서 생성한 이펙트 액터의 OnEffectDeactivateDelegate 델리게이트에는 OnEffectDeactivate 함수가 기본적으로 바인딩되어 있습니다. 또한, 동적으로 생성한 이펙트 액터의 경우 OnDynamicEffectDeacitvate 함수가 추가로 바인딩됩니다.
// UPREffectSystemComponent.h
/**
* 동적으로 생성한 NiagaraEffect 및 해당 NiagaraEffect를 제거할 때 사용하는 TimerHandle을 저장한 구조체입니다.
*/
USTRUCT(Atomic, BlueprintType)
struct FPRDynamicNiagaraEffectPool
{
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "DynamicNiagaraEffectPool")
TMap<TObjectPtr<APRNiagaraEffect>, FTimerHandle> Effects;
};
private:
/** 월드에 동적 Spawn한 NiagaraEffect의 Pool입니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "PREffectSystem|NiagaraEffect", meta = (AllowPrivateAccess = "true"))
TMap<TObjectPtr<UNiagaraSystem>, FPRDynamicNiagaraEffectPool> DynamicNiagaraEffectPool;
private:
/** 인자로 받은 NiagaraEffect가 비활성화될 때 실행하는 함수입니다. */
UFUNCTION()
void OnNiagaraEffectDeactivate(APREffect* Effect);
/** 동적으로 생성한 NiagaraEffect가 비활성화될 때 실행하는 함수입니다. */
UFUNCTION()
void OnDynamicNiagaraEffectDeactivate(APREffect* Effect);
/** 동적으로 생성한 NiagaraEffect를 제거하는 함수입니다. */
UFUNCTION()
void DynamicNiagaraEffectDestroy(APRNiagaraEffect* NiagaraEffect);
// UPREffectSystemComponent.cpp
void UPREffectSystemComponent::OnNiagaraEffectDeactivate(APREffect* Effect)
{
APRNiagaraEffect* NiagaraEffect = Cast<APRNiagaraEffect>(Effect);
if(IsValid(NiagaraEffect) && NiagaraEffect->GetNiagaraEffect() != nullptr)
{
FPRActivateIndexList* ActivateIndexList = ActivateNiagaraEffectIndexList.Find(NiagaraEffect->GetNiagaraEffectAsset());
if(ActivateIndexList != nullptr)
{
// 비활성화된 NiagaraEffect의 Index를 ActivateIndexList에서 제거합니다.
ActivateIndexList->Indexes.Remove(NiagaraEffect->GetPoolIndex());
}
}
}
void UPREffectSystemComponent::OnDynamicNiagaraEffectDeactivate(APREffect* Effect)
{
APRNiagaraEffect* NiagaraEffect = Cast<APRNiagaraEffect>(Effect);
if(IsValid(NiagaraEffect))
{
if(DynamicLifespan > 0.0f)
{
// 타이머로 동적 수명이 끝난 후 DynamicNiagaraEffectDestroy 함수를 실행합니다.
FTimerHandle DynamicEffectDestroyTimerHandle;
FTimerDelegate DynamicEffectDestroyDelegate = FTimerDelegate::CreateUObject(this, &UPREffectSystemComponent::DynamicNiagaraEffectDestroy, NiagaraEffect);
GetWorld()->GetTimerManager().SetTimer(DynamicEffectDestroyTimerHandle, DynamicEffectDestroyDelegate, DynamicLifespan, false);
// 타이머를 DynamicDestroyNiagaraEffectList에 저장합니다.
FPRDynamicNiagaraEffectPool* DynamicPair = DynamicNiagaraEffectPool.Find(NiagaraEffect->GetNiagaraEffectAsset());
if(DynamicPair != nullptr)
{
DynamicPair->Effects.Emplace(NiagaraEffect, DynamicEffectDestroyTimerHandle);
}
else
{
FPRDynamicNiagaraEffectPool DynamicEffectPool;
DynamicEffectPool.Effects.Emplace(NiagaraEffect, DynamicEffectDestroyTimerHandle);
DynamicNiagaraEffectPool.Emplace(NiagaraEffect->GetNiagaraEffectAsset(), DynamicEffectPool);
}
}
else
{
// 동적 수명이 없을 경우 타이머를 실행하지 않고 바로 NiagaraEffect를 제거합니다.
DynamicNiagaraEffectDestroy(NiagaraEffect);
}
}
}
void UPREffectSystemComponent::DynamicNiagaraEffectDestroy(APRNiagaraEffect* NiagaraEffect)
{
// DynamicNiagaraEffectPool에서 해당 NiagaraEffect를 제거합니다.
FPRDynamicNiagaraEffectPool* DynamicEffectPool = DynamicNiagaraEffectPool.Find(NiagaraEffect->GetNiagaraEffectAsset());
if(DynamicEffectPool != nullptr)
{
DynamicEffectPool->Effects.Remove(NiagaraEffect);
}
// UsedNiagaraEffectIndexList에서 해당 NiagaraEffect의 PoolIndex를 제거합니다.
FPRUsedIndexList* UsedIndexList = UsedNiagaraEffectIndexList.Find(NiagaraEffect->GetNiagaraEffectAsset());
if(UsedIndexList != nullptr)
{
UsedIndexList->Indexes.Remove(NiagaraEffect->GetPoolIndex());
}
// NiagaraEffectPool에서 해당 NiagaraEffect를 제거하고, 월드에서 제거합니다.
FPRNiagaraEffectPool* EffectPool = NiagaraEffectPool.Find(NiagaraEffect->GetNiagaraEffectAsset());
if(EffectPool != nullptr)
{
EffectPool->Effects.Remove(NiagaraEffect);
NiagaraEffect->Destroy();
}
}
이펙트 액터가 비활성화되면, 델리게이트에 바인딩된 OnEffectDeactivate 함수가 실행되어 ActivateIndexList에서 비활성화된 이펙트 엑터의 Index를 제거합니다.
동적으로 생성된 이펙트 액터의 경우, OnEffectDeactivate 함수와 OnDynamicEffectDeactivate 함수가 실행됩니다. 동적으로 생성된 이펙트 액터를 비활성화된 후 DynamicLifespan 만큼의 타이머를 설정하여, 이펙트 액터를 Key 값으로 하고 타이머를 Value 값으로 하는 DynamicDestroyEffectList에 저장합니다. 타이머는 DynamicLifespan만큼 지나면 DynamicEffectDestroy 함수를 실행하여 동적으로 생성된 이펙트 액터를 월드에서 제거합니다.
정리하자면, 일반적인 이펙트 액터는 수명이 다하면 비활성화되어 월드에 존재하지만, 동적으로 생성한 이펙트 액터의 경우 수명이 다하면 비활성화되어 또 다른 동적 수명이 작동됩니다. 동적 수명이 다할 경우 월드에서 제거됩니다. 도중에 이펙트 액터가 활성화되면 동적 수명의 타이머를 제거하고, 다시 비활성화될 경우 동적 수명을 작동합니다.
'Project > Replica' 카테고리의 다른 글
[Project Replica] PoolableInterface / EffectSytstem (0) | 2024.06.29 |
---|---|
[Project Replica] PoolableInterface / BaseObjectPoolSystem (0) | 2024.06.25 |
[Project Replica] Vaulting / Vault / 장애물 뛰어넘기 (0) | 2024.04.15 |
[Project Replica] DamageSystem 대미지 시스템 (0) | 2024.01.11 |
[Project Replica] AI 생성 시스템 AISpawnSystem (1) | 2023.12.04 |