파티클 시각 효과 추가
1️⃣파티클 시스템(Particle System) 기본 개념 이해하기
- 파티클 시스템이란?
- 게임 내에서 다양한 시각적 효과를 구현하기 위한 도구
- 다수의 작은 입자(Particle)들이 모여 움직이면서 특정 모양, 색상 혹은 애니메이션 효과를 만들어냄
- 파티클 시스템을 사용해 효과적인 VFX(Visual Effects)를 구현할 수 있도록 풍부한 기능을 제공
- Cascade VS Niagara
- Cascade
- 언리얼 3부터 제공된 오래된 파티클 편집 툴
- 여전히 호환되지만 신규 기능 업데이트는 주로 Niagara위주로 이뤄지고 있음
- 초급자가 배우거나 상대적으로간단하고 빠르게 결과를 볼 수 있음
- 레거시 프로젝트나 기존 아티스트 툴채인에서 많이 사용
- 복잡하거나 고급스러운 VFX 연출에는 한계
- Niagara
- 엔진 4이후 새롭게 도입된 차세대 파티클 시스탬
- 5에선 공식적으로 권장함
- 모듈 단위로 다양한 파티클 동작을 정교하게 제어 가능
- 블프나 머터리얼, 스크립팅과 유기적으로 연동되어 고급 VFX를 쉽게 만들 수 있음
- GPU 파티클, 신규 기능 업데이트가 빠름
- Cascade
- 이름 앞에 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
);
}
}
- 람다함수
- 익명함수(이름이 없는 함수)
- [] 캡처 리스트 : 람다를 실행할 때 바깥 스코프에서 값을 가져와 사용할 수 있게 만드는 것
- 함수를 직접 구현하기 번거롭고, 간단하게 명령을 내려야 하는데 함수처럼 사용하고 싶을 때 사용
'Unreal Bootcamp > Unreal C++' 카테고리의 다른 글
19.UI 애니메이션 효과 및 3D 위젯 UI 구현 (0) | 2025.02.13 |
---|---|
18.메뉴 UI 구현 (0) | 2025.02.12 |
17. UI 위젯 설계와 실시간 데이터 (1) | 2025.02.12 |
16.게임 루프 설계를 통한 게임 흐름 제어 (0) | 2025.02.11 |
15.캐릭터 체력 및 점수 관리 시스템 (0) | 2025.02.10 |