💭회고
오늘은 게임의 몰입감과 품질을 결정하는 요소인 '파티클 효과'와 '사운드 효과'를 CPP로 구현하는 법에 대해 복습해보았다.
🗺️마인드맵
📒학습 내용
게임 이펙트의 중요성
AAA급 게임과 인디 게임의 가장 큰 차이점 중 하나는 바로 시청각적 효과의 퀄리티.
전문적인 VFX(Visual Effects)와 SFX(Sound Effects)는 게임의 완성도를 결정하는 핵심 요소...!
파티클과 사운드 시스템 이해하기
파티클 시스템: Niagara vs Cascade
Niagara | Cascade | |
권장 버전 | UE4.26 이상 | UE4.25 이하(레거시) |
성능 | GPU 가속 지원 | CPU 기반 |
구조 | 모듈식 아키텍처 | 고정 구조 |
유연성 | 높음 (커스텀 로직 구현 가능) | 제한적 |
학습 곡선 | 다소 가파름 | 상대적으로 쉬움 |
적합한 용도 | 복잡한 고품질 이펙트 | 단순한 이펙트, 빠른 프로토타이핑 |
🔍 실무 팁: 현업에서는 UE5 프로젝트에서는 거의 예외 없이 Niagara를 사용. Niagara는 초기 설정이 다소 복잡하지만, 한번 익숙해지면 노드 기반 인터페이스와 GPU 가속을 통해 훨씬 더 복잡하고 최적화된 효과를 만들 수 있다.
사운드 시스템: Sound Wave vs Sound Cue
Sound Wave | Sound Cue | |
형태 | 단일 오디오 파일 (.wav, .mp3 등) | 복합 사운드 에셋 |
복잡성 | 단순 | 복잡한 오디오 제어 가능 |
기능 | 기본 재생만 가능 | 랜덤 재생, 믹싱, 페이드 인/아웃 등 |
적합한 용도 | 단순 효과음 | 복합적인 사운드 효과, 환경음 |
🔍 실무 팁: 게임 개발 초기 단계에서는 Sound Wave로 빠르게 구현하고, 후반 폴리싱 단계에서 Sound Cue로 업그레이드하는 전략이 효과적이라고 한다. 예를 들어, 발소리는 처음에는 단일 사운드로 구현하고 나중에 바닥 재질에 따라 다른 소리가 나도록 Sound Cue로 발전시킬 수 있다.
기본 아이템 클래스 설계
효과적인 이펙트 시스템을 구현하기 위해서는 먼저 기본 아이템 클래스가 필요합니다. 상속 구조를 활용하면 코드 재사용성을 높이고 유지보수를 용이하게 할 수 있다.
ABaseItem 클래스 구조
// BaseItem.h
UCLASS()
class GAME_API ABaseItem : public AActor
{
GENERATED_BODY()
public:
ABaseItem();
protected:
// 기본 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USceneComponent* Scene;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USphereComponent* Collision;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UStaticMeshComponent* StaticMesh;
// 아이템 속성
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FName ItemType;
// 이펙트 관련 속성
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
UParticleSystem* PickupParticle;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
USoundBase* PickupSound;
// 콜백 함수
UFUNCTION()
virtual void OnItemOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult);
// 아이템 활성화
UFUNCTION(BlueprintCallable, Category = "Item")
virtual void ActivateItem(AActor* Activator);
// 아이템 소멸
UFUNCTION(BlueprintCallable, Category = "Item")
virtual void DestroyItem();
};
생성자 및 초기화 코드
// BaseItem.cpp
ABaseItem::ABaseItem()
{
PrimaryActorTick.bCanEverTick = false; // 불필요한 Tick 비활성화
// 컴포넌트 생성 및 계층 구조 설정
Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
RootComponent = Scene;
Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
Collision->SetupAttachment(Scene);
Collision->SetSphereRadius(100.0f);
Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMesh->SetupAttachment(Scene);
StaticMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// 오버랩 이벤트 바인딩
Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
}
시각 효과 구현하기
이제 기본 클래스를 바탕으로 아이템 획득 시 발생하는 파티클 효과를 구현해보자.
ActivateItem 함수 구현
void ABaseItem::ActivateItem(AActor* Activator)
{
// 파티클 효과 생성
if (PickupParticle)
{
UParticleSystemComponent* Particle = UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
PickupParticle,
GetActorLocation(),
GetActorRotation(),
true
);
// 파티클 자동 소멸을 위한 타이머 설정
if (Particle)
{
// 약한 포인터(TWeakObjectPtr)를 사용해 메모리 안전성 확보
TWeakObjectPtr<UParticleSystemComponent> WeakParticle(Particle);
FTimerHandle DestroyParticleTimerHandle;
GetWorld()->GetTimerManager().SetTimer(
DestroyParticleTimerHandle,
[WeakParticle]()
{
if (WeakParticle.IsValid())
WeakParticle->DestroyComponent();
},
2.0f, // 2초 후 소멸
false // 반복 없음
);
}
}
// 사운드 효과는 다음 섹션에서 구현
// 아이템 소멸
DestroyItem();
}
🔍 파티클 효과를 생성할 때 UGameplayStatics::SpawnEmitterAtLocation의 마지막 파라미터를 true로 설정하면 파티클이 생성 즉시 자동으로 재생된다. 별도로 Activate() 함수를 호출할 필요가 없어 코드가 간결해진다.(모바일 게임이라면 타이머를 1초 이내로 줄여야 한다. 저사양 디바이스에서 파티클이 쌓이면 바로 렉이 걸린다. 디버깅할 때 Stat FPS 명령어로 성능을 체크하자)
람다 함수와 약한 포인터 활용
위 코드에서 주목할 점은 타이머와 람다 함수, 그리고 약한 포인터(TWeakObjectPtr)의 활용이다:
- 타이머: GetWorld()->GetTimerManager().SetTimer()를 사용해 2초 후 파티클을 소멸시키는 작업을 예약.
- 람다 함수: 간결한 문법으로 콜백 함수를 정의할 수 있다.
- 약한 포인터: TWeakObjectPtr을 사용해 파티클 컴포넌트가 다른 이유로 소멸되었을 때 발생할 수 있는 댕글링 포인터(dangling pointer) 문제 방지.
핵심 요약: 시각 효과를 구현할 때는 UGameplayStatics::SpawnEmitterAtLocation을 사용하여 파티클을 생성하고, 타이머와 약한 포인터를 활용하여 메모리 누수를 방지한다. 이는 게임 성능 최적화에 중요한 역할을 한다.
청각 효과 구현하기
이제 앞서 구현한 ActivateItem 함수에 사운드 효과를 추가해보자
ActivateItem 함수 확장
void ABaseItem::ActivateItem(AActor* Activator)
{
// 파티클 효과 (이전 섹션과 동일)
if (PickupParticle)
{
// ... 파티클 생성 코드 ...
}
// 사운드 효과 추가
if (PickupSound)
{
UGameplayStatics::PlaySoundAtLocation(
GetWorld(), // 월드 컨텍스트
PickupSound, // 재생할 사운드
GetActorLocation(), // 사운드 위치
1.0f, // 볼륨 (기본값 1.0)
1.0f, // 피치 (기본값 1.0)
0.0f, // 시작 시간 (기본값 0.0)
nullptr, // 감쇠 설정 (기본값 nullptr)
nullptr // 동시성 설정 (기본값 nullptr)
);
}
// 아이템 소멸
DestroyItem();
}
🔍 사운드의 공간감을 더 정교하게 제어하기 위해 볼륨과 피치를 상황에 맞게 동적으로 조절하는 경우가 많다. 예를 들어, 플레이어의 체력이 낮을 때는 획득 사운드의 피치를 낮추고 볼륨을 높여 긴박감을 더할 수 있다.
특수 아이템 예시: 폭발형 지뢰
AMineItem 클래스 구조
// MineItem.h
UCLASS()
class GAME_API AMineItem : public ABaseItem
{
GENERATED_BODY()
public:
AMineItem();
protected:
// 폭발 관련 컴포넌트 및 속성
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USphereComponent* ExplosionCollision;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine")
float ExplosionDelay;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine")
float ExplosionRadius;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine")
int32 ExplosionDamage;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine")
bool bHasExploded;
// 폭발 이펙트
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine|Effects")
UParticleSystem* ExplosionParticle;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine|Effects")
USoundBase* ExplosionSound;
// 타이머 핸들
FTimerHandle ExplosionTimerHandle;
// 오버라이드 함수
virtual void ActivateItem(AActor* Activator) override;
// 폭발 함수
UFUNCTION()
void Explode();
};
AMineItem 구현
// MineItem.cpp
AMineItem::AMineItem()
{
// 기본 속성 초기화
ExplosionDelay = 5.0f;
ExplosionRadius = 300.0f;
ExplosionDamage = 30;
bHasExploded = false;
// 폭발 범위 컴포넌트 설정
ExplosionCollision = CreateDefaultSubobject<USphereComponent>(TEXT("ExplosionCollision"));
ExplosionCollision->SetupAttachment(Scene);
ExplosionCollision->SetSphereRadius(ExplosionRadius);
ExplosionCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 초기에는 비활성화
}
void AMineItem::ActivateItem(AActor* Activator)
{
// 아이템이 이미 활성화되었다면 무시
if (bHasExploded)
return;
// 부모 클래스의 ActivateItem 호출 (파티클/사운드 효과)
Super::ActivateItem(Activator);
// 폭발 타이머 설정
GetWorld()->GetTimerManager().SetTimer(
ExplosionTimerHandle,
this,
&AMineItem::Explode,
ExplosionDelay,
false
);
// 아이템 소멸 방지 (폭발 후 소멸하도록)
SetActorHiddenInGame(true); // 시각적으로만 숨김
Collision->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 충돌 비활성화
}
void AMineItem::Explode()
{
// 이미 폭발했으면 무시
if (bHasExploded)
return;
bHasExploded = true;
// 폭발 파티클 효과
if (ExplosionParticle)
{
UParticleSystemComponent* Particle = UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
ExplosionParticle,
GetActorLocation(),
GetActorRotation(),
true
);
// 파티클 자동 소멸 타이머
if (Particle)
{
TWeakObjectPtr<UParticleSystemComponent> WeakParticle(Particle);
FTimerHandle DestroyParticleTimerHandle;
GetWorld()->GetTimerManager().SetTimer(
DestroyParticleTimerHandle,
[WeakParticle]()
{
if (WeakParticle.IsValid())
WeakParticle->DestroyComponent();
},
2.0f,
false
);
}
}
// 폭발 사운드 효과
if (ExplosionSound)
{
UGameplayStatics::PlaySoundAtLocation(
GetWorld(),
ExplosionSound,
GetActorLocation()
);
}
// 폭발 범위 내 액터에 데미지 적용
ExplosionCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
TArray<AActor*> OverlappingActors;
ExplosionCollision->GetOverlappingActors(OverlappingActors);
for (AActor* Actor : OverlappingActors)
{
if (Actor && Actor->ActorHasTag("Player"))
{
UGameplayStatics::ApplyDamage(
Actor, // 데미지를 받을 액터
ExplosionDamage, // 데미지 양
nullptr, // 데미지 인스티게이터 (null=자기 자신)
this, // 데미지 주체
UDamageType::StaticClass() // 데미지 타입
);
}
}
// 최종 소멸
DestroyItem();
}
🔍 폭발 효과를 더 정교하게 구현하기 위해 다음과 같은 기법들을 자주 사용한다고 한다:
- 거리에 따른 데미지 감소: 폭발 중심에서 멀어질수록 데미지가 감소하도록 설정
- 카메라 쉐이크: 폭발 시 카메라에 흔들림 효과 추가
- 포스트 프로세싱: 잠시 화면 색조를 바꾸어 폭발의 임팩트 강화
- 물리 충격파: 주변 물체에 물리적 힘을 가해 날아가게 하는 효과
성능 최적화 팁
이펙트 시스템은 게임 성능에 큰 영향을 미칠 수 있으므로, 효율적인 구현이 중요!
파티클 자동 소멸 타이머
앞서 구현한 코드처럼 파티클 효과는 일정 시간 후 자동으로 소멸되도록 타이머를 설정하는 것이 좋다. 이는 메모리 누수를 방지하고 게임 성능을 유지하는 데 중요하다!
오브젝트 풀링
잦은 생성과 소멸이 필요한 이펙트의 경우, 매번 새로 생성하는 것보다 오브젝트 풀(Object Pool)을 구현하는 것이 효율적이다.
// 간단한 오브젝트 풀링 예시
TArray<UParticleSystemComponent*> ParticlePool;
int32 PoolSize = 10;
// 초기화 시 풀 생성
void InitializeParticlePool()
{
for (int32 i = 0; i < PoolSize; i++)
{
UParticleSystemComponent* Particle = NewObject<UParticleSystemComponent>(this);
Particle->SetTemplate(DefaultParticleSystem);
Particle->bAutoActivate = false;
Particle->RegisterComponent();
ParticlePool.Add(Particle);
}
}
// 파티클 획득 및 사용
UParticleSystemComponent* GetParticleFromPool()
{
for (UParticleSystemComponent* Particle : ParticlePool)
{
if (!Particle->IsActive())
{
return Particle;
}
}
// 풀이 모두 사용 중이면 첫 번째 파티클 재활용
UParticleSystemComponent* Particle = ParticlePool[0];
Particle->DeactivateImmediate();
return Particle;
}
LOD(Level of Detail) 시스템 활용
거리에 따라 파티클의 복잡도를 조절하는 LOD 시스템을 사용하면 멀리 있는 이펙트의 리소스 사용량을 줄일 수 있다.
Niagara는 이러한 LOD 시스템을 기본적으로 지원한다.
콜리전 최적화
폭발 효과와 같이 영역을 검사해야 하는 경우, 모든 액터를 검사하는 것보다 관련 태그나 채널을 사용하여 검사 범위를 제한하는 것이 효율적이다.
// 비효율적인 방법
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AActor::StaticClass(), AllActors);
for (AActor* Actor : AllActors)
{
// 거리 계산 후 데미지 적용
}
// 효율적인 방법
TArray<AActor*> OverlappingActors;
ExplosionCollision->GetOverlappingActors(OverlappingActors, ACharacter::
🟣오늘의 옵시디언 현황
'Dev. > UE 언리얼 엔진' 카테고리의 다른 글
[TIL_250305] 언리얼 엔진 자막 달기 (0) | 2025.03.05 |
---|---|
[TIL_250304] 언리얼 엔진 미니맵 제작 과정 가이드 (0) | 2025.03.04 |
[TIL_250227] 언리얼 엔진 조명 설정, 애니메이션 블렌드, 오디오 Waveform Editor 정리 (0) | 2025.02.27 |
[TIL_250226] 언리얼 엔진 사운드 이펙트 구현: 무기 장전 & 엘리베이터 트리거 (0) | 2025.02.26 |
[TIL_250225] 언리얼 엔진으로 구현하는 표면 기반 발소리 & 3D 사운드 시스템 (0) | 2025.02.25 |