파티클 시각 효과 추가

1️⃣파티클 시스템(Particle System) 기본 개념 이해하기

  • 파티클 시스템이란?
    • 게임 내에서 다양한 시각적 효과를 구현하기 위한 도구
    • 다수의 작은 입자(Particle)들이 모여 움직이면서 특정 모양, 색상 혹은 애니메이션 효과를 만들어냄
    • 파티클 시스템을 사용해 효과적인 VFX(Visual Effects)를 구현할 수 있도록 풍부한 기능을 제공
  • Cascade VS Niagara
    • Cascade
      • 언리얼 3부터 제공된 오래된 파티클 편집 툴
      • 여전히 호환되지만 신규 기능 업데이트는 주로 Niagara위주로 이뤄지고 있음
        • 초급자가 배우거나 상대적으로간단하고 빠르게 결과를 볼 수 있음
        • 레거시 프로젝트나 기존 아티스트 툴채인에서 많이 사용
        • 복잡하거나 고급스러운 VFX 연출에는 한계
    • Niagara
      • 엔진 4이후 새롭게 도입된 차세대 파티클 시스탬
      • 5에선 공식적으로 권장함
        • 모듈 단위로 다양한 파티클 동작을 정교하게 제어 가능
        • 블프나 머터리얼, 스크립팅과 유기적으로 연동되어 고급 VFX를 쉽게 만들 수 있음
        • GPU 파티클, 신규 기능 업데이트가 빠름
  • 이름 앞에 P_나 Niagara_와 같은 접두사를 붙이면 프로젝트에서 쉽게 구분 가능

2️⃣아이템 획득 파티클

  • 모든 아이템은 상호작용 시 ActivateItem함수를 활용하여 실행하도록 설계하였으며, 여기에 파티클을붙인다면 BaseItem을 상속받는 아이템들 모두 파티클을 설정할 수 있다
  • 또한 파티클은 이펙트가 발생한 후 특정 시간 뒤에 사라지도록 해야한다
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInterface.h"
#include "BaseItem.generated.h"

class USphereComponent;

UCLASS()
class SPARTAPROJECT_API ABaseItem : public AActor, public IItemInterface
{
		GENERATED_BODY()
	
public:	
		ABaseItem();

protected:
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
		FName ItemType;
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
		USceneComponent* Scene;
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
		USphereComponent* Collision;
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
		UStaticMeshComponent* StaticMesh;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
		UParticleSystem* PickupParticle;

		virtual void OnItemOverlap(
				UPrimitiveComponent* OverlappedComp,
				AActor* OtherActor,
				UPrimitiveComponent* OtherComp,
				int32 OtherBodyIndex,
				bool bFromSweep,
				const FHitResult& SweepResult) override;
		virtual void OnItemEndOverlap(
				UPrimitiveComponent* OverlappedComp,
				AActor* OtherActor,
				UPrimitiveComponent* OtherComp,
				int32 OtherBodyIndex) override;
		virtual void ActivateItem(AActor* Activator) override;
		virtual FName GetItemType() const override;
	
		void DestroyItem();
};

#include "BaseItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"

ABaseItem::ABaseItem()
{
		PrimaryActorTick.bCanEverTick = false;
	
		Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
		SetRootComponent(Scene);
	
		Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
		Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
		Collision->SetupAttachment(Scene);
	
		StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
		StaticMesh->SetupAttachment(Collision);
	
		Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
		Collision->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);
}

void ABaseItem::OnItemOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex,
	bool bFromSweep,
	const FHitResult& SweepResult)
{
		if (OtherActor && OtherActor->ActorHasTag("Player"))
		{
				ActivateItem(OtherActor);
		}
}

void ABaseItem::OnItemEndOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex)
{
}

void ABaseItem::ActivateItem(AActor* Activator)
{
		UParticleSystemComponent* Particle = nullptr;
	
		if (PickupParticle)
		{
				Particle = UGameplayStatics::SpawnEmitterAtLocation(
					GetWorld(),
					PickupParticle,
					GetActorLocation(),
					GetActorRotation(),
					true
				);
		}
	
		if (Particle)
		{
				FTimerHandle DestroyParticleTimerHandle;
				TWeakObjectPtr<UParticleSystemComponent> WeakParticle = Particle;
						
				GetWorld()->GetTimerManager().SetTimer(
					DestroyParticleTimerHandle,
					[WeakParticle]()
					{
							if (WeakParticle.IsValid())
							{
									WeakParticle->DestroyComponent();
							}
					},
					2.0f,
					false
				);
		}
}

FName ABaseItem::GetItemType() const
{
		return ItemType;
}

void ABaseItem::DestroyItem()
{
		Destroy();
}
 
  • 빌드 후 에디터로 돌아가 파티클을 붙여줄 아이템 블프를 열어 Details창에 Effect라는 카테고리에 이펙트를 설정해준다
  • 컴파일 후 이펙트가 생성되고 사라지는지 확인해준다

3️⃣지뢰 아이템 파티클 추가

#pragma once

#include "CoreMinimal.h"
#include "BaseItem.h"
#include "MineItem.generated.h"

UCLASS()
class SPARTAPROJECT_API AMineItem : public ABaseItem
{
		GENERATED_BODY()

public:
		AMineItem();
	
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
		USphereComponent* ExplosionCollision;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
		UParticleSystem* ExplosionParticle;
	
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
		float ExplosionDelay;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
		float ExplosionRadius;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
		int32 ExplosionDamage;
	
		bool bHasExploded;
		FTimerHandle ExplosionTimerHandle;
	
		virtual void ActivateItem(AActor* Activator) override;
		
		void Explode();
};


#include "MineItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"

AMineItem::AMineItem()
{
		ExplosionDelay = 5.0f;
		ExplosionRadius = 300.0f;
		ExplosionDamage = 30.0f;
		ItemType = "Mine";
		bHasExploded = false;
	
		ExplosionCollision = CreateDefaultSubobject<USphereComponent>(TEXT("ExplosionCollision"));
		ExplosionCollision->InitSphereRadius(ExplosionRadius);
		ExplosionCollision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
		ExplosionCollision->SetupAttachment(Scene);
}

void AMineItem::ActivateItem(AActor* Activator)
{
		if (bHasExploded) return;
	
		Super::ActivateItem(Activator);
	
		GetWorld()->GetTimerManager().SetTimer(
			ExplosionTimerHandle,
			this,
			&AMineItem::Explode,
			ExplosionDelay,
			false
		);
	
		bHasExploded = true;
}

void AMineItem::Explode()
{
		UParticleSystemComponent* Particle = nullptr;
	
		if (ExplosionParticle)
		{
				Particle = UGameplayStatics::SpawnEmitterAtLocation(
					GetWorld(),
					ExplosionParticle,
					GetActorLocation(),
					GetActorRotation(),
					false
				);
		}
		
		TArray<AActor*> OverlappingActors;
		ExplosionCollision->GetOverlappingActors(OverlappingActors);
	
		for (AActor* Actor : OverlappingActors)
		{
			if (Actor && Actor->ActorHasTag("Player"))
			{
				UGameplayStatics::ApplyDamage(
					Actor,
					ExplosionDamage,
					nullptr,
					this,
					UDamageType::StaticClass()
				);
			}
		}
	
		DestroyItem();
	
		if (Particle)
		{
				FTimerHandle DestroyParticleTimerHandle;
				TWeakObjectPtr<UParticleSystemComponent> WeakParticle = Particle;
						
				GetWorld()->GetTimerManager().SetTimer(
					DestroyParticleTimerHandle,
					[WeakParticle]()
					{
							if (WeakParticle.IsValid())
							{
									WeakParticle->DestroyComponent();
							}
					},
					2.0f,
					false
				);
		}
}
  • 발동이 여러번 되는 것을 막기위해 HasExplode 를 설정해준다
  • Explode() 에서 SpawnEmitterAtLocation()을 사용해 폭발 이펙트를 하나 더 지정해준다
  • 빌드 후 에디터의 디테일 페널에 ExplosionEffect라는 프로퍼티가 나타나는데 여기서 폭발 파티클을 할당해준뒤 재생이 되는지 확인해본다
  • 이 외 파티클 활용 팁
    • 성능 고려
      • 파티클은 반복 재생시 성능 부하가 커질 수 있다
      • 불필요하게 많은 파티클이 생성되지 않도록 한다
    • 라이프 타임 조정
      • 필요에 따라 Looping 옵션을 사용해 부착 대상 컴포넌트를 지정해줘야한다
    • 부착
      • 아이템이 이동/회전하는 동안 파티클도 같이 움직여야 한다면 SpawnEmitterAttached()를 사용해 부착 대상 컴포넌트를 지정해줘야함

사운드 효과 추가

1️⃣언리얼 사운드 효과와 종류

  • 사운드 웨이브(Sound Wave)
    • 한개의 오디오 파일을 뜻함 .wav
    • 게임에서 재생할 기본 오디오 데이터 그 자체
    • 단일 음원(싱글 트랙) 재생용으로 사용
    • 내부적으로 Wave 탭 또는 사운드 웨이브 에셋 으로 관리
    • 재생시 별도의 추가 효과를 넣기 힘들고, 순수히 오디오 파일을 재생하는 형태
  • 사운드 큐(Sound Cue)
    • 엔진의 오디오 편집 그래프 시스템
    • 여러개의 사운드를 조합하거나, 랜덤/시퀀셜 재생, 믹싱, 페이드 등 다양한 처리를 시각적 노드그래프로 설정 가능
    • Sound Cue Editor라는 전용 편집창에서 다양한 로직을 구성 가능
    • 사운드 에셋이 늘어나고, 상황에 따라 실시간 변화하는 사운드를 만들고 싶을 때 필수적
    • 배경음악의 구간 전환(Intro-Loop-Outro) ,발걸음 , 소리의 무작이 재생, 충격 사운드 재생, 피치 랜덤화 같은 동적 사운드에 활용
  • 언제 사용?
    • 사운드 웨이브
      • 단일 오디오 파일을 그대로 재생
      • 간단한 UI 사운드, 짧은 효과음등
    • 사운드 큐
      • 여러 사운드 웨이브 조합, 랜덤/페이드/모듈레이션 등 고급 기능을 적용할 때
      • 같은 이벤트라도 여러 소리를 번갈아 재생하거나, 재생 로직을 노드 그래프로 구성할 때

2️⃣아이템 획득 사운드 적용

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInterface.h"
#include "BaseItem.generated.h"

class USphereComponent;

UCLASS()
class SPARTAPROJECT_API ABaseItem : public AActor, public IItemInterface
{
		GENERATED_BODY()
	
public:	
		ABaseItem();

protected:
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
		FName ItemType;
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
		USceneComponent* Scene;
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
		USphereComponent* Collision;
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
		UStaticMeshComponent* StaticMesh;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
		UParticleSystem* PickupParticle;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
		USoundBase* PickupSound;
	
		virtual void OnItemOverlap(
				UPrimitiveComponent* OverlappedComp,
				AActor* OtherActor,
				UPrimitiveComponent* OtherComp,
				int32 OtherBodyIndex,
				bool bFromSweep,
				const FHitResult& SweepResult) override;
		virtual void OnItemEndOverlap(
				UPrimitiveComponent* OverlappedComp,
				AActor* OtherActor,
				UPrimitiveComponent* OtherComp,
				int32 OtherBodyIndex) override;
		virtual void ActivateItem(AActor* Activator) override;
		virtual FName GetItemType() const override;
	
		void DestroyItem();
};

#include "BaseItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"

ABaseItem::ABaseItem()
{
		PrimaryActorTick.bCanEverTick = false;
	
		Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
		SetRootComponent(Scene);
	
		Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
		Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
		Collision->SetupAttachment(Scene);
	
		StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
		StaticMesh->SetupAttachment(Collision);
	
		Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
		Collision->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);
}

void ABaseItem::OnItemOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex,
	bool bFromSweep,
	const FHitResult& SweepResult)
{
		if (OtherActor && OtherActor->ActorHasTag("Player"))
		{
			GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Overlap!!!")));
			ActivateItem(OtherActor);
		}
}

void ABaseItem::OnItemEndOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex)
{
}

void ABaseItem::ActivateItem(AActor* Activator)
{
	UParticleSystemComponent* Particle = nullptr;

	if (PickupParticle)
	{
			Particle = UGameplayStatics::SpawnEmitterAtLocation(
				GetWorld(),
				PickupParticle,
				GetActorLocation(),
				GetActorRotation(),
				true
			);
	}

	if (PickupSound)
	{
			UGameplayStatics::PlaySoundAtLocation(
				GetWorld(),
				PickupSound,
				GetActorLocation()
			);
	}

	if (Particle)
	{
			FTimerHandle DestroyParticleTimerHandle;
	
			GetWorld()->GetTimerManager().SetTimer(
				DestroyParticleTimerHandle,
				[Particle]()
				{
						Particle->DestroyComponent();
				},
				2.0f,
				false
			);
		}
}

FName ABaseItem::GetItemType() const
{
		return ItemType;
}

void ABaseItem::DestroyItem()
{
		Destroy();
}
  • 빌드 후 블프에서 원하는 사운드를 지정 후 확인해보자

3️⃣지뢰 아이템 사운드 추가

#pragma once

#include "CoreMinimal.h"
#include "BaseItem.h"
#include "MineItem.generated.h"

UCLASS()
class SPARTAPROJECT_API AMineItem : public ABaseItem
{
		GENERATED_BODY()

public:
		AMineItem();
	
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
		USphereComponent* ExplosionCollision;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
		UParticleSystem* ExplosionParticle;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
		USoundBase* ExplosionSound;
	
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
		float ExplosionDelay;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
		float ExplosionRadius;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
		int32 ExplosionDamage;
	
		bool bHasExploded;
		FTimerHandle ExplosionTimerHandle;
	
		virtual void ActivateItem(AActor* Activator) override;
		
		void Explode();
};


#include "MineItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"

AMineItem::AMineItem()
{
		ExplosionDelay = 5.0f;
		ExplosionRadius = 300.0f;
		ExplosionDamage = 30.0f;
		ItemType = "Mine";
		bHasExploded = false;
	
		ExplosionCollision = CreateDefaultSubobject<USphereComponent>(TEXT("ExplosionCollision"));
		ExplosionCollision->InitSphereRadius(ExplosionRadius);
		ExplosionCollision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
		ExplosionCollision->SetupAttachment(Scene);
}

void AMineItem::ActivateItem(AActor* Activator)
{
		if (bHasExploded) return;
	
		Super::ActivateItem(Activator);
	
		GetWorld()->GetTimerManager().SetTimer(
				ExplosionTimerHandle,
				this,
				&AMineItem::Explode,
				ExplosionDelay,
				false
		);
	
		bHasExploded = true;
}

void AMineItem::Explode()
{
		UParticleSystemComponent* Particle = nullptr;
	
		if (ExplosionParticle)
		{
				Particle = UGameplayStatics::SpawnEmitterAtLocation(
						GetWorld(),
						ExplosionParticle,
						GetActorLocation(),
						GetActorRotation(),
						false
				);
		}
	
		if (ExplosionSound)
		{
				UGameplayStatics::PlaySoundAtLocation(
						GetWorld(),
						ExplosionSound,
						GetActorLocation()
				);
		}
	
		TArray<AActor*> OverlappingActors;
		ExplosionCollision->GetOverlappingActors(OverlappingActors);
	
		for (AActor* Actor : OverlappingActors)
		{
			if (Actor && Actor->ActorHasTag("Player"))
			{
					UGameplayStatics::ApplyDamage(
							Actor,
							ExplosionDamage,
							nullptr,
							this,
							UDamageType::StaticClass()
					);
			}
		}
	
		DestroyItem();
	
		if (Particle)
		{
				FTimerHandle DestroyParticleTimerHandle;
		
				GetWorld()->GetTimerManager().SetTimer(
					DestroyParticleTimerHandle,
					[Particle]()
					{
							Particle->DestroyComponent();
					},
					2.0f,
					false
			);
		}
}
  • 람다함수
    • 익명함수(이름이 없는 함수)
    • [] 캡처 리스트 : 람다를 실행할 때 바깥 스코프에서 값을 가져와 사용할 수 있게 만드는 것
    • 함수를 직접 구현하기 번거롭고, 간단하게 명령을 내려야 하는데 함수처럼 사용하고 싶을 때 사용